Skip to content
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

feat: adding the support for labelPlacement globally #4346

Open
wants to merge 3 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
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
16 changes: 5 additions & 11 deletions packages/components/date-input/src/use-date-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down
16 changes: 5 additions & 11 deletions packages/components/date-input/src/use-time-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down
14 changes: 12 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 @@ -10,7 +10,12 @@ 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 {HTMLNextUIProps, mapPropsVariants, useProviderContext} from "@nextui-org/system";
import {
HTMLNextUIProps,
mapPropsVariants,
useLabelPlacement,
useProviderContext,
} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {useDOMRef} from "@nextui-org/react-utils";
import {useLocalizedStringFormatter} from "@react-aria/i18n";
Expand Down Expand Up @@ -234,10 +239,15 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
"data-invalid": dataAttr(originalProps?.isInvalid),
} as DateInputProps;

const labelPlacement = useLabelPlacement({
labelPlacement: originalProps.labelPlacement,
label,
});

const timeInputProps = {
...userTimeInputProps,
size: "sm",
labelPlacement: "outside-left",
macci001 marked this conversation as resolved.
Show resolved Hide resolved
labelPlacement,
label: userTimeInputProps?.label || stringFormatter.format("time"),
placeholderValue: timePlaceholder,
hourCycle: props.hourCycle,
Expand Down
22 changes: 8 additions & 14 deletions packages/components/date-picker/src/use-date-range-picker.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -395,7 +389,7 @@ export function useDateRangePicker<T extends DateValue>({
const getDateInputGroupProps = () => {
return {
as,
label: originalProps.label,
label,
description,
endContent,
errorMessage,
Expand Down Expand Up @@ -423,7 +417,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
12 changes: 5 additions & 7 deletions packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mapPropsVariants,
PropGetter,
SharedSelection,
useLabelPlacement,
useProviderContext,
} from "@nextui-org/system";
import {select} from "@nextui-org/theme";
Expand Down Expand Up @@ -343,13 +344,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 =
Expand Down
1 change: 1 addition & 0 deletions packages/core/system/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {useLabelPlacement} from "./use-label-placement";
21 changes: 21 additions & 0 deletions packages/core/system/src/hooks/use-label-placement.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
2 changes: 2 additions & 0 deletions packages/core/system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export type {ProviderContextProps} from "./provider-context";

export {NextUIProvider} from "./provider";
export {ProviderContext, useProviderContext} from "./provider-context";

export {useLabelPlacement} from "./hooks";
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