feat: global labelPlacement prop (#4346)

* feat: adding the support for labelPlacement globally

* chore: reafctoring

* chore: updating the dependency

* chore(changeset): update package name

* chore: adding Marcus's suggestions

---------

Co-authored-by: աӄա <wingkwong.code@gmail.com>
This commit is contained in:
Maharshi Alpesh 2025-01-30 18:19:32 +05:30 committed by GitHub
parent f9c2be4509
commit 7804de0d89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 109 additions and 64 deletions

View File

@ -0,0 +1,10 @@
---
"@heroui/date-picker": patch
"@heroui/date-input": patch
"@heroui/select": patch
"@heroui/input": patch
"@heroui/system": patch
"@heroui/theme": patch
---
Adding support for global labelPlacement prop. (ENG-1694)

View File

@ -142,6 +142,15 @@ interface AppProviderProps {
<Spacer y={2}/>
`labelPlacement`
- **Description**: Determines the position where label should appear, such as inside, outside or outside-left of the component.
- **Type**: `string` | `undefined`
- **Possible Values**: `inside` | `outside` | `outside-left` | `undefined`
- **Default**: `undefined`
<Spacer y={2}/>
`disableAnimation`
- **Description**: Disables animations globally. This will also avoid `framer-motion` features to be loaded in the bundle which can potentially reduce the bundle size.

View File

@ -34,8 +34,8 @@
"postpack": "clean-package restore"
},
"peerDependencies": {
"@heroui/system": ">=2.4.0",
"@heroui/theme": ">=2.4.0",
"@heroui/system": ">=2.4.8",
"@heroui/theme": ">=2.4.7",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
},

View File

@ -10,7 +10,7 @@ import type {DateInputGroupProps} from "./date-input-group";
import {useLocale} from "@react-aria/i18n";
import {createCalendar, CalendarDate, DateFormatter} from "@internationalized/date";
import {mergeProps} from "@react-aria/utils";
import {PropGetter, useProviderContext} from "@heroui/system";
import {PropGetter, useLabelPlacement, useProviderContext} from "@heroui/system";
import {HTMLHeroUIProps, mapPropsVariants} from "@heroui/system";
import {useDOMRef} from "@heroui/react-utils";
import {useDateField as useAriaDateField} from "@react-aria/datepicker";
@ -191,16 +191,10 @@ export function useDateInput<T extends DateValue>(originalProps: UseDateInputPro
const isInvalid = isInvalidProp || ariaIsInvalid;
const labelPlacement = useMemo<DateInputVariantProps["labelPlacement"]>(() => {
if (
(!originalProps.labelPlacement || originalProps.labelPlacement === "inside") &&
!props.label
) {
return "outside";
}
return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, props.label]);
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";

View File

@ -6,7 +6,7 @@ import type {DateInputGroupProps} from "./date-input-group";
import {useLocale} from "@react-aria/i18n";
import {mergeProps} from "@react-aria/utils";
import {PropGetter, useProviderContext} from "@heroui/system";
import {PropGetter, useLabelPlacement, useProviderContext} from "@heroui/system";
import {HTMLHeroUIProps, mapPropsVariants} from "@heroui/system";
import {useDOMRef} from "@heroui/react-utils";
import {useTimeField as useAriaTimeField} from "@react-aria/datepicker";
@ -133,16 +133,10 @@ export function useTimeInput<T extends TimeValue>(originalProps: UseTimeInputPro
const baseStyles = clsx(classNames?.base, className);
const labelPlacement = useMemo<DateInputVariantProps["labelPlacement"]>(() => {
if (
(!originalProps.labelPlacement || originalProps.labelPlacement === "inside") &&
!props.label
) {
return "outside";
}
return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, props.label]);
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";

View File

@ -34,8 +34,8 @@
"postpack": "clean-package restore"
},
"peerDependencies": {
"@heroui/system": ">=2.4.0",
"@heroui/theme": ">=2.4.0",
"@heroui/system": ">=2.4.8",
"@heroui/theme": ">=2.4.7",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"

View File

@ -1,5 +1,4 @@
import type {DateValue} from "@internationalized/date";
import type {DateInputVariantProps} from "@heroui/theme";
import type {TimeInputProps} from "@heroui/date-input";
import type {ButtonProps} from "@heroui/button";
import type {RangeCalendarProps} from "@heroui/calendar";
@ -14,7 +13,7 @@ import type {DateInputGroupProps} from "@heroui/date-input";
import type {DateRangePickerSlots, SlotsToClasses} from "@heroui/theme";
import type {DateInputProps} from "@heroui/date-input";
import {useProviderContext} from "@heroui/system";
import {useLabelPlacement, useProviderContext} from "@heroui/system";
import {useMemo, useRef} from "react";
import {useDateRangePickerState} from "@react-stately/datepicker";
import {useDateRangePicker as useAriaDateRangePicker} from "@react-aria/datepicker";
@ -60,6 +59,7 @@ export type UseDateRangePickerProps<T extends DateValue> = Props<T> & AriaDateRa
export function useDateRangePicker<T extends DateValue>({
as,
label,
isInvalid: isInvalidProp,
description,
startContent,
@ -143,16 +143,10 @@ export function useDateRangePicker<T extends DateValue>({
const showTimeField = !!timeGranularity;
const labelPlacement = useMemo<DateInputVariantProps["labelPlacement"]>(() => {
if (
(!originalProps.labelPlacement || originalProps.labelPlacement === "inside") &&
!originalProps.label
) {
return "outside";
}
return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, originalProps.label]);
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";
@ -395,7 +389,7 @@ export function useDateRangePicker<T extends DateValue>({
const getDateInputGroupProps = () => {
return {
as,
label: originalProps.label,
label,
description,
endContent,
errorMessage,
@ -423,7 +417,7 @@ export function useDateRangePicker<T extends DateValue>({
return {
state,
label: originalProps.label,
label,
slots,
classNames,
startContent,

View File

@ -36,8 +36,8 @@
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0",
"@heroui/theme": ">=2.4.0",
"@heroui/system": ">=2.4.0"
"@heroui/theme": ">=2.4.7",
"@heroui/system": ">=2.4.8"
},
"dependencies": {
"@heroui/form": "workspace:*",

View File

@ -1,7 +1,13 @@
import type {InputVariantProps, SlotsToClasses, InputSlots} from "@heroui/theme";
import type {AriaTextFieldOptions} from "@react-aria/textfield";
import {HTMLHeroUIProps, mapPropsVariants, PropGetter, useProviderContext} from "@heroui/system";
import {
HTMLHeroUIProps,
mapPropsVariants,
PropGetter,
useLabelPlacement,
useProviderContext,
} from "@heroui/system";
import {useSafeLayoutEffect} from "@heroui/use-safe-layout-effect";
import {AriaTextFieldProps} from "@react-types/textfield";
import {useFocusRing} from "@react-aria/focus";
@ -222,13 +228,10 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
const isInvalid = validationState === "invalid" || isAriaInvalid;
const labelPlacement = useMemo<InputVariantProps["labelPlacement"]>(() => {
if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) {
return "outside";
}
return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, label]);
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const errorMessage =
typeof props.errorMessage === "function"

View File

@ -34,8 +34,8 @@
"postpack": "clean-package restore"
},
"peerDependencies": {
"@heroui/system": ">=2.4.0",
"@heroui/theme": ">=2.4.0",
"@heroui/system": ">=2.4.8",
"@heroui/theme": ">=2.4.7",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"

View File

@ -7,6 +7,7 @@ import {
mapPropsVariants,
PropGetter,
SharedSelection,
useLabelPlacement,
useProviderContext,
} from "@heroui/system";
import {select} from "@heroui/theme";
@ -346,13 +347,10 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
const {focusProps, isFocused, isFocusVisible} = useFocusRing();
const {isHovered, hoverProps} = useHover({isDisabled: originalProps.isDisabled});
const labelPlacement = useMemo<SelectVariantProps["labelPlacement"]>(() => {
if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) {
return "outside";
}
return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, label]);
const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});
const hasPlaceholder = !!placeholder;
const shouldLabelBeOutside =

View File

@ -0,0 +1 @@
export {useLabelPlacement} from "./use-label-placement";

View File

@ -0,0 +1,21 @@
import {useMemo} from "react";
import {useProviderContext} from "../provider-context";
export function useLabelPlacement(props: {
labelPlacement?: "inside" | "outside" | "outside-left";
label?: React.ReactNode;
}) {
const globalContext = useProviderContext();
const globalLabelPlacement = globalContext?.labelPlacement;
return useMemo(() => {
const labelPlacement = props.labelPlacement ?? globalLabelPlacement ?? "inside";
if (labelPlacement === "inside" && !props.label) {
return "outside";
}
return labelPlacement;
}, [props.labelPlacement, globalLabelPlacement, props.label]);
}

View File

@ -33,3 +33,5 @@ export type {ProviderContextProps} from "./provider-context";
export {HeroUIProvider} from "./provider";
export {ProviderContext, useProviderContext} from "./provider-context";
export {useLabelPlacement} from "./hooks";

View File

@ -11,6 +11,13 @@ export type ProviderContextProps = {
* @default false
*/
disableAnimation?: boolean;
/**
* Position where the label should appear.
*
* @default undefined
*/
labelPlacement?: "inside" | "outside" | "outside-left" | undefined;
/**
/**
* Whether to disable the ripple effect in the whole application.
* If `disableAnimation` is set to `true`, this prop will be ignored.

View File

@ -58,6 +58,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
reducedMotion = "never",
validationBehavior,
locale = "en-US",
labelPlacement,
// if minDate / maxDate are not specified in `defaultDates`
// then they will be set in `use-date-input.ts` or `use-calendar-base.ts`
defaultDates,
@ -85,6 +86,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
disableAnimation,
disableRipple,
validationBehavior,
labelPlacement,
};
}, [
createCalendar,
@ -93,6 +95,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
disableAnimation,
disableRipple,
validationBehavior,
labelPlacement,
]);
return (

View File

@ -217,7 +217,6 @@ const dateInput = tv({
color: "default",
size: "md",
fullWidth: true,
labelPlacement: "inside",
isDisabled: false,
},
compoundVariants: [

View File

@ -258,7 +258,6 @@ const input = tv({
color: "default",
size: "md",
fullWidth: true,
labelPlacement: "inside",
isDisabled: false,
isMultiline: false,
},

View File

@ -219,7 +219,6 @@ const select = tv({
variant: "flat",
color: "default",
size: "md",
labelPlacement: "inside",
fullWidth: true,
isDisabled: false,
isMultiline: false,

View File

@ -7,13 +7,13 @@ import "./style.css";
import {withStrictModeSwitcher} from "./addons/react-strict-mode";
const decorators: Preview["decorators"] = [
(Story, {globals: {locale, disableAnimation}}) => {
(Story, {globals: {locale, disableAnimation, labelPlacement}}) => {
const direction =
// @ts-ignore
locale && new Intl.Locale(locale)?.textInfo?.direction === "rtl" ? "rtl" : undefined;
return (
<HeroUIProvider locale={locale} disableAnimation={disableAnimation}>
<HeroUIProvider locale={locale} disableAnimation={disableAnimation} labelPlacement={labelPlacement}>
<div className="bg-dark" lang={locale} dir={direction}>
<Story />
</div>
@ -127,6 +127,18 @@ const globalTypes: Preview["globalTypes"] = {
],
},
},
labelPlacement: {
name: "Label Placement",
description: "Position of label.",
toolbar: {
icon: "component",
items: [
{value: "inside", title: "Inside"},
{value: "outside", title: "Outside"},
{value: "outside-left", title: "Outside Left"},
],
},
}
};
const preview: Preview = {