From 15caeaa8297be8a22b733ec69ee5ae9ad7ccb61d Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Fri, 5 Apr 2024 18:00:13 -0300 Subject: [PATCH 01/12] feat(date-picker): first iteration --- .../components/calendar/src/calendar-base.tsx | 4 +- packages/components/calendar/src/calendar.tsx | 2 +- .../calendar/src/range-calendar.tsx | 2 +- .../calendar/src/use-calendar-base.ts | 1 + .../components/date-input/src/date-input.tsx | 2 +- .../date-input/src/use-date-input.ts | 5 -- packages/components/date-picker/README.md | 24 +++++ .../__tests__/date-picker.test.tsx | 19 ++++ packages/components/date-picker/package.json | 68 ++++++++++++++ .../date-picker/src/date-picker.tsx | 80 +++++++++++++++++ packages/components/date-picker/src/index.ts | 10 +++ .../date-picker/src/use-date-picker.ts | 89 +++++++++++++++++++ .../stories/date-picker.stories.tsx | 49 ++++++++++ packages/components/date-picker/tsconfig.json | 10 +++ .../components/date-picker/tsup.config.ts | 8 ++ .../popover/src/free-solo-popover.tsx | 12 +-- .../popover/src/popover-content.tsx | 15 +--- .../components/popover/src/use-popover.ts | 28 ++++-- .../core/theme/src/components/calendar.ts | 3 +- .../core/theme/src/components/date-input.ts | 4 +- .../core/theme/src/components/date-picker.ts | 26 ++++++ packages/core/theme/src/components/index.ts | 1 + .../framer-utils/src/transition-utils.ts | 4 +- pnpm-lock.yaml | 87 +++++++++++++++--- 24 files changed, 504 insertions(+), 49 deletions(-) create mode 100644 packages/components/date-picker/README.md create mode 100644 packages/components/date-picker/__tests__/date-picker.test.tsx create mode 100644 packages/components/date-picker/package.json create mode 100644 packages/components/date-picker/src/date-picker.tsx create mode 100644 packages/components/date-picker/src/index.ts create mode 100644 packages/components/date-picker/src/use-date-picker.ts create mode 100644 packages/components/date-picker/stories/date-picker.stories.tsx create mode 100644 packages/components/date-picker/tsconfig.json create mode 100644 packages/components/date-picker/tsup.config.ts create mode 100644 packages/core/theme/src/components/date-picker.ts diff --git a/packages/components/calendar/src/calendar-base.tsx b/packages/components/calendar/src/calendar-base.tsx index cb2e7fd1bb..ada247795b 100644 --- a/packages/components/calendar/src/calendar-base.tsx +++ b/packages/components/calendar/src/calendar-base.tsx @@ -113,7 +113,7 @@ export function CalendarBase(props: CalendarBaseProps) { } const calendarContent = ( - <> +
{calendars}
- +
); return ( diff --git a/packages/components/calendar/src/calendar.tsx b/packages/components/calendar/src/calendar.tsx index ab6e3ab13b..6b1eee3f76 100644 --- a/packages/components/calendar/src/calendar.tsx +++ b/packages/components/calendar/src/calendar.tsx @@ -10,7 +10,7 @@ import {CalendarBase} from "./calendar-base"; interface Props extends Omit, "isHeaderWrapperExpanded"> {} function Calendar(props: Props, ref: ForwardedRef) { - const {context, getBaseCalendarProps} = useCalendar({...props, ref}); + const {context, getBaseCalendarProps} = useCalendar({...props, ref}); return ( diff --git a/packages/components/calendar/src/range-calendar.tsx b/packages/components/calendar/src/range-calendar.tsx index 59d2c6d499..a245f012dc 100644 --- a/packages/components/calendar/src/range-calendar.tsx +++ b/packages/components/calendar/src/range-calendar.tsx @@ -17,7 +17,7 @@ interface Props > {} function RangeCalendar(props: Props, ref: ForwardedRef) { - const {context, getBaseCalendarProps} = useRangeCalendar({...props, ref}); + const {context, getBaseCalendarProps} = useRangeCalendar({...props, ref}); return ( diff --git a/packages/components/calendar/src/use-calendar-base.ts b/packages/components/calendar/src/use-calendar-base.ts index 46f7cb8194..8b008aaf21 100644 --- a/packages/components/calendar/src/use-calendar-base.ts +++ b/packages/components/calendar/src/use-calendar-base.ts @@ -126,6 +126,7 @@ interface Props extends NextUIBaseProps { * prevButton:"prev-button-classes", * header:"header-classes", * title:"title-classes", + * content:"content-classes", * gridWrapper:"grid-wrapper-classes", * grid:"grid-classes", * gridHeader:"grid-header-classes", diff --git a/packages/components/date-input/src/date-input.tsx b/packages/components/date-input/src/date-input.tsx index f1da3e9d66..43a26181a2 100644 --- a/packages/components/date-input/src/date-input.tsx +++ b/packages/components/date-input/src/date-input.tsx @@ -31,7 +31,7 @@ function DateInput(props: Props, ref: ForwardedRef({ ...props, ref, }); diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index daa988044c..d6077f7b3e 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -5,7 +5,6 @@ import type {DateValue, Calendar} from "@internationalized/date"; import type {ReactRef} from "@nextui-org/react-utils"; import {PropGetter, useProviderContext} from "@nextui-org/system"; -import {CalendarDate} from "@internationalized/date"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef, filterDOMProps} from "@nextui-org/react-utils"; import {useLocale} from "@react-aria/i18n"; @@ -115,8 +114,6 @@ export function useDateInput(originalProps: UseDateInputPro validationState, validationBehavior = "native", shouldForceLeadingZeros = true, - minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), - maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), createCalendar: createCalendarProp = providerContext?.createCalendar ?? null, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, errorMessage: errorMessageProp, @@ -133,8 +130,6 @@ export function useDateInput(originalProps: UseDateInputPro const state = useDateFieldState({ ...originalProps, locale, - minValue, - maxValue, isInvalid: isInvalidProp, shouldForceLeadingZeros, createCalendar: diff --git a/packages/components/date-picker/README.md b/packages/components/date-picker/README.md new file mode 100644 index 0000000000..0c5a4a1c6f --- /dev/null +++ b/packages/components/date-picker/README.md @@ -0,0 +1,24 @@ +# @nextui-org/date-picker + +A Quick description of the component + +> This is an internal utility, not intended for public usage. + +## Installation + +```sh +yarn add @nextui-org/date-picker +# or +npm i @nextui-org/date-picker +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md) +for details. + +## License + +This project is licensed under the terms of the +[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE). diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx new file mode 100644 index 0000000000..ac87c20e4d --- /dev/null +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import {render} from "@testing-library/react"; + +import {DatePicker} from "../src"; + +describe("DatePicker", () => { + it("should render correctly", () => { + const wrapper = render(); + + expect(() => wrapper.unmount()).not.toThrow(); + }); + + it("ref should be forwarded", () => { + const ref = React.createRef(); + + render(); + expect(ref.current).not.toBeNull(); + }); +}); diff --git a/packages/components/date-picker/package.json b/packages/components/date-picker/package.json new file mode 100644 index 0000000000..74d0843778 --- /dev/null +++ b/packages/components/date-picker/package.json @@ -0,0 +1,68 @@ +{ + "name": "@nextui-org/date-picker", + "version": "2.0.0", + "description": "A date picker combines a DateInput and a Calendar popover to allow users to enter or select a date and time value.", + "keywords": [ + "date-picker" + ], + "author": "Junior Garcia ", + "homepage": "https://nextui.org", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nextui-org/nextui.git", + "directory": "packages/components/date-picker" + }, + "bugs": { + "url": "https://github.com/nextui-org/nextui/issues" + }, + "scripts": { + "build": "tsup src --dts", + "build:fast": "tsup src", + "dev": "pnpm build:fast --watch", + "clean": "rimraf dist .turbo", + "typecheck": "tsc --noEmit", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.0.0", + "react": ">=18", + "react-dom": ">=18" + }, + "dependencies": { + "@nextui-org/react-utils": "workspace:*", + "@nextui-org/shared-utils": "workspace:*", + "@nextui-org/popover": "workspace:*", + "@nextui-org/calendar": "workspace:*", + "@nextui-org/button": "workspace:*", + "@nextui-org/date-input": "workspace:*", + "@nextui-org/shared-icons": "workspace:*", + "@react-stately/overlays": "^3.6.3", + "@react-stately/utils": "^3.8.0", + "@internationalized/date": "^3.5.2", + "@react-aria/datepicker": "^3.9.3", + "@react-aria/i18n": "^3.8.4", + "@react-stately/datepicker": "^3.9.2", + "@react-types/datepicker": "^3.7.2", + "@react-types/shared": "3.21.0", + "@react-aria/utils": "^3.21.1" + }, + "devDependencies": { + "@nextui-org/system": "workspace:*", + "@nextui-org/theme": "workspace:*", + "clean-package": "2.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "clean-package": "../../../clean-package.config.json" +} diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx new file mode 100644 index 0000000000..c2a7155115 --- /dev/null +++ b/packages/components/date-picker/src/date-picker.tsx @@ -0,0 +1,80 @@ +import type {DateValue} from "@internationalized/date"; +import type {ForwardedRef, ReactElement, Ref} from "react"; + +import {forwardRef} from "@nextui-org/system"; +import {Button} from "@nextui-org/button"; +import {DateInput} from "@nextui-org/date-input"; +import {FreeSoloPopover} from "@nextui-org/popover"; +import {Calendar} from "@nextui-org/calendar"; +import {AnimatePresence} from "framer-motion"; +import {CalendarBoldIcon} from "@nextui-org/shared-icons"; + +import {UseDatePickerProps, useDatePicker} from "./use-date-picker"; + +export interface Props extends UseDatePickerProps {} + +function DatePicker(props: Props, ref: ForwardedRef) { + const { + Component, + domRef, + state, + slots, + targetRef, + labelProps, + groupProps, + dialogProps, + fieldProps, + calendarProps, + buttonProps, + disableAnimation, + } = useDatePicker({...props, ref}); + + const popoverContent = state.isOpen ? ( + + + + ) : null; + + return ( + +
+
{props.label}
+
+ + + + } + /> +
+ {disableAnimation ? popoverContent : {popoverContent}} +
+
+ ); +} + +DatePicker.displayName = "NextUI.DatePicker"; + +export type DatePickerProps = Props & {ref?: Ref}; + +// forwardRef doesn't support generic parameters, so cast the result to the correct type +export default forwardRef(DatePicker) as ( + props: DatePickerProps, +) => ReactElement; diff --git a/packages/components/date-picker/src/index.ts b/packages/components/date-picker/src/index.ts new file mode 100644 index 0000000000..7f848bfa74 --- /dev/null +++ b/packages/components/date-picker/src/index.ts @@ -0,0 +1,10 @@ +import DatePicker from "./date-picker"; + +// export types +export type {DatePickerProps} from "./date-picker"; + +// export hooks +export {useDatePicker} from "./use-date-picker"; + +// export component +export {DatePicker}; diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts new file mode 100644 index 0000000000..f3024f5fb4 --- /dev/null +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -0,0 +1,89 @@ +import type {DatePickerVariantProps} from "@nextui-org/theme"; +import type {DateValue} from "@internationalized/date"; +import type {AriaDatePickerProps} from "@react-types/datepicker"; +import type {DatePickerState} from "@react-stately/datepicker"; + +import {useDatePickerState} from "@react-stately/datepicker"; +import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; +import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; +import {datePicker} from "@nextui-org/theme"; +import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; +import {objectToDeps} from "@nextui-org/shared-utils"; +import {useMemo, useRef} from "react"; + +type NextUIBaseProps = Omit< + HTMLNextUIProps<"div">, + keyof AriaDatePickerProps | "onChange" +>; + +interface Props extends NextUIBaseProps { + /** + * Ref to the DOM node. + */ + ref?: ReactRef; + /** + * Whether to disable all animations in the date picker. Including the DateInput, Button, Calendar, and Popover. + * + * @default false + */ + disableAnimation?: boolean; +} + +export type UseDatePickerProps = Props & + DatePickerVariantProps & + AriaDatePickerProps; + +export function useDatePicker(originalProps: UseDatePickerProps) { + const [props, variantProps] = mapPropsVariants(originalProps, datePicker.variantKeys); + + const {ref, as, disableAnimation = false, className} = props; + + const Component = as || "div"; + + const domRef = useDOMRef(ref); + let targetRef = useRef(null); + + let state: DatePickerState = useDatePickerState({ + ...originalProps, + shouldCloseOnSelect: () => !state.hasTime, + }); + + let { + groupProps, + labelProps, + fieldProps, + descriptionProps, + errorMessageProps, + buttonProps, + dialogProps, + calendarProps, + } = useAriaDatePicker(originalProps, state, targetRef); + + const slots = useMemo( + () => + datePicker({ + ...variantProps, + className, + }), + [objectToDeps(variantProps), className], + ); + + return { + state, + Component, + domRef, + targetRef, + groupProps, + labelProps, + fieldProps, + disableAnimation, + descriptionProps, + errorMessageProps, + buttonProps, + dialogProps, + calendarProps, + slots, + }; +} + +export type UseDatePickerReturn = ReturnType; diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx new file mode 100644 index 0000000000..8760e257dc --- /dev/null +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import {Meta} from "@storybook/react"; +import {datePicker} from "@nextui-org/theme"; + +import {DatePicker, DatePickerProps} from "../src"; + +export default { + title: "Components/DatePicker", + component: DatePicker, + argTypes: { + color: { + control: {type: "select"}, + options: ["default", "primary", "secondary", "success", "warning", "danger"], + }, + radius: { + control: {type: "select"}, + options: ["none", "sm", "md", "lg", "full"], + }, + size: { + control: {type: "select"}, + options: ["sm", "md", "lg"], + }, + isDisabled: { + control: { + type: "boolean", + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} as Meta; + +const defaultProps = { + ...datePicker.defaultVariants, +}; + +const Template = (args: DatePickerProps) => ; + +export const Default = { + render: Template, + args: { + ...defaultProps, + }, +}; diff --git a/packages/components/date-picker/tsconfig.json b/packages/components/date-picker/tsconfig.json new file mode 100644 index 0000000000..5d012f6e61 --- /dev/null +++ b/packages/components/date-picker/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "tailwind-variants": ["../../../node_modules/tailwind-variants"] + }, + }, + "include": ["src", "index.ts"] +} diff --git a/packages/components/date-picker/tsup.config.ts b/packages/components/date-picker/tsup.config.ts new file mode 100644 index 0000000000..3e2bcff6cc --- /dev/null +++ b/packages/components/date-picker/tsup.config.ts @@ -0,0 +1,8 @@ +import {defineConfig} from "tsup"; + +export default defineConfig({ + clean: true, + target: "es2019", + format: ["cjs", "esm"], + banner: {js: '"use client";'}, +}); diff --git a/packages/components/popover/src/free-solo-popover.tsx b/packages/components/popover/src/free-solo-popover.tsx index 1cf7fa9f58..fe86a6e112 100644 --- a/packages/components/popover/src/free-solo-popover.tsx +++ b/packages/components/popover/src/free-solo-popover.tsx @@ -17,8 +17,8 @@ import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils"; import {usePopover, UsePopoverProps, UsePopoverReturn} from "./use-popover"; -export interface FreeSoloPopoverProps extends UsePopoverProps { - children: React.ReactNode; +export interface FreeSoloPopoverProps extends Omit { + children: React.ReactNode | ((titleProps: React.DOMAttributes) => React.ReactNode); } type FreeSoloPopoverWrapperProps = { @@ -57,13 +57,13 @@ const FreeSoloPopoverWrapper = ({ ); }; -const FreeSoloPopover = forwardRef<"div", FreeSoloPopoverProps>((props, ref) => { +const FreeSoloPopover = forwardRef<"div", FreeSoloPopoverProps>(({children, ...props}, ref) => { const { Component, state, - children, placement, backdrop, + titleProps, portalContainer, disableAnimation, motionProps, @@ -111,7 +111,9 @@ const FreeSoloPopover = forwardRef<"div", FreeSoloPopoverProps>((props, ref) => {...getDialogProps()} > {!isNonModal && } -
{children}
+
+ {typeof children === "function" ? children(titleProps) : children} +
diff --git a/packages/components/popover/src/popover-content.tsx b/packages/components/popover/src/popover-content.tsx index accb4ec898..2e944a8182 100644 --- a/packages/components/popover/src/popover-content.tsx +++ b/packages/components/popover/src/popover-content.tsx @@ -1,13 +1,11 @@ import type {AriaDialogProps} from "@react-aria/dialog"; import type {HTMLMotionProps} from "framer-motion"; -import {DOMAttributes, ReactNode, useMemo, useRef, useCallback, ReactElement} from "react"; +import {DOMAttributes, ReactNode, useMemo, useCallback, ReactElement} from "react"; import {forwardRef} from "@nextui-org/system"; import {DismissButton} from "@react-aria/overlays"; import {TRANSITION_VARIANTS} from "@nextui-org/framer-utils"; import {m, domAnimation, LazyMotion} from "framer-motion"; -import {useDialog} from "@react-aria/dialog"; -import {mergeProps} from "@react-aria/utils"; import {HTMLNextUIProps} from "@nextui-org/system"; import {RemoveScroll} from "react-remove-scroll"; import {getTransformOrigins} from "@nextui-org/aria-utils"; @@ -27,8 +25,9 @@ const PopoverContent = forwardRef<"div", PopoverContentProps>((props, _) => { Component: OverlayComponent, isOpen, placement, - motionProps, backdrop, + motionProps, + titleProps, disableAnimation, shouldBlockScroll, getPopoverProps, @@ -41,16 +40,10 @@ const PopoverContent = forwardRef<"div", PopoverContentProps>((props, _) => { const Component = as || OverlayComponent || "div"; - const dialogRef = useRef(null); - const {dialogProps, titleProps} = useDialog({}, dialogRef); - - // Not needed in the popover context, the popover role comes from getPopoverProps - delete dialogProps.role; - const content = ( <> {!isNonModal && } - +
{typeof children === "function" ? children(titleProps) : children}
diff --git a/packages/components/popover/src/use-popover.ts b/packages/components/popover/src/use-popover.ts index 5ae2b90d77..0f704f310c 100644 --- a/packages/components/popover/src/use-popover.ts +++ b/packages/components/popover/src/use-popover.ts @@ -1,5 +1,6 @@ import type {PopoverVariantProps, SlotsToClasses, PopoverSlots} from "@nextui-org/theme"; import type {HTMLMotionProps} from "framer-motion"; +import type {PressEvent} from "@react-types/shared"; import {RefObject, Ref, useEffect} from "react"; import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; @@ -13,7 +14,7 @@ import {popover} from "@nextui-org/theme"; import {mergeProps, mergeRefs} from "@react-aria/utils"; import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {useMemo, useCallback, useRef} from "react"; -import {PressEvent} from "@react-types/shared"; +import {AriaDialogProps, useDialog} from "@react-aria/dialog"; import {useReactAriaPopover, ReactAriaPopoverProps} from "./use-aria-popover"; @@ -35,6 +36,12 @@ export interface Props extends HTMLNextUIProps<"div"> { * @default true */ shouldBlockScroll?: boolean; + /** + * Custom props to be passed to the dialog container. + * + * @default {} + */ + dialogProps?: AriaDialogProps; /** * Type of overlay that is opened by the trigger. */ @@ -79,8 +86,8 @@ export function usePopover(originalProps: UsePopoverProps) { const { as, - children, ref, + children, state: stateProp, triggerRef: triggerRefProp, scrollRef, @@ -95,6 +102,7 @@ export function usePopover(originalProps: UsePopoverProps) { shouldCloseOnBlur, portalContainer, updatePositionDeps, + dialogProps: dialogPropsProp, placement: placementProp = "top", triggerType = "dialog", showArrow = false, @@ -116,7 +124,7 @@ export function usePopover(originalProps: UsePopoverProps) { const domTriggerRef = useRef(null); const wasTriggerPressedRef = useRef(false); - + const dialogRef = useRef(null); const triggerRef = triggerRefProp || domTriggerRef; const disableAnimation = originalProps.disableAnimation ?? false; @@ -163,6 +171,11 @@ export function usePopover(originalProps: UsePopoverProps) { const {isFocusVisible, isFocused, focusProps} = useFocusRing(); + const {dialogProps, titleProps} = useDialog({}, dialogRef); + + // Not needed in the popover context, the popover role comes from getPopoverProps + delete dialogProps.role; + const slots = useMemo( () => popover({ @@ -180,13 +193,14 @@ export function usePopover(originalProps: UsePopoverProps) { }); const getDialogProps: PropGetter = (props = {}) => ({ + ref: dialogRef, "data-slot": "base", "data-open": dataAttr(state.isOpen), "data-focus": dataAttr(isFocused), "data-arrow": dataAttr(showArrow), "data-focus-visible": dataAttr(isFocusVisible), "data-placement": getArrowPlacement(ariaPlacement, placementProp), - ...mergeProps(focusProps, props), + ...mergeProps(focusProps, dialogProps, dialogPropsProp, props), className: slots.base({class: clsx(baseStyles)}), style: { // this prevent the dialog to have a default outline @@ -206,7 +220,10 @@ export function usePopover(originalProps: UsePopoverProps) { ); const placement = useMemo( - () => (getShouldUseAxisPlacement(ariaPlacement, placementProp) ? ariaPlacement : placementProp), + () => + getShouldUseAxisPlacement(ariaPlacement, placementProp) + ? ariaPlacement || placementProp + : placementProp, [ariaPlacement, placementProp], ); @@ -291,6 +308,7 @@ export function usePopover(originalProps: UsePopoverProps) { triggerRef, placement, isNonModal, + titleProps, popoverRef: domRef, portalContainer, isOpen: state.isOpen, diff --git a/packages/core/theme/src/components/calendar.ts b/packages/core/theme/src/components/calendar.ts index a637f5f47c..51d53cb0f9 100644 --- a/packages/core/theme/src/components/calendar.ts +++ b/packages/core/theme/src/components/calendar.ts @@ -21,13 +21,14 @@ const calendar = tv({ ], header: "flex w-full items-center justify-center gap-2 z-10", title: "text-default-500 text-small font-medium", + content: "w-fit", gridWrapper: "flex max-w-full overflow-auto pb-2 h-auto relative", grid: "w-full border-collapse z-0", gridHeader: "bg-content1 shadow-[0px_20px_20px_0px_rgb(0_0_0/0.05)]", gridHeaderRow: "px-4 pb-2 flex justify-center text-default-400", gridHeaderCell: "flex w-8 justify-center items-center font-medium text-small", gridBody: "", - gridBodyRow: "flex mt-2 justify-center items-center first:mt-2", + gridBodyRow: "flex justify-center items-center first:mt-2", cell: "py-0.5 px-0", cellButton: [ "w-8 h-8 flex items-center text-foreground justify-center rounded-full", diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index 70cc148f0e..48bdd5a2de 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -159,12 +159,12 @@ const dateInput = tv({ }, labelPlacement: { outside: { - base: "flex flex-col pb-[calc(theme(fontSize.tiny)_+8px)] gap-y-1.5", + base: "flex flex-col data-[has-helper=true]:pb-[calc(theme(fontSize.tiny)_+8px)] gap-y-1.5", label: "w-full text-foreground", helperWrapper: "absolute top-[calc(100%_+_2px)] left-0 rtl:right-0", }, "outside-left": { - base: "flex-row items-center pb-[calc(theme(fontSize.tiny)_+_8px)] gap-x-2 flex-nowrap", + base: "flex-row items-center data-[has-helper=true]:pb-[calc(theme(fontSize.tiny)_+_8px)] gap-x-2 flex-nowrap", label: "relative text-foreground", inputWrapper: "relative flex-1", helperWrapper: "absolute top-[calc(100%_+_2px)] left-0 rtl:right-0", diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts new file mode 100644 index 0000000000..b0ee8f1d32 --- /dev/null +++ b/packages/core/theme/src/components/date-picker.ts @@ -0,0 +1,26 @@ +import type {VariantProps} from "tailwind-variants"; + +import {tv} from "../utils/tv"; + +/** + * DatePicker wrapper **Tailwind Variants** component + * + * @example + */ +const datePicker = tv({ + slots: { + base: "", + button: "-mr-2", + popoverContent: "p-0 w-full", + calendar: "w-64 shadow-none", + }, + variants: {}, + defaultVariants: {}, + compoundVariants: [], +}); + +export type DatePickerReturnType = ReturnType; +export type DatePickerVariantProps = VariantProps; +export type DatePickerSlots = keyof ReturnType; + +export {datePicker}; diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts index 7e471e6aca..7776b24c52 100644 --- a/packages/core/theme/src/components/index.ts +++ b/packages/core/theme/src/components/index.ts @@ -37,3 +37,4 @@ export * from "./breadcrumbs"; export * from "./autocomplete"; export * from "./calendar"; export * from "./date-input"; +export * from "./date-picker"; diff --git a/packages/utilities/framer-utils/src/transition-utils.ts b/packages/utilities/framer-utils/src/transition-utils.ts index 2287eaac5b..bab9937548 100644 --- a/packages/utilities/framer-utils/src/transition-utils.ts +++ b/packages/utilities/framer-utils/src/transition-utils.ts @@ -87,11 +87,11 @@ export const TRANSITION_VARIANTS: Variants = { }, exit: { opacity: 0, - transform: "scale(0.7)", + transform: "scale(0.96)", transition: { type: "easeOut", bounce: 0, - duration: 0.18, + duration: 0.15, }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38e67f7525..692d6ac1a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1313,6 +1313,73 @@ importers: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + packages/components/date-picker: + dependencies: + '@internationalized/date': + specifier: ^3.5.2 + version: 3.5.2 + '@nextui-org/button': + specifier: workspace:* + version: link:../button + '@nextui-org/calendar': + specifier: workspace:* + version: link:../calendar + '@nextui-org/date-input': + specifier: workspace:* + version: link:../date-input + '@nextui-org/popover': + specifier: workspace:* + version: link:../popover + '@nextui-org/react-utils': + specifier: workspace:* + version: link:../../utilities/react-utils + '@nextui-org/shared-icons': + specifier: workspace:* + version: link:../../utilities/shared-icons + '@nextui-org/shared-utils': + specifier: workspace:* + version: link:../../utilities/shared-utils + '@react-aria/datepicker': + specifier: ^3.9.3 + version: 3.9.3(react-dom@18.2.0)(react@18.2.0) + '@react-aria/i18n': + specifier: ^3.8.4 + version: 3.10.2(react@18.2.0) + '@react-aria/utils': + specifier: ^3.21.1 + version: 3.23.2(react@18.2.0) + '@react-stately/datepicker': + specifier: ^3.9.2 + version: 3.9.2(react@18.2.0) + '@react-stately/overlays': + specifier: ^3.6.3 + version: 3.6.5(react@18.2.0) + '@react-stately/utils': + specifier: ^3.8.0 + version: 3.9.1(react@18.2.0) + '@react-types/datepicker': + specifier: ^3.7.2 + version: 3.7.2(react@18.2.0) + '@react-types/shared': + specifier: 3.21.0 + version: 3.21.0(react@18.2.0) + devDependencies: + '@nextui-org/system': + specifier: workspace:* + version: link:../../core/system + '@nextui-org/theme': + specifier: workspace:* + version: link:../../core/theme + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + packages/components/divider: dependencies: '@nextui-org/react-rsc-utils': @@ -6570,16 +6637,10 @@ packages: '@swc/helpers': 0.5.3 dev: false - /@internationalized/string@3.1.1: - resolution: {integrity: sha512-fvSr6YRoVPgONiVIUhgCmIAlifMVCeej/snPZVzbzRPxGpHl3o1GRe+d/qh92D8KhgOciruDUH8I5mjdfdjzfA==} - dependencies: - '@swc/helpers': 0.5.3 - /@internationalized/string@3.2.1: resolution: {integrity: sha512-vWQOvRIauvFMzOO+h7QrdsJmtN1AXAFVcaLWP9AseRN2o7iHceZ6bIXhBD4teZl8i91A3gxKnWBlGgjCwU6MFQ==} dependencies: '@swc/helpers': 0.5.3 - dev: false /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -9524,7 +9585,7 @@ packages: '@react-aria/focus': 3.14.3(react@18.2.0) '@react-aria/overlays': 3.18.1(react-dom@18.2.0)(react@18.2.0) '@react-aria/utils': 3.23.2(react@18.2.0) - '@react-stately/overlays': 3.6.3(react@18.2.0) + '@react-stately/overlays': 3.6.5(react@18.2.0) '@react-types/dialog': 3.5.6(react@18.2.0) '@react-types/shared': 3.21.0(react@18.2.0) '@swc/helpers': 0.5.3 @@ -9619,7 +9680,7 @@ packages: '@internationalized/date': 3.5.2 '@internationalized/message': 3.1.1 '@internationalized/number': 3.3.0 - '@internationalized/string': 3.1.1 + '@internationalized/string': 3.2.1 '@react-aria/ssr': 3.8.0(react@18.2.0) '@react-aria/utils': 3.21.1(react@18.2.0) '@react-types/shared': 3.21.0(react@18.2.0) @@ -9750,12 +9811,12 @@ packages: '@react-aria/focus': 3.14.3(react@18.2.0) '@react-aria/i18n': 3.10.2(react@18.2.0) '@react-aria/interactions': 3.21.1(react@18.2.0) - '@react-aria/ssr': 3.8.0(react@18.2.0) + '@react-aria/ssr': 3.9.2(react@18.2.0) '@react-aria/utils': 3.23.2(react@18.2.0) '@react-aria/visually-hidden': 3.8.6(react@18.2.0) - '@react-stately/overlays': 3.6.3(react@18.2.0) + '@react-stately/overlays': 3.6.5(react@18.2.0) '@react-types/button': 3.9.2(react@18.2.0) - '@react-types/overlays': 3.8.3(react@18.2.0) + '@react-types/overlays': 3.8.5(react@18.2.0) '@react-types/shared': 3.21.0(react@18.2.0) '@swc/helpers': 0.5.3 react: 18.2.0 @@ -10200,7 +10261,7 @@ packages: peerDependencies: react: ^18.2.0 dependencies: - '@react-stately/overlays': 3.6.3(react@18.2.0) + '@react-stately/overlays': 3.6.5(react@18.2.0) '@react-stately/utils': 3.9.1(react@18.2.0) '@react-types/menu': 3.9.5(react@18.2.0) '@react-types/shared': 3.21.0(react@18.2.0) @@ -10331,7 +10392,7 @@ packages: peerDependencies: react: ^18.2.0 dependencies: - '@react-stately/overlays': 3.6.3(react@18.2.0) + '@react-stately/overlays': 3.6.5(react@18.2.0) '@react-stately/utils': 3.9.1(react@18.2.0) '@react-types/tooltip': 3.4.5(react@18.2.0) '@swc/helpers': 0.5.3 From a7248f9ba6557926cb12f7b067a45d4c4261fb35 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Fri, 5 Apr 2024 18:01:46 -0300 Subject: [PATCH 02/12] chore(date-picker): update date-picker README.md with improved description --- packages/components/date-picker/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/date-picker/README.md b/packages/components/date-picker/README.md index 0c5a4a1c6f..1ad9b0f3fd 100644 --- a/packages/components/date-picker/README.md +++ b/packages/components/date-picker/README.md @@ -1,8 +1,6 @@ # @nextui-org/date-picker -A Quick description of the component - -> This is an internal utility, not intended for public usage. +A date picker combines a DateInput and a Calendar popover to allow users to enter or select a date and time value. ## Installation From ca12ada122a9979ceef77e349820d813ed67b6b0 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Fri, 5 Apr 2024 20:42:20 -0300 Subject: [PATCH 03/12] feat(date-picker): code organized, integration done --- .../components/date-input/src/date-input.tsx | 11 +- .../date-input/src/use-date-input.ts | 11 + .../date-picker/src/date-picker.tsx | 63 ++---- .../date-picker/src/use-date-picker.ts | 193 +++++++++++++++--- .../stories/date-picker.stories.tsx | 4 +- .../core/theme/src/components/date-input.ts | 5 +- .../core/theme/src/components/date-picker.ts | 5 +- 7 files changed, 218 insertions(+), 74 deletions(-) diff --git a/packages/components/date-input/src/date-input.tsx b/packages/components/date-input/src/date-input.tsx index 43a26181a2..75ad7e497d 100644 --- a/packages/components/date-input/src/date-input.tsx +++ b/packages/components/date-input/src/date-input.tsx @@ -28,6 +28,7 @@ function DateInput(props: Props, ref: ForwardedRef(props: Props, ref: ForwardedRef{label} : null; + const labelContent = label ? {label} : null; const helperWrapper = useMemo(() => { if (!hasHelper) return null; @@ -81,10 +82,12 @@ function DateInput(props: Props, ref: ForwardedRef {shouldLabelBeOutside ? labelContent : null}
- {startContent} {!shouldLabelBeOutside ? labelContent : null} - {inputContent} - {endContent} +
+ {startContent} + {inputContent} + {endContent} +
{shouldLabelBeOutside ? helperWrapper : null}
{!shouldLabelBeOutside ? helperWrapper : null} diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index d6077f7b3e..8f1869a735 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -255,6 +255,16 @@ export function useDateInput(originalProps: UseDateInputPro }; }; + const getInnerWrapperProps: PropGetter = (props) => { + return { + ...props, + "data-slot": "inner-wrapper", + className: slots.innerWrapper({ + class: classNames?.innerWrapper, + }), + }; + }; + const getHelperWrapperProps: PropGetter = (props) => { return { ...props, @@ -302,6 +312,7 @@ export function useDateInput(originalProps: UseDateInputPro getFieldProps, getInputProps, getInputWrapperProps, + getInnerWrapperProps, getHelperWrapperProps, getErrorMessageProps, getDescriptionProps, diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index c2a7155115..3f5f5521e1 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -1,6 +1,7 @@ import type {DateValue} from "@internationalized/date"; import type {ForwardedRef, ReactElement, Ref} from "react"; +import {cloneElement, isValidElement} from "react"; import {forwardRef} from "@nextui-org/system"; import {Button} from "@nextui-org/button"; import {DateInput} from "@nextui-org/date-input"; @@ -16,56 +17,36 @@ export interface Props extends UseDatePickerProps {} function DatePicker(props: Props, ref: ForwardedRef) { const { Component, - domRef, state, - slots, - targetRef, - labelProps, - groupProps, - dialogProps, - fieldProps, - calendarProps, - buttonProps, + selectorIcon, disableAnimation, + getBaseProps, + getDateInputProps, + getPopoverProps, + getSelectorButtonProps, + getSelectorIconProps, + getCalendarProps, } = useDatePicker({...props, ref}); + const selectorContent = isValidElement(selectorIcon) ? ( + cloneElement(selectorIcon, getSelectorIconProps()) + ) : ( + + ); + const popoverContent = state.isOpen ? ( - - + + ) : null; return ( - -
-
{props.label}
-
- - - - } - /> -
- {disableAnimation ? popoverContent : {popoverContent}} -
+ + {selectorContent}} + /> + {disableAnimation ? popoverContent : {popoverContent}} ); } diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index f3024f5fb4..2962126f34 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -1,14 +1,21 @@ -import type {DatePickerVariantProps} from "@nextui-org/theme"; +import type {DatePickerVariantProps, DatePickerSlots, SlotsToClasses} from "@nextui-org/theme"; import type {DateValue} from "@internationalized/date"; import type {AriaDatePickerProps} from "@react-types/datepicker"; +import type {DateInputProps} from "@nextui-org/date-input"; +import type {DOMAttributes, PropGetter} from "@nextui-org/system"; import type {DatePickerState} from "@react-stately/datepicker"; +import type {ButtonProps} from "@nextui-org/button"; +import type {CalendarProps} from "@nextui-org/calendar"; +import type {PopoverProps} from "@nextui-org/popover"; +import type {ReactNode} from "react"; import {useDatePickerState} from "@react-stately/datepicker"; import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {datePicker} from "@nextui-org/theme"; +import {chain, mergeProps} from "@react-aria/utils"; import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; -import {objectToDeps} from "@nextui-org/shared-utils"; +import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; import {useMemo, useRef} from "react"; type NextUIBaseProps = Omit< @@ -21,22 +28,66 @@ interface Props extends NextUIBaseProps { * Ref to the DOM node. */ ref?: ReactRef; + /** + * The icon to toggle the date picker popover. Usually a calendar icon. + */ + selectorIcon?: ReactNode; + /** + * Props to be passed to the popover component. + * + * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 5 } + */ + popoverProps?: Partial; + /** + * Props to be passed to the selector button component. + * @default { size: "sm", variant: "light", radius: "full", isIconOnly: true } + */ + selectorButtonProps?: Partial; + /** + * Props to be passed to the calendar component. + * @default {} + */ + calendarProps?: Partial; /** * Whether to disable all animations in the date picker. Including the DateInput, Button, Calendar, and Popover. * * @default false */ disableAnimation?: boolean; + /** + * Classname or List of classes to change the classNames of the element. + * if `className` is passed, it will be added to the base slot. + * + * @example + * ```ts + * + * ``` + */ + classNames?: SlotsToClasses; } export type UseDatePickerProps = Props & DatePickerVariantProps & - AriaDatePickerProps; + DateInputProps; export function useDatePicker(originalProps: UseDatePickerProps) { const [props, variantProps] = mapPropsVariants(originalProps, datePicker.variantKeys); - const {ref, as, disableAnimation = false, className} = props; + const { + ref, + as, + selectorIcon, + popoverProps = {}, + selectorButtonProps = {}, + calendarProps: userCalendarProps = {}, + disableAnimation = false, + className, + classNames, + ...otherProps + } = props; const Component = as || "div"; @@ -48,16 +99,11 @@ export function useDatePicker(originalProps: UseDatePickerP shouldCloseOnSelect: () => !state.hasTime, }); - let { - groupProps, - labelProps, - fieldProps, - descriptionProps, - errorMessageProps, - buttonProps, - dialogProps, - calendarProps, - } = useAriaDatePicker(originalProps, state, targetRef); + let {groupProps, fieldProps, buttonProps, dialogProps, calendarProps} = useAriaDatePicker( + originalProps, + state, + targetRef, + ); const slots = useMemo( () => @@ -68,21 +114,120 @@ export function useDatePicker(originalProps: UseDatePickerP [objectToDeps(variantProps), className], ); + const baseStyles = clsx(classNames?.base, className); + + const slotsProps: { + inputProps: DateInputProps; + popoverProps: UseDatePickerProps["popoverProps"]; + selectorButtonProps: ButtonProps; + calendarProps: CalendarProps; + } = { + inputProps: mergeProps( + { + ref: targetRef, + onClick: state.toggle, + fullWidth: true, + isClearable: false, + disableAnimation, + }, + otherProps, + ), + popoverProps: mergeProps( + { + offset: 5, + placement: "bottom", + triggerScaleOnOpen: false, + disableAnimation, + }, + popoverProps, + ), + selectorButtonProps: mergeProps( + { + isIconOnly: true, + radius: "full", + size: "sm", + variant: "light", + disableAnimation, + }, + selectorButtonProps, + ), + calendarProps: mergeProps( + { + disableAnimation, + }, + userCalendarProps, + ), + }; + + const getBaseProps: PropGetter = () => ({ + ...groupProps, + "data-invalid": dataAttr(originalProps?.isInvalid), + "data-open": dataAttr(state.isOpen), + className: slots.base({class: baseStyles}), + }); + + const getDateInputProps = () => { + return { + ...mergeProps(fieldProps, slotsProps.inputProps), + onClick: chain(slotsProps.inputProps.onClick, otherProps.onClick), + } as unknown as DateInputProps; + }; + + const getPopoverProps = (props: DOMAttributes = {}) => { + return { + ...props, + state, + dialogProps, + triggerRef: targetRef, + classNames: { + content: slots.popoverContent({ + class: clsx( + classNames?.popoverContent, + slotsProps.popoverProps?.classNames?.["content"], + props.className, + ), + }), + }, + } as unknown as PopoverProps; + }; + + const getCalendarProps = () => { + return { + ...calendarProps, + ...slotsProps.calendarProps, + "data-slot": "calendar", + className: slots.calendar({class: classNames?.calendar}), + } as unknown as CalendarProps; + }; + + const getSelectorButtonProps = () => { + return { + ...buttonProps, + ...slotsProps.selectorButtonProps, + "data-slot": "selector-button", + className: slots.selectorButton({class: classNames?.selectorButton}), + } as unknown as ButtonProps; + }; + + const getSelectorIconProps = () => { + return { + "data-slot": "selector-icon", + className: slots.selectorIcon({class: classNames?.selectorIcon}), + }; + }; + return { state, Component, domRef, - targetRef, - groupProps, - labelProps, - fieldProps, + selectorIcon, disableAnimation, - descriptionProps, - errorMessageProps, - buttonProps, - dialogProps, - calendarProps, - slots, + getBaseProps, + getDateInputProps, + getPopoverProps, + getSelectorButtonProps, + getCalendarProps, + getSelectorIconProps, }; } diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index 8760e257dc..f187628b4f 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -28,7 +28,7 @@ export default { }, decorators: [ (Story) => ( -
+
), @@ -36,6 +36,8 @@ export default { } as Meta; const defaultProps = { + label: "Birth Date", + className: "max-w-[256px]", ...datePicker.defaultVariants, }; diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index 48bdd5a2de..c7984a215f 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -22,9 +22,10 @@ const dateInput = tv({ "cursor-text tap-highlight-transparent shadow-sm ", ], input: "flex h-full gap-x-0.5 w-full font-normal", + innerWrapper: "flex items-center w-full gap-x-2 h-6", // this wraps the input and the start/end content segment: [ - "group -ml-0.5 px-0.5 py-0.5 box-content tabular-nums text-start", - "inline-block my-auto outline-none focus:shadow-sm rounded-md", + "group -ml-0.5 px-0.5 my-auto box-content tabular-nums text-start", + "inline-block outline-none focus:shadow-sm rounded-md", "text-foreground-500 data-[editable=true]:text-inherit", "data-[placeholder=true]:text-foreground-500", // isInvalid=true diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index b0ee8f1d32..9f4bc51cb8 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -9,8 +9,9 @@ import {tv} from "../utils/tv"; */ const datePicker = tv({ slots: { - base: "", - button: "-mr-2", + base: "w-full", + selectorButton: "-mr-2", + selectorIcon: "text-lg text-default-400 pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-64 shadow-none", }, From 71b529934d8154d6da0d8c4048063cd818eaa864 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 09:26:22 -0300 Subject: [PATCH 04/12] fix(date-picker): min and max value + styles --- .../date-input/src/use-date-input.ts | 7 +++- .../date-picker/src/use-date-picker.ts | 16 +++++++- .../stories/date-picker.stories.tsx | 27 ++++++++++++-- .../core/theme/src/components/date-input.ts | 37 ++++++++++++++----- .../core/theme/src/components/date-picker.ts | 4 +- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index 8f1869a735..214cb72ff5 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -4,10 +4,11 @@ import type {SupportedCalendars} from "@nextui-org/system"; import type {DateValue, Calendar} from "@internationalized/date"; import type {ReactRef} from "@nextui-org/react-utils"; +import {useLocale} from "@react-aria/i18n"; +import {CalendarDate} from "@internationalized/date"; import {PropGetter, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef, filterDOMProps} from "@nextui-org/react-utils"; -import {useLocale} from "@react-aria/i18n"; import {useDateField as useAriaDateField} from "@react-aria/datepicker"; import {useDateFieldState} from "@react-stately/datepicker"; import {createCalendar} from "@internationalized/date"; @@ -114,6 +115,8 @@ export function useDateInput(originalProps: UseDateInputPro validationState, validationBehavior = "native", shouldForceLeadingZeros = true, + minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), + maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), createCalendar: createCalendarProp = providerContext?.createCalendar ?? null, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, errorMessage: errorMessageProp, @@ -130,6 +133,8 @@ export function useDateInput(originalProps: UseDateInputPro const state = useDateFieldState({ ...originalProps, locale, + minValue, + maxValue, isInvalid: isInvalidProp, shouldForceLeadingZeros, createCalendar: diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index 2962126f34..0f8ad02f3a 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -2,13 +2,14 @@ import type {DatePickerVariantProps, DatePickerSlots, SlotsToClasses} from "@nex import type {DateValue} from "@internationalized/date"; import type {AriaDatePickerProps} from "@react-types/datepicker"; import type {DateInputProps} from "@nextui-org/date-input"; -import type {DOMAttributes, PropGetter} from "@nextui-org/system"; import type {DatePickerState} from "@react-stately/datepicker"; import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; import type {ReactNode} from "react"; +import {DOMAttributes, PropGetter, useProviderContext} from "@nextui-org/system"; +import {CalendarDate} from "@internationalized/date"; import {useDatePickerState} from "@react-stately/datepicker"; import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; @@ -76,6 +77,8 @@ export type UseDatePickerProps = Props & export function useDatePicker(originalProps: UseDatePickerProps) { const [props, variantProps] = mapPropsVariants(originalProps, datePicker.variantKeys); + const providerContext = useProviderContext(); + const { ref, as, @@ -83,6 +86,8 @@ export function useDatePicker(originalProps: UseDatePickerP popoverProps = {}, selectorButtonProps = {}, calendarProps: userCalendarProps = {}, + minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), + maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), disableAnimation = false, className, classNames, @@ -96,6 +101,8 @@ export function useDatePicker(originalProps: UseDatePickerP let state: DatePickerState = useDatePickerState({ ...originalProps, + minValue, + maxValue, shouldCloseOnSelect: () => !state.hasTime, }); @@ -125,7 +132,8 @@ export function useDatePicker(originalProps: UseDatePickerP inputProps: mergeProps( { ref: targetRef, - onClick: state.toggle, + minValue, + maxValue, fullWidth: true, isClearable: false, disableAnimation, @@ -153,6 +161,10 @@ export function useDatePicker(originalProps: UseDatePickerP ), calendarProps: mergeProps( { + color: + originalProps.color === "default" || !originalProps.color + ? "primary" + : originalProps.color, disableAnimation, }, userCalendarProps, diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index f187628b4f..2f610454c2 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import {Meta} from "@storybook/react"; -import {datePicker} from "@nextui-org/theme"; +import {datePicker, dateInput} from "@nextui-org/theme"; import {DatePicker, DatePickerProps} from "../src"; @@ -8,18 +8,36 @@ export default { title: "Components/DatePicker", component: DatePicker, argTypes: { + variant: { + control: { + type: "select", + }, + options: ["flat", "faded", "bordered", "underlined"], + }, color: { - control: {type: "select"}, + control: { + type: "select", + }, options: ["default", "primary", "secondary", "success", "warning", "danger"], }, radius: { - control: {type: "select"}, + control: { + type: "select", + }, options: ["none", "sm", "md", "lg", "full"], }, size: { - control: {type: "select"}, + control: { + type: "select", + }, options: ["sm", "md", "lg"], }, + labelPlacement: { + control: { + type: "select", + }, + options: ["inside", "outside", "outside-left"], + }, isDisabled: { control: { type: "boolean", @@ -38,6 +56,7 @@ export default { const defaultProps = { label: "Birth Date", className: "max-w-[256px]", + ...dateInput.defaultVariants, ...datePicker.defaultVariants, }; diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index c7984a215f..56f24b3f4d 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -19,15 +19,15 @@ const dateInput = tv({ ], inputWrapper: [ "relative px-3 gap-3 w-full inline-flex flex-row items-center", - "cursor-text tap-highlight-transparent shadow-sm ", + "cursor-text tap-highlight-transparent shadow-sm", ], input: "flex h-full gap-x-0.5 w-full font-normal", - innerWrapper: "flex items-center w-full gap-x-2 h-6", // this wraps the input and the start/end content + innerWrapper: "flex items-center text-default-400 w-full gap-x-2 h-6", // this wraps the input and the start/end content segment: [ "group -ml-0.5 px-0.5 my-auto box-content tabular-nums text-start", "inline-block outline-none focus:shadow-sm rounded-md", - "text-foreground-500 data-[editable=true]:text-inherit", - "data-[placeholder=true]:text-foreground-500", + "text-foreground-500 data-[editable=true]:text-foreground", + "data-[editable=true]:data-[placeholder=true]:text-foreground-500", // isInvalid=true "data-[invalid=true]:text-danger-300 data-[invalid=true]:data-[editable=true]:text-danger", "data-[invalid=true]:focus:bg-danger-400/50 dark:data-[invalid=true]:focus:bg-danger-400/20", @@ -222,8 +222,10 @@ const dateInput = tv({ variant: "flat", color: "primary", class: { + innerWrapper: "text-primary", inputWrapper: ["bg-primary-50", "hover:bg-primary-100", "focus-within:bg-primary-50"], - segment: "text-primary-300 data-[editable=true]:text-primary", + segment: + "text-primary-300 data-[editable=true]:data-[placeholder=true]:text-primary-300 data-[editable=true]:text-primary", label: "text-primary", }, }, @@ -231,8 +233,10 @@ const dateInput = tv({ variant: "flat", color: "secondary", class: { + innerWrapper: "text-secondary", inputWrapper: ["bg-secondary-50", "hover:bg-secondary-100", "focus-within:bg-secondary-50"], - segment: "text-secondary-300 data-[editable=true]:text-secondary", + segment: + "text-secondary-300 data-[editable=true]:data-[placeholder=true]:text-secondary-300 data-[editable=true]:text-secondary", label: "text-secondary", }, }, @@ -240,9 +244,10 @@ const dateInput = tv({ variant: "flat", color: "success", class: { + innerWrapper: "text-success-600 dark:text-success", inputWrapper: ["bg-success-50", "hover:bg-success-100", "focus-within:bg-success-50"], segment: - "text-success-300 data-[editable=true]:text-success-600 data-[editable=true]:focus:text-success-600", + "text-success-400 data-[editable=true]:data-[placeholder=true]:text-success-400 data-[editable=true]:text-success-600 data-[editable=true]:focus:text-success-600", label: "text-success-600 dark:text-success", }, }, @@ -250,9 +255,10 @@ const dateInput = tv({ variant: "flat", color: "warning", class: { + innerWrapper: "text-warning-600 dark:text-warning", inputWrapper: ["bg-warning-50", "hover:bg-warning-100", "focus-within:bg-warning-50"], segment: - "text-warning-300 data-[editable=true]:text-warning-600 data-[editable=true]:focus:text-warning-600", + "text-warning-400 data-[editable=true]:data-[placeholder=true]:text-warning-400 data-[editable=true]:text-warning-600 data-[editable=true]:focus:text-warning-600", label: "text-warning-600 dark:text-warning", }, }, @@ -260,8 +266,11 @@ const dateInput = tv({ variant: "flat", color: "danger", class: { + innerWrapper: "text-danger", inputWrapper: ["bg-danger-50", "hover:bg-danger-100", "focus-within:bg-danger-50"], - segment: "text-danger-300 data-[editable=true]:text-danger", + segment: + "text-danger-300 data-[editable=true]:data-[placeholder=true]:text-danger-300 data-[editable=true]:text-danger", + label: "text-danger", }, }, // bordered & color @@ -269,6 +278,7 @@ const dateInput = tv({ variant: ["bordered", "faded"], color: "primary", class: { + innerWrapper: "text-primary", inputWrapper: ["focus-within:border-primary", "focus-within:hover:border-primary"], label: "text-primary", }, @@ -277,6 +287,7 @@ const dateInput = tv({ variant: ["bordered", "faded"], color: "secondary", class: { + innerWrapper: "text-secondary", inputWrapper: ["focus-within:border-secondary", "focus-within:hover:border-secondary"], label: "text-secondary", }, @@ -285,6 +296,7 @@ const dateInput = tv({ variant: ["bordered", "faded"], color: "success", class: { + innerWrapper: "text-success", inputWrapper: ["focus-within:border-success", "focus-within:hover:border-success"], label: "text-success", }, @@ -293,6 +305,7 @@ const dateInput = tv({ variant: ["bordered", "faded"], color: "warning", class: { + innerWrapper: "text-warning", inputWrapper: ["focus-within:border-warning", "focus-within:hover:border-warning"], label: "text-warning", }, @@ -301,6 +314,7 @@ const dateInput = tv({ variant: ["bordered", "faded"], color: "danger", class: { + innerWrapper: "text-danger", inputWrapper: ["focus-within:border-danger", "focus-within:hover:border-danger"], label: "text-danger", }, @@ -310,6 +324,7 @@ const dateInput = tv({ variant: "underlined", color: "primary", class: { + innerWrapper: "text-primary", inputWrapper: "after:bg-primary", label: "text-primary", }, @@ -318,6 +333,7 @@ const dateInput = tv({ variant: "underlined", color: "secondary", class: { + innerWrapper: "text-secondary", inputWrapper: "after:bg-secondary", label: "text-secondary", }, @@ -326,6 +342,7 @@ const dateInput = tv({ variant: "underlined", color: "success", class: { + innerWrapper: "text-success", inputWrapper: "after:bg-success", label: "text-success", }, @@ -334,6 +351,7 @@ const dateInput = tv({ variant: "underlined", color: "warning", class: { + innerWrapper: "text-warning", inputWrapper: "after:bg-warning", label: "text-warning", }, @@ -342,6 +360,7 @@ const dateInput = tv({ variant: "underlined", color: "danger", class: { + innerWrapper: "text-danger", inputWrapper: "after:bg-danger", label: "text-danger", }, diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 9f4bc51cb8..ade8cdec9d 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -10,8 +10,8 @@ import {tv} from "../utils/tv"; const datePicker = tv({ slots: { base: "w-full", - selectorButton: "-mr-2", - selectorIcon: "text-lg text-default-400 pointer-events-none flex-shrink-0", + selectorButton: "-mr-2 text-inherit", + selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-64 shadow-none", }, From 8fb1f59695c555aa7370e2e104f84fd56af59a99 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 10:05:30 -0300 Subject: [PATCH 05/12] fix(date-picker): popover offset adn calendar styles --- .../date-picker/src/use-date-picker.ts | 20 ++++--- .../stories/date-picker.stories.tsx | 27 ++++++++++ .../popover/src/free-solo-popover.tsx | 54 +++++++++---------- .../components/popover/src/use-popover.ts | 2 +- .../core/theme/src/components/date-picker.ts | 2 + 5 files changed, 71 insertions(+), 34 deletions(-) diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index 0f8ad02f3a..c3f0f4c282 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -36,7 +36,7 @@ interface Props extends NextUIBaseProps { /** * Props to be passed to the popover component. * - * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 5 } + * @default { placement: "bottom", triggerScaleOnOpen: false, offset: 13 } */ popoverProps?: Partial; /** @@ -123,6 +123,8 @@ export function useDatePicker(originalProps: UseDatePickerP const baseStyles = clsx(classNames?.base, className); + const isDefaultColor = originalProps.color === "default" || !originalProps.color; + const slotsProps: { inputProps: DateInputProps; popoverProps: UseDatePickerProps["popoverProps"]; @@ -142,7 +144,7 @@ export function useDatePicker(originalProps: UseDatePickerP ), popoverProps: mergeProps( { - offset: 5, + offset: 13, placement: "bottom", triggerScaleOnOpen: false, disableAnimation, @@ -162,7 +164,10 @@ export function useDatePicker(originalProps: UseDatePickerP calendarProps: mergeProps( { color: - originalProps.color === "default" || !originalProps.color + (originalProps.variant === "bordered" || originalProps.variant === "underlined") && + isDefaultColor + ? "foreground" + : isDefaultColor ? "primary" : originalProps.color, disableAnimation, @@ -187,9 +192,8 @@ export function useDatePicker(originalProps: UseDatePickerP const getPopoverProps = (props: DOMAttributes = {}) => { return { - ...props, state, - dialogProps, + ...mergeProps(slotsProps.popoverProps, dialogProps, props), triggerRef: targetRef, classNames: { content: slots.popoverContent({ @@ -208,7 +212,11 @@ export function useDatePicker(originalProps: UseDatePickerP ...calendarProps, ...slotsProps.calendarProps, "data-slot": "calendar", - className: slots.calendar({class: classNames?.calendar}), + classNames: { + base: slots.calendar({class: classNames?.calendar}), + headerWrapper: slots.calendarHeader({class: classNames?.calendarHeader}), + gridWrapper: slots.calendarGrid({class: classNames?.calendarGrid}), + }, } as unknown as CalendarProps; }; diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index 2f610454c2..75ec723c0a 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -62,9 +62,36 @@ const defaultProps = { const Template = (args: DatePickerProps) => ; +const LabelPlacementTemplate = (args: DatePickerProps) => ( +
+ + + +
+); + export const Default = { render: Template, args: { ...defaultProps, }, }; + +export const WithMonthAndYearPickers = { + render: Template, + args: { + ...defaultProps, + variant: "bordered", + calendarProps: { + showMonthAndYearPickers: true, + }, + }, +}; + +export const LabelPlacement = { + render: LabelPlacementTemplate, + + args: { + ...defaultProps, + }, +}; diff --git a/packages/components/popover/src/free-solo-popover.tsx b/packages/components/popover/src/free-solo-popover.tsx index fe86a6e112..9af4ab15cf 100644 --- a/packages/components/popover/src/free-solo-popover.tsx +++ b/packages/components/popover/src/free-solo-popover.tsx @@ -28,34 +28,34 @@ type FreeSoloPopoverWrapperProps = { motionProps?: UsePopoverProps["motionProps"]; } & React.HTMLAttributes; -const FreeSoloPopoverWrapper = ({ - children, - motionProps, - placement, - disableAnimation, - style = {}, - ...otherProps -}: FreeSoloPopoverWrapperProps) => { - return disableAnimation ? ( -
{children}
- ) : ( - - +const FreeSoloPopoverWrapper = forwardRef<"div", FreeSoloPopoverWrapperProps>( + ({children, motionProps, placement, disableAnimation, style = {}, ...otherProps}, ref) => { + return disableAnimation ? ( +
{children} - - - ); -}; +
+ ) : ( + + + {children} + + + ); + }, +); + +FreeSoloPopoverWrapper.displayName = "NextUI.FreeSoloPopoverWrapper"; const FreeSoloPopover = forwardRef<"div", FreeSoloPopoverProps>(({children, ...props}, ref) => { const { diff --git a/packages/components/popover/src/use-popover.ts b/packages/components/popover/src/use-popover.ts index 0f704f310c..fe3b094adc 100644 --- a/packages/components/popover/src/use-popover.ts +++ b/packages/components/popover/src/use-popover.ts @@ -152,7 +152,7 @@ export function usePopover(originalProps: UsePopoverProps) { isNonModal, popoverRef: domRef, placement: placementProp, - offset: offset, + offset, scrollRef, isDismissable, shouldCloseOnBlur, diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index ade8cdec9d..f232311d23 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -14,6 +14,8 @@ const datePicker = tv({ selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-64 shadow-none", + calendarHeader: "w-64", + calendarGrid: "w-64", }, variants: {}, defaultVariants: {}, From 40773eb2bddbbe7c77db341ec6dae05db820ed59 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 10:36:46 -0300 Subject: [PATCH 06/12] feat(date-picker): stories added --- .../calendar/src/use-calendar-picker.ts | 2 + .../date-input/stories/date-input.stories.tsx | 4 +- .../date-picker/src/date-picker.tsx | 3 +- .../date-picker/src/use-date-picker.ts | 45 ++- .../stories/date-picker.stories.tsx | 260 +++++++++++++++++- .../core/theme/src/components/calendar.ts | 3 +- .../core/theme/src/components/date-input.ts | 6 +- .../core/theme/src/components/date-picker.ts | 16 +- 8 files changed, 320 insertions(+), 19 deletions(-) diff --git a/packages/components/calendar/src/use-calendar-picker.ts b/packages/components/calendar/src/use-calendar-picker.ts index 7877381f49..fd93535779 100644 --- a/packages/components/calendar/src/use-calendar-picker.ts +++ b/packages/components/calendar/src/use-calendar-picker.ts @@ -209,6 +209,8 @@ export function useCalendarPicker(props: CalendarPickerProps) { nextValue = value + 3; break; case "Escape": + case "Enter": + case " ": setIsHeaderExpanded?.(false); headerRef?.current?.focus(); diff --git a/packages/components/date-input/stories/date-input.stories.tsx b/packages/components/date-input/stories/date-input.stories.tsx index 1b1f972f41..fc6081f75e 100644 --- a/packages/components/date-input/stories/date-input.stories.tsx +++ b/packages/components/date-input/stories/date-input.stories.tsx @@ -183,7 +183,7 @@ export const WithoutLabel = { args: { ...defaultProps, label: null, - "aria-label": "Birthday", + "aria-label": "Birth date", }, }; @@ -192,7 +192,7 @@ export const WithDescription = { args: { ...defaultProps, - description: "Please enter your birthday", + description: "Please enter your birth date", }, }; diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index 3f5f5521e1..25ff2d514e 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -12,7 +12,8 @@ import {CalendarBoldIcon} from "@nextui-org/shared-icons"; import {UseDatePickerProps, useDatePicker} from "./use-date-picker"; -export interface Props extends UseDatePickerProps {} +export interface Props + extends Omit, "hasMultipleMonths"> {} function DatePicker(props: Props, ref: ForwardedRef) { const { diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index c3f0f4c282..b03e897788 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -33,6 +33,24 @@ interface Props extends NextUIBaseProps { * The icon to toggle the date picker popover. Usually a calendar icon. */ selectorIcon?: ReactNode; + /** + * Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. + * @default visible + */ + pageBehavior?: CalendarProps["pageBehavior"]; + /** + * The number of months to display at once. Up to 3 months are supported. + * Passing a number greater than 1 will disable the `showMonthAndYearPickers` prop. + * + * @default 1 + */ + visibleMonths?: CalendarProps["visibleMonths"]; + /** + * Whether the calendar should show month and year pickers. + * + * @default false + */ + showMonthAndYearPickers?: CalendarProps["showMonthAndYearPickers"]; /** * Props to be passed to the popover component. * @@ -83,6 +101,9 @@ export function useDatePicker(originalProps: UseDatePickerP ref, as, selectorIcon, + visibleMonths = 1, + pageBehavior = "visible", + showMonthAndYearPickers = false, popoverProps = {}, selectorButtonProps = {}, calendarProps: userCalendarProps = {}, @@ -112,18 +133,10 @@ export function useDatePicker(originalProps: UseDatePickerP targetRef, ); - const slots = useMemo( - () => - datePicker({ - ...variantProps, - className, - }), - [objectToDeps(variantProps), className], - ); - const baseStyles = clsx(classNames?.base, className); const isDefaultColor = originalProps.color === "default" || !originalProps.color; + const hasMultipleMonths = visibleMonths > 1; const slotsProps: { inputProps: DateInputProps; @@ -163,6 +176,9 @@ export function useDatePicker(originalProps: UseDatePickerP ), calendarProps: mergeProps( { + visibleMonths, + pageBehavior, + showMonthAndYearPickers, color: (originalProps.variant === "bordered" || originalProps.variant === "underlined") && isDefaultColor @@ -176,6 +192,16 @@ export function useDatePicker(originalProps: UseDatePickerP ), }; + const slots = useMemo( + () => + datePicker({ + ...variantProps, + hasMultipleMonths, + className, + }), + [objectToDeps(variantProps), hasMultipleMonths, className], + ); + const getBaseProps: PropGetter = () => ({ ...groupProps, "data-invalid": dataAttr(originalProps?.isInvalid), @@ -214,6 +240,7 @@ export function useDatePicker(originalProps: UseDatePickerP "data-slot": "calendar", classNames: { base: slots.calendar({class: classNames?.calendar}), + content: slots.calendarContent({class: classNames?.calendarContent}), headerWrapper: slots.calendarHeader({class: classNames?.calendarHeader}), gridWrapper: slots.calendarGrid({class: classNames?.calendarGrid}), }, diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index 75ec723c0a..dc9e25c347 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -1,6 +1,16 @@ import React from "react"; import {Meta} from "@storybook/react"; import {datePicker, dateInput} from "@nextui-org/theme"; +import { + DateValue, + getLocalTimeZone, + now, + parseAbsoluteToLocal, + parseDate, + parseZonedDateTime, + today, +} from "@internationalized/date"; +import {I18nProvider, useDateFormatter} from "@react-aria/i18n"; import {DatePicker, DatePickerProps} from "../src"; @@ -70,6 +80,92 @@ const LabelPlacementTemplate = (args: DatePickerProps) => (
); +const ControlledTemplate = (args: DatePickerProps) => { + const [value, setValue] = React.useState(parseDate("2024-04-04")); + + let formatter = useDateFormatter({dateStyle: "full"}); + + return ( +
+
+ +

+ Selected date: {value ? formatter.format(value.toDate(getLocalTimeZone())) : "--"} +

+
+ +
+ ); +}; + +const TimeZonesTemplate = (args: DatePickerProps) => ( +
+ + +
+); + +const GranularityTemplate = (args: DatePickerProps) => { + let [date, setDate] = React.useState(parseAbsoluteToLocal("2021-04-07T18:45:22Z")); + + return ( +
+ + + + +
+ ); +}; + +const InternationalCalendarsTemplate = (args: DatePickerProps) => { + let [date, setDate] = React.useState(parseAbsoluteToLocal("2021-04-07T18:45:22Z")); + + return ( +
+ + + +
+ ); +}; + export const Default = { render: Template, args: { @@ -82,9 +178,7 @@ export const WithMonthAndYearPickers = { args: { ...defaultProps, variant: "bordered", - calendarProps: { - showMonthAndYearPickers: true, - }, + showMonthAndYearPickers: true, }, }; @@ -95,3 +189,163 @@ export const LabelPlacement = { ...defaultProps, }, }; + +export const Controlled = { + render: ControlledTemplate, + args: { + ...defaultProps, + }, +}; + +export const Required = { + render: Template, + args: { + ...defaultProps, + isRequired: true, + }, +}; + +export const Disabled = { + render: Template, + args: { + ...defaultProps, + isDisabled: true, + defaultValue: parseDate("2024-04-04"), + }, +}; + +export const ReadOnly = { + render: Template, + args: { + ...defaultProps, + isReadOnly: true, + defaultValue: parseDate("2024-04-04"), + }, +}; + +export const WithoutLabel = { + render: Template, + + args: { + ...defaultProps, + label: null, + "aria-label": "Birth date", + }, +}; + +export const WithDescription = { + render: Template, + + args: { + ...defaultProps, + description: "Please enter your birth date", + }, +}; + +export const SelectorIcon = { + render: Template, + + args: { + ...defaultProps, + selectorIcon: ( + + + + + + + + ), + }, +}; + +export const WithErrorMessage = { + render: Template, + + args: { + ...defaultProps, + errorMessage: "Please enter a valid date", + }, +}; + +export const IsInvalid = { + render: Template, + + args: { + ...defaultProps, + variant: "bordered", + isInvalid: true, + defaultValue: parseDate("2024-04-04"), + errorMessage: "Please enter a valid date", + }, +}; + +export const TimeZones = { + render: TimeZonesTemplate, + + args: { + ...defaultProps, + label: "Event date", + defaultValue: parseZonedDateTime("2022-11-07T00:45[America/Los_Angeles]"), + }, +}; + +export const Granularity = { + render: GranularityTemplate, + + args: { + ...defaultProps, + }, +}; + +export const InternationalCalendars = { + render: InternationalCalendarsTemplate, + + args: { + ...defaultProps, + }, +}; + +export const MinDateValue = { + render: Template, + + args: { + ...defaultProps, + minValue: today(getLocalTimeZone()), + defaultValue: parseDate("2024-04-03"), + }, +}; + +export const MaxDateValue = { + render: Template, + + args: { + ...defaultProps, + maxValue: today(getLocalTimeZone()), + defaultValue: parseDate("2024-04-05"), + }, +}; + +export const VisibleMonths = { + render: Template, + + args: { + ...defaultProps, + visibleMonths: 2, + }, +}; + +export const PageBehavior = { + render: Template, + args: { + ...defaultProps, + visibleMonths: 2, + pageBehavior: "single", + }, +}; diff --git a/packages/core/theme/src/components/calendar.ts b/packages/core/theme/src/components/calendar.ts index 51d53cb0f9..9888d3fe6e 100644 --- a/packages/core/theme/src/components/calendar.ts +++ b/packages/core/theme/src/components/calendar.ts @@ -7,12 +7,13 @@ const calendar = tv({ slots: { base: [ "relative w-fit max-w-full shadow-small inline-block", - "rounded-large overflow-scroll bg-default-50 dark:bg-background", + "rounded-large overflow-x-scroll bg-default-50 dark:bg-background", ], prevButton: [], nextButton: [], headerWrapper: [ "px-4 py-2 flex items-center justify-between gap-2 bg-content1", + "[&_.chevron-icon]:flex-none", // month/year picker wrapper "after:content-['']", "after:bg-content1 origin-top", diff --git a/packages/core/theme/src/components/date-input.ts b/packages/core/theme/src/components/date-input.ts index 56f24b3f4d..ed70100f01 100644 --- a/packages/core/theme/src/components/date-input.ts +++ b/packages/core/theme/src/components/date-input.ts @@ -22,7 +22,11 @@ const dateInput = tv({ "cursor-text tap-highlight-transparent shadow-sm", ], input: "flex h-full gap-x-0.5 w-full font-normal", - innerWrapper: "flex items-center text-default-400 w-full gap-x-2 h-6", // this wraps the input and the start/end content + innerWrapper: [ + "flex items-center text-default-400 w-full gap-x-2 h-6", + // isInValid=true + "group-data-[invalid=true]:text-danger", + ], // this wraps the input and the start/end content segment: [ "group -ml-0.5 px-0.5 my-auto box-content tabular-nums text-start", "inline-block outline-none focus:shadow-sm rounded-md", diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index f232311d23..677f3bbb9d 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -9,15 +9,27 @@ import {tv} from "../utils/tv"; */ const datePicker = tv({ slots: { - base: "w-full", + base: "group w-full", selectorButton: "-mr-2 text-inherit", selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-64 shadow-none", + calendarContent: "", calendarHeader: "w-64", calendarGrid: "w-64", }, - variants: {}, + variants: { + // @internal + hasMultipleMonths: { + true: { + calendar: "w-auto", + calendarContent: "w-full", + calendarHeader: "w-auto", + calendarGrid: "w-fit", // TODO: fix when disableAnimation is false + }, + false: {}, + }, + }, defaultVariants: {}, compoundVariants: [], }); From c96685bf742c5dc16f310fc7561251bb0cb230c5 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 14:56:39 -0300 Subject: [PATCH 07/12] fix(date-picker): calendar width properly handled --- .../components/calendar/src/calendar-base.tsx | 21 +++++++++++-------- .../date-picker/src/use-date-picker.ts | 17 +++++++++++++++ .../core/theme/src/components/date-picker.ts | 14 ++++++------- .../framer-utils/src/resizable-panel.tsx | 12 +++++------ 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/packages/components/calendar/src/calendar-base.tsx b/packages/components/calendar/src/calendar-base.tsx index ada247795b..06c714a9b9 100644 --- a/packages/components/calendar/src/calendar-base.tsx +++ b/packages/components/calendar/src/calendar-base.tsx @@ -113,7 +113,7 @@ export function CalendarBase(props: CalendarBaseProps) { } const calendarContent = ( -
+ <>
{calendars}
-
+ ); return ( @@ -143,15 +143,18 @@ export function CalendarBase(props: CalendarBaseProps) {

{calendarProps["aria-label"]}

{disableAnimation ? ( - calendarContent +
+ {calendarContent} +
) : ( - + - <> - - {calendarContent} - - + + {calendarContent} + )} diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index b03e897788..937dbc1b01 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -45,6 +45,12 @@ interface Props extends NextUIBaseProps { * @default 1 */ visibleMonths?: CalendarProps["visibleMonths"]; + /** + * The width to be applied to the calendar component. + * + * @default 256 + */ + calendarWidth?: number; /** * Whether the calendar should show month and year pickers. * @@ -103,6 +109,7 @@ export function useDatePicker(originalProps: UseDatePickerP selectorIcon, visibleMonths = 1, pageBehavior = "visible", + calendarWidth = 256, showMonthAndYearPickers = false, popoverProps = {}, selectorButtonProps = {}, @@ -244,6 +251,16 @@ export function useDatePicker(originalProps: UseDatePickerP headerWrapper: slots.calendarHeader({class: classNames?.calendarHeader}), gridWrapper: slots.calendarGrid({class: classNames?.calendarGrid}), }, + style: mergeProps( + hasMultipleMonths + ? { + // @ts-ignore + "--visible-months": visibleMonths, + "--calendar-width": `${calendarWidth}px`, + } + : {}, + slotsProps.calendarProps.style, + ), } as unknown as CalendarProps; }; diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 677f3bbb9d..46cb7beb55 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -13,19 +13,19 @@ const datePicker = tv({ selectorButton: "-mr-2 text-inherit", selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", - calendar: "w-64 shadow-none", + calendar: "w-[var(--calendar-width)] shadow-none", calendarContent: "", - calendarHeader: "w-64", - calendarGrid: "w-64", + calendarHeader: "w-[var(--calendar-width)]", + calendarGrid: "w-[var(--calendar-width)]", }, variants: { // @internal hasMultipleMonths: { true: { - calendar: "w-auto", - calendarContent: "w-full", - calendarHeader: "w-auto", - calendarGrid: "w-fit", // TODO: fix when disableAnimation is false + calendar: "w-full", + calendarContent: "w-[calc(var(--visible-months)_*_var(--calendar-width))]", + calendarHeader: "w-full", + calendarGrid: "w-full", }, false: {}, }, diff --git a/packages/utilities/framer-utils/src/resizable-panel.tsx b/packages/utilities/framer-utils/src/resizable-panel.tsx index 8fc653abeb..0b32f8e49d 100644 --- a/packages/utilities/framer-utils/src/resizable-panel.tsx +++ b/packages/utilities/framer-utils/src/resizable-panel.tsx @@ -1,15 +1,14 @@ -import type {ReactNode, Ref} from "react"; +import type {Ref} from "react"; import {forwardRef} from "react"; import {domAnimation, LazyMotion, m} from "framer-motion"; import {useMeasure} from "@nextui-org/use-measure"; +import {HTMLNextUIProps} from "@nextui-org/system"; /** * Props for the ResizablePanel component. */ -export interface ResizablePanelProps { - children?: ReactNode; -} +export interface ResizablePanelProps extends HTMLNextUIProps<"div"> {} const ResizablePanel = forwardRef( (originalProps: ResizablePanelProps, ref: Ref) => { @@ -25,9 +24,10 @@ const ResizablePanel = forwardRef( width: bounds.width && bounds?.width > 0 ? bounds.width : "auto", height: bounds.height && bounds.height > 0 ? bounds.height : "auto", }} - {...props} > -
{children}
+
+ {children} +
); From 67903945047926651e30c1487c983c754cce1fa7 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 15:50:46 -0300 Subject: [PATCH 08/12] feat(date-picker): styles simplified --- .../components/calendar/src/calendar-base.tsx | 8 +- .../calendar/stories/calendar.stories.tsx | 3 + packages/components/date-picker/package.json | 1 + .../date-picker/src/date-picker.tsx | 8 +- .../date-picker/src/use-date-picker.ts | 25 ++- .../stories/date-picker.stories.tsx | 142 +++++++++++++++++- .../core/theme/src/components/date-picker.ts | 8 +- pnpm-lock.yaml | 3 + 8 files changed, 181 insertions(+), 17 deletions(-) diff --git a/packages/components/calendar/src/calendar-base.tsx b/packages/components/calendar/src/calendar-base.tsx index 06c714a9b9..142bb10d9d 100644 --- a/packages/components/calendar/src/calendar-base.tsx +++ b/packages/components/calendar/src/calendar-base.tsx @@ -152,9 +152,11 @@ export function CalendarBase(props: CalendarBaseProps) { data-slot="content" > - - {calendarContent} - + <> + + {calendarContent} + +
)} diff --git a/packages/components/calendar/stories/calendar.stories.tsx b/packages/components/calendar/stories/calendar.stories.tsx index 3bc124c093..e9a70b1c56 100644 --- a/packages/components/calendar/stories/calendar.stories.tsx +++ b/packages/components/calendar/stories/calendar.stories.tsx @@ -205,6 +205,9 @@ const PresetsTemplate = (args: CalendarProps) => { 14 days } + classNames={{ + content: "w-full", + }} focusedValue={value} nextButtonProps={{ variant: "bordered", diff --git a/packages/components/date-picker/package.json b/packages/components/date-picker/package.json index 74d0843778..c448467f9f 100644 --- a/packages/components/date-picker/package.json +++ b/packages/components/date-picker/package.json @@ -60,6 +60,7 @@ "devDependencies": { "@nextui-org/system": "workspace:*", "@nextui-org/theme": "workspace:*", + "@nextui-org/radio": "workspace:*", "clean-package": "2.2.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index 25ff2d514e..ae1e7cdb46 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -27,6 +27,8 @@ function DatePicker(props: Props, ref: ForwardedRef({...props, ref}); const selectorContent = isValidElement(selectorIcon) ? ( @@ -37,7 +39,11 @@ function DatePicker(props: Props, ref: ForwardedRef - +
) : null; diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index 937dbc1b01..a281674b61 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -51,6 +51,14 @@ interface Props extends NextUIBaseProps { * @default 256 */ calendarWidth?: number; + /** + * Top content to be rendered in the calendar component. + */ + CalendarTopContent?: CalendarProps["topContent"]; + /** + * Bottom content to be rendered in the calendar component. + */ + CalendarBottomContent?: CalendarProps["bottomContent"]; /** * Whether the calendar should show month and year pickers. * @@ -72,7 +80,11 @@ interface Props extends NextUIBaseProps { * Props to be passed to the calendar component. * @default {} */ - calendarProps?: Partial; + calendarProps?: Partial>; + /** + * Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. + */ + isDateUnavailable?: CalendarProps["isDateUnavailable"]; /** * Whether to disable all animations in the date picker. Including the DateInput, Button, Calendar, and Popover. * @@ -110,10 +122,13 @@ export function useDatePicker(originalProps: UseDatePickerP visibleMonths = 1, pageBehavior = "visible", calendarWidth = 256, + isDateUnavailable, showMonthAndYearPickers = false, popoverProps = {}, selectorButtonProps = {}, calendarProps: userCalendarProps = {}, + CalendarTopContent, + CalendarBottomContent, minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), disableAnimation = false, @@ -185,6 +200,7 @@ export function useDatePicker(originalProps: UseDatePickerP { visibleMonths, pageBehavior, + isDateUnavailable, showMonthAndYearPickers, color: (originalProps.variant === "bordered" || originalProps.variant === "underlined") && @@ -247,18 +263,15 @@ export function useDatePicker(originalProps: UseDatePickerP "data-slot": "calendar", classNames: { base: slots.calendar({class: classNames?.calendar}), - content: slots.calendarContent({class: classNames?.calendarContent}), - headerWrapper: slots.calendarHeader({class: classNames?.calendarHeader}), - gridWrapper: slots.calendarGrid({class: classNames?.calendarGrid}), }, style: mergeProps( hasMultipleMonths ? { // @ts-ignore "--visible-months": visibleMonths, - "--calendar-width": `${calendarWidth}px`, } : {}, + {"--calendar-width": `${calendarWidth}px`}, slotsProps.calendarProps.style, ), } as unknown as CalendarProps; @@ -286,6 +299,8 @@ export function useDatePicker(originalProps: UseDatePickerP domRef, selectorIcon, disableAnimation, + CalendarTopContent, + CalendarBottomContent, getBaseProps, getDateInputProps, getPopoverProps, diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index dc9e25c347..116f569fd5 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -4,13 +4,19 @@ import {datePicker, dateInput} from "@nextui-org/theme"; import { DateValue, getLocalTimeZone, + isWeekend, now, parseAbsoluteToLocal, parseDate, parseZonedDateTime, + startOfMonth, + startOfWeek, today, } from "@internationalized/date"; -import {I18nProvider, useDateFormatter} from "@react-aria/i18n"; +import {I18nProvider, useDateFormatter, useLocale} from "@react-aria/i18n"; +import {Button, ButtonGroup} from "@nextui-org/button"; +import {Radio, RadioGroup} from "@nextui-org/radio"; +import {cn} from "@nextui-org/system"; import {DatePicker, DatePickerProps} from "../src"; @@ -166,6 +172,123 @@ const InternationalCalendarsTemplate = (args: DatePickerProps) => { ); }; +const PresetsTemplate = (args: DatePickerProps) => { + let defaultDate = today(getLocalTimeZone()); + + const [value, setValue] = React.useState(defaultDate); + + let {locale} = useLocale(); + let formatter = useDateFormatter({dateStyle: "full"}); + + let now = today(getLocalTimeZone()); + let nextWeek = startOfWeek(now.add({weeks: 1}), locale); + let nextMonth = startOfMonth(now.add({months: 1})); + + const CustomRadio = (props) => { + const {children, ...otherProps} = props; + + return ( + + {children} + + ); + }; + + return ( +
+ + Exact dates + 1 day + 2 days + 3 days + 7 days + 14 days + + } + CalendarTopContent={ + + + + + + } + calendarProps={{ + focusedValue: value, + onFocusChange: setValue, + nextButtonProps: { + variant: "bordered", + }, + prevButtonProps: { + variant: "bordered", + }, + }} + value={value} + onChange={setValue} + {...args} + label="Event date" + /> +

+ Selected date: {value ? formatter.format(value.toDate(getLocalTimeZone())) : "--"} +

+
+ ); +}; + +const UnavailableDatesTemplate = (args: DatePickerProps) => { + let now = today(getLocalTimeZone()); + + let disabledRanges = [ + [now, now.add({days: 5})], + [now.add({days: 14}), now.add({days: 16})], + [now.add({days: 23}), now.add({days: 24})], + ]; + + let {locale} = useLocale(); + + let isDateUnavailable = (date) => + isWeekend(date, locale) || + disabledRanges.some( + (interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0, + ); + + return ( + + ); +}; + export const Default = { render: Template, args: { @@ -309,6 +432,7 @@ export const InternationalCalendars = { args: { ...defaultProps, + showMonthAndYearPickers: true, }, }; @@ -332,6 +456,15 @@ export const MaxDateValue = { }, }; +export const UnavailableDates = { + render: UnavailableDatesTemplate, + args: { + ...defaultProps, + defaultValue: today(getLocalTimeZone()), + unavailableDates: [today(getLocalTimeZone())], + }, +}; + export const VisibleMonths = { render: Template, @@ -349,3 +482,10 @@ export const PageBehavior = { pageBehavior: "single", }, }; + +export const Presets = { + render: PresetsTemplate, + args: { + ...defaultProps, + }, +}; diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 46cb7beb55..df911638e2 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -14,18 +14,12 @@ const datePicker = tv({ selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-[var(--calendar-width)] shadow-none", - calendarContent: "", - calendarHeader: "w-[var(--calendar-width)]", - calendarGrid: "w-[var(--calendar-width)]", }, variants: { // @internal hasMultipleMonths: { true: { - calendar: "w-full", - calendarContent: "w-[calc(var(--visible-months)_*_var(--calendar-width))]", - calendarHeader: "w-full", - calendarGrid: "w-full", + calendar: "w-[calc(var(--visible-months)_*_var(--calendar-width))]", }, false: {}, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 692d6ac1a3..d5824fae56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1364,6 +1364,9 @@ importers: specifier: 3.21.0 version: 3.21.0(react@18.2.0) devDependencies: + '@nextui-org/radio': + specifier: workspace:* + version: link:../radio '@nextui-org/system': specifier: workspace:* version: link:../../core/system From e1e693b5604dd4e99384f93141e3c97847272a22 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sat, 6 Apr 2024 17:55:45 -0300 Subject: [PATCH 09/12] chore(date-picker): almost all test passing --- .../components/calendar/src/calendar-base.tsx | 4 +- .../calendar/src/use-calendar-base.ts | 7 + .../components/calendar/src/use-calendar.ts | 2 + .../calendar/src/use-range-calendar.ts | 2 + .../date-input/__tests__/date-input.test.tsx | 15 +- .../date-input/src/use-date-input.ts | 42 ++- .../__tests__/date-picker.test.tsx | 340 +++++++++++++++++- packages/components/date-picker/package.json | 1 + .../date-picker/src/date-picker.tsx | 6 +- .../date-picker/src/use-date-picker.ts | 88 +++-- .../stories/date-picker.stories.tsx | 6 +- .../popover/src/popover-content.tsx | 7 +- .../components/popover/src/use-popover.ts | 3 - .../react-rsc-utils/src/filter-dom-props.ts | 26 +- pnpm-lock.yaml | 3 + 15 files changed, 472 insertions(+), 80 deletions(-) diff --git a/packages/components/calendar/src/calendar-base.tsx b/packages/components/calendar/src/calendar-base.tsx index 142bb10d9d..0d5def77b1 100644 --- a/packages/components/calendar/src/calendar-base.tsx +++ b/packages/components/calendar/src/calendar-base.tsx @@ -22,6 +22,7 @@ import {useCalendarContext} from "./calendar-context"; export interface CalendarBaseProps extends HTMLNextUIProps<"div"> { Component?: As; + showHelper?: boolean; topContent?: ReactNode; bottomContent?: ReactNode; calendarProps: HTMLAttributes; @@ -36,6 +37,7 @@ export interface CalendarBaseProps extends HTMLNextUIProps<"div"> { export function CalendarBase(props: CalendarBaseProps) { const { Component = "div", + showHelper, topContent, bottomContent, calendarProps, @@ -171,7 +173,7 @@ export function CalendarBase(props: CalendarBaseProps) { onClick={() => state.focusNextPage()} /> - {state.isValueInvalid && ( + {state.isValueInvalid && showHelper && (
({ locale, minValue, maxValue, + showHelper, weekdayStyle, visibleDuration, shouldFilterDOMProps, @@ -81,6 +82,7 @@ export function useCalendar({ const getBaseCalendarProps = (props = {}): CalendarBaseProps => { return { Component, + showHelper, topContent, bottomContent, buttonPickerProps, diff --git a/packages/components/calendar/src/use-range-calendar.ts b/packages/components/calendar/src/use-range-calendar.ts index 8d42035fec..ba2cabab6b 100644 --- a/packages/components/calendar/src/use-range-calendar.ts +++ b/packages/components/calendar/src/use-range-calendar.ts @@ -31,6 +31,7 @@ export function useRangeCalendar({ children, domRef, locale, + showHelper, minValue, maxValue, weekdayStyle, @@ -73,6 +74,7 @@ export function useRangeCalendar({ const getBaseCalendarProps = (props = {}): CalendarBaseProps => { return { Component, + showHelper, topContent, bottomContent, calendarRef: domRef, diff --git a/packages/components/date-input/__tests__/date-input.test.tsx b/packages/components/date-input/__tests__/date-input.test.tsx index 9208cdccd5..7cebfa60b2 100644 --- a/packages/components/date-input/__tests__/date-input.test.tsx +++ b/packages/components/date-input/__tests__/date-input.test.tsx @@ -1,11 +1,20 @@ /* eslint-disable jsx-a11y/no-autofocus */ import * as React from "react"; import {act, fireEvent, render} from "@testing-library/react"; -import {CalendarDate, CalendarDateTime, ZonedDateTime} from "@internationalized/date"; +import {CalendarDate, CalendarDateTime, DateValue, ZonedDateTime} from "@internationalized/date"; import {pointerMap, triggerPress} from "@nextui-org/test-utils"; import userEvent from "@testing-library/user-event"; -import {DateInput} from "../src"; +import {DateInput as DateInputBase, DateInputProps} from "../src"; + +/** + * Custom date-input to disable animations and avoid issues with react-motion and jest + */ +const DateInput = React.forwardRef((props: DateInputProps, ref: React.Ref) => { + return ; +}); + +DateInput.displayName = "DateInput"; describe("DateInput", () => { let user; @@ -296,7 +305,7 @@ describe("DateInput", () => { it("supports form reset", async () => { function Test() { - let [value, setValue] = React.useState(new CalendarDate(2020, 2, 3)); + let [value, setValue] = React.useState(new CalendarDate(2020, 2, 3)); return (
diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index 214cb72ff5..63a5e95287 100644 --- a/packages/components/date-input/src/use-date-input.ts +++ b/packages/components/date-input/src/use-date-input.ts @@ -3,9 +3,11 @@ import type {AriaDatePickerProps} from "@react-types/datepicker"; import type {SupportedCalendars} from "@nextui-org/system"; import type {DateValue, Calendar} from "@internationalized/date"; import type {ReactRef} from "@nextui-org/react-utils"; +import type {DOMAttributes, GroupDOMAttributes} from "@react-types/shared"; import {useLocale} from "@react-aria/i18n"; import {CalendarDate} from "@internationalized/date"; +import {mergeProps} from "@react-aria/utils"; import {PropGetter, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {useDOMRef, filterDOMProps} from "@nextui-org/react-utils"; @@ -26,6 +28,16 @@ interface Props extends NextUIBaseProps { * Ref to the DOM node. */ ref?: ReactRef; + /** Props for the grouping element containing the date field and button. */ + groupProps?: GroupDOMAttributes; + /** Props for the date picker's visible label element, if any. */ + labelProps?: DOMAttributes; + /** Props for the date field. */ + fieldProps?: DOMAttributes; + /** Props for the description element, if any. */ + descriptionProps?: DOMAttributes; + /** Props for the error message element, if any. */ + errorMessageProps?: DOMAttributes; /** * The value of the hidden input. */ @@ -106,6 +118,7 @@ export function useDateInput(originalProps: UseDateInputPro const { ref, as, + label, inputRef: inputRefProp, description, startContent, @@ -113,6 +126,11 @@ export function useDateInput(originalProps: UseDateInputPro className, classNames, validationState, + groupProps = {}, + labelProps: labelPropsProp, + fieldProps: fieldPropsProp, + errorMessageProps: errorMessagePropsProp, + descriptionProps: descriptionPropsProp, validationBehavior = "native", shouldForceLeadingZeros = true, minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), @@ -132,6 +150,7 @@ export function useDateInput(originalProps: UseDateInputPro const {locale} = useLocale(); const state = useDateFieldState({ ...originalProps, + label, locale, minValue, maxValue, @@ -152,7 +171,7 @@ export function useDateInput(originalProps: UseDateInputPro descriptionProps, errorMessageProps, isInvalid: ariaIsInvalid, - } = useAriaDateField({...originalProps, validationBehavior, inputRef}, state, domRef); + } = useAriaDateField({...originalProps, label, validationBehavior, inputRef}, state, domRef); const baseStyles = clsx(classNames?.base, className); @@ -220,8 +239,7 @@ export function useDateInput(originalProps: UseDateInputPro const getLabelProps: PropGetter = (props) => { return { - ...props, - ...labelProps, + ...mergeProps(labelProps, labelPropsProp, props), "data-slot": "label", className: slots.label({ class: clsx(classNames?.label, props?.className), @@ -239,8 +257,13 @@ export function useDateInput(originalProps: UseDateInputPro const getFieldProps: PropGetter = (props) => { return { - ...props, - ...fieldProps, + ...mergeProps( + filterDOMProps(fieldProps, { + omitDataProps: true, + }), + fieldPropsProp, + props, + ), ref: domRef, "data-slot": "input", className: slots.input({ @@ -252,6 +275,7 @@ export function useDateInput(originalProps: UseDateInputPro const getInputWrapperProps: PropGetter = (props) => { return { ...props, + ...groupProps, "data-slot": "input-wrapper", className: slots.inputWrapper({ class: classNames?.inputWrapper, @@ -282,8 +306,7 @@ export function useDateInput(originalProps: UseDateInputPro const getErrorMessageProps: PropGetter = (props = {}) => { return { - ...props, - ...errorMessageProps, + ...mergeProps(errorMessageProps, errorMessagePropsProp, props), "data-slot": "error-message", className: slots.errorMessage({class: clsx(classNames?.errorMessage, props?.className)}), }; @@ -291,8 +314,7 @@ export function useDateInput(originalProps: UseDateInputPro const getDescriptionProps: PropGetter = (props = {}) => { return { - ...props, - ...descriptionProps, + ...mergeProps(descriptionProps, descriptionPropsProp, props), "data-slot": "description", className: slots.description({class: clsx(classNames?.description, props?.className)}), }; @@ -303,9 +325,9 @@ export function useDateInput(originalProps: UseDateInputPro state, domRef, slots, + label, hasHelper, shouldLabelBeOutside, - label: originalProps?.label, classNames, description, errorMessage, diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index ac87c20e4d..64604c9cd5 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -1,19 +1,341 @@ +/* eslint-disable jsx-a11y/no-autofocus */ import * as React from "react"; -import {render} from "@testing-library/react"; +import {render, act, fireEvent, waitFor} from "@testing-library/react"; +import {pointerMap, triggerPress} from "@nextui-org/test-utils"; +import userEvent from "@testing-library/user-event"; +import {CalendarDate, CalendarDateTime} from "@internationalized/date"; -import {DatePicker} from "../src"; +import {DatePicker as DatePickerBase, DatePickerProps} from "../src"; + +/** + * Custom date-picker to disable animations and avoid issues with react-motion and jest + */ +const DatePicker = React.forwardRef((props: DatePickerProps, ref: React.Ref) => { + return ; +}); + +DatePicker.displayName = "DatePicker"; + +function getTextValue(el: any) { + if ( + el.className?.includes?.("DatePicker-placeholder") && + el.attributes?.getNamedItem("data-placeholder")?.value === "true" + ) { + return ""; + } + + return [...el.childNodes] + .map((el) => (el.nodeType === 3 ? el.textContent : getTextValue(el))) + .join(""); +} describe("DatePicker", () => { - it("should render correctly", () => { - const wrapper = render(); + let user; + + beforeAll(() => { + user = userEvent.setup({delay: null, pointerMap}); + jest.useFakeTimers(); + }); + afterEach(() => { + act(() => { + jest.runAllTimers(); + }); + }); + + describe("Basics", () => { + it("should render correctly", () => { + const wrapper = render(); + + expect(() => wrapper.unmount()).not.toThrow(); + }); + + it("ref should be forwarded", () => { + const ref = React.createRef(); + + render(); + expect(ref.current).not.toBeNull(); + }); + + it("should render a datepicker with a specified date", function () { + let {getAllByRole} = render(); + + let combobox = getAllByRole("group")[0]; + + expect(combobox).toBeVisible(); + expect(combobox).not.toHaveAttribute("aria-disabled"); + expect(combobox).not.toHaveAttribute("aria-invalid"); + + let segments = getAllByRole("spinbutton"); + + expect(segments.length).toBe(3); + + expect(getTextValue(segments[0])).toBe("2"); + expect(segments[0].getAttribute("aria-label")).toBe("month, "); + expect(segments[0].getAttribute("aria-valuenow")).toBe("2"); + expect(segments[0].getAttribute("aria-valuetext")).toBe("2 – February"); + expect(segments[0].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[0].getAttribute("aria-valuemax")).toBe("12"); + + expect(getTextValue(segments[1])).toBe("3"); + expect(segments[1].getAttribute("aria-label")).toBe("day, "); + expect(segments[1].getAttribute("aria-valuenow")).toBe("3"); + expect(segments[1].getAttribute("aria-valuetext")).toBe("3"); + expect(segments[1].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[1].getAttribute("aria-valuemax")).toBe("28"); + + expect(getTextValue(segments[2])).toBe("2019"); + expect(segments[2].getAttribute("aria-label")).toBe("year, "); + expect(segments[2].getAttribute("aria-valuenow")).toBe("2019"); + expect(segments[2].getAttribute("aria-valuetext")).toBe("2019"); + expect(segments[2].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[2].getAttribute("aria-valuemax")).toBe("9999"); + }); + + it('should render a datepicker with granularity="second"', function () { + let {getAllByRole} = render( + , + ); + + let combobox = getAllByRole("group")[0]; + + expect(combobox).toBeVisible(); + expect(combobox).not.toHaveAttribute("aria-disabled"); + expect(combobox).not.toHaveAttribute("aria-invalid"); + + let segments = getAllByRole("spinbutton"); + + expect(segments.length).toBe(7); + + expect(getTextValue(segments[0])).toBe("2"); + expect(segments[0].getAttribute("aria-label")).toBe("month, "); + expect(segments[0].getAttribute("aria-valuenow")).toBe("2"); + expect(segments[0].getAttribute("aria-valuetext")).toBe("2 – February"); + expect(segments[0].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[0].getAttribute("aria-valuemax")).toBe("12"); + + expect(getTextValue(segments[1])).toBe("3"); + expect(segments[1].getAttribute("aria-label")).toBe("day, "); + expect(segments[1].getAttribute("aria-valuenow")).toBe("3"); + expect(segments[1].getAttribute("aria-valuetext")).toBe("3"); + expect(segments[1].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[1].getAttribute("aria-valuemax")).toBe("28"); + + expect(getTextValue(segments[2])).toBe("2019"); + expect(segments[2].getAttribute("aria-label")).toBe("year, "); + expect(segments[2].getAttribute("aria-valuenow")).toBe("2019"); + expect(segments[2].getAttribute("aria-valuetext")).toBe("2019"); + expect(segments[2].getAttribute("aria-valuemin")).toBe("1"); + expect(segments[2].getAttribute("aria-valuemax")).toBe("9999"); + + expect(getTextValue(segments[3])).toBe("12"); + expect(segments[3].getAttribute("aria-label")).toBe("hour, "); + expect(segments[3].getAttribute("aria-valuenow")).toBe("0"); + expect(segments[3].getAttribute("aria-valuetext")).toBe("12 AM"); + expect(segments[3].getAttribute("aria-valuemin")).toBe("0"); + expect(segments[3].getAttribute("aria-valuemax")).toBe("11"); + + expect(getTextValue(segments[4])).toBe("00"); + expect(segments[4].getAttribute("aria-label")).toBe("minute, "); + expect(segments[4].getAttribute("aria-valuenow")).toBe("0"); + expect(segments[4].getAttribute("aria-valuetext")).toBe("00"); + expect(segments[4].getAttribute("aria-valuemin")).toBe("0"); + expect(segments[4].getAttribute("aria-valuemax")).toBe("59"); + + expect(getTextValue(segments[5])).toBe("00"); + expect(segments[5].getAttribute("aria-label")).toBe("second, "); + expect(segments[5].getAttribute("aria-valuenow")).toBe("0"); + expect(segments[5].getAttribute("aria-valuetext")).toBe("00"); + expect(segments[5].getAttribute("aria-valuemin")).toBe("0"); + expect(segments[5].getAttribute("aria-valuemax")).toBe("59"); + + expect(getTextValue(segments[6])).toBe("AM"); + expect(segments[6].getAttribute("aria-label")).toBe("AM/PM, "); + expect(segments[6].getAttribute("aria-valuetext")).toBe("AM"); + }); + + it("should support autoFocus", function () { + let {getAllByRole} = render(); - expect(() => wrapper.unmount()).not.toThrow(); + expect(document.activeElement).toBe(getAllByRole("spinbutton")[0]); + }); + + it("should pass through data attributes", function () { + let {getByTestId} = render(); + + expect(getByTestId("foo")).toHaveAttribute("role", "group"); + }); }); - it("ref should be forwarded", () => { - const ref = React.createRef(); + describe("Events", () => { + let onBlurSpy = jest.fn(); + let onFocusChangeSpy = jest.fn(); + let onFocusSpy = jest.fn(); + let onKeyDownSpy = jest.fn(); + let onKeyUpSpy = jest.fn(); + + afterEach(() => { + onBlurSpy.mockClear(); + onFocusChangeSpy.mockClear(); + onFocusSpy.mockClear(); + onKeyDownSpy.mockClear(); + onKeyUpSpy.mockClear(); + }); + + it("should focus field, move a segment, and open popover and does not blur", async function () { + let {getByRole, getAllByRole} = render( + , + ); + let segments = getAllByRole("spinbutton"); + let button = getByRole("button"); + + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).not.toHaveBeenCalled(); + expect(onFocusSpy).not.toHaveBeenCalled(); + + await user.tab(); + expect(segments[0]).toHaveFocus(); + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); + expect(onFocusSpy).toHaveBeenCalledTimes(1); + + await user.tab(); + expect(segments[1]).toHaveFocus(); + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); + expect(onFocusSpy).toHaveBeenCalledTimes(1); + + triggerPress(button); + act(() => jest.runAllTimers()); + + let dialog = getByRole("dialog"); + + expect(dialog).toBeVisible(); + }); + + it("should focus field and leave to blur", async function () { + let {getAllByRole} = render( + , + ); + let segments = getAllByRole("spinbutton"); + + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).not.toHaveBeenCalled(); + expect(onFocusSpy).not.toHaveBeenCalled(); + + await user.tab(); + expect(segments[0]).toHaveFocus(); + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); + expect(onFocusSpy).toHaveBeenCalledTimes(1); + + await user.click(document.body); + expect(document.body).toHaveFocus(); + expect(onBlurSpy).toHaveBeenCalledTimes(1); + expect(onFocusChangeSpy).toHaveBeenCalledTimes(2); + expect(onFocusSpy).toHaveBeenCalledTimes(1); + }); + + it("should open popover and call picker onFocus", function () { + let {getByRole} = render( + , + ); + let button = getByRole("button"); + + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).not.toHaveBeenCalled(); + expect(onFocusSpy).not.toHaveBeenCalled(); + + triggerPress(button); + act(() => jest.runAllTimers()); + + let dialog = getByRole("dialog"); + + expect(dialog).toBeVisible(); + expect(onBlurSpy).not.toHaveBeenCalled(); + }); + + it("should open and close popover and only call blur when focus leaves picker", async function () { + let {getByRole} = render( + , + ); + let button = getByRole("button"); + + expect(onBlurSpy).not.toHaveBeenCalled(); + expect(onFocusChangeSpy).not.toHaveBeenCalled(); + expect(onFocusSpy).not.toHaveBeenCalled(); + + triggerPress(button); + act(() => jest.runAllTimers()); + + let dialog = getByRole("dialog"); + + expect(dialog).toBeVisible(); + + //@ts-ignore + fireEvent.keyDown(document.activeElement, {key: "Escape"}); + //@ts-ignore + fireEvent.keyUp(document.activeElement, {key: "Escape"}); + act(() => jest.runAllTimers()); + + await waitFor(() => { + expect(dialog).not.toBeInTheDocument(); + }); // wait for animation + + // now that it's been unmounted, run the raf callback + act(() => { + jest.runAllTimers(); + }); + + expect(dialog).not.toBeInTheDocument(); + expect(document.activeElement).toBe(button); + expect(button).toHaveFocus(); + + await user.tab(); + expect(document.body).toHaveFocus(); + }); + + it("should trigger right arrow key event for segment navigation", async function () { + let {getAllByRole} = render( + , + ); + let segments = getAllByRole("spinbutton"); + + expect(onKeyDownSpy).not.toHaveBeenCalled(); + expect(onKeyUpSpy).not.toHaveBeenCalled(); + + await user.tab(); + expect(segments[0]).toHaveFocus(); + expect(onKeyDownSpy).not.toHaveBeenCalled(); + expect(onKeyUpSpy).toHaveBeenCalledTimes(2); + + // @ts-ignore + fireEvent.keyDown(document.activeElement, {key: "ArrowRight"}); + // @ts-ignore + fireEvent.keyUp(document.activeElement, {key: "ArrowRight"}); - render(); - expect(ref.current).not.toBeNull(); + expect(segments[1]).toHaveFocus(); + expect(onKeyDownSpy).toHaveBeenCalledTimes(1); + expect(onKeyUpSpy).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/packages/components/date-picker/package.json b/packages/components/date-picker/package.json index c448467f9f..ca8e7dc915 100644 --- a/packages/components/date-picker/package.json +++ b/packages/components/date-picker/package.json @@ -61,6 +61,7 @@ "@nextui-org/system": "workspace:*", "@nextui-org/theme": "workspace:*", "@nextui-org/radio": "workspace:*", + "@nextui-org/test-utils": "workspace:*", "clean-package": "2.2.0", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index ae1e7cdb46..dd5ef775ac 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -17,11 +17,9 @@ export interface Props function DatePicker(props: Props, ref: ForwardedRef) { const { - Component, state, selectorIcon, disableAnimation, - getBaseProps, getDateInputProps, getPopoverProps, getSelectorButtonProps, @@ -48,13 +46,13 @@ function DatePicker(props: Props, ref: ForwardedRef + <> {selectorContent}} /> {disableAnimation ? popoverContent : {popoverContent}} - + ); } diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index a281674b61..d020ddea30 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -6,18 +6,18 @@ import type {DatePickerState} from "@react-stately/datepicker"; import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; -import type {ReactNode} from "react"; -import {DOMAttributes, PropGetter, useProviderContext} from "@nextui-org/system"; +import {ReactNode} from "react"; +import {DOMAttributes, useProviderContext} from "@nextui-org/system"; import {CalendarDate} from "@internationalized/date"; import {useDatePickerState} from "@react-stately/datepicker"; import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; import {datePicker} from "@nextui-org/theme"; -import {chain, mergeProps} from "@react-aria/utils"; -import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; +import {mergeProps} from "@react-aria/utils"; +import {useDOMRef} from "@nextui-org/react-utils"; import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; -import {useMemo, useRef} from "react"; +import {useMemo} from "react"; type NextUIBaseProps = Omit< HTMLNextUIProps<"div">, @@ -25,10 +25,6 @@ type NextUIBaseProps = Omit< >; interface Props extends NextUIBaseProps { - /** - * Ref to the DOM node. - */ - ref?: ReactRef; /** * The icon to toggle the date picker popover. Usually a calendar icon. */ @@ -108,7 +104,7 @@ interface Props extends NextUIBaseProps { export type UseDatePickerProps = Props & DatePickerVariantProps & - DateInputProps; + Omit, "groupProps" | "fieldProps" | "labelProps" | "errorMessageProps">; export function useDatePicker(originalProps: UseDatePickerProps) { const [props, variantProps] = mapPropsVariants(originalProps, datePicker.variantKeys); @@ -117,7 +113,6 @@ export function useDatePicker(originalProps: UseDatePickerP const { ref, - as, selectorIcon, visibleMonths = 1, pageBehavior = "visible", @@ -137,10 +132,7 @@ export function useDatePicker(originalProps: UseDatePickerP ...otherProps } = props; - const Component = as || "div"; - const domRef = useDOMRef(ref); - let targetRef = useRef(null); let state: DatePickerState = useDatePickerState({ ...originalProps, @@ -149,34 +141,32 @@ export function useDatePicker(originalProps: UseDatePickerP shouldCloseOnSelect: () => !state.hasTime, }); - let {groupProps, fieldProps, buttonProps, dialogProps, calendarProps} = useAriaDatePicker( - originalProps, - state, - targetRef, - ); + let { + groupProps, + labelProps, + fieldProps, + buttonProps, + dialogProps, + calendarProps, + descriptionProps, + errorMessageProps, + } = useAriaDatePicker(originalProps, state, domRef); const baseStyles = clsx(classNames?.base, className); const isDefaultColor = originalProps.color === "default" || !originalProps.color; const hasMultipleMonths = visibleMonths > 1; + // delete event handlers since they are handled by the useAriaDatePicker hook + delete otherProps.onBlur; + delete otherProps.onFocus; + delete otherProps.onFocusChange; + const slotsProps: { - inputProps: DateInputProps; popoverProps: UseDatePickerProps["popoverProps"]; selectorButtonProps: ButtonProps; calendarProps: CalendarProps; } = { - inputProps: mergeProps( - { - ref: targetRef, - minValue, - maxValue, - fullWidth: true, - isClearable: false, - disableAnimation, - }, - otherProps, - ), popoverProps: mergeProps( { offset: 13, @@ -198,6 +188,7 @@ export function useDatePicker(originalProps: UseDatePickerP ), calendarProps: mergeProps( { + showHelper: false, visibleMonths, pageBehavior, isDateUnavailable, @@ -225,25 +216,35 @@ export function useDatePicker(originalProps: UseDatePickerP [objectToDeps(variantProps), hasMultipleMonths, className], ); - const getBaseProps: PropGetter = () => ({ - ...groupProps, - "data-invalid": dataAttr(originalProps?.isInvalid), - "data-open": dataAttr(state.isOpen), - className: slots.base({class: baseStyles}), - }); - const getDateInputProps = () => { return { - ...mergeProps(fieldProps, slotsProps.inputProps), - onClick: chain(slotsProps.inputProps.onClick, otherProps.onClick), + ref: domRef, + groupProps, + labelProps, + errorMessageProps, + descriptionProps, + ...mergeProps( + fieldProps, + { + minValue, + maxValue, + fullWidth: true, + disableAnimation, + }, + otherProps, + ), + "data-invalid": dataAttr(originalProps?.isInvalid), + "data-open": dataAttr(state.isOpen), + className: slots.base({class: baseStyles}), } as unknown as DateInputProps; }; const getPopoverProps = (props: DOMAttributes = {}) => { return { state, - ...mergeProps(slotsProps.popoverProps, dialogProps, props), - triggerRef: targetRef, + dialogProps, + ...mergeProps(slotsProps.popoverProps, props), + triggerRef: domRef, classNames: { content: slots.popoverContent({ class: clsx( @@ -295,13 +296,10 @@ export function useDatePicker(originalProps: UseDatePickerP return { state, - Component, - domRef, selectorIcon, disableAnimation, CalendarTopContent, CalendarBottomContent, - getBaseProps, getDateInputProps, getPopoverProps, getSelectorButtonProps, diff --git a/packages/components/date-picker/stories/date-picker.stories.tsx b/packages/components/date-picker/stories/date-picker.stories.tsx index 116f569fd5..2025895a1d 100644 --- a/packages/components/date-picker/stories/date-picker.stories.tsx +++ b/packages/components/date-picker/stories/date-picker.stories.tsx @@ -79,7 +79,7 @@ const defaultProps = { const Template = (args: DatePickerProps) => ; const LabelPlacementTemplate = (args: DatePickerProps) => ( -
+
@@ -105,7 +105,7 @@ const ControlledTemplate = (args: DatePickerProps) => { }; const TimeZonesTemplate = (args: DatePickerProps) => ( -
+
( labelPlacement="outside" /> ((props, _) => { onClose, } = usePopoverContext(); + const dialogProps = getDialogProps(otherProps); + + // Not needed in the popover context, the popover role comes from getPopoverProps + delete dialogProps.role; + const Component = as || OverlayComponent || "div"; const content = ( <> {!isNonModal && } - +
{typeof children === "function" ? children(titleProps) : children}
diff --git a/packages/components/popover/src/use-popover.ts b/packages/components/popover/src/use-popover.ts index fe3b094adc..d64e1c4cd8 100644 --- a/packages/components/popover/src/use-popover.ts +++ b/packages/components/popover/src/use-popover.ts @@ -173,9 +173,6 @@ export function usePopover(originalProps: UsePopoverProps) { const {dialogProps, titleProps} = useDialog({}, dialogRef); - // Not needed in the popover context, the popover role comes from getPopoverProps - delete dialogProps.role; - const slots = useMemo( () => popover({ diff --git a/packages/utilities/react-rsc-utils/src/filter-dom-props.ts b/packages/utilities/react-rsc-utils/src/filter-dom-props.ts index 24900279f5..02a6a5d41f 100644 --- a/packages/utilities/react-rsc-utils/src/filter-dom-props.ts +++ b/packages/utilities/react-rsc-utils/src/filter-dom-props.ts @@ -23,6 +23,14 @@ interface Options { * A Set of event names that should be excluded from the filter. */ omitEventNames?: Set; + /** + * Whether to omit data-* props. + */ + omitDataProps?: boolean; + /** + * Whether to omit event props. + */ + omitEventProps?: boolean; } const propRe = /^(data-.*)$/; @@ -38,7 +46,15 @@ export function filterDOMProps( props: DOMProps & AriaLabelingProps, opts: Options = {}, ): DOMProps & AriaLabelingProps { - let {labelable = true, enabled = true, propNames, omitPropNames, omitEventNames} = opts; + let { + labelable = true, + enabled = true, + propNames, + omitPropNames, + omitEventNames, + omitDataProps, + omitEventProps, + } = opts; let filteredProps = {}; if (!enabled) { @@ -57,6 +73,14 @@ export function filterDOMProps( continue; } + if (omitDataProps && propRe.test(prop)) { + continue; + } + + if (omitEventProps && funcRe.test(prop)) { + continue; + } + if ( (Object.prototype.hasOwnProperty.call(props, prop) && (DOMPropNames.has(prop) || diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5824fae56..70e4e9a61a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1370,6 +1370,9 @@ importers: '@nextui-org/system': specifier: workspace:* version: link:../../core/system + '@nextui-org/test-utils': + specifier: workspace:* + version: link:../../utilities/test-utils '@nextui-org/theme': specifier: workspace:* version: link:../../core/theme From 2c8b280135432066b213455aeae05e90c9a262f8 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sun, 7 Apr 2024 09:58:33 -0300 Subject: [PATCH 10/12] fix(date-picker): test and styles --- .../autocomplete/src/autocomplete.tsx | 7 +--- .../date-input/__tests__/date-input.test.tsx | 38 +++++++++++-------- .../date-input/src/use-date-input.ts | 24 +----------- .../__tests__/date-picker.test.tsx | 36 ++++++++++++++---- .../date-picker/src/use-date-picker.ts | 23 ++++------- .../core/theme/src/components/date-picker.ts | 1 + 6 files changed, 62 insertions(+), 67 deletions(-) diff --git a/packages/components/autocomplete/src/autocomplete.tsx b/packages/components/autocomplete/src/autocomplete.tsx index ac2aa0743e..b60dc6b7b4 100644 --- a/packages/components/autocomplete/src/autocomplete.tsx +++ b/packages/components/autocomplete/src/autocomplete.tsx @@ -32,12 +32,7 @@ function Autocomplete(props: Props, ref: ForwardedRef({...props, ref}); const popoverContent = isOpen ? ( - false} - state={state} - > + diff --git a/packages/components/date-input/__tests__/date-input.test.tsx b/packages/components/date-input/__tests__/date-input.test.tsx index 7cebfa60b2..aa48657e87 100644 --- a/packages/components/date-input/__tests__/date-input.test.tsx +++ b/packages/components/date-input/__tests__/date-input.test.tsx @@ -66,12 +66,12 @@ describe("DateInput", () => { />, ); - await act(() => { - user.tab(); + await act(async () => { + await user.tab(); }); - await act(() => { - user.keyboard("01011980"); + await act(async () => { + await user.keyboard("01011980"); }); expect(tree.getByText("Date unavailable.")).toBeInTheDocument(); @@ -206,15 +206,17 @@ describe("DateInput", () => { expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).not.toHaveBeenCalled(); expect(onFocusSpy).not.toHaveBeenCalled(); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(segments[0]).toHaveFocus(); expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); expect(onFocusSpy).toHaveBeenCalledTimes(1); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(segments[1]).toHaveFocus(); expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); @@ -235,18 +237,22 @@ describe("DateInput", () => { expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).not.toHaveBeenCalled(); expect(onFocusSpy).not.toHaveBeenCalled(); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(segments[0]).toHaveFocus(); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(segments[1]).toHaveFocus(); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(segments[2]).toHaveFocus(); expect(onBlurSpy).toHaveBeenCalledTimes(0); - - await user.tab(); + await act(async () => { + await user.tab(); + }); expect(onBlurSpy).toHaveBeenCalledTimes(1); expect(onFocusChangeSpy).toHaveBeenCalledTimes(2); expect(onFocusSpy).toHaveBeenCalledTimes(1); diff --git a/packages/components/date-input/src/use-date-input.ts b/packages/components/date-input/src/use-date-input.ts index 63a5e95287..08d0a19a31 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 {CalendarDate} from "@internationalized/date"; import {mergeProps} from "@react-aria/utils"; import {PropGetter, useProviderContext} from "@nextui-org/system"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; -import {useDOMRef, filterDOMProps} from "@nextui-org/react-utils"; +import {useDOMRef} from "@nextui-org/react-utils"; import {useDateField as useAriaDateField} from "@react-aria/datepicker"; import {useDateFieldState} from "@react-stately/datepicker"; import {createCalendar} from "@internationalized/date"; @@ -138,14 +138,12 @@ export function useDateInput(originalProps: UseDateInputPro createCalendar: createCalendarProp = providerContext?.createCalendar ?? null, isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false, errorMessage: errorMessageProp, - ...otherProps } = props; const domRef = useDOMRef(ref); const inputRef = useDOMRef(inputRefProp); const Component = as || "div"; - const shouldFilterDOMProps = typeof Component === "string"; const {locale} = useLocale(); const state = useDateFieldState({ @@ -212,15 +210,6 @@ export function useDateInput(originalProps: UseDateInputPro ); const getBaseProps: PropGetter = () => { - // filter other props that are included in fieldProps to avoid duplication - const filteredUserProps = Object.keys(otherProps).reduce((acc, key) => { - if (!fieldProps[key as keyof typeof fieldProps]) { - acc[key] = otherProps[key as keyof typeof otherProps]; - } - - return acc; - }, {} as Record); - return { "data-slot": "base", "data-has-helper": dataAttr(hasHelper), @@ -231,9 +220,6 @@ export function useDateInput(originalProps: UseDateInputPro "data-has-start-content": dataAttr(!!startContent), "data-has-end-content": dataAttr(!!endContent), className: slots.base({class: baseStyles}), - ...filterDOMProps(filteredUserProps, { - enabled: shouldFilterDOMProps, - }), }; }; @@ -257,15 +243,9 @@ export function useDateInput(originalProps: UseDateInputPro const getFieldProps: PropGetter = (props) => { return { - ...mergeProps( - filterDOMProps(fieldProps, { - omitDataProps: true, - }), - fieldPropsProp, - props, - ), ref: domRef, "data-slot": "input", + ...mergeProps(fieldProps, fieldPropsProp, props), className: slots.input({ class: clsx(classNames?.input, props?.className), }), diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 64604c9cd5..8ceb4c0857 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -197,19 +197,26 @@ describe("DatePicker", () => { expect(onFocusChangeSpy).not.toHaveBeenCalled(); expect(onFocusSpy).not.toHaveBeenCalled(); - await user.tab(); + await act(async () => { + await user.tab(); + }); + expect(segments[0]).toHaveFocus(); expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); expect(onFocusSpy).toHaveBeenCalledTimes(1); - await user.tab(); + await act(async () => { + await user.tab(); + }); + expect(segments[1]).toHaveFocus(); expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); expect(onFocusSpy).toHaveBeenCalledTimes(1); triggerPress(button); + act(() => jest.runAllTimers()); let dialog = getByRole("dialog"); @@ -232,13 +239,19 @@ describe("DatePicker", () => { expect(onFocusChangeSpy).not.toHaveBeenCalled(); expect(onFocusSpy).not.toHaveBeenCalled(); - await user.tab(); + await act(async () => { + await user.tab(); + }); + expect(segments[0]).toHaveFocus(); expect(onBlurSpy).not.toHaveBeenCalled(); expect(onFocusChangeSpy).toHaveBeenCalledTimes(1); expect(onFocusSpy).toHaveBeenCalledTimes(1); - await user.click(document.body); + await act(() => { + user.click(document.body); + }); + expect(document.body).toHaveFocus(); expect(onBlurSpy).toHaveBeenCalledTimes(1); expect(onFocusChangeSpy).toHaveBeenCalledTimes(2); @@ -254,6 +267,7 @@ describe("DatePicker", () => { onFocusChange={onFocusChangeSpy} />, ); + let button = getByRole("button"); expect(onBlurSpy).not.toHaveBeenCalled(); @@ -261,6 +275,7 @@ describe("DatePicker", () => { expect(onFocusSpy).not.toHaveBeenCalled(); triggerPress(button); + act(() => jest.runAllTimers()); let dialog = getByRole("dialog"); @@ -295,6 +310,7 @@ describe("DatePicker", () => { fireEvent.keyDown(document.activeElement, {key: "Escape"}); //@ts-ignore fireEvent.keyUp(document.activeElement, {key: "Escape"}); + act(() => jest.runAllTimers()); await waitFor(() => { @@ -310,7 +326,10 @@ describe("DatePicker", () => { expect(document.activeElement).toBe(button); expect(button).toHaveFocus(); - await user.tab(); + await act(async () => { + await user.tab(); + }); + expect(document.body).toHaveFocus(); }); @@ -323,10 +342,13 @@ describe("DatePicker", () => { expect(onKeyDownSpy).not.toHaveBeenCalled(); expect(onKeyUpSpy).not.toHaveBeenCalled(); - await user.tab(); + await act(async () => { + await user.tab(); + }); + expect(segments[0]).toHaveFocus(); expect(onKeyDownSpy).not.toHaveBeenCalled(); - expect(onKeyUpSpy).toHaveBeenCalledTimes(2); + expect(onKeyUpSpy).toHaveBeenCalledTimes(1); // @ts-ignore fireEvent.keyDown(document.activeElement, {key: "ArrowRight"}); diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index d020ddea30..cb55795e5a 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -129,7 +129,6 @@ export function useDatePicker(originalProps: UseDatePickerP disableAnimation = false, className, classNames, - ...otherProps } = props; const domRef = useDOMRef(ref); @@ -157,11 +156,6 @@ export function useDatePicker(originalProps: UseDatePickerP const isDefaultColor = originalProps.color === "default" || !originalProps.color; const hasMultipleMonths = visibleMonths > 1; - // delete event handlers since they are handled by the useAriaDatePicker hook - delete otherProps.onBlur; - delete otherProps.onFocus; - delete otherProps.onFocusChange; - const slotsProps: { popoverProps: UseDatePickerProps["popoverProps"]; selectorButtonProps: ButtonProps; @@ -223,16 +217,12 @@ export function useDatePicker(originalProps: UseDatePickerP labelProps, errorMessageProps, descriptionProps, - ...mergeProps( - fieldProps, - { - minValue, - maxValue, - fullWidth: true, - disableAnimation, - }, - otherProps, - ), + ...mergeProps(fieldProps, { + minValue, + maxValue, + fullWidth: true, + disableAnimation, + }), "data-invalid": dataAttr(originalProps?.isInvalid), "data-open": dataAttr(state.isOpen), className: slots.base({class: baseStyles}), @@ -264,6 +254,7 @@ export function useDatePicker(originalProps: UseDatePickerP "data-slot": "calendar", classNames: { base: slots.calendar({class: classNames?.calendar}), + content: slots.calendarContent({class: classNames?.calendarContent}), }, style: mergeProps( hasMultipleMonths diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index df911638e2..6f3d8ab53a 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -14,6 +14,7 @@ const datePicker = tv({ selectorIcon: "text-lg text-inherit pointer-events-none flex-shrink-0", popoverContent: "p-0 w-full", calendar: "w-[var(--calendar-width)] shadow-none", + calendarContent: "w-[var(--calendar-width)]", }, variants: { // @internal From db01ba241ac9f61b1300a0dfd706873b79771163 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Sun, 7 Apr 2024 10:01:50 -0300 Subject: [PATCH 11/12] chore(date-picker): calendar popover tests added --- .../__tests__/date-picker.test.tsx | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 8ceb4c0857..79b1b0ebdf 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -360,4 +360,80 @@ describe("DatePicker", () => { expect(onKeyUpSpy).toHaveBeenCalledTimes(2); }); }); + + describe("Calendar popover", function () { + it("should emit onChange when selecting a date in the calendar in controlled mode", function () { + let onChange = jest.fn(); + let {getByRole, getAllByRole, queryByLabelText} = render( + , + ); + + let combobox = getAllByRole("group")[0]; + + expect(getTextValue(combobox)).toBe("2/3/2019"); + + let button = getByRole("button"); + + triggerPress(button); + + let dialog = getByRole("dialog"); + + expect(dialog).toBeVisible(); + + expect(queryByLabelText("Time")).toBeNull(); + + let cells = getAllByRole("gridcell"); + let selected = cells.find((cell) => cell.getAttribute("aria-selected") === "true"); + + // @ts-ignore + expect(selected.children[0]).toHaveAttribute( + "aria-label", + "Sunday, February 3, 2019 selected", + ); + + // @ts-ignore + triggerPress(selected.nextSibling.children[0]); + + expect(dialog).not.toBeInTheDocument(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(new CalendarDate(2019, 2, 4)); + expect(getTextValue(combobox)).toBe("2/3/2019"); // controlled + }); + + it("should emit onChange when selecting a date in the calendar in uncontrolled mode", function () { + let onChange = jest.fn(); + let {getByRole, getAllByRole} = render( + , + ); + + let combobox = getAllByRole("group")[0]; + + expect(getTextValue(combobox)).toBe("2/3/2019"); + + let button = getByRole("button"); + + triggerPress(button); + + let dialog = getByRole("dialog"); + + expect(dialog).toBeVisible(); + + let cells = getAllByRole("gridcell"); + let selected = cells.find((cell) => cell.getAttribute("aria-selected") === "true"); + + // @ts-ignore + expect(selected.children[0]).toHaveAttribute( + "aria-label", + "Sunday, February 3, 2019 selected", + ); + + // @ts-ignore + triggerPress(selected.nextSibling.children[0]); + + expect(dialog).not.toBeInTheDocument(); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(new CalendarDate(2019, 2, 4)); + expect(getTextValue(combobox)).toBe("2/4/2019"); // uncontrolled + }); + }); }); From 1d537e90fd05c47d6c55ea54140b1e5d9422a7cd Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Mon, 8 Apr 2024 11:42:47 -0300 Subject: [PATCH 12/12] fix(date-picker): props to be passed to the date-input --- .../__tests__/date-picker.test.tsx | 10 ++- .../date-picker/src/date-picker.tsx | 3 +- .../date-picker/src/use-date-picker.ts | 62 +++++++++++++++---- .../core/theme/src/components/date-picker.ts | 5 +- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/packages/components/date-picker/__tests__/date-picker.test.tsx b/packages/components/date-picker/__tests__/date-picker.test.tsx index 79b1b0ebdf..9a8e39e247 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.tsx +++ b/packages/components/date-picker/__tests__/date-picker.test.tsx @@ -11,7 +11,15 @@ import {DatePicker as DatePickerBase, DatePickerProps} from "../src"; * Custom date-picker to disable animations and avoid issues with react-motion and jest */ const DatePicker = React.forwardRef((props: DatePickerProps, ref: React.Ref) => { - return ; + return ( + + ); }); DatePicker.displayName = "DatePicker"; diff --git a/packages/components/date-picker/src/date-picker.tsx b/packages/components/date-picker/src/date-picker.tsx index dd5ef775ac..ac29e03bde 100644 --- a/packages/components/date-picker/src/date-picker.tsx +++ b/packages/components/date-picker/src/date-picker.tsx @@ -18,6 +18,7 @@ export interface Props function DatePicker(props: Props, ref: ForwardedRef) { const { state, + endContent, selectorIcon, disableAnimation, getDateInputProps, @@ -49,7 +50,7 @@ function DatePicker(props: Props, ref: ForwardedRef {selectorContent}} + endContent={} /> {disableAnimation ? popoverContent : {popoverContent}} diff --git a/packages/components/date-picker/src/use-date-picker.ts b/packages/components/date-picker/src/use-date-picker.ts index cb55795e5a..3ce116fede 100644 --- a/packages/components/date-picker/src/use-date-picker.ts +++ b/packages/components/date-picker/src/use-date-picker.ts @@ -1,4 +1,3 @@ -import type {DatePickerVariantProps, DatePickerSlots, SlotsToClasses} from "@nextui-org/theme"; import type {DateValue} from "@internationalized/date"; import type {AriaDatePickerProps} from "@react-types/datepicker"; import type {DateInputProps} from "@nextui-org/date-input"; @@ -7,9 +6,14 @@ import type {ButtonProps} from "@nextui-org/button"; import type {CalendarProps} from "@nextui-org/calendar"; import type {PopoverProps} from "@nextui-org/popover"; +import { + DatePickerVariantProps, + DatePickerSlots, + SlotsToClasses, + dateInput, +} from "@nextui-org/theme"; import {ReactNode} from "react"; -import {DOMAttributes, useProviderContext} from "@nextui-org/system"; -import {CalendarDate} from "@internationalized/date"; +import {DOMAttributes} from "@nextui-org/system"; import {useDatePickerState} from "@react-stately/datepicker"; import {useDatePicker as useAriaDatePicker} from "@react-aria/datepicker"; import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system"; @@ -95,11 +99,22 @@ interface Props extends NextUIBaseProps { * ```ts * * ``` */ - classNames?: SlotsToClasses; + classNames?: SlotsToClasses & DateInputProps["classNames"]; } export type UseDatePickerProps = Props & @@ -107,31 +122,41 @@ export type UseDatePickerProps = Props & Omit, "groupProps" | "fieldProps" | "labelProps" | "errorMessageProps">; export function useDatePicker(originalProps: UseDatePickerProps) { - const [props, variantProps] = mapPropsVariants(originalProps, datePicker.variantKeys); - - const providerContext = useProviderContext(); + const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys); const { + as, ref, + label, selectorIcon, + inputRef, + isInvalid, + errorMessage, + description, + startContent, + endContent, + validationState, + validationBehavior, visibleMonths = 1, pageBehavior = "visible", calendarWidth = 256, isDateUnavailable, + shouldForceLeadingZeros, showMonthAndYearPickers = false, popoverProps = {}, selectorButtonProps = {}, calendarProps: userCalendarProps = {}, CalendarTopContent, CalendarBottomContent, - minValue = providerContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1), - maxValue = providerContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31), - disableAnimation = false, + minValue, + maxValue, + createCalendar, className, classNames, } = props; const domRef = useDOMRef(ref); + const disableAnimation = originalProps.disableAnimation ?? false; let state: DatePickerState = useDatePickerState({ ...originalProps, @@ -212,12 +237,23 @@ export function useDatePicker(originalProps: UseDatePickerP const getDateInputProps = () => { return { + as, + label, ref: domRef, + inputRef, + description, + startContent, + validationState, + validationBehavior, + shouldForceLeadingZeros, + isInvalid, + errorMessage, groupProps, labelProps, + createCalendar, errorMessageProps, descriptionProps, - ...mergeProps(fieldProps, { + ...mergeProps(variantProps, fieldProps, { minValue, maxValue, fullWidth: true, @@ -226,6 +262,7 @@ export function useDatePicker(originalProps: UseDatePickerP "data-invalid": dataAttr(originalProps?.isInvalid), "data-open": dataAttr(state.isOpen), className: slots.base({class: baseStyles}), + classNames, } as unknown as DateInputProps; }; @@ -287,6 +324,7 @@ export function useDatePicker(originalProps: UseDatePickerP return { state, + endContent, selectorIcon, disableAnimation, CalendarTopContent, diff --git a/packages/core/theme/src/components/date-picker.ts b/packages/core/theme/src/components/date-picker.ts index 6f3d8ab53a..9ebf4d7b6d 100644 --- a/packages/core/theme/src/components/date-picker.ts +++ b/packages/core/theme/src/components/date-picker.ts @@ -25,8 +25,9 @@ const datePicker = tv({ false: {}, }, }, - defaultVariants: {}, - compoundVariants: [], + defaultVariants: { + hasMultipleMonths: false, + }, }); export type DatePickerReturnType = ReturnType;