diff --git a/.changeset/pretty-teachers-vanish.md b/.changeset/pretty-teachers-vanish.md new file mode 100644 index 00000000000..26483f5217a --- /dev/null +++ b/.changeset/pretty-teachers-vanish.md @@ -0,0 +1,29 @@ +--- +"@salt-ds/lab": patch +--- + +DatePicker and Calendar API improvements + +- `CalendarCarousel` has been renamed to `CarouselDateGrid` so it's more obvious of the content +- `Calendar` previously used `children` to define the `CalendarNavigation`. + We have now changed that so the `children` defines `CalendarNavigation`, `CalendarWeekHeader` and `CalendarDateGrid` + This enables more flexibility in both layout and configuration of the `Calendar` elements. + A typical Calendar will now look like this, + +``` + + + + + +``` + +`CalendarNavigation` - provides year/month dropdowns and forward/back controls for the visible month. +`CalendarWeekHeader` - provides a header for `CalendarDateGrid` indicating the day of the week. +`CalendarDateGrid` - provides a grid of buttons that represent the days from a calendar month. + +- cleaned up selection API, removed `select`, use `setSelectedDate` instead +- fix issues with `Calendar` offset selection +- updated examples, more consistent helper text, error text to match spec +- test improvements to create a known state for tests and avoid failures based on locale differences +- cleaned up Storybook imports in e2e tests diff --git a/packages/lab/src/__tests__/__e2e__/calendar/Calendar.cy.tsx b/packages/lab/src/__tests__/__e2e__/calendar/Calendar.cy.tsx index 9a6ffeea24c..1bb8259c1f0 100644 --- a/packages/lab/src/__tests__/__e2e__/calendar/Calendar.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/calendar/Calendar.cy.tsx @@ -12,24 +12,19 @@ import * as calendarStories from "@stories/calendar/calendar.stories"; import { composeStories } from "@storybook/react"; import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility"; +const composedStories = composeStories(calendarStories); const { Single, CustomDayRender, - CustomHeader, HideYearDropdown, MinMaxDate, + TodayButton, TwinCalendars, WithLocale, -} = composeStories(calendarStories); +} = composedStories; describe("GIVEN a Calendar", () => { - // TODO design a compliant Header example - const { - default: _ignored, - CustomHeader: _CustomHeader, - ...allAxeStories - } = calendarStories; - checkAccessibility(allAxeStories); + checkAccessibility(composedStories); const testDate = parseDate("2022-02-03"); const testTimeZone = "Europe/London"; @@ -351,7 +346,7 @@ describe("GIVEN a Calendar", () => { it("SHOULD render custom headers", () => { cy.mount( - , + , ); // Simulate clicking the "Today" button cy.findByRole("button", { name: /Today/i }).click(); diff --git a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.range.cy.tsx b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.range.cy.tsx index 2c768e154e9..5ed79cef81b 100644 --- a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.range.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.range.cy.tsx @@ -6,13 +6,6 @@ import { today, } from "@internationalized/date"; import * as datePickerStories from "@stories/date-picker/date-picker.stories"; -import { - RangeControlled, - RangeWithConfirmation, - RangeWithCustomPanel, - RangeWithFormField, - RangeWithMinMaxDate, -} from "@stories/date-picker/date-picker.stories"; import { composeStories } from "@storybook/react"; import React from "react"; import { formatDate } from "../../../calendar"; @@ -25,7 +18,14 @@ import { } from "../../../date-picker"; const composedStories = composeStories(datePickerStories); -const { Range } = composedStories; +const { + Range, + RangeControlled, + RangeWithConfirmation, + RangeWithCustomPanel, + RangeWithFormField, + RangeWithMinMaxDate, +} = composedStories; describe("GIVEN a DatePicker where selectionVariant is range", () => { const testLocale = "en-GB"; @@ -61,11 +61,19 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { }; it("SHOULD only be able to select a date between min/max", () => { + const selectedDateChangeSpy = cy.stub().as("selectedDateChangeSpy"); cy.mount( - , + , ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed + cy.findAllByRole("application").should("have.length", 2); // Verify that dates outside the min/max range are disabled cy.findByRole("button", { name: formatDay(parseDate("2030-01-14")), @@ -79,6 +87,14 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { cy.findByRole("button", { name: formatDay(parseDate("2031-01-16")), }).should("have.attr", "aria-disabled", "true"); + // Simulate selecting a date outside the min/max range + cy.findByRole("button", { + name: formatDay(parseDate("2030-01-14")), + }) + .realHover() + .realClick(); + cy.findAllByRole("application").should("have.length", 2); + cy.get("@selectedDateChangeSpy").should("not.have.been.called"); // Simulate selecting a date within the min/max range cy.findByRole("button", { name: formatDay(parseDate("2030-01-15")), @@ -96,6 +112,14 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { "have.value", formatDate(parseDate("2031-01-15"), testLocale), ); + cy.get("@selectedDateChangeSpy").should( + "have.been.calledWith", + { + startDate: parseDate("2030-01-15"), + endDate: parseDate("2031-01-15"), + }, + { startDate: false, endDate: false }, + ); }); it("SHOULD support validation", () => { @@ -163,7 +187,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); - // Verify that the custom panel is displayed + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Simulate selecting a tenor option cy.findByRole("option", { @@ -199,7 +223,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { cy.mount( { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Simulate selecting an unconfirmed date cy.findByRole("button", { @@ -265,7 +290,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { cy.mount( { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Simulate selecting a new date range cy.findByRole("button", { @@ -344,6 +370,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Verify that the default selected dates are highlighted in the calendar cy.findByRole("button", { @@ -360,6 +387,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Simulate selecting a new start date cy.findByRole("button", { @@ -394,7 +422,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { cy.mount( , ); @@ -409,6 +437,7 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Verify that the selected dates are highlighted in the calendar cy.findByRole("button", { @@ -423,12 +452,13 @@ describe("GIVEN a DatePicker where selectionVariant is range", () => { cy.mount( , ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findAllByRole("application").should("have.length", 2); // Simulate selecting a new start date cy.findByRole("button", { diff --git a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx index e196a4b0bb6..a9dbb14b822 100644 --- a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx @@ -6,15 +6,6 @@ import { today, } from "@internationalized/date"; import * as datePickerStories from "@stories/date-picker/date-picker.stories"; -import { - SingleControlled, - SingleWithConfirmation, - SingleWithCustomPanel, - SingleWithCustomParser, - SingleWithFormField, - SingleWithMinMaxDate, - SingleWithToday, -} from "@stories/date-picker/date-picker.stories"; import { composeStories } from "@storybook/react"; import React from "react"; import { formatDate } from "../../../calendar"; @@ -27,7 +18,16 @@ import { } from "../../../date-picker"; const composedStories = composeStories(datePickerStories); -const { Single } = composedStories; +const { + Single, + SingleControlled, + SingleWithConfirmation, + SingleWithCustomPanel, + SingleWithCustomParser, + SingleWithFormField, + SingleWithMinMaxDate, + SingleWithTodayButton, +} = composedStories; describe("GIVEN a DatePicker where selectionVariant is single", () => { const testLocale = "en-GB"; @@ -47,12 +47,52 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { }); }; + it("SHOULD support validation", () => { + const selectedDateChangeSpy = cy.stub().as("selectedDateChangeSpy"); + cy.mount( + , + ); + // Simulate entering a valid date + cy.findByRole("textbox").click().clear().type(initialDateValue); + cy.realPress("Tab"); + cy.findByRole("textbox").should("have.value", initialDateValue); + cy.get("@selectedDateChangeSpy").should("have.been.calledOnce"); + cy.get("@selectedDateChangeSpy").should( + "have.been.calledWith", + initialDate, + ); + // Simulate entering an invalid date + cy.findByRole("textbox").click().clear().type("bad date"); + cy.realPress("Tab"); + cy.get("@selectedDateChangeSpy").should("have.been.calledTwice"); + cy.get("@selectedDateChangeSpy").should("have.been.calledWith", null); + cy.findByRole("textbox").click().clear().type(updatedFormattedDateValue); + cy.realPress("Tab"); + cy.get("@selectedDateChangeSpy").should("have.been.calledThrice"); + cy.get("@selectedDateChangeSpy").should( + "have.been.calledWith", + updatedDate, + ); + }); + it("SHOULD only be able to select a date between min/max", () => { + const selectedDateChangeSpy = cy.stub().as("selectedDateChangeSpy"); cy.mount( - , + , ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed + cy.findByRole("application").should("exist"); // Verify that dates outside the min/max range are disabled cy.findByRole("button", { name: formatDay(parseDate("2030-01-14")), @@ -76,6 +116,14 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.findByRole("button", { name: formatDay(parseDate("2031-01-16")), }).should("have.attr", "aria-disabled", "true"); + // Simulate selecting a date outside the min/max range + cy.findByRole("button", { + name: formatDay(parseDate("2031-01-16")), + }) + .realHover() + .realClick(); + cy.findByRole("application").should("exist"); + cy.get("@selectedDateChangeSpy").should("not.have.been.called"); // Simulate selecting a date within the min/max range cy.findByRole("button", { name: formatDay(parseDate("2031-01-15")), @@ -86,37 +134,10 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { "have.value", formatDate(parseDate("2031-01-15"), testLocale), ); - }); - - it("SHOULD support validation", () => { - const selectedDateChangeSpy = cy.stub().as("selectedDateChangeSpy"); - cy.mount( - , - ); - // Simulate entering a valid date - cy.findByRole("textbox").click().clear().type(initialDateValue); - cy.realPress("Tab"); - cy.findByRole("textbox").should("have.value", initialDateValue); - cy.get("@selectedDateChangeSpy").should("have.been.calledOnce"); - cy.get("@selectedDateChangeSpy").should( - "have.been.calledWith", - initialDate, - ); - // Simulate entering an invalid date - cy.findByRole("textbox").click().clear().type("bad date"); - cy.realPress("Tab"); - cy.get("@selectedDateChangeSpy").should("have.been.calledTwice"); - cy.get("@selectedDateChangeSpy").should("have.been.calledWith", null); - cy.findByRole("textbox").click().clear().type(updatedFormattedDateValue); - cy.realPress("Tab"); - cy.get("@selectedDateChangeSpy").should("have.been.calledThrice"); cy.get("@selectedDateChangeSpy").should( "have.been.calledWith", - updatedDate, + parseDate("2031-01-15"), + false, ); }); @@ -131,6 +152,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); // Simulate selecting a tenor option cy.findByRole("option", { @@ -155,7 +177,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { it("SHOULD support custom panel with Today button", () => { const selectedDateChangeSpy = cy.stub().as("selectedDateChangeSpy"); cy.mount( - { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); - // Simulate clicking the "Today" button - cy.findByRole("button", { name: "Today" }).realClick(); + // Simulate clicking the "Select Today" button + cy.findByRole("button", { name: "Select Today" }).realClick(); // Verify that the calendar is closed and today's date is displayed cy.findByRole("application").should("not.exist"); cy.realPress("Tab"); @@ -188,7 +211,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.mount( { .should("have.value", formatDate(initialDate, testLocale)); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); // Simulate selecting an unconfirmed date cy.findByRole("button", { name: formatDay(updatedDate) }).realClick(); @@ -232,7 +256,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.mount( { .should("have.value", formatDate(initialDate, testLocale)); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); // Simulate selecting a new date cy.findByRole("button", { name: formatDay(updatedDate) }).realClick(); @@ -284,13 +309,18 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.get("@selectedDateChangeSpy").should( "have.been.calledWith", initialDate, + false, ); // Simulate entering a custom parsed date cy.findByRole("textbox").click().clear().type("+7"); cy.realPress("Tab"); cy.get("@selectedDateChangeSpy").should("have.been.calledTwice"); const newDate = initialDate.add({ days: 7 }); - cy.get("@selectedDateChangeSpy").should("have.been.calledWith", newDate); + cy.get("@selectedDateChangeSpy").should( + "have.been.calledWith", + newDate, + false, + ); cy.document() .find("input") .should("have.value", formatDate(newDate, testLocale)); @@ -305,6 +335,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.findByRole("textbox").should("have.value", initialDateValue); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); // Verify that the default selected date is highlighted in the calendar cy.findByRole("button", { name: formatDay(initialDate) }).should( @@ -320,6 +351,8 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed + cy.findByRole("application").should("exist"); // Simulate selecting a new date cy.findByRole("button", { name: formatDay(updatedDate), @@ -339,7 +372,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.mount( , ); @@ -347,6 +380,7 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.findByRole("textbox").should("have.value", initialDateValue); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed cy.findByRole("application").should("exist"); // Verify that the selected date is highlighted in the calendar cy.findByRole("button", { name: formatDay(initialDate) }).should( @@ -360,13 +394,15 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => { cy.mount( , ); // Simulate opening the calendar cy.findByRole("button", { name: "Open Calendar" }).realClick(); + // Verify that the calendar is displayed + cy.findByRole("application").should("exist"); // Simulate selecting a new date cy.findByRole("button", { name: formatDay(updatedDate), diff --git a/packages/lab/src/calendar/Calendar.tsx b/packages/lab/src/calendar/Calendar.tsx index 683a941f2f1..c05761a9eb0 100644 --- a/packages/lab/src/calendar/Calendar.tsx +++ b/packages/lab/src/calendar/Calendar.tsx @@ -4,14 +4,8 @@ import { type ComponentPropsWithoutRef, type ReactNode, forwardRef, - useCallback, } from "react"; -import { - CalendarCarousel, - type CalendarCarouselProps, -} from "./internal/CalendarCarousel"; import { CalendarContext } from "./internal/CalendarContext"; -import { CalendarWeekHeader } from "./internal/CalendarWeekHeader"; import { type UseCalendarMultiSelectProps, type UseCalendarOffsetProps, @@ -33,27 +27,11 @@ export interface CalendarBaseProps extends ComponentPropsWithoutRef<"div"> { /** * The content to be rendered inside the Calendar. */ - children?: ReactNode; - - /** - * Additional class names to apply to the Calendar. - */ - className?: string; - - /** - * Function to render the contents of a day. - */ - renderDayContents?: CalendarCarouselProps["renderDayContents"]; - - /** - * Props for the tooltip component. - */ - TooltipProps?: CalendarCarouselProps["TooltipProps"]; - + children: ReactNode; /** * If `true`, hides dates that are out of the selectable range. */ - hideOutOfRangeDates?: CalendarCarouselProps["hideOutOfRangeDates"]; + hideOutOfRangeDates?: boolean; } /** @@ -117,34 +95,71 @@ const withBaseName = makePrefixer("saltCalendar"); export const Calendar = forwardRef( function Calendar(props, ref) { - const { children, className, renderDayContents, TooltipProps, ...rest } = - props; - const targetWindow = useWindow(); useComponentCssInjection({ testId: "salt-calendar", css: calendarCss, window: targetWindow, }); - - const { state, helpers } = useCalendar({ - ...rest, - }); - - const { setCalendarFocused } = helpers; - - const handleFocus = useCallback(() => { - setCalendarFocused(true); - }, [setCalendarFocused]); - - const handleBlur = useCallback(() => { - setCalendarFocused(false); - }, [setCalendarFocused]); - + const { + children, + className, + selectedDate, + defaultSelectedDate, + visibleMonth: visibleMonthProp, + timeZone, + locale, + defaultVisibleMonth, + onSelectedDateChange, + onVisibleMonthChange, + hideOutOfRangeDates, + isDayUnselectable, + isDayHighlighted, + isDayDisabled, + minDate, + maxDate, + selectionVariant, + onHoveredDateChange, + hoveredDate, + ...propsRest + } = props; + let startDateOffset: CalendarOffsetProps["startDateOffset"]; + let endDateOffset: CalendarOffsetProps["startDateOffset"]; + let rest: Partial; + if (selectionVariant === "offset") { + ({ startDateOffset, endDateOffset, ...rest } = + propsRest as CalendarOffsetProps); + } else { + rest = propsRest; + } + // biome-ignore lint/suspicious/noExplicitAny: type guard + const useCalendarProps: any = { + selectedDate, + defaultSelectedDate, + visibleMonth: visibleMonthProp, + timeZone, + locale, + defaultVisibleMonth, + onSelectedDateChange, + onVisibleMonthChange, + isDayUnselectable, + isDayHighlighted, + isDayDisabled, + minDate, + maxDate, + selectionVariant, + onHoveredDateChange, + hideOutOfRangeDates, + hoveredDate, + startDateOffset, + endDateOffset, + }; + const { state, helpers } = useCalendar(useCalendarProps); const calendarLabel = new DateFormatter(state.locale, { month: "long", year: "numeric", }).format(state.visibleMonth.toDate(state.timeZone)); + return ( ( role="application" aria-label={calendarLabel} ref={ref} + {...rest} > - {children ?? null} - - + {children} ); diff --git a/packages/lab/src/calendar/internal/CalendarCarousel.css b/packages/lab/src/calendar/CalendarDateGrid.css similarity index 52% rename from packages/lab/src/calendar/internal/CalendarCarousel.css rename to packages/lab/src/calendar/CalendarDateGrid.css index 33377d2a2da..44c95585515 100644 --- a/packages/lab/src/calendar/internal/CalendarCarousel.css +++ b/packages/lab/src/calendar/CalendarDateGrid.css @@ -1,14 +1,14 @@ -.saltCalendarCarousel-track { +.saltCalendarDateGrid-grid { display: grid; grid-auto-flow: column; } -.saltCalendarCarousel-track > * { +.saltCalendarDateGrid-grid > * { position: absolute; left: 0; width: 100%; } -.saltCalendarCarousel-track > :nth-child(2) { +.saltCalendarDateGrid-grid > :nth-child(2) { position: relative; } diff --git a/packages/lab/src/calendar/internal/CalendarCarousel.tsx b/packages/lab/src/calendar/CalendarDateGrid.tsx similarity index 57% rename from packages/lab/src/calendar/internal/CalendarCarousel.tsx rename to packages/lab/src/calendar/CalendarDateGrid.tsx index 6183908d961..fc2b69ebf13 100644 --- a/packages/lab/src/calendar/internal/CalendarCarousel.tsx +++ b/packages/lab/src/calendar/CalendarDateGrid.tsx @@ -1,37 +1,59 @@ import { type DateValue, isSameMonth } from "@internationalized/date"; import { makePrefixer, useIsomorphicLayoutEffect } from "@salt-ds/core"; -import { forwardRef, useEffect, useRef, useState } from "react"; -import { useCalendarContext } from "./CalendarContext"; -import { CalendarMonth, type CalendarMonthProps } from "./CalendarMonth"; +import { + type ComponentPropsWithoutRef, + type FocusEventHandler, + forwardRef, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { useCalendarContext } from "./internal/CalendarContext"; +import { + CalendarMonth, + type CalendarMonthProps, +} from "./internal/CalendarMonth"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; -import calendarCarouselCss from "./CalendarCarousel.css"; -import { formatDate, monthDiff } from "./utils"; +import calendarDateGridCss from "./CalendarDateGrid.css"; +import { formatDate, monthDiff } from "./internal/utils"; -export type CalendarCarouselProps = Omit; +export interface CalendarDateGridProps extends ComponentPropsWithoutRef<"div"> { + /** + * Props getter to pass to each CalendarMonth element + */ + getCalendarMonthProps?: (date: DateValue) => Omit; +} function getMonths(month: DateValue) { return [month.subtract({ months: 1 }), month, month.add({ months: 1 })]; } -const withBaseName = makePrefixer("saltCalendarCarousel"); +const withBaseName = makePrefixer("saltCalendarDateGrid"); -export const CalendarCarousel = forwardRef< +export const CalendarDateGrid = forwardRef< HTMLDivElement, - CalendarCarouselProps ->(function CalendarCarousel(props, ref) { - const { ...rest } = props; + CalendarDateGridProps +>(function CalendarDateGrid(props, ref) { + const { + onFocus, + onBlur, + getCalendarMonthProps = () => undefined, + ...rest + } = props; const targetWindow = useWindow(); useComponentCssInjection({ - testId: "salt-calendar-carousel", - css: calendarCarouselCss, + testId: "salt-calendar-date-grid", + css: calendarDateGridCss, window: targetWindow, }); const { state: { visibleMonth, locale }, + helpers: { setCalendarFocused }, } = useCalendarContext(); const containerRef = useRef(null); const diffIndex = (a: DateValue, b: DateValue) => monthDiff(a, b); @@ -61,6 +83,22 @@ export const CalendarCarousel = forwardRef< return undefined; }, [formatDate(visibleMonth, locale)]); + const handleFocus: FocusEventHandler = useCallback( + (event) => { + setCalendarFocused(true); + onFocus?.(event); + }, + [setCalendarFocused, onFocus], + ); + + const handleBlur: FocusEventHandler = useCallback( + (event) => { + setCalendarFocused(false); + onBlur?.(event); + }, + [setCalendarFocused, onBlur], + ); + return (
-
+
{months.map((date, index) => (
- +
))}
diff --git a/packages/lab/src/calendar/CalendarNavigation.tsx b/packages/lab/src/calendar/CalendarNavigation.tsx index 2351363d056..c697f7fa8da 100644 --- a/packages/lab/src/calendar/CalendarNavigation.tsx +++ b/packages/lab/src/calendar/CalendarNavigation.tsx @@ -371,7 +371,8 @@ export const CalendarNavigation = forwardRef< > @@ -189,11 +225,13 @@ function renderDayContents(day: DateValue) { export const CustomDayRender: StoryFn = (args) => { return ( - + + + + ({ renderDayContents })} + /> + ); }; @@ -209,64 +247,63 @@ MinMaxDate.args = { maxDate: endOfMonth(today(getLocalTimeZone())), }; -export const ExpandedYears = Template.bind({}); - -ExpandedYears.args = { - minDate: startOfYear(today(getLocalTimeZone()).subtract({ years: 5 })), - maxDate: startOfYear(today(getLocalTimeZone()).add({ years: 5 })), -}; - export const TwinCalendars: StoryFn< CalendarRangeProps & React.RefAttributes -> = (args) => { +> = ({ selectionVariant, ...args }) => { const [hoveredDate, setHoveredDate] = useState(null); const handleHoveredDateChange: CalendarProps["onHoveredDateChange"] = ( - _event, + event, newHoveredDate, ) => { setHoveredDate(newHoveredDate); + args?.onHoveredDateChange?.(event, newHoveredDate); }; const [selectedDate, setSelectedDate] = useState< UseCalendarSelectionRangeProps["selectedDate"] >(args.defaultSelectedDate || null); const handleSelectedDateChange: UseCalendarSelectionRangeProps["onSelectedDateChange"] = - (_event, newSelectedDate) => { + (event, newSelectedDate) => { setSelectedDate(newSelectedDate); + args?.onSelectedDateChange?.(event, newSelectedDate); }; return (
+ + + +
); @@ -275,6 +312,8 @@ export const TwinCalendars: StoryFn< export const WithLocale: StoryFn = (args) => ( + + ); @@ -284,5 +323,7 @@ export const Bordered: StoryFn = (args) => ( MonthDropdownProps={{ bordered: true }} YearDropdownProps={{ bordered: true }} /> + +
); diff --git a/packages/lab/stories/date-picker/date-picker.stories.tsx b/packages/lab/stories/date-picker/date-picker.stories.tsx index e4acd9b8371..637669af378 100644 --- a/packages/lab/stories/date-picker/date-picker.stories.tsx +++ b/packages/lab/stories/date-picker/date-picker.stories.tsx @@ -18,7 +18,6 @@ import { FormFieldLabel as FormLabel, } from "@salt-ds/core"; import { - type DateInputRangeParserResult, type DateInputSingleParserResult, DatePicker, DatePickerActions, @@ -31,7 +30,6 @@ import { type DatePickerSingleProps, type DateRangeSelection, type RangeDatePickerState, - type SingleDatePickerError, type SingleDatePickerState, type SingleDateSelection, formatDate, @@ -41,7 +39,7 @@ import { } from "@salt-ds/lab"; import type { Meta, StoryFn } from "@storybook/react"; import type React from "react"; -import { useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { CustomDatePickerPanel } from "./CustomDatePickerPanel"; export default { @@ -85,15 +83,24 @@ function formatSingleDate( return date; } -const DatePickerSingleTemplate: StoryFn = (args) => { +const DatePickerSingleTemplate: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -103,15 +110,27 @@ const DatePickerSingleTemplate: StoryFn = (args) => { ); }; -const DatePickerRangeTemplate: StoryFn = (args) => { +const DatePickerRangeTemplate: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( { - console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -129,20 +148,30 @@ Range.args = { selectionVariant: "range", }; -export const SingleControlled: StoryFn = (args) => { +export const SingleControlled: StoryFn = ({ + selectionVariant, + defaultSelectedDate, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const [selectedDate, setSelectedDate] = useState( - args?.selectedDate ?? null, + defaultSelectedDate ?? null, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setSelectedDate], + ); + return ( { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} > @@ -152,20 +181,33 @@ export const SingleControlled: StoryFn = (args) => { ); }; -export const RangeControlled: StoryFn = (args) => { +export const RangeControlled: StoryFn = ({ + selectionVariant, + defaultSelectedDate, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const [selectedDate, setSelectedDate] = useState( - args?.selectedDate ?? null, + defaultSelectedDate ?? null, ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setSelectedDate], + ); + return ( { - console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} > @@ -175,25 +217,29 @@ export const RangeControlled: StoryFn = (args) => { ); }; -export const SingleWithMinMaxDate: StoryFn = (args) => { - const [selectedDate, setSelectedDate] = useState( - null, +export const SingleWithMinMaxDate: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const helperText = "Select date between 15/01/2030 and 15/01/2031"; + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], ); - const helperText = "Valid between 15/01/2030 and 15/01/2031"; + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} minDate={new CalendarDate(2030, 1, 15)} maxDate={new CalendarDate(2031, 1, 15)} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -208,27 +254,32 @@ export const SingleWithMinMaxDate: StoryFn = (args) => { ); }; -export const RangeWithMinMaxDate: StoryFn = (args) => { - const [selectedDate, setSelectedDate] = useState( - null, +export const RangeWithMinMaxDate: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const helperText = "Select date between 15/01/2030 and 15/01/2031"; + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], ); - const helperText = "Valid between 15/01/2030 and 15/01/2031"; + return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - setSelectedDate(newSelectedDate); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} minDate={new CalendarDate(2030, 1, 15)} maxDate={new CalendarDate(2031, 1, 15)} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -244,30 +295,38 @@ export const RangeWithMinMaxDate: StoryFn = (args) => { ); }; -export const SingleWithInitialError: StoryFn = ( - args, -) => { - const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; +export const SingleWithInitialError: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(errorHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( "error", ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setHelperText], ); return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -279,32 +338,47 @@ export const SingleWithInitialError: StoryFn = ( ); }; -export const RangeWithInitialError: StoryFn = (args) => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; +export const RangeWithInitialError: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = + "Select range DD MMM YYYY - DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(errorHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( "error", ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], + ); return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} defaultSelectedDate={{ startDate: new CalendarDate(2024, 6, 9) }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > = (args) => { ); }; -export const SingleWithFormField: StoryFn = (args) => { - const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; +export const SingleWithFormField: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], ); return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -355,36 +439,46 @@ export const SingleWithFormField: StoryFn = (args) => { ); }; -export const RangeWithFormField: StoryFn = (args) => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; +export const RangeWithFormField: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = + "Select range DD MMM YYYY - DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + setValidationStatus(validationStatus); + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], ); return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - setSelectedDate(newSelectedDate); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -396,21 +490,30 @@ export const RangeWithFormField: StoryFn = (args) => { ); }; -export const SingleWithCustomPanel: StoryFn = (args) => { +export const SingleWithCustomPanel: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; const minDate = today(getLocalTimeZone()); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -425,23 +528,33 @@ export const SingleWithCustomPanel: StoryFn = (args) => { ); }; -export const RangeWithCustomPanel: StoryFn = (args) => { +export const RangeWithCustomPanel: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; const minDate = today(getLocalTimeZone()); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -464,27 +577,40 @@ const TodayButton = () => { }) as SingleDatePickerState; return ( - +
+ +
); }; -export const SingleWithToday: StoryFn = (args) => { +export const SingleWithTodayButton: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -492,6 +618,9 @@ export const SingleWithToday: StoryFn = (args) => { + + + @@ -503,40 +632,60 @@ export const SingleWithToday: StoryFn = (args) => { ); }; -export const SingleWithConfirmation: StoryFn = ( - args, -) => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; +export const SingleWithConfirmation: StoryFn = ({ + selectionVariant, + defaultSelectedDate, + onApply: onApplyProp, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const applyButtonRef = useRef(null); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); const [selectedDate, setSelectedDate] = useState( - args.selectedDate || null, + defaultSelectedDate ?? null, + ); + const handleApply = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onApplyProp?.(newSelectedDate, error); + }, + [onApplyProp, setSelectedDate, setHelperText], + ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + setSelectedDate(newSelectedDate); + applyButtonRef?.current?.focus(); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, applyButtonRef?.current, setSelectedDate], ); + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setValidationStatus(error ? "error" : undefined); - args?.onApply?.(newSelectedDate, error); - }} - onSelectedDateChange={(newSelectedDate, error) => { - setSelectedDate(newSelectedDate); - applyButtonRef?.current?.focus(); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} > - + @@ -553,46 +702,76 @@ export const SingleWithConfirmation: StoryFn = ( ); }; -export const RangeWithConfirmation: StoryFn = (args) => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; +export const RangeWithConfirmation: StoryFn = ({ + selectionVariant, + defaultSelectedDate, + onApply: onApplyProp, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = + "Select range (DD MMM YYYY - DD MMM YYYY) e.g. 09 Jun 2024"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const applyButtonRef = useRef(null); const minDate = today(getLocalTimeZone()); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); const [selectedDate, setSelectedDate] = useState( - args.selectedDate || null, + defaultSelectedDate ?? null, + ); + const handleApply = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + onApplyProp?.(newSelectedDate, error); + }, + [onApplyProp, setValidationStatus, setHelperText], + ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + setSelectedDate(newSelectedDate); + if (newSelectedDate?.startDate && newSelectedDate?.endDate) { + applyButtonRef?.current?.focus(); + } + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [applyButtonRef?.current, onSelectedDateChangeProp, setSelectedDate], ); return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - args?.onApply?.(newSelectedDate, error); - }} - onSelectedDateChange={(newSelectedDate, error) => { - setSelectedDate(newSelectedDate); - if (newSelectedDate?.startDate && newSelectedDate?.endDate) { - applyButtonRef?.current?.focus(); - } - args?.onSelectedDateChange?.(newSelectedDate, error); - }} > @@ -615,57 +794,78 @@ export const RangeWithConfirmation: StoryFn = (args) => { ); }; -export const SingleWithCustomParser: StoryFn = ( - args, -) => { - const helperText = +export const SingleWithCustomParser: StoryFn = ({ + selectionVariant, + defaultSelectedDate, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024) or +/-D (e.g. +7)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); const [selectedDate, setSelectedDate] = useState( - null, + defaultSelectedDate ?? null, + ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [ + onSelectedDateChangeProp, + setValidationStatus, + setSelectedDate, + setHelperText, + ], + ); + const handleParse = useCallback( + (inputDate: string): DateInputSingleParserResult => { + if (!inputDate?.length) { + return { date: null, error: false }; + } + const parsedDate = inputDate; + const offsetMatch = parsedDate?.match(/^([+-]?\d+)$/); + if (offsetMatch) { + const offsetDays = Number.parseInt(offsetMatch[1], 10); + let offsetDate = selectedDate + ? selectedDate + : today(getLocalTimeZone()); + offsetDate = offsetDate.add({ days: offsetDays }); + return { + date: new CalendarDate( + offsetDate.year, + offsetDate.month, + offsetDate.day, + ), + error: false, + }; + } + return parseCalendarDate(parsedDate || ""); + }, + [selectedDate], ); return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} > - { - if (!inputDate?.length) { - return { date: null, error: false }; - } - const parsedDate = inputDate; - const offsetMatch = parsedDate?.match(/^([+-]?\d+)$/); - if (offsetMatch) { - const offsetDays = Number.parseInt(offsetMatch[1], 10); - let offsetDate = selectedDate - ? selectedDate - : today(getLocalTimeZone()); - offsetDate = offsetDate.add({ days: offsetDays }); - return { - date: new CalendarDate( - offsetDate.year, - offsetDate.month, - offsetDate.day, - ), - error: false, - }; - } - return parseCalendarDate(parsedDate || ""); - }} - /> + @@ -675,68 +875,45 @@ export const SingleWithCustomParser: StoryFn = ( ); }; -export const SingleWithLocaleEnUS: StoryFn = (args) => { +export const SingleWithLocaleEnUS: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const locale = "en-US"; - const [selectedDate, setSelectedDate] = useState( - null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - - const parseDateEnUS = (dateString: string): DateInputSingleParserResult => { - if (!dateString?.length) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [, month, day, year] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; - }; - - const formatDateEnUS = (date: DateValue | null) => { - return date - ? new DateFormatter(locale, { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; - }; + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${newSelectedDate}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], + ); return ( Select a date { - console.log(`Selected date: ${formatDateEnUS(newSelectedDate)}`); - setValidationStatus(error ? "error" : undefined); - setSelectedDate(newSelectedDate); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > - + - + {helperText} @@ -744,44 +921,33 @@ export const SingleWithLocaleEnUS: StoryFn = (args) => { ); }; -export const SingleWithLocaleZhCN: StoryFn = (args) => { +export const SingleWithLocaleZhCN: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const locale = "zh-CN"; - const formatDateZhCN = (date: DateValue | null) => { - return date - ? new DateFormatter(locale, { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; - }; - const parseDateZhCN = (dateString: string): DateInputSingleParserResult => { - if (!dateString?.length) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [_, year, month, day] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; - }; - - const [selectedDate, setSelectedDate] = useState( - args.selectedDate || null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${newSelectedDate ?? null}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], + ); + const formatMonth = (date: DateValue) => formatDate(date, locale, { month: "long", @@ -798,30 +964,18 @@ export const SingleWithLocaleZhCN: StoryFn = (args) => { Select a date { - console.log( - `Selected date: ${formatDateZhCN(newSelectedDate ?? null)}`, - ); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > - + ({ renderDayContents }), + }} CalendarNavigationProps={{ formatMonth }} /> @@ -831,82 +985,61 @@ export const SingleWithLocaleZhCN: StoryFn = (args) => { ); }; -export const RangeWithLocaleEsES: StoryFn = (args) => { +export const RangeWithLocaleEsES: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { const locale = "es-ES"; - const [selectedDate, setSelectedDate] = useState( - null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - - const parseDateEsES = ( - dateString: string | undefined, - ): DateInputRangeParserResult => { - if (!dateString) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [, day, month, year] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; - }; - - const formatDateEsES = (date: DateValue | null) => { - return date - ? new DateFormatter(locale, { + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log( + `Selected date range: ${formatDateRange(newSelectedDate, locale, { day: "2-digit", month: "2-digit", year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; - }; + })}`, + ); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp, setValidationStatus, setHelperText], + ); return ( Select a date { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate, locale, { - day: "2-digit", - month: "2-digit", - year: "numeric", - })}`, - ); - setSelectedDate(newSelectedDate); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > - + - + {helperText} @@ -914,15 +1047,24 @@ export const RangeWithLocaleEsES: StoryFn = (args) => { ); }; -export const SingleBordered: StoryFn = (args) => { +export const SingleBordered: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -937,15 +1079,30 @@ export const SingleBordered: StoryFn = (args) => { ); }; -export const RangeBordered: StoryFn = (args) => { +export const RangeBordered: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( { - console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > @@ -1033,6 +1190,7 @@ const DatePickerTimeInput: React.FC = () => { const zonedStartTime = selectedDate?.startDate as ZonedDateTime; const zonedEndTime = selectedDate?.endDate as ZonedDateTime; + return ( <> @@ -1041,7 +1199,7 @@ const DatePickerTimeInput: React.FC = () => { type={"time"} value={ zonedStartTime - ? `${zonedStartTime.hour}:${zonedStartTime.minute}` + ? `${zonedStartTime.hour.toString().padStart(2, "0")}:${zonedStartTime.minute.toString().padStart(2, "0")}` : "" } onChange={handleStartTimeChange} @@ -1050,36 +1208,56 @@ const DatePickerTimeInput: React.FC = () => { aria-label="end date time" type={"time"} value={ - zonedEndTime ? `${zonedEndTime.hour}:${zonedEndTime.minute}` : "" + zonedEndTime + ? `${zonedEndTime.hour.toString().padStart(2, "0")}:${zonedEndTime.minute.toString().padStart(2, "0")}` + : "" } onChange={handleEndTimeChange} /> ); }; -export const WithExperimentalTime: StoryFn = (args) => { +export const WithExperimentalTime: StoryFn = ({ + selectionVariant, + onSelectedDateChange: onSelectedDateChangeProp, + ...args +}) => { + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const timeFormatter = new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, // Use 24-hour format + }); + const nativeStartTime = + newSelectedDate?.startDate?.toDate(getLocalTimeZone()) ?? null; + const nativeEndTime = + newSelectedDate?.endDate?.toDate(getLocalTimeZone()) ?? null; + console.log( + `Selected time range: ${nativeStartTime ? timeFormatter.format(nativeStartTime) : null} - ${nativeEndTime ? timeFormatter.format(nativeEndTime) : null}`, + ); + onSelectedDateChangeProp?.(newSelectedDate, error); + }, + [onSelectedDateChangeProp], + ); + return ( { - console.log( - `Selected date range: ${formatDateRange( - newSelectedDate, - getCurrentLocale(), - { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }, - )}`, - ); - args?.onSelectedDateChange?.(newSelectedDate, error); - }} + onSelectedDateChange={handleSelectedDateChange} + {...args} > diff --git a/site/docs/components/calendar/examples.mdx b/site/docs/components/calendar/examples.mdx index 668e9108b07..afd5bb186f1 100644 --- a/site/docs/components/calendar/examples.mdx +++ b/site/docs/components/calendar/examples.mdx @@ -11,16 +11,19 @@ data: - ## Single date selection When the `selectionVariant` prop is set to "single", - the `Calendar` component allows users to select only a single date, providing - a straightforward and focused date selection experience. + +## Single date selection + +When the `selectionVariant` prop is set to "single", the `Calendar` component allows users to select only a single date, providing a straightforward and focused date selection experience. + - ## Date range selection When the `selectionVariant` prop is set to "range", - the `Calendar` component allows users to select a date range, including both a - start date and an end date, enabling a comprehensive date selection - experience. + +## Date range selection + +When the `selectionVariant` prop is set to "range", the `Calendar` component allows users to select a date range, including both a start date and an end date, enabling a comprehensive date selection experience. + - ## Multi-select When the `selectionVariant` prop is set to "multiselect", the - `Calendar` component allows users to select multiple individual dates, - providing flexibility for selecting non-consecutive dates. + +## Multi-select + +When the `selectionVariant` prop is set to "multiselect", the `Calendar` component allows users to select multiple individual dates, providing flexibility for selecting non-consecutive dates. + - ## Offset selection When the `selectionVariant` prop is set to "offset", the - `Calendar` component allows users to select a date range by choosing a single - start date, with the end date automatically determined based on a predefined - offset. + +## Offset selection + +When the `selectionVariant` prop is set to "offset", the `Calendar` component allows users to select a date range by choosing a single start date, with the end date automatically determined based on a predefined offset. + - ## Unselectable dates Unselectable dates are visually distinct from selectable - dates. When a user attempts to select an unselectable date, the calendar - displays a tooltip with additional information. To implement this, use the - `isDayUnselectable` prop to pass a function that determines whether a day is - unselectable and provides the tooltip description. + +## Unselectable dates + +Unselectable dates are visually distinct from selectable dates. When a user attempts to select an unselectable date, the calendar displays a tooltip with additional information. To implement this, use the `isDayUnselectable` prop to pass a function that determines whether a day is unselectable and provides the tooltip description. + - ## Disabled dates Disabled dates are not selectable. To implement this, use - the `isDayUnselectable` prop to pass a function that determines whether a day - is unselectable and provides the tooltip description. + +## Disabled dates + +Disabled dates are not selectable. To implement this, use the `isDayUnselectable` prop to pass a function that determines whether a day is unselectable and provides the tooltip description. + - ## Highlighted dates Highlighted dates are marked by a triangle in the top - right corner, indicating an event associated with that day. When a user hovers - over a highlighted date, the calendar displays a tooltip with additional - information. To implement this, use the `isDayHighlighted` prop to pass a - function that determines whether a day is highlighted and provides the tooltip - description. + +## Highlighted dates + +Highlighted dates are marked by a triangle in the top right corner, indicating an event associated with that day. When a user hovers over a highlighted date, the calendar displays a tooltip with additional information. To implement this, use the `isDayHighlighted` prop to pass a function that determines whether a day is highlighted and provides the tooltip description. + - ## Custom day rendering To customize the rendering of a day element, use the - `renderDayContents` prop with the `Calendar` component. This prop accepts a - function that is passed a `DateValue` and returns the custom element for that - day. This can be useful for scenarios where you need to display additional - information or apply custom styling to specific dates. + +## Custom day rendering + +To customize the rendering of a day element, use the `renderDayContents` prop with the `Calendar` component. This prop accepts a function that is passed a `DateValue` and returns the custom element for that day. This can be useful for scenarios where you need to display additional information or apply custom styling to specific dates. + - ## Hide out-of-range dates By default, dates outside the visible month are - out-of-range and visually distinct from the current month's dates. To hide - these out-of-range dates, use the `hideOutOfRangeDates` prop. + +## Hide out-of-range dates + +By default, dates outside the visible month are out-of-range and visually distinct from the current month's dates. To hide these out-of-range dates, use the `hideOutOfRangeDates` prop. + - ## Hide year dropdown To hide the year dropdown, use the `hideYearDropdown` - prop with the `CalendarNavigation` component. This can be useful in scenarios - such as when the calendar is composed with other calendars or when the date - selection does not require a year. + +## Hide year dropdown + +To hide the year dropdown, use the `hideYearDropdown` prop with the `CalendarNavigation` component. This can be useful in scenarios such as when the calendar is composed with other calendars or when the date selection does not require a year. + - ## Constrained date selection To constrain the selected date within a specific - range, use the `minDate` and/or `maxDate` props. If `minDate` is not provided, - it defaults to the year 1900. If `maxDate` is undefined, it defaults to the - year 2100. This ensures that the selected date remains within the specified - range. + +## Constrained date selection + +To constrain the selected date within a specific range, use the `minDate` and/or `maxDate` props. If `minDate` is not provided, it defaults to the year 1900. If `maxDate` is undefined, it defaults to the year 2100. This ensures that the selected date remains within the specified range. + - ## Bordered calendar A `Calendar` component with a border provides a visually - distinct area for selecting dates, enhancing the user interface by clearly - delineating the calendar boundaries. This styling can improve usability and - accessibility by making the calendar controls more noticeable and easier to - interact with. + +## Bordered calendar + +A `Calendar` component with a border provides a visually distinct area for selecting dates, enhancing the user interface by clearly delineating the calendar boundaries. This styling can improve usability and accessibility by making the calendar controls more noticeable and easier to interact with. + - ## Twin calendars The Twin Calendars example demonstrates how to use the - `Calendar` component with the `minDate` and `maxDate` props to create an - effective date range picker. By configuring two `Calendar` components side by - side, users can easily select a start date and an end date within a specified - range. + +## Twin calendars + +The Twin Calendars example demonstrates how to use the `Calendar` component with the `minDate` and `maxDate` props to create an effective date range picker. By configuring two `Calendar` components side by side, users can easily select a start date and an end date within a specified range. + - ## Locale When the `locale` prop is provided, the `Calendar` component uses it - to define the calendar's locale, ensuring that date formats, month names, and - other locale-specific elements are displayed according to the specified - locale. + +## Locale + +When the `locale` prop is provided, the `Calendar` component uses it to define the calendar's locale, ensuring that date formats, month names, and other locale-specific elements are displayed according to the specified locale. + - ## Custom header When the `children` prop is used, the `Calendar` component - allows consumers to customize the calendar header, typically by composing it - with the `CalendarNavigation` component for forward/back month controls and - month/year dropdowns, but it can also be used to add additional controls such - as a 'Today' button. + +## With today button + +When the `children` prop is used, the `Calendar` component allows consumers to customize the calendar header, typically by composing it with the `CalendarNavigation` component for forward/back month controls and month/year dropdowns, but it can also be used to add additional controls such as a 'Today' button. + diff --git a/site/docs/components/calendar/usage.mdx b/site/docs/components/calendar/usage.mdx index ea296a82378..a24f5f03485 100644 --- a/site/docs/components/calendar/usage.mdx +++ b/site/docs/components/calendar/usage.mdx @@ -12,10 +12,14 @@ data: -## Compose a standard DatePicker -To use the `Calendar` component, compose it with children that define the calendar header, such as `CalendarNavigation`. -The `CalendarNavigation` component provides year/month dropdowns and forward/back controls for the visible month. +## Compose a standard Calendar + +To use the `Calendar` component, compose it with the provided children + +`CalendarNavigation` - provides year/month dropdowns and forward/back controls for the visible month. +`CalendarWeekHeader` - provides a header for `CalendarDateGrid` indicating the day of the week. +`CalendarDateGrid` - provides a grid of buttons that represent the days from a calendar month. @@ -23,11 +27,11 @@ The `CalendarNavigation` component provides year/month dropdowns and forward/bac - ## Customising the DatePicker Alternatively, you can compose it with your - own navigation component or add additional controls. + ## Customising the Calendar Alternatively, you can compose it with your own + calendar controls or add additional content. @@ -42,8 +46,48 @@ Use the `Calendar` component when you need a versatile date selection tool that ## Import +### Calendar + To import `Calendar` from the Salt lab package, use: ```js import { Calendar } from "@salt-ds/lab"; ``` + +### CalendarNavigation + +To import `CalendarNavigation` from the Salt lab package, use: + +```js +import { CalendarNavigation } from "@salt-ds/lab"; +``` + +### CalendarWeekHeader + +To import `CalendarWeekHeader` from the Salt lab package, use: + +```js +import { CalendarWeekHeader } from "@salt-ds/lab"; +``` + +### CalendarDateGrid + +To import `CalendarDateGrid` from the Salt lab package, use: + +```js +import { CalendarDateGrid } from "@salt-ds/lab"; +``` + +## Props + +### Calendar + + + +### CalendarWeekHeader + + + +### CalendarDateGrid + + diff --git a/site/docs/components/date-input/usage.mdx b/site/docs/components/date-input/usage.mdx index bd6e57df8d0..52fc79c23bd 100644 --- a/site/docs/components/date-input/usage.mdx +++ b/site/docs/components/date-input/usage.mdx @@ -88,8 +88,8 @@ import { DateInputRange } from "@salt-ds/lab"; ### DateInputSingle - + ### DateInputRange - + diff --git a/site/docs/components/date-picker/examples.mdx b/site/docs/components/date-picker/examples.mdx index eabcc94ae55..1bde10b7582 100644 --- a/site/docs/components/date-picker/examples.mdx +++ b/site/docs/components/date-picker/examples.mdx @@ -11,14 +11,16 @@ data: - ## Single date selection + +## Single date An uncontrolled `DatePicker`, composed of a `DateInput` and a `Calendar` component, allows users to select a single date when the `selectionVariant` prop is set to "single", without requiring explicit state management in the parent component, making it ideal for simple date selection tasks. -## Date range selection + +## Date range An uncontrolled `DatePicker`, composed of a `DateInput` and a `Calendar` component, allows users to select a date range when the `selectionVariant` prop is set to "range", without requiring explicit state management in the parent component, making it ideal for selecting start and end dates for various applications. @@ -29,7 +31,8 @@ An uncontrolled `DatePicker`, composed of a `DateInput` and a `Calendar` compone displayName="Single date selection controlled" exampleName="SingleControlled" > -## Controlled single date selection + +## Controlled single date A controlled `DatePicker`, composed of a `DateInput` and a `Calendar` component, allows users to select a single date when the `selectionVariant` prop is set to "single", with the parent component explicitly managing the date state, providing greater control over the date selection process. @@ -40,7 +43,8 @@ A controlled `DatePicker`, composed of a `DateInput` and a `Calendar` component, displayName="Date range controlled" exampleName="RangeControlled" > -## Controlled date range selection + +## Controlled date range A controlled `DatePicker`, composed of a `DateInput` and a `Calendar` component, allows users to select a date range when the `selectionVariant` prop is set to "range", with the parent component explicitly managing the start and end date states, providing greater control over the date range selection process. @@ -51,7 +55,8 @@ A controlled `DatePicker`, composed of a `DateInput` and a `Calendar` component, displayName="Single date selection within FormField" exampleName="SingleWithFormField" > -## Single date selection within FormField + +## Single date within FormField A `DatePicker` with `selectionVariant` set to "single", inside a `FormField`, provides the field with a visible label, help text, and a status message for validation feedback. @@ -62,7 +67,8 @@ A `DatePicker` with `selectionVariant` set to "single", inside a `FormField`, pr displayName="Date range within FormField" exampleName="RangeWithFormField" > - ## Date range selection within FormField + +## Date range within FormField A `DatePicker` with `selectionVariant` set to "range", inside a `FormField`, provides the field with a visible label, help text, and a status message for validation feedback. @@ -73,7 +79,8 @@ A `DatePicker` with `selectionVariant` set to "range", inside a `FormField`, pro displayName="Single date selection with error" exampleName="SingleWithInitialError" > -## Single date selection with initial error + +## Single with initial error When used with `FormField`, the `validationStatus` prop can be used to recreate an initial error state. @@ -84,7 +91,8 @@ When used with `FormField`, the `validationStatus` prop can be used to recreate displayName="Date range with error" exampleName="RangeWithInitialError" > -## Date range selection with initial error + +## Range with initial error When used with `FormField`, the `validationStatus` prop can be used to recreate an initial error state. @@ -95,7 +103,8 @@ When used with `FormField`, the `validationStatus` prop can be used to recreate displayName="Constrained single date selection" exampleName="SingleWithMinMaxDate" > -## Single date selection with min/max date + +## Single with min/max date To constrain the selected date within a specific range, use the `minDate` and/or `maxDate` props. If `minDate` is not provided, it defaults to the year 1900. If `maxDate` is undefined, it defaults to the year 2100. This ensures that the selected date remains within the specified range. @@ -106,7 +115,8 @@ To constrain the selected date within a specific range, use the `minDate` and/or displayName="Constrained date range selection" exampleName="RangeWithMinMaxDate" > -## Date range selection with min/max date + +## Range with min/max date To constrain the selected date within a specific range, use the `minDate` and/or `maxDate` props. If `minDate` is not provided, it defaults to the year 1900. If `maxDate` is undefined, it defaults to the year 2100. This ensures that the selected date remains within the specified range. @@ -117,7 +127,8 @@ To constrain the selected date within a specific range, use the `minDate` and/or displayName="Single date selection with custom content" exampleName="SingleWithCustomPanel" > -## Single date selection with custom content + +## Single with custom content The composed nature of the `DatePicker` enables you to control the content of the overlay using the `children` prop. Use the recommended panels or compose them with your own. @@ -128,7 +139,8 @@ The composed nature of the `DatePicker` enables you to control the content of th displayName="Date range with custom content" exampleName="RangeWithCustomPanel" > -## Date range selection with custom content + +## Range with custom content The composed nature of the `DatePicker` enables you to control the content of the overlay using the `children` prop. Use the recommended panels or compose them with your own. @@ -137,11 +149,12 @@ The composed nature of the `DatePicker` enables you to control the content of th -## Single date selection with today button -When the `children` prop is used, the `DatePicker` component allows consumers to customize the overlay content. In this example, we add a custom panel for quick date selection. +## Single with today button + +The `children` prop enables you to compose additional controls to your Date Picker component, such as a 'Today' button. @@ -150,7 +163,8 @@ When the `children` prop is used, the `DatePicker` component allows consumers to displayName="Single selection with applied confirmation" exampleName="SingleWithConfirmation" > -## Single date selection with confirmation + +## Single with confirmation You can change dates upon selection, or apply the selection with additional controls. @@ -161,7 +175,8 @@ You can change dates upon selection, or apply the selection with additional cont displayName="Date range with applied confirmation" exampleName="RangeWithConfirmation" > -## Date range selection with confirmation + +## Range with confirmation You can change dates upon selection, or apply the selection with additional controls. @@ -172,7 +187,8 @@ You can change dates upon selection, or apply the selection with additional cont displayName="Custom parser" exampleName="SingleWithCustomParser" > -## Single date selection with custom parser + +## Single with custom parser A custom parser can be used to handle specific date formats or shorthand date entries. @@ -183,7 +199,8 @@ A custom parser can be used to handle specific date formats or shorthand date en displayName="Single selection with locale en-US" exampleName="SingleWithLocaleEnUS" > -## Single date selection with locale + +## Single with locale When the `locale` prop is provided, the `DatePicker` component uses it to define the locale, ensuring that date formats, month names, and other locale-specific elements are displayed according to the specified locale. @@ -194,7 +211,8 @@ When the `locale` prop is provided, the `DatePicker` component uses it to define displayName="Date range with locale es-ES" exampleName="RangeWithLocaleEsES" > -## Date range selection with locale + +## Range with locale When the `locale` prop is provided, the `DatePicker` component uses it to define the locale, ensuring that date formats, month names, and other locale-specific elements are displayed according to the specified locale. @@ -205,7 +223,8 @@ When the `locale` prop is provided, the `DatePicker` component uses it to define displayName="Bordered single selection" exampleName="SingleBordered" > -## Bordered single date selection + +## Bordered single A `DatePicker` component with a border provides a visually distinct area for selecting dates, enhancing the user interface by clearly delineating the component boundaries. This styling can improve usability and accessibility by making the calendar controls more noticeable and easier to interact with. @@ -216,7 +235,8 @@ A `DatePicker` component with a border provides a visually distinct area for sel displayName="Bordered date range" exampleName="RangeBordered" > -## Bordered date range selection + +## Bordered range A `DatePicker` component with a border provides a visually distinct area for selecting dates, enhancing the user interface by clearly delineating the component boundaries. This styling can improve usability and accessibility by making the calendar controls more noticeable and easier to interact with. diff --git a/site/docs/components/date-picker/usage.mdx b/site/docs/components/date-picker/usage.mdx index 30d62c01ea1..06b43bae9b6 100644 --- a/site/docs/components/date-picker/usage.mdx +++ b/site/docs/components/date-picker/usage.mdx @@ -72,41 +72,41 @@ import { DatePicker } from "@salt-ds/lab"; ### DatePicker - + ### DatePickerSingleInput - + `DatePickerSingleInput` composes a `DateInputSingle` that uses the `DatePicker` context for single date selection. ### DatePickerRangeInput - + `DatePickerRangeInput` composes a `DateInputRange` that uses the `DatePicker` context for date range selection. ### DatePickerSinglePanel - + `DatePickerSinglePanel` composes a `Calendar` that uses the `DatePicker` context for single date selection. ### DatePickerRangePanel - + `DatePickerRangePanel` composes a `Calendar` that uses the `DatePicker` context for date range selection. ### DatePickerOverlay - + `DatePickerOverlay` is the container component for the `DatePicker` overlay. ### DatePickerActions - + `DatePickerActions` provides the controls to apply or cancel a date selection. @@ -114,10 +114,10 @@ import { DatePicker } from "@salt-ds/lab"; The `useDatePicker` hook provides `state` and `helpers` to manage the `DatePicker` state. - + ### useDatePickerOverlay The `useDatePickerOverlay` hook provides `state` and `helpers` to manage the `DatePickerOverlay` state. - + diff --git a/site/src/examples/calendar/Bordered.tsx b/site/src/examples/calendar/Bordered.tsx index 53e7dd712b9..6d1513d90be 100644 --- a/site/src/examples/calendar/Bordered.tsx +++ b/site/src/examples/calendar/Bordered.tsx @@ -1,5 +1,10 @@ import { getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const Bordered = (): ReactElement => ( @@ -11,5 +16,7 @@ export const Bordered = (): ReactElement => ( MonthDropdownProps={{ bordered: true }} YearDropdownProps={{ bordered: true }} /> + + ); diff --git a/site/src/examples/calendar/CustomDayRender.tsx b/site/src/examples/calendar/CustomDayRender.tsx index 655428f1def..0080c8d3a2f 100644 --- a/site/src/examples/calendar/CustomDayRender.tsx +++ b/site/src/examples/calendar/CustomDayRender.tsx @@ -3,7 +3,13 @@ import { type DateValue, getLocalTimeZone, } from "@internationalized/date"; -import { Calendar, getCurrentLocale } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, + getCurrentLocale, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; function renderDayContents(day: DateValue) { @@ -12,9 +18,11 @@ function renderDayContents(day: DateValue) { } export const CustomDayRender = (): ReactElement => ( - + + + + ({ renderDayContents })} + /> + ); diff --git a/site/src/examples/calendar/DisabledDates.tsx b/site/src/examples/calendar/DisabledDates.tsx index 53d034ddfcc..440a051fde1 100644 --- a/site/src/examples/calendar/DisabledDates.tsx +++ b/site/src/examples/calendar/DisabledDates.tsx @@ -1,5 +1,11 @@ import { type DateValue, getDayOfWeek } from "@internationalized/date"; -import { Calendar, CalendarNavigation, getCurrentLocale } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, + getCurrentLocale, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; // Saturday & Sunday @@ -9,5 +15,7 @@ const isDayDisabled = (date: DateValue) => export const DisabledDates = (): ReactElement => ( + + ); diff --git a/site/src/examples/calendar/HideOutOfRangeDates.tsx b/site/src/examples/calendar/HideOutOfRangeDates.tsx index 6c7a326f2eb..57c3fc37e38 100644 --- a/site/src/examples/calendar/HideOutOfRangeDates.tsx +++ b/site/src/examples/calendar/HideOutOfRangeDates.tsx @@ -1,6 +1,15 @@ -import { Calendar } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const HideOutOfRangeDates = (): ReactElement => ( - + + + + + ); diff --git a/site/src/examples/calendar/HideYearDropdown.tsx b/site/src/examples/calendar/HideYearDropdown.tsx index 3a5c63cd1e5..63c1e10f038 100644 --- a/site/src/examples/calendar/HideYearDropdown.tsx +++ b/site/src/examples/calendar/HideYearDropdown.tsx @@ -1,7 +1,14 @@ -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const HideYearDropdown = (): ReactElement => ( + + ); diff --git a/site/src/examples/calendar/HighlightedDates.tsx b/site/src/examples/calendar/HighlightedDates.tsx index 801acf5f8a9..467f395f52a 100644 --- a/site/src/examples/calendar/HighlightedDates.tsx +++ b/site/src/examples/calendar/HighlightedDates.tsx @@ -3,7 +3,12 @@ import { isEqualDay, startOfMonth, } from "@internationalized/date"; -import { Calendar, CalendarNavigation, getCurrentLocale } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; // Start of month @@ -13,5 +18,7 @@ const isDayHighlighted = (date: DateValue) => export const HighlightedDates = (): ReactElement => ( + + ); diff --git a/site/src/examples/calendar/MinMaxDate.tsx b/site/src/examples/calendar/MinMaxDate.tsx index 83a081df897..fbbfa405dd0 100644 --- a/site/src/examples/calendar/MinMaxDate.tsx +++ b/site/src/examples/calendar/MinMaxDate.tsx @@ -4,7 +4,12 @@ import { startOfMonth, today, } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const MinMaxDate = (): ReactElement => ( @@ -15,5 +20,7 @@ export const MinMaxDate = (): ReactElement => ( maxDate={endOfMonth(today(getLocalTimeZone()))} > + + ); diff --git a/site/src/examples/calendar/Multiselect.tsx b/site/src/examples/calendar/Multiselect.tsx index a9fef3813bb..2876f09b4ee 100644 --- a/site/src/examples/calendar/Multiselect.tsx +++ b/site/src/examples/calendar/Multiselect.tsx @@ -1,11 +1,16 @@ import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const Multiselect = (): ReactElement => ( ( ]} > + + ); diff --git a/site/src/examples/calendar/Offset.tsx b/site/src/examples/calendar/Offset.tsx index 2a960b38684..d8af4116053 100644 --- a/site/src/examples/calendar/Offset.tsx +++ b/site/src/examples/calendar/Offset.tsx @@ -1,5 +1,10 @@ import { getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const Offset = (): ReactElement => ( @@ -7,10 +12,12 @@ export const Offset = (): ReactElement => ( selectionVariant="offset" endDateOffset={(date) => date.add({ days: 2 })} defaultSelectedDate={{ - startDate: today(getLocalTimeZone()).subtract({ days: 2 }), - endDate: today(getLocalTimeZone()), + startDate: today(getLocalTimeZone()), + endDate: today(getLocalTimeZone()).add({ days: 2 }), }} > + + ); diff --git a/site/src/examples/calendar/Range.tsx b/site/src/examples/calendar/Range.tsx index 903701187cc..53e6b9e95d2 100644 --- a/site/src/examples/calendar/Range.tsx +++ b/site/src/examples/calendar/Range.tsx @@ -1,5 +1,10 @@ import { getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const Range = (): ReactElement => ( ( }} > + + ); diff --git a/site/src/examples/calendar/Single.tsx b/site/src/examples/calendar/Single.tsx index 4295ff81b43..82dc3f3230d 100644 --- a/site/src/examples/calendar/Single.tsx +++ b/site/src/examples/calendar/Single.tsx @@ -1,5 +1,10 @@ import { getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const Single = (): ReactElement => ( @@ -8,5 +13,7 @@ export const Single = (): ReactElement => ( defaultSelectedDate={today(getLocalTimeZone())} > + + ); diff --git a/site/src/examples/calendar/CustomHeader.tsx b/site/src/examples/calendar/TodayButton.tsx similarity index 62% rename from site/src/examples/calendar/CustomHeader.tsx rename to site/src/examples/calendar/TodayButton.tsx index 77de7f481bc..32973cf944e 100644 --- a/site/src/examples/calendar/CustomHeader.tsx +++ b/site/src/examples/calendar/TodayButton.tsx @@ -1,13 +1,15 @@ import { getLocalTimeZone, startOfMonth, today } from "@internationalized/date"; -import { Button, StackLayout } from "@salt-ds/core"; +import { Button, Divider, StackLayout } from "@salt-ds/core"; import { Calendar, + CalendarDateGrid, CalendarNavigation, + CalendarWeekHeader, type UseCalendarSelectionSingleProps, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import React, { type ReactElement, useState } from "react"; -export const CustomHeader = (): ReactElement => { +export const TodayButton = (): ReactElement => { const [selectedDate, setSelectedDate] = useState< UseCalendarSelectionSingleProps["selectedDate"] >(today(getLocalTimeZone()).subtract({ years: 1 })); @@ -26,7 +28,15 @@ export const CustomHeader = (): ReactElement => { > - diff --git a/site/src/examples/calendar/TwinCalendars.tsx b/site/src/examples/calendar/TwinCalendars.tsx index 3d34431e492..c3d4eeb949a 100644 --- a/site/src/examples/calendar/TwinCalendars.tsx +++ b/site/src/examples/calendar/TwinCalendars.tsx @@ -6,8 +6,10 @@ import { } from "@internationalized/date"; import { Calendar, + CalendarDateGrid, CalendarNavigation, type CalendarProps, + CalendarWeekHeader, type UseCalendarSelectionRangeProps, } from "@salt-ds/lab"; import { type ReactElement, useState } from "react"; @@ -40,9 +42,10 @@ export const TwinCalendars = (): ReactElement => { : startOfMonth(today(getLocalTimeZone())) } selectedDate={selectedDate} - hideOutOfRangeDates > + + { ? startOfMonth(selectedDate.endDate) : startOfMonth(today(getLocalTimeZone()).add({ months: 1 })) } - hideOutOfRangeDates > + +
); diff --git a/site/src/examples/calendar/UnselectableDates.tsx b/site/src/examples/calendar/UnselectableDates.tsx index a36f94480c5..8d3531a74da 100644 --- a/site/src/examples/calendar/UnselectableDates.tsx +++ b/site/src/examples/calendar/UnselectableDates.tsx @@ -1,5 +1,11 @@ import { type DateValue, getDayOfWeek } from "@internationalized/date"; -import { Calendar, CalendarNavigation, getCurrentLocale } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, + getCurrentLocale, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; // Saturday & Sunday @@ -11,5 +17,7 @@ const isDayUnselectable = (date: DateValue) => export const UnselectableDates = (): ReactElement => ( + + ); diff --git a/site/src/examples/calendar/WithLocale.tsx b/site/src/examples/calendar/WithLocale.tsx index d66b5a67836..18d82a9983d 100644 --- a/site/src/examples/calendar/WithLocale.tsx +++ b/site/src/examples/calendar/WithLocale.tsx @@ -1,9 +1,15 @@ -import { getLocalTimeZone, today } from "@internationalized/date"; -import { Calendar, CalendarNavigation } from "@salt-ds/lab"; +import { + Calendar, + CalendarDateGrid, + CalendarNavigation, + CalendarWeekHeader, +} from "@salt-ds/lab"; import type { ReactElement } from "react"; export const WithLocale = (): ReactElement => ( + + ); diff --git a/site/src/examples/calendar/index.ts b/site/src/examples/calendar/index.ts index 436eff30888..99639c06d6b 100644 --- a/site/src/examples/calendar/index.ts +++ b/site/src/examples/calendar/index.ts @@ -8,8 +8,8 @@ export * from "./DisabledDates"; export * from "./HighlightedDates"; export * from "./HideOutOfRangeDates"; export * from "./HideYearDropdown"; -export * from "./CustomHeader"; export * from "./CustomDayRender"; export * from "./MinMaxDate"; +export * from "./TodayButton"; export * from "./TwinCalendars"; export * from "./WithLocale"; diff --git a/site/src/examples/date-picker/Range.tsx b/site/src/examples/date-picker/Range.tsx index 2e566501f5a..a2d29fa40a1 100644 --- a/site/src/examples/date-picker/Range.tsx +++ b/site/src/examples/date-picker/Range.tsx @@ -7,7 +7,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import type { ReactElement } from "react"; +import { type ReactElement, useCallback } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -23,16 +23,23 @@ function formatDateRange( : endDate; return `Start date: ${formattedStartDate}, End date: ${formattedEndDate}`; } -export const Range = (): ReactElement => ( - { +export const Range = (): ReactElement => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: DateRangeSelection | null) => { console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - }} - > - - - - - -); + }, + [], + ); + + return ( + + + + + + + ); +}; diff --git a/site/src/examples/date-picker/RangeBordered.tsx b/site/src/examples/date-picker/RangeBordered.tsx index d3dbc249f5f..dceab327b63 100644 --- a/site/src/examples/date-picker/RangeBordered.tsx +++ b/site/src/examples/date-picker/RangeBordered.tsx @@ -7,7 +7,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import React, { type ReactElement } from "react"; +import React, { type ReactElement, useCallback } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -24,25 +24,32 @@ function formatDateRange( return `Start date: ${formattedStartDate}, End date: ${formattedEndDate}`; } -export const RangeBordered = (): ReactElement => ( - { +export const RangeBordered = (): ReactElement => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: DateRangeSelection | null) => { console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - }} - > - - - - - -); + }, + [], + ); + + return ( + + + + + + + ); +}; diff --git a/site/src/examples/date-picker/RangeControlled.tsx b/site/src/examples/date-picker/RangeControlled.tsx index d0e49877a51..685fcc54e02 100644 --- a/site/src/examples/date-picker/RangeControlled.tsx +++ b/site/src/examples/date-picker/RangeControlled.tsx @@ -7,7 +7,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -28,14 +28,22 @@ export const RangeControlled = (): ReactElement => { const [selectedDate, setSelectedDate] = useState( null, ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + }, + [setSelectedDate], + ); + return ( { - console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/RangeWithConfirmation.tsx b/site/src/examples/date-picker/RangeWithConfirmation.tsx index a1ebaa3a8f1..74d005b55a0 100644 --- a/site/src/examples/date-picker/RangeWithConfirmation.tsx +++ b/site/src/examples/date-picker/RangeWithConfirmation.tsx @@ -17,7 +17,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import React, { type ReactElement, useRef, useState } from "react"; +import React, { type ReactElement, useCallback, useRef, useState } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -46,7 +46,10 @@ function isValidDateRange(date: DateRangeSelection | null) { } export const RangeWithConfirmation = (): ReactElement => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; + const defaultHelperText = + "Select range (DD MMM YYYY - DD MMM YYYY) e.g. 09 Jun 2024"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const applyButtonRef = useRef(null); const minDate = today(getLocalTimeZone()); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( @@ -55,6 +58,37 @@ export const RangeWithConfirmation = (): ReactElement => { const [selectedDate, setSelectedDate] = useState( null, ); + const handleApply = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + }, + [setValidationStatus, setHelperText], + ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: DateRangeSelection | null) => { + setSelectedDate(newSelectedDate); + if (newSelectedDate?.startDate && newSelectedDate?.endDate) { + applyButtonRef?.current?.focus(); + } + }, + [applyButtonRef?.current, setSelectedDate], + ); return ( @@ -63,25 +97,9 @@ export const RangeWithConfirmation = (): ReactElement => { selectionVariant="range" minDate={minDate} maxDate={minDate.add({ years: 50 })} + onApply={handleApply} + onSelectedDateChange={handleSelectedDateChange} selectedDate={selectedDate} - onApply={(newSelectedDate, error) => { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - }} - onSelectedDateChange={(newSelectedDate, error) => { - setSelectedDate(newSelectedDate); - if (newSelectedDate?.startDate && newSelectedDate?.endDate) { - applyButtonRef?.current?.focus(); - } - }} > diff --git a/site/src/examples/date-picker/RangeWithCustomPanel.tsx b/site/src/examples/date-picker/RangeWithCustomPanel.tsx index 2c9e0c361f6..28ed2b9f941 100644 --- a/site/src/examples/date-picker/RangeWithCustomPanel.tsx +++ b/site/src/examples/date-picker/RangeWithCustomPanel.tsx @@ -13,7 +13,7 @@ import { getCurrentLocale, } from "@salt-ds/lab"; import { CustomDatePickerPanel } from "@salt-ds/lab/stories/date-picker/CustomDatePickerPanel"; -import React, { type ReactElement } from "react"; +import React, { type ReactElement, useCallback } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -33,16 +33,21 @@ function formatDateRange( export const RangeWithCustomPanel = (): ReactElement => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; const minDate = today(getLocalTimeZone()); + const handleSelectedDateChange = useCallback( + (newSelectedDate: DateRangeSelection | null) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + }, + [], + ); + return ( Select a date range { - console.log(`Selected date: ${formatDateRange(newSelectedDate)}`); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/RangeWithFormField.tsx b/site/src/examples/date-picker/RangeWithFormField.tsx index 75f76ef7d2f..9193f0416bc 100644 --- a/site/src/examples/date-picker/RangeWithFormField.tsx +++ b/site/src/examples/date-picker/RangeWithFormField.tsx @@ -12,7 +12,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -41,12 +41,31 @@ function isValidDateRange(date: DateRangeSelection | null) { } export const RangeWithFormField = (): ReactElement => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; + const defaultHelperText = + "Select range DD MMM YYYY - DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + setValidationStatus(validationStatus); + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + }, + [setValidationStatus, setHelperText], ); return ( @@ -54,20 +73,7 @@ export const RangeWithFormField = (): ReactElement => { Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - setSelectedDate(newSelectedDate); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/RangeWithInitialError.tsx b/site/src/examples/date-picker/RangeWithInitialError.tsx index e11120dd185..1af51ecaded 100644 --- a/site/src/examples/date-picker/RangeWithInitialError.tsx +++ b/site/src/examples/date-picker/RangeWithInitialError.tsx @@ -13,7 +13,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -42,29 +42,40 @@ function isValidDateRange(date: DateRangeSelection | null) { } export const RangeWithInitialError = (): ReactElement => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; + const defaultHelperText = + "Select range DD MMM YYYY - DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(errorHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( "error", ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { startDate: string | false; endDate: string | false }, + ) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + }, + [setValidationStatus, setHelperText], + ); return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - }} defaultSelectedDate={{ startDate: new CalendarDate(2024, 6, 9) }} + onSelectedDateChange={handleSelectedDateChange} > { - if (!dateString) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [, day, month, year] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; -}; - -const formatDateEsES = (date: DateValue | null) => { - return date - ? new DateFormatter("es-ES", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; -}; - export const RangeWithLocaleEsES = (): ReactElement => { const locale = "es-ES"; - const [selectedDate, setSelectedDate] = useState( - null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); + const handleSelectedDateChange = useCallback( + ( + newSelectedDate: DateRangeSelection | null, + error: { + startDate: string | false; + endDate: string | false; + }, + ) => { + console.log( + `Selected date range: ${formatDateRange(newSelectedDate, locale, { + day: "2-digit", + month: "2-digit", + year: "numeric", + })}`, + ); + const validationStatus = + !error.startDate && !error.endDate && isValidDateRange(newSelectedDate) + ? undefined + : "error"; + if (validationStatus === "error") { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(validationStatus); + }, + [setValidationStatus, setHelperText], + ); return ( Select a date { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate, locale, { - day: "2-digit", - month: "2-digit", - year: "numeric", - })}`, - ); - setSelectedDate(newSelectedDate); - const validationStatus = - !error.startDate && - !error.endDate && - isValidDateRange(newSelectedDate) - ? undefined - : "error"; - setValidationStatus(validationStatus); - }} + onSelectedDateChange={handleSelectedDateChange} > - + - + {helperText} diff --git a/site/src/examples/date-picker/RangeWithMinMaxDate.tsx b/site/src/examples/date-picker/RangeWithMinMaxDate.tsx index 7c9ffec39a3..c63dd19718c 100644 --- a/site/src/examples/date-picker/RangeWithMinMaxDate.tsx +++ b/site/src/examples/date-picker/RangeWithMinMaxDate.tsx @@ -13,7 +13,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatDateRange( dateRange: DateRangeSelection | null, @@ -31,24 +31,22 @@ function formatDateRange( } export const RangeWithMinMaxDate = (): ReactElement => { - const [selectedDate, setSelectedDate] = useState( - null, + const helperText = "Select date between 15/01/2030 and 15/01/2031"; + const handleSelectedDateChange = useCallback( + (newSelectedDate: DateRangeSelection | null) => { + console.log(`Selected date range: ${formatDateRange(newSelectedDate)}`); + }, + [], ); - const helperText = "Valid between 15/01/2030 and 15/01/2031"; + return ( Select a date range { - console.log( - `Selected date range: ${formatDateRange(newSelectedDate)}`, - ); - setSelectedDate(newSelectedDate); - }} minDate={new CalendarDate(2030, 1, 15)} maxDate={new CalendarDate(2031, 1, 15)} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/Single.tsx b/site/src/examples/date-picker/Single.tsx index a54baf5ae20..7e69b8123db 100644 --- a/site/src/examples/date-picker/Single.tsx +++ b/site/src/examples/date-picker/Single.tsx @@ -4,10 +4,11 @@ import { DatePickerOverlay, DatePickerSingleInput, DatePickerSinglePanel, + type SingleDateSelection, formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import type { ReactElement } from "react"; +import { type ReactElement, useCallback } from "react"; function formatSingleDate( date: DateValue | null, @@ -20,16 +21,23 @@ function formatSingleDate( return date; } -export const Single = (): ReactElement => ( - { +export const Single = (): ReactElement => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - }} - > - - - - - -); + }, + [], + ); + + return ( + + + + + + + ); +}; diff --git a/site/src/examples/date-picker/SingleBordered.tsx b/site/src/examples/date-picker/SingleBordered.tsx index 9ec591c46f2..947673bf0a7 100644 --- a/site/src/examples/date-picker/SingleBordered.tsx +++ b/site/src/examples/date-picker/SingleBordered.tsx @@ -4,10 +4,11 @@ import { DatePickerOverlay, DatePickerSingleInput, DatePickerSinglePanel, + type SingleDateSelection, formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import React, { type ReactElement } from "react"; +import React, { type ReactElement, useCallback } from "react"; function formatSingleDate( date: DateValue | null, @@ -20,21 +21,28 @@ function formatSingleDate( return date; } -export const SingleBordered = (): ReactElement => ( - { +export const SingleBordered = (): ReactElement => { + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - }} - > - - - - - -); + }, + [], + ); + + return ( + + + + + + + ); +}; diff --git a/site/src/examples/date-picker/SingleControlled.tsx b/site/src/examples/date-picker/SingleControlled.tsx index 00e48054ebf..1ca0cc16a17 100644 --- a/site/src/examples/date-picker/SingleControlled.tsx +++ b/site/src/examples/date-picker/SingleControlled.tsx @@ -8,7 +8,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -25,14 +25,19 @@ export const SingleControlled = (): ReactElement => { const [selectedDate, setSelectedDate] = useState( null, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + }, + [setSelectedDate], + ); + return ( { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/SingleWithConfirmation.tsx b/site/src/examples/date-picker/SingleWithConfirmation.tsx index 14aec3aaa89..06e13082318 100644 --- a/site/src/examples/date-picker/SingleWithConfirmation.tsx +++ b/site/src/examples/date-picker/SingleWithConfirmation.tsx @@ -17,7 +17,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import React, { type ReactElement, useRef, useState } from "react"; +import React, { type ReactElement, useCallback, useRef, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -31,7 +31,9 @@ function formatSingleDate( } export const SingleWithConfirmation = (): ReactElement => { - const helperText = "Select range (DD MMM YYYY - DD MMM YYYY)"; + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const applyButtonRef = useRef(null); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, @@ -39,26 +41,40 @@ export const SingleWithConfirmation = (): ReactElement => { const [selectedDate, setSelectedDate] = useState( null, ); + const handleApply = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setSelectedDate, setHelperText], + ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { + setSelectedDate(newSelectedDate); + applyButtonRef?.current?.focus(); + }, + [applyButtonRef?.current, setSelectedDate], + ); + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setValidationStatus(error ? "error" : undefined); - }} - onSelectedDateChange={(newSelectedDate, error) => { - setSelectedDate(newSelectedDate); - applyButtonRef?.current?.focus(); - }} > - + diff --git a/site/src/examples/date-picker/SingleWithCustomPanel.tsx b/site/src/examples/date-picker/SingleWithCustomPanel.tsx index 757e1ac4830..2dd445e8623 100644 --- a/site/src/examples/date-picker/SingleWithCustomPanel.tsx +++ b/site/src/examples/date-picker/SingleWithCustomPanel.tsx @@ -12,11 +12,12 @@ import { DatePicker, DatePickerOverlay, DatePickerSingleInput, + type SingleDateSelection, formatDate, getCurrentLocale, } from "@salt-ds/lab"; import { CustomDatePickerPanel } from "@salt-ds/lab/stories/date-picker/CustomDatePickerPanel"; -import React, { type ReactElement } from "react"; +import React, { type ReactElement, useCallback } from "react"; function formatSingleDate( date: DateValue | null, @@ -32,6 +33,13 @@ function formatSingleDate( export const SingleWithCustomPanel = (): ReactElement => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; const minDate = today(getLocalTimeZone()); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + }, + [], + ); + return ( Select a date @@ -39,9 +47,7 @@ export const SingleWithCustomPanel = (): ReactElement => { minDate={minDate} maxDate={minDate.add({ years: 50 })} selectionVariant="single" - onSelectedDateChange={(newSelectedDate, _error) => { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/SingleWithCustomParser.tsx b/site/src/examples/date-picker/SingleWithCustomParser.tsx index 1bc1d759f5a..e70736a9600 100644 --- a/site/src/examples/date-picker/SingleWithCustomParser.tsx +++ b/site/src/examples/date-picker/SingleWithCustomParser.tsx @@ -20,7 +20,7 @@ import { getCurrentLocale, parseCalendarDate, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -34,52 +34,65 @@ function formatSingleDate( } export const SingleWithCustomParser = (): ReactElement => { - const helperText = + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024) or +/-D (e.g. +7)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); const [selectedDate, setSelectedDate] = useState( null, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + setSelectedDate(newSelectedDate); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setValidationStatus, setSelectedDate, setHelperText], + ); + const handleParse = useCallback( + (inputDate: string): DateInputSingleParserResult => { + if (!inputDate?.length) { + return { date: null, error: false }; + } + const parsedDate = inputDate; + const offsetMatch = parsedDate?.match(/^([+-]?\d+)$/); + if (offsetMatch) { + const offsetDays = Number.parseInt(offsetMatch[1], 10); + let offsetDate = selectedDate + ? selectedDate + : today(getLocalTimeZone()); + offsetDate = offsetDate.add({ days: offsetDays }); + return { + date: new CalendarDate( + offsetDate.year, + offsetDate.month, + offsetDate.day, + ), + error: false, + }; + } + return parseCalendarDate(parsedDate || ""); + }, + [], + ); return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - }} > - { - if (!inputDate?.length) { - return { date: null, error: false }; - } - const parsedDate = inputDate; - const offsetMatch = parsedDate?.match(/^([+-]?\d+)$/); - if (offsetMatch) { - const offsetDays = Number.parseInt(offsetMatch[1], 10); - let offsetDate = selectedDate - ? selectedDate - : today(getLocalTimeZone()); - offsetDate = offsetDate.add({ days: offsetDays }); - return { - date: new CalendarDate( - offsetDate.year, - offsetDate.month, - offsetDate.day, - ), - error: false, - }; - } - return parseCalendarDate(parsedDate || ""); - }} - /> + diff --git a/site/src/examples/date-picker/SingleWithFormField.tsx b/site/src/examples/date-picker/SingleWithFormField.tsx index 1e2170eb627..fabc0baacf7 100644 --- a/site/src/examples/date-picker/SingleWithFormField.tsx +++ b/site/src/examples/date-picker/SingleWithFormField.tsx @@ -13,7 +13,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -27,12 +27,23 @@ function formatSingleDate( } export const SingleWithFormField = (): ReactElement => { - const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setValidationStatus, setHelperText], ); return ( @@ -40,12 +51,7 @@ export const SingleWithFormField = (): ReactElement => { Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/SingleWithInitialError.tsx b/site/src/examples/date-picker/SingleWithInitialError.tsx index 5d592ea344c..3e805599e3e 100644 --- a/site/src/examples/date-picker/SingleWithInitialError.tsx +++ b/site/src/examples/date-picker/SingleWithInitialError.tsx @@ -13,7 +13,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -27,12 +27,23 @@ function formatSingleDate( } export const SingleWithInitialError = (): ReactElement => { - const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const defaultHelperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(errorHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( "error", ); - const [selectedDate, setSelectedDate] = useState( - null, + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setValidationStatus, setHelperText], ); return ( @@ -40,12 +51,7 @@ export const SingleWithInitialError = (): ReactElement => { Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - }} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/SingleWithLocaleEnUS.tsx b/site/src/examples/date-picker/SingleWithLocaleEnUS.tsx index 17f0c4410a0..151aa204e58 100644 --- a/site/src/examples/date-picker/SingleWithLocaleEnUS.tsx +++ b/site/src/examples/date-picker/SingleWithLocaleEnUS.tsx @@ -1,84 +1,51 @@ -import { - CalendarDate, - DateFormatter, - type DateValue, - getLocalTimeZone, -} from "@internationalized/date"; import { FormField, FormFieldHelperText as FormHelperText, FormFieldLabel as FormLabel, } from "@salt-ds/core"; import { - type DateInputSingleParserResult, DatePicker, DatePickerOverlay, DatePickerSingleInput, DatePickerSinglePanel, type SingleDateSelection, + formatDate, } from "@salt-ds/lab"; -import React, { type ReactElement, useState } from "react"; - -const parseDateEnUS = (dateString: string): DateInputSingleParserResult => { - if (!dateString?.length) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [, month, day, year] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; -}; - -const formatDateEnUS = (date: DateValue | null) => { - return date - ? new DateFormatter("en-US", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; -}; +import React, { type ReactElement, useCallback, useState } from "react"; export const SingleWithLocaleEnUS = (): ReactElement => { const locale = "en-US"; - const [selectedDate, setSelectedDate] = useState( - null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${newSelectedDate}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setValidationStatus, setHelperText], + ); return ( Select a date { - console.log(`Selected date: ${formatDateEnUS(newSelectedDate)}`); - setValidationStatus(error ? "error" : undefined); - setSelectedDate(newSelectedDate); - }} + onSelectedDateChange={handleSelectedDateChange} > - + - + {helperText} diff --git a/site/src/examples/date-picker/SingleWithLocaleZhCN.tsx b/site/src/examples/date-picker/SingleWithLocaleZhCN.tsx index 1566f9d03fe..7b9c17bdcc7 100644 --- a/site/src/examples/date-picker/SingleWithLocaleZhCN.tsx +++ b/site/src/examples/date-picker/SingleWithLocaleZhCN.tsx @@ -1,5 +1,4 @@ import { - CalendarDate, DateFormatter, type DateValue, getLocalTimeZone, @@ -10,7 +9,6 @@ import { FormFieldLabel as FormLabel, } from "@salt-ds/core"; import { - type DateInputSingleParserResult, DatePicker, DatePickerOverlay, DatePickerSingleInput, @@ -19,47 +17,30 @@ import { type SingleDateSelection, formatDate, } from "@salt-ds/lab"; -import React, { type ReactElement, useState } from "react"; - -const formatDateZhCN = (date: DateValue | null) => { - return date - ? new DateFormatter("zh-CN", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date.toDate(getLocalTimeZone())) - : ""; -}; - -const parseDateZhCN = (dateString: string): DateInputSingleParserResult => { - if (!dateString?.length) { - return { date: null, error: false }; - } - const dateParts = dateString.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/); - if (!dateParts) { - return { date: null, error: "invalid date" }; - } - const [_, year, month, day] = dateParts; - return { - date: new CalendarDate( - Number.parseInt(year, 10), - Number.parseInt(month, 10), - Number.parseInt(day, 10), - ), - error: false, - }; -}; +import React, { type ReactElement, useCallback, useState } from "react"; export const SingleWithLocaleZhCN = (): ReactElement => { const locale = "zh-CN"; - const [selectedDate, setSelectedDate] = useState( - null, - ); - const helperText = `Locale ${locale}`; + const defaultHelperText = `Locale ${locale}`; + const errorHelperText = "Please enter a valid date in DD MMM YYYY format"; + const [helperText, setHelperText] = useState(defaultHelperText); const [validationStatus, setValidationStatus] = useState<"error" | undefined>( undefined, ); + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null, error: string | false) => { + console.log(`Selected date: ${newSelectedDate ?? null}`); + if (error) { + setHelperText(errorHelperText); + } else { + setHelperText(defaultHelperText); + } + setValidationStatus(error ? "error" : undefined); + }, + [setValidationStatus, setHelperText], + ); + const formatMonth = (date: DateValue) => formatDate(date, locale, { month: "long", @@ -77,27 +58,16 @@ export const SingleWithLocaleZhCN = (): ReactElement => { Select a date { - console.log( - `Selected date: ${formatDateZhCN(newSelectedDate ?? null)}`, - ); - setSelectedDate(newSelectedDate); - setValidationStatus(error ? "error" : undefined); - }} + onSelectedDateChange={handleSelectedDateChange} > - + ({ renderDayContents }), + }} CalendarNavigationProps={{ formatMonth }} /> diff --git a/site/src/examples/date-picker/SingleWithMinMaxDate.tsx b/site/src/examples/date-picker/SingleWithMinMaxDate.tsx index f1378417ecd..b684158e0bd 100644 --- a/site/src/examples/date-picker/SingleWithMinMaxDate.tsx +++ b/site/src/examples/date-picker/SingleWithMinMaxDate.tsx @@ -13,7 +13,7 @@ import { formatDate, getCurrentLocale, } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; +import { type ReactElement, useCallback, useState } from "react"; function formatSingleDate( date: DateValue | null, @@ -27,22 +27,22 @@ function formatSingleDate( } export const SingleWithMinMaxDate = (): ReactElement => { - const [selectedDate, setSelectedDate] = useState( - null, + const helperText = "Select date between 15/01/2030 and 15/01/2031"; + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + }, + [], ); - const helperText = "Valid between 15/01/2030 and 15/01/2031"; + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - setSelectedDate(newSelectedDate); - }} minDate={new CalendarDate(2030, 1, 15)} maxDate={new CalendarDate(2031, 1, 15)} + onSelectedDateChange={handleSelectedDateChange} > diff --git a/site/src/examples/date-picker/SingleWithToday.tsx b/site/src/examples/date-picker/SingleWithTodayButton.tsx similarity index 58% rename from site/src/examples/date-picker/SingleWithToday.tsx rename to site/src/examples/date-picker/SingleWithTodayButton.tsx index e0debd29dee..2909457f09b 100644 --- a/site/src/examples/date-picker/SingleWithToday.tsx +++ b/site/src/examples/date-picker/SingleWithTodayButton.tsx @@ -5,6 +5,7 @@ import { } from "@internationalized/date"; import { Button, + Divider, FlexItem, FlexLayout, FormField, @@ -17,11 +18,12 @@ import { DatePickerSingleInput, DatePickerSinglePanel, type SingleDatePickerState, + type SingleDateSelection, formatDate, getCurrentLocale, useDatePickerContext, } from "@salt-ds/lab"; -import React, { type ReactElement } from "react"; +import React, { type ReactElement, useCallback } from "react"; const TodayButton = () => { const { @@ -29,14 +31,17 @@ const TodayButton = () => { } = useDatePickerContext({ selectionVariant: "single", }) as SingleDatePickerState; - return ( - +
+ +
); }; @@ -51,23 +56,39 @@ function formatSingleDate( return date; } -export const SingleWithToday = (): ReactElement => { +export const SingleWithTodayButton = (): ReactElement => { const helperText = "Date format DD MMM YYYY (e.g. 09 Jun 2024)"; + const handleSelectedDateChange = useCallback( + (newSelectedDate: SingleDateSelection | null) => { + console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); + }, + [], + ); + return ( Select a date { - console.log(`Selected date: ${formatSingleDate(newSelectedDate)}`); - }} + onSelectedDateChange={handleSelectedDateChange} > + + + {helperText} + + + + + + + + diff --git a/site/src/examples/date-picker/index.ts b/site/src/examples/date-picker/index.ts index b53e578e12f..38d075f5ff2 100644 --- a/site/src/examples/date-picker/index.ts +++ b/site/src/examples/date-picker/index.ts @@ -8,7 +8,7 @@ export * from "./SingleWithLocaleEnUS"; export * from "./SingleWithLocaleZhCN"; export * from "./SingleWithMinMaxDate"; export * from "./SingleWithFormField"; -export * from "./SingleWithToday"; +export * from "./SingleWithTodayButton"; export * from "./SingleBordered"; export * from "./Range"; export * from "./RangeControlled";