diff --git a/.changeset/chilly-dancers-switch.md b/.changeset/chilly-dancers-switch.md new file mode 100644 index 0000000000..468180d55d --- /dev/null +++ b/.changeset/chilly-dancers-switch.md @@ -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) diff --git a/apps/docs/content/docs/api-references/heroui-provider.mdx b/apps/docs/content/docs/api-references/heroui-provider.mdx index a175fa443f..50eb7f1c41 100644 --- a/apps/docs/content/docs/api-references/heroui-provider.mdx +++ b/apps/docs/content/docs/api-references/heroui-provider.mdx @@ -142,6 +142,15 @@ interface AppProviderProps { +`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` + + + `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. diff --git a/packages/components/date-input/package.json b/packages/components/date-input/package.json index 59adec5d9e..25f08384bb 100644 --- a/packages/components/date-input/package.json +++ b/packages/components/date-input/package.json @@ -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" }, diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index 03a182bb04..455b81529c 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -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(originalProps: UseDateInputPro const isInvalid = isInvalidProp || ariaIsInvalid; - const labelPlacement = useMemo(() => { - 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"; diff --git a/packages/components/date-input/src/use-time-input.ts b/packages/components/date-input/src/use-time-input.ts index d3ac3c59f8..f54ef02912 100644 --- a/packages/components/date-input/src/use-time-input.ts +++ b/packages/components/date-input/src/use-time-input.ts @@ -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(originalProps: UseTimeInputPro const baseStyles = clsx(classNames?.base, className); - const labelPlacement = useMemo(() => { - 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"; diff --git a/packages/components/date-picker/package.json b/packages/components/date-picker/package.json index 3ed9bfcf10..99418cb11b 100644 --- a/packages/components/date-picker/package.json +++ b/packages/components/date-picker/package.json @@ -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" diff --git a/packages/components/date-picker/src/use-date-range-picker.ts b/packages/components/date-picker/src/use-date-range-picker.ts index 9fc462a39f..b44df7fb22 100644 --- a/packages/components/date-picker/src/use-date-range-picker.ts +++ b/packages/components/date-picker/src/use-date-range-picker.ts @@ -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 = Props & AriaDateRa export function useDateRangePicker({ as, + label, isInvalid: isInvalidProp, description, startContent, @@ -143,16 +143,10 @@ export function useDateRangePicker({ const showTimeField = !!timeGranularity; - const labelPlacement = useMemo(() => { - 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({ const getDateInputGroupProps = () => { return { as, - label: originalProps.label, + label, description, endContent, errorMessage, @@ -423,7 +417,7 @@ export function useDateRangePicker({ return { state, - label: originalProps.label, + label, slots, classNames, startContent, diff --git a/packages/components/input/package.json b/packages/components/input/package.json index 9f7163350f..d6de7ae00d 100644 --- a/packages/components/input/package.json +++ b/packages/components/input/package.json @@ -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:*", diff --git a/packages/components/input/src/use-input.ts b/packages/components/input/src/use-input.ts index dd14ad7834..0eb3409e36 100644 --- a/packages/components/input/src/use-input.ts +++ b/packages/components/input/src/use-input.ts @@ -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(() => { - 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" diff --git a/packages/components/select/package.json b/packages/components/select/package.json index ef9c8f87fd..bfaf6d5921 100644 --- a/packages/components/select/package.json +++ b/packages/components/select/package.json @@ -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" diff --git a/packages/components/select/src/use-select.ts b/packages/components/select/src/use-select.ts index 332def0aa3..b0cb636568 100644 --- a/packages/components/select/src/use-select.ts +++ b/packages/components/select/src/use-select.ts @@ -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(originalProps: UseSelectProps) { const {focusProps, isFocused, isFocusVisible} = useFocusRing(); const {isHovered, hoverProps} = useHover({isDisabled: originalProps.isDisabled}); - const labelPlacement = useMemo(() => { - 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 = diff --git a/packages/core/system/src/hooks/index.ts b/packages/core/system/src/hooks/index.ts new file mode 100644 index 0000000000..752604dc49 --- /dev/null +++ b/packages/core/system/src/hooks/index.ts @@ -0,0 +1 @@ +export {useLabelPlacement} from "./use-label-placement"; diff --git a/packages/core/system/src/hooks/use-label-placement.ts b/packages/core/system/src/hooks/use-label-placement.ts new file mode 100644 index 0000000000..33c4bfdd14 --- /dev/null +++ b/packages/core/system/src/hooks/use-label-placement.ts @@ -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]); +} diff --git a/packages/core/system/src/index.ts b/packages/core/system/src/index.ts index 1d7d8865e4..fefa8326f0 100644 --- a/packages/core/system/src/index.ts +++ b/packages/core/system/src/index.ts @@ -33,3 +33,5 @@ export type {ProviderContextProps} from "./provider-context"; export {HeroUIProvider} from "./provider"; export {ProviderContext, useProviderContext} from "./provider-context"; + +export {useLabelPlacement} from "./hooks"; diff --git a/packages/core/system/src/provider-context.ts b/packages/core/system/src/provider-context.ts index e48da34436..45e8e2e1fd 100644 --- a/packages/core/system/src/provider-context.ts +++ b/packages/core/system/src/provider-context.ts @@ -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. diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx index 6fb385c6da..ac70161702 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -58,6 +58,7 @@ export const HeroUIProvider: React.FC = ({ 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 = ({ disableAnimation, disableRipple, validationBehavior, + labelPlacement, }; }, [ createCalendar, @@ -93,6 +95,7 @@ export const HeroUIProvider: React.FC = ({ disableAnimation, disableRipple, validationBehavior, + labelPlacement, ]); return ( diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index a1923a1276..3f43bbc1ab 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -217,7 +217,6 @@ const dateInput = tv({ color: "default", size: "md", fullWidth: true, - labelPlacement: "inside", isDisabled: false, }, compoundVariants: [ diff --git a/packages/core/theme/src/components/input.ts b/packages/core/theme/src/components/input.ts index 53466ea08f..17f5f1fec7 100644 --- a/packages/core/theme/src/components/input.ts +++ b/packages/core/theme/src/components/input.ts @@ -258,7 +258,6 @@ const input = tv({ color: "default", size: "md", fullWidth: true, - labelPlacement: "inside", isDisabled: false, isMultiline: false, }, diff --git a/packages/core/theme/src/components/select.ts b/packages/core/theme/src/components/select.ts index 32d20ff1dd..7fa752ca35 100644 --- a/packages/core/theme/src/components/select.ts +++ b/packages/core/theme/src/components/select.ts @@ -219,7 +219,6 @@ const select = tv({ variant: "flat", color: "default", size: "md", - labelPlacement: "inside", fullWidth: true, isDisabled: false, isMultiline: false, diff --git a/packages/storybook/.storybook/preview.tsx b/packages/storybook/.storybook/preview.tsx index 0123755dfa..05c7128caf 100644 --- a/packages/storybook/.storybook/preview.tsx +++ b/packages/storybook/.storybook/preview.tsx @@ -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 ( - +
@@ -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 = {