mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
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:
parent
f9c2be4509
commit
7804de0d89
10
.changeset/chilly-dancers-switch.md
Normal file
10
.changeset/chilly-dancers-switch.md
Normal 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)
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:*",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 =
|
||||
|
||||
1
packages/core/system/src/hooks/index.ts
Normal file
1
packages/core/system/src/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {useLabelPlacement} from "./use-label-placement";
|
||||
21
packages/core/system/src/hooks/use-label-placement.ts
Normal file
21
packages/core/system/src/hooks/use-label-placement.ts
Normal 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]);
|
||||
}
|
||||
@ -33,3 +33,5 @@ export type {ProviderContextProps} from "./provider-context";
|
||||
|
||||
export {HeroUIProvider} from "./provider";
|
||||
export {ProviderContext, useProviderContext} from "./provider-context";
|
||||
|
||||
export {useLabelPlacement} from "./hooks";
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -217,7 +217,6 @@ const dateInput = tv({
|
||||
color: "default",
|
||||
size: "md",
|
||||
fullWidth: true,
|
||||
labelPlacement: "inside",
|
||||
isDisabled: false,
|
||||
},
|
||||
compoundVariants: [
|
||||
|
||||
@ -258,7 +258,6 @@ const input = tv({
|
||||
color: "default",
|
||||
size: "md",
|
||||
fullWidth: true,
|
||||
labelPlacement: "inside",
|
||||
isDisabled: false,
|
||||
isMultiline: false,
|
||||
},
|
||||
|
||||
@ -219,7 +219,6 @@ const select = tv({
|
||||
variant: "flat",
|
||||
color: "default",
|
||||
size: "md",
|
||||
labelPlacement: "inside",
|
||||
fullWidth: true,
|
||||
isDisabled: false,
|
||||
isMultiline: false,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user