Skip to content

feat: global labelPlacement prop #4346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/chilly-dancers-switch.md
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions apps/docs/content/docs/api-references/nextui-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 6 additions & 6 deletions packages/components/date-input/src/use-date-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,15 @@ 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
) {
const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, props.label]);
return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";

Expand Down
12 changes: 6 additions & 6 deletions packages/components/date-input/src/use-time-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,15 @@ 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
) {
const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, props.label]);
return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";

Expand Down
15 changes: 13 additions & 2 deletions packages/components/date-picker/src/use-date-picker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {ValueBase} from "@react-types/shared";

import {dataAttr} from "@nextui-org/shared-utils";
import {dateInput, DatePickerVariantProps} from "@nextui-org/theme";
import {useCallback} from "react";
import {useCallback, useMemo} from "react";
import {HTMLNextUIProps, mapPropsVariants, useProviderContext} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {useDOMRef} from "@nextui-org/react-utils";
Expand Down Expand Up @@ -234,10 +234,21 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
"data-invalid": dataAttr(originalProps?.isInvalid),
} as DateInputProps;

const labelPlacement = useMemo<TimeInputProps["labelPlacement"]>(() => {
const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const timeInputProps = {
...userTimeInputProps,
size: "sm",
labelPlacement: "outside-left",
labelPlacement,
label: userTimeInputProps?.label || stringFormatter.format("time"),
placeholderValue: timePlaceholder,
hourCycle: props.hourCycle,
Expand Down
17 changes: 9 additions & 8 deletions packages/components/date-picker/src/use-date-range-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type UseDateRangePickerProps<T extends DateValue> = Props<T> & AriaDateRa

export function useDateRangePicker<T extends DateValue>({
as,
label,
isInvalid: isInvalidProp,
description,
startContent,
Expand Down Expand Up @@ -144,15 +145,15 @@ export function useDateRangePicker<T extends DateValue>({
const showTimeField = !!timeGranularity;

const labelPlacement = useMemo<DateInputVariantProps["labelPlacement"]>(() => {
if (
(!originalProps.labelPlacement || originalProps.labelPlacement === "inside") &&
!originalProps.label
) {
const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, originalProps.label]);
return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const shouldLabelBeOutside = labelPlacement === "outside" || labelPlacement === "outside-left";

Expand Down Expand Up @@ -395,7 +396,7 @@ export function useDateRangePicker<T extends DateValue>({
const getDateInputGroupProps = () => {
return {
as,
label: originalProps.label,
label,
description,
endContent,
errorMessage,
Expand Down Expand Up @@ -423,7 +424,7 @@ export function useDateRangePicker<T extends DateValue>({

return {
state,
label: originalProps.label,
label,
slots,
classNames,
startContent,
Expand Down
20 changes: 16 additions & 4 deletions packages/components/input/src/use-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,15 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
if (isFileTypeInput) {
// if `labelPlacement` is not defined, choose `outside` instead
// since the default value `inside` is not supported in file input
if (!originalProps.labelPlacement) return "outside";
if (!originalProps.labelPlacement) {
if (globalContext?.labelPlacement === "inside") {
warn(
"Input with file type doesn't support inside label. Hence, labelPlacement from NextUI Provider is not applicable. Converting to outside ...",
);
}

return "outside";
}

// throw a warning if `labelPlacement` is `inside`
// and change it to `outside`
Expand All @@ -241,12 +249,16 @@ export function useInput<T extends HTMLInputElement | HTMLTextAreaElement = HTML
return "outside";
}
}
if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) {

const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, label]);
return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const errorMessage =
typeof props.errorMessage === "function"
Expand Down
9 changes: 6 additions & 3 deletions packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,15 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
const {isHovered, hoverProps} = useHover({isDisabled: originalProps.isDisabled});

const labelPlacement = useMemo<SelectVariantProps["labelPlacement"]>(() => {
if ((!originalProps.labelPlacement || originalProps.labelPlacement === "inside") && !label) {
const labelPlacement =
originalProps.labelPlacement ?? globalContext?.labelPlacement ?? "inside";

if (labelPlacement === "inside" && !label) {
return "outside";
}

return originalProps.labelPlacement ?? "inside";
}, [originalProps.labelPlacement, label]);
return labelPlacement;
}, [originalProps.labelPlacement, globalContext?.labelPlacement, label]);

const hasPlaceholder = !!placeholder;
const shouldLabelBeOutside =
Expand Down
7 changes: 7 additions & 0 deletions packages/core/system/src/provider-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions packages/core/system/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const NextUIProvider: React.FC<NextUIProviderProps> = ({
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,
Expand Down Expand Up @@ -85,6 +86,7 @@ export const NextUIProvider: React.FC<NextUIProviderProps> = ({
disableAnimation,
disableRipple,
validationBehavior,
labelPlacement,
};
}, [
createCalendar,
Expand All @@ -93,6 +95,7 @@ export const NextUIProvider: React.FC<NextUIProviderProps> = ({
disableAnimation,
disableRipple,
validationBehavior,
labelPlacement,
]);

return (
Expand Down
1 change: 0 additions & 1 deletion packages/core/theme/src/components/date-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ const dateInput = tv({
color: "default",
size: "md",
fullWidth: true,
labelPlacement: "inside",
isDisabled: false,
},
compoundVariants: [
Expand Down
1 change: 0 additions & 1 deletion packages/core/theme/src/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ const input = tv({
color: "default",
size: "md",
fullWidth: true,
labelPlacement: "inside",
isDisabled: false,
isMultiline: false,
},
Expand Down
1 change: 0 additions & 1 deletion packages/core/theme/src/components/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ const select = tv({
variant: "flat",
color: "default",
size: "md",
labelPlacement: "inside",
fullWidth: true,
isDisabled: false,
isMultiline: false,
Expand Down
16 changes: 14 additions & 2 deletions packages/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<NextUIProvider locale={locale} disableAnimation={disableAnimation}>
<NextUIProvider locale={locale} disableAnimation={disableAnimation} labelPlacement={labelPlacement}>
<div className="bg-dark" lang={locale} dir={direction}>
<Story />
</div>
Expand Down Expand Up @@ -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 = {
Expand Down
Loading