Skip to content
5 changes: 3 additions & 2 deletions packages/components/calendar/src/calendar-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function CalendarPicker(props: CalendarPickerProps) {
yearsListRef,
classNames,
getItemRef,
isHeaderExpanded,
onPickerItemPressed,
onPickerItemKeyDown,
} = useCalendarPicker(props);
Expand Down Expand Up @@ -82,7 +83,7 @@ export function CalendarPicker(props: CalendarPickerProps) {
ref={(node) => getItemRef(node, month.value, "months")}
className={slots?.pickerItem({class: classNames?.pickerItem})}
data-value={month.value}
tabIndex={state.focusedDate?.month === month.value ? 0 : -1}
tabIndex={!isHeaderExpanded || state.focusedDate?.month !== month.value ? -1 : 0}
onKeyDown={(e) => onPickerItemKeyDown(e, month.value, "months")}
onPress={(e) => onPickerItemPressed(e, "months")}
>
Expand All @@ -103,7 +104,7 @@ export function CalendarPicker(props: CalendarPickerProps) {
ref={(node) => getItemRef(node, year.value, "years")}
className={slots?.pickerItem({class: classNames?.pickerItem})}
data-value={year.value}
tabIndex={state.focusedDate?.year === year.value ? 0 : -1}
tabIndex={!isHeaderExpanded || state.focusedDate?.year !== year.value ? -1 : 0}
onKeyDown={(e) => onPickerItemKeyDown(e, year.value, "years")}
onPress={(e) => onPickerItemPressed(e, "years")}
>
Expand Down
1 change: 1 addition & 0 deletions packages/components/calendar/src/use-calendar-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export function useCalendarPicker(props: CalendarPickerProps) {
monthsListRef,
yearsListRef,
getItemRef,
isHeaderExpanded,
onPickerItemPressed,
onPickerItemKeyDown,
};
Expand Down
45 changes: 45 additions & 0 deletions packages/components/date-input/src/date-input-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type {InputHTMLAttributes} from "react";
import type {GroupDOMAttributes} from "@react-types/shared";
import type {DateInputReturnType, DateInputSlots, SlotsToClasses} from "@nextui-org/theme";
import type {DateFieldState} from "@react-stately/datepicker";
import type {HTMLNextUIProps} from "@nextui-org/system";

import {forwardRef} from "react";

import {DateInputSegment} from "./date-input-segment";

type NextUIBaseProps = Omit<HTMLNextUIProps<"div">, keyof GroupDOMAttributes | "onChange">;

export interface DateInputFieldProps extends NextUIBaseProps, GroupDOMAttributes {
/** State for the date field. */
state: DateFieldState;
/** Props for the hidden input element for HTML form submission. */
inputProps: InputHTMLAttributes<HTMLInputElement>;
/** DateInput classes slots. */
slots: DateInputReturnType;
/** DateInput classes. */
classNames?: SlotsToClasses<DateInputSlots>;
}

export const DateInputField = forwardRef<"div", DateInputFieldProps>((props, ref) => {
const {as, state, slots, inputProps, classNames, ...otherProps} = props;

const Component = as || "div";

return (
<Component {...otherProps} ref={ref}>
{state.segments.map((segment, i) => (
<DateInputSegment
key={i}
classNames={classNames}
segment={segment}
slots={slots}
state={state}
/>
))}
<input {...inputProps} />
</Component>
);
});

DateInputField.displayName = "NextUI.DateInputField";
111 changes: 111 additions & 0 deletions packages/components/date-input/src/date-input-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type {HTMLAttributes, ReactElement, ReactNode} from "react";
import type {GroupDOMAttributes} from "@react-types/shared";

import {useMemo} from "react";
import {forwardRef} from "@nextui-org/system";
import {dataAttr} from "@nextui-org/shared-utils";

// TODO: Use HelpTextProps from "@react-types/shared"; once we upgrade react-aria packages to the latest version.
export interface ValidationResult {
/** Whether the input value is invalid. */
isInvalid: boolean;
/** The current error messages for the input if it is invalid, otherwise an empty array. */
validationErrors: string[];
/** The native validation details for the input. */
validationDetails: ValidityState;
}

export interface DateInputGroupProps extends ValidationResult {
children?: ReactElement | ReactElement[];
shouldLabelBeOutside?: boolean;
label?: ReactNode;
startContent?: React.ReactNode;
endContent?: React.ReactNode;
groupProps?: GroupDOMAttributes;
wrapperProps?: HTMLAttributes<HTMLElement>; // <- inner wrapper props
helperWrapperProps?: HTMLAttributes<HTMLElement>;
labelProps?: HTMLAttributes<HTMLElement>;
descriptionProps?: HTMLAttributes<HTMLElement>;
errorMessageProps?: HTMLAttributes<HTMLElement>;
/** A description for the field. Provides a hint such as specific requirements for what to choose. */
description?: ReactNode;
/** An error message for the field. */
errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
}

export const DateInputGroup = forwardRef<"div", DateInputGroupProps>((props, ref) => {
const {
as,
label,
children,
description,
startContent,
endContent,
errorMessage: errorMessageProp,
shouldLabelBeOutside,
isInvalid,
groupProps,
labelProps,
wrapperProps,
helperWrapperProps,
errorMessageProps,
descriptionProps,
validationErrors,
validationDetails,
...otherProps
} = props;

const Component = as || "div";

const labelContent = label ? <span {...labelProps}>{label}</span> : null;

const errorMessage =
typeof errorMessageProp === "function"
? errorMessageProp({
isInvalid,
validationErrors,
validationDetails,
})
: errorMessageProp || validationErrors?.join(" ");

const hasHelper = !!description || !!errorMessage;

const helperWrapper = useMemo(() => {
if (!hasHelper) return null;

return (
<div {...helperWrapperProps}>
{errorMessage ? (
<div {...errorMessageProps}>{errorMessage}</div>
) : description ? (
<div {...descriptionProps}>{description}</div>
) : null}
</div>
);
}, [
hasHelper,
errorMessage,
description,
helperWrapperProps,
errorMessageProps,
descriptionProps,
]);

return (
<Component {...otherProps} ref={ref} data-has-helper={dataAttr(hasHelper)}>
{shouldLabelBeOutside ? labelContent : null}
<div {...groupProps}>
{!shouldLabelBeOutside ? labelContent : null}
<div {...wrapperProps}>
{startContent}
{children}
{endContent}
</div>
{shouldLabelBeOutside ? helperWrapper : null}
</div>
{!shouldLabelBeOutside ? helperWrapper : null}
</Component>
);
});

DateInputGroup.displayName = "NextUI.DateInputGroup";
98 changes: 16 additions & 82 deletions packages/components/date-input/src/date-input.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,31 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import type {DateValue} from "@internationalized/date";
import type {ForwardedRef, ReactElement, Ref} from "react";

import {useMemo} from "react";
import {forwardRef} from "@nextui-org/system";

import {UseDateInputProps, useDateInput} from "./use-date-input";
import {DateInputSegment} from "./date-input-segment";
import {DateInputGroup} from "./date-input-group";
import {DateInputField} from "./date-input-field";

export interface Props<T extends DateValue> extends UseDateInputProps<T> {}

function DateInput<T extends DateValue>(props: Props<T>, ref: ForwardedRef<HTMLDivElement>) {
const {
Component,
state,
label,
slots,
hasHelper,
errorMessage,
description,
startContent,
endContent,
shouldLabelBeOutside,
classNames,
getBaseProps,
getInputProps,
getFieldProps,
getLabelProps,
getInputWrapperProps,
getInnerWrapperProps,
getDescriptionProps,
getHelperWrapperProps,
getErrorMessageProps,
} = useDateInput<T>({
...props,
ref,
});

const labelContent = label ? <span {...getLabelProps()}>{label}</span> : null;

const helperWrapper = useMemo(() => {
if (!hasHelper) return null;

return (
<div {...getHelperWrapperProps()}>
{errorMessage ? (
<div {...getErrorMessageProps()}>{errorMessage}</div>
) : description ? (
<div {...getDescriptionProps()}>{description}</div>
) : null}
</div>
);
}, [
hasHelper,
errorMessage,
description,
getHelperWrapperProps,
getErrorMessageProps,
getDescriptionProps,
]);

const inputContent = useMemo(
() => (
<div {...getFieldProps()}>
{state.segments.map((segment, i) => (
<DateInputSegment
key={i}
classNames={classNames}
segment={segment}
slots={slots}
state={state}
/>
))}
<input {...getInputProps()} />
</div>
),
[state, slots, classNames?.segment, getFieldProps],
);
const {state, slots, classNames, getBaseGroupProps, getInputProps, getFieldProps} =
useDateInput<T>({
...props,
ref,
});

return (
<Component {...getBaseProps()}>
{shouldLabelBeOutside ? labelContent : null}
<div {...getInputWrapperProps()}>
{!shouldLabelBeOutside ? labelContent : null}
<div {...getInnerWrapperProps()}>
{startContent}
{inputContent}
{endContent}
</div>
{shouldLabelBeOutside ? helperWrapper : null}
</div>
{!shouldLabelBeOutside ? helperWrapper : null}
</Component>
<DateInputGroup {...getBaseGroupProps()}>
<DateInputField
classNames={classNames}
inputProps={getInputProps()}
slots={slots}
state={state}
{...getFieldProps()}
/>
</DateInputGroup>
);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/components/date-input/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ export type {DateInputProps} from "./date-input";
export type {TimeInputProps} from "./time-input";
export type {DateValue as DateInputValue} from "@react-types/datepicker";
export type {TimeValue as TimeInputValue} from "@react-types/datepicker";
export type {DateInputGroupProps} from "./date-input-group";
export type {DateInputFieldProps} from "./date-input-field";

// export hooks
export {useDateInput} from "./use-date-input";
export {useTimeInput} from "./use-time-input";

// export components
export {DateInputGroup} from "./date-input-group";
export {DateInputField} from "./date-input-field";
export {DateInputSegment} from "./date-input-segment";
export {DateInput, TimeInput};
Loading