diff --git a/.changeset/chilly-dancers-switch.md b/.changeset/chilly-dancers-switch.md new file mode 100644 index 0000000000..97fe3da37d --- /dev/null +++ b/.changeset/chilly-dancers-switch.md @@ -0,0 +1,10 @@ +--- +"@nextui-org/date-picker": patch +"@nextui-org/date-input": patch +"@nextui-org/select": patch +"@nextui-org/input": patch +"@nextui-org/system": patch +"@nextui-org/theme": patch +--- + +Adding support for global labelPlacement prop.(ENG-1694) diff --git a/apps/docs/content/docs/api-references/nextui-provider.mdx b/apps/docs/content/docs/api-references/nextui-provider.mdx index c64fd50061..60c223f467 100644 --- a/apps/docs/content/docs/api-references/nextui-provider.mdx +++ b/apps/docs/content/docs/api-references/nextui-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 b93da09a42..37eebc1157 100644 --- a/packages/components/date-input/package.json +++ b/packages/components/date-input/package.json @@ -34,8 +34,8 @@ "postpack": "clean-package restore" }, "peerDependencies": { - "@nextui-org/system": ">=2.4.0", - "@nextui-org/theme": ">=2.4.0", + "@nextui-org/system": ">=2.4.3", + "@nextui-org/theme": ">=2.4.5", "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 e34065333e..28bddbd40a 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 "@nextui-org/system"; +import {PropGetter, useLabelPlacement, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef} from "@nextui-org/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 7b540a626b..3dcd6c1351 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 "@nextui-org/system"; +import {PropGetter, useLabelPlacement, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef} from "@nextui-org/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 10dbe6f3a1..e26f503b0b 100644 --- a/packages/components/date-picker/package.json +++ b/packages/components/date-picker/package.json @@ -34,8 +34,8 @@ "postpack": "clean-package restore" }, "peerDependencies": { - "@nextui-org/system": ">=2.4.0", - "@nextui-org/theme": ">=2.4.0", + "@nextui-org/system": ">=2.4.3", + "@nextui-org/theme": ">=2.4.5", "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 2dabebb84d..e0b55c347e 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 "@nextui-org/theme"; import type {TimeInputProps} from "@nextui-org/date-input"; import type {ButtonProps} from "@nextui-org/button"; import type {RangeCalendarProps} from "@nextui-org/calendar"; @@ -14,7 +13,7 @@ import type {DateInputGroupProps} from "@nextui-org/date-input"; import type {DateRangePickerSlots, SlotsToClasses} from "@nextui-org/theme"; import type {DateInputProps} from "@nextui-org/date-input"; -import {useProviderContext} from "@nextui-org/system"; +import {useLabelPlacement, useProviderContext} from "@nextui-org/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 5ad2742260..49805572a9 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", - "@nextui-org/theme": ">=2.4.0", - "@nextui-org/system": ">=2.4.0" + "@nextui-org/system": ">=2.4.3", + "@nextui-org/theme": ">=2.4.5" }, "dependencies": { "@nextui-org/form": "workspace:*", diff --git a/packages/components/input/src/use-input.ts b/packages/components/input/src/use-input.ts index d8cae3c3d7..af3d22a4fb 100644 --- a/packages/components/input/src/use-input.ts +++ b/packages/components/input/src/use-input.ts @@ -231,7 +231,15 @@ export function useInput=2.4.0", - "@nextui-org/theme": ">=2.4.0", + "@nextui-org/system": ">=2.4.3", + "@nextui-org/theme": ">=2.4.5", "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 f4c01d92f6..cf4e9e8f2d 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 "@nextui-org/system"; import {select} from "@nextui-org/theme"; @@ -343,13 +344,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 316a00756c..25433c7e5d 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 {NextUIProvider} 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 c33e2266a4..b6e863cc39 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 45e85e775f..c6cc1a3be3 100644 --- a/packages/core/system/src/provider.tsx +++ b/packages/core/system/src/provider.tsx @@ -58,6 +58,7 @@ export const NextUIProvider: 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 NextUIProvider: React.FC = ({ disableAnimation, disableRipple, validationBehavior, + labelPlacement, }; }, [ createCalendar, @@ -93,6 +95,7 @@ export const NextUIProvider: 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 4334d08d48..70773a85ba 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 5cb6ef14a5..5e5d40fd67 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 67c216126e..4d0c4e835b 100644 --- a/packages/core/theme/src/components/select.ts +++ b/packages/core/theme/src/components/select.ts @@ -209,7 +209,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 c519a071c4..c294cc65af 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 = {