Skip to content

Commit

Permalink
Date picker review2 (#4291)
Browse files Browse the repository at this point in the history
Co-authored-by: origami-z <[email protected]>
  • Loading branch information
mark-tate and origami-z authored Nov 28, 2024
1 parent 7d32518 commit b272497
Show file tree
Hide file tree
Showing 135 changed files with 15,160 additions and 7,071 deletions.
126 changes: 126 additions & 0 deletions .changeset/healthy-tigers-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
"@salt-ds/date-adapters": patch
"@salt-ds/lab": patch
---

DatePicker, DateInput, Calendar Lab updates

We are excited to introduce a new Salt package, `@salt-ds/date-adapters`, currently in pre-release/lab status to gather your valuable feedback.

This package includes supported adapters for Salt's date-based controls:

- `AdapterDateFns` for [date-fns](https://date-fns.org/)
- `AdapterDayjs` for [dayjs](https://day.js.org/)
- `AdapterLuxon` for [luxon](https://moment.github.io/luxon/)
- `AdapterMoment` (legacy) for [moment](https://momentjs.com/)

> **Note:** As `moment` is no longer actively maintained by its creators, `AdapterMoment` is published in a deprecated form to assist in transitioning to a newer date framework.
Salt adapters are integrated with a new `LocalizationProvider`, enabling a date-based API accessible through `useLocalization`. Typically, you only need to add one `LocalizationProvider` per application, although they can be nested if necessary.

`@salt-ds/adapters` uses peer dependencies for the supported date libraries. It is the responsibility of the application author to include the required dependencies in their build. Additionally, the application author is responsible for configuring the date libraries, including any necessary extensions or loading dependencies for supported locales.

**Example Usage**

An app that renders a Salt date-based control may look like this:

```jsx
import { AdapterDateFns } from "@salt-ds/date-adapters";
import {
Calendar,
CalendarNavigation,
CalendarWeekHeader,
CalendarGrid,
LocalizationProvider,
} from "@salt-ds/lab";

const MyApp = () => (
<SaltProvider density="high" mode="light">
<LocalizationProvider DateAdapter={AdapterDateFns}>
<Calendar selectionVariant="single">
<CalendarNavigation />
<CalendarWeekHeader />
<CalendarGrid />
</Calendar>
</LocalizationProvider>
</SaltProvider>
);
```

A `DateInput` within an app that uses `LocalizationProvider` might be implemented as follows:

```jsx
const MyDateInput = () => {
const { dateAdapter } = useLocalization();

function handleDateChange<TDate extends DateFrameworkType>(
event: SyntheticEvent,
date: TDate | null,
details: DateInputSingleDetails
) {
console.log(
`Selected date: ${dateAdapter.isValid(date) ? dateAdapter.format(date, "DD MMM YYYY") : date}`
);

const { value, errors } = details;
if (errors?.length && value) {
console.log(
`Error(s): ${errors
.map(({ type, message }) => `type=${type} message=${message}`)
.join(", ")}`
);
console.log(`Original Value: ${value}`);
}
}

return <DateInputSingle onDateChange={handleDateChange} />;
};
```
A `DatePicker` within an app that uses `LocalizationProvider` might be implemented as follows:
```jsx
const MyDatePicker = () => {
const { dateAdapter } = useLocalization();
const handleSelectionChange = useCallback(
(
event: SyntheticEvent,
date: SingleDateSelection<DateFrameworkType> | null,
details: DateInputSingleDetails | undefined,
) => {
const { value, errors } = details || {};
console.log(
`Selected date: ${dateAdapter.isValid(date) ? dateAdapter.format(date, "DD MMM YYYY") : date}`
);
if (errors?.length && value) {
console.log(
`Error(s): ${errors
.map(({ type, message }) => `type=${type} message=${message}`)
.join(", ")}`
);
console.log(`Original Value: ${value}`);
}
},
);

return (
<DatePicker
selectionVariant="single"
onSelectionChange={handleSelectionChange}
>
<DatePickerTrigger>
<DatePickerSingleInput />
</DatePickerTrigger>
<DatePickerOverlay>
<DatePickerSinglePanel />
</DatePickerOverlay>
</DatePicker>
);
};
```
In addition to configuring adapters, `LocalizationProvider` offers props to define locale and fallback min/max dates for all date-based controls.
Additional date adapters can be added , as long as they conform to the `SaltDateAdapter` interface.
For more detailed examples, please refer to the documentation for `DateInput`, `Calendar`, and `DatePicker`.
17 changes: 17 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "./styles.css";
import { SaltProvider, SaltProviderNext } from "@salt-ds/core";
import { DocsContainer } from "@storybook/addon-docs";
import { withDateMock } from "docs/decorators/withDateMock";
import { withLocalization } from "docs/decorators/withLocalization";
import { withResponsiveWrapper } from "docs/decorators/withResponsiveWrapper";
import { withScaffold } from "docs/decorators/withScaffold";
import { WithTextSpacingWrapper } from "docs/decorators/withTextSpacingWrapper";
Expand Down Expand Up @@ -109,6 +110,20 @@ export const globalTypes: GlobalTypes = {
title: "Component Style Injection",
},
},
dateAdapter: {
name: "Date Adapter",
description: "Date adapter type",
defaultValue: "date-fns",
toolbar: {
items: [
{ value: "date-fns", title: "date-fns" },
{ value: "dayjs", title: "dayjs" },
{ value: "luxon", title: "luxon" },
{ value: "moment", title: "moment (legacy)" },
],
title: "Date Adapter",
},
},
...themeNextGlobals,
};

Expand All @@ -118,6 +133,7 @@ export const argTypes: ArgTypes = {

export const parameters: Parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
mockDate: "2024-05-06",
layout: "centered",
// Show props description in Controls panel
controls: { expanded: true, sort: "requiredFirst" },
Expand Down Expand Up @@ -181,6 +197,7 @@ export const decorators = [
withScaffold,
withResponsiveWrapper,
withTheme,
withLocalization,
WithTextSpacingWrapper,
withDateMock,
];
Expand Down
5 changes: 5 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ async function getViteConfig(config: UserConfig) {
__dirname,
"./dist/salt-ds-countries",
),
"@salt-ds/date-adapters": path.resolve(
__dirname,
"./dist/salt-ds-date-adapters",
),
"@salt-ds/data-grid": path.resolve(
__dirname,
"./dist/salt-ds-data-grid",
Expand All @@ -69,6 +73,7 @@ async function getViteConfig(config: UserConfig) {
optimizeDeps: {
include: [
"@salt-ds/core",
"@salt-ds/data-adapters",
"@salt-ds/data-grid",
"@salt-ds/lab",
"@salt-ds/icons",
Expand Down
123 changes: 98 additions & 25 deletions cypress/support/commands.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { ReactNode } from "react";
import "@testing-library/cypress/add-commands";
import type { MountOptions, MountReturn } from "cypress/react";
import { mount as cypressMount } from "cypress/react18";
import "cypress-axe";
import { SaltProvider } from "@salt-ds/core";
import type {
DateFrameworkType,
SaltDateAdapter,
} from "@salt-ds/date-adapters";
import { LocalizationProvider } from "@salt-ds/lab";
import type { Options } from "cypress-axe";
import type { ReactNode } from "react";
import { AnnouncementListener } from "./AnnouncementListener";
import { type PerformanceResult, PerformanceTester } from "./PerformanceTester";

Expand Down Expand Up @@ -32,7 +37,7 @@ declare global {
*/
setDensity(theme: SupportedDensity): Chainable<void>;
/**
* Set Density
* Check a11y with Axe
*
* @example
* cy.checkAxeComponent()
Expand All @@ -42,37 +47,60 @@ declare global {
enableFailures?: boolean,
): Chainable<void>;

mountPerformance: (
/**
* Set the date adapter to be used by mounted tests
* @param adapter
*/
setDateAdapter(
adapter: SaltDateAdapter<DateFrameworkType>,
): Chainable<void>;

/**
* Set the date locale used by the date adapter
* @param any
*/
// biome-ignore lint/suspicious/noExplicitAny: locale type varies between Date frameworks
setDateLocale(locale: any): Chainable<void>;
mountPerformance(
jsx: ReactNode,
options?: MountOptions,
) => Chainable<MountReturn>;
mount: (jsx: ReactNode, options?: MountOptions) => Chainable<MountReturn>;

): Chainable<MountReturn>;
mount(jsx: ReactNode, options?: MountOptions): Chainable<MountReturn>;
getRenderCount(): Chainable<number>;

getRenderTime(): Chainable<number>;

paste(string: string): Chainable<void>;
}
}
}

Cypress.Commands.add("setMode", function (mode) {
Cypress.Commands.add("setMode", (mode: SupportedThemeMode) => {
if (SupportedThemeModeValues.includes(mode)) {
this.mode;
Cypress.env("mode", mode);
} else {
cy.log("Unsupported mode", mode);
}
});

Cypress.Commands.add("setDensity", function (density) {
Cypress.Commands.add("setDensity", (density: SupportedDensity) => {
if (SupportedDensityValues.includes(density)) {
this.density = density;
Cypress.env("density", density);
} else {
cy.log("Unsupported density", density);
}
});

Cypress.Commands.add(
"setDateAdapter",
// biome-ignore lint/suspicious/noExplicitAny: locale type varies between Date frameworks
(adapter: SaltDateAdapter<DateFrameworkType, any>) => {
Cypress.env("dateAdapter", adapter);
},
);
// biome-ignore lint/suspicious/noExplicitAny: locale type varies between Date frameworks
Cypress.Commands.add("setDateLocale", (locale: any) => {
Cypress.env("dateLocale", locale);
});

Cypress.Commands.add(
"checkAxeComponent",
(options: Options = {}, enableFailures = false) => {
Expand All @@ -94,20 +122,56 @@ Cypress.Commands.add(
},
);

Cypress.Commands.add("mount", function (children, options) {
const handleAnnouncement = (announcement: string) => {
// @ts-ignore
cy.state("announcement", announcement);
};
Cypress.Commands.add(
"mount",
// biome-ignore lint/suspicious/noExplicitAny: locale type varies between Date frameworks
<TDate extends DateFrameworkType, TLocale = any>(
children: ReactNode,
options?: MountOptions,
): Cypress.Chainable<MountReturn> => {
const handleAnnouncement = (announcement: string) => {
// @ts-ignore
cy.state("announcement", announcement);
};

const density: "touch" | "low" | "medium" | "high" | undefined =
Cypress.env("density");
const mode: "light" | "dark" | undefined = Cypress.env("mode");
const dateAdapter: SaltDateAdapter<DateFrameworkType> | undefined =
Cypress.env("dateAdapter");
// biome-ignore lint/suspicious/noExplicitAny: locale type varies between Date frameworks
const dateLocale: any = Cypress.env("dateLocale");

if (!SupportedDensityValues.includes(density as SupportedDensity)) {
throw new Error(`Invalid density value: ${density}`);
}
if (!SupportedThemeModeValues.includes(mode as SupportedThemeMode)) {
throw new Error(`Invalid mode value: ${mode}`);
}

return cypressMount(
<SaltProvider density={this.density} mode={this.mode}>
{children}
<AnnouncementListener onAnnouncement={handleAnnouncement} />
</SaltProvider>,
options,
);
});
const content = (
<SaltProvider density={density} mode={mode}>
{dateAdapter ? (
<LocalizationProvider
// biome-ignore lint/suspicious/noExplicitAny: ignore type
DateAdapter={dateAdapter.constructor as any}
locale={dateLocale}
>
{children}
<AnnouncementListener onAnnouncement={handleAnnouncement} />
</LocalizationProvider>
) : (
<>
{children}
<AnnouncementListener onAnnouncement={handleAnnouncement} />
</>
)}
</SaltProvider>
);

return cypressMount(content, options);
},
);

Cypress.Commands.add("mountPerformance", (children, options) => {
const handleRender = (result: PerformanceResult) => {
Expand Down Expand Up @@ -159,3 +223,12 @@ Cypress.on("uncaught:exception", (err) => {
return false;
}
});

// Set default values for density and mode
const defaultDensity: SupportedDensity = "medium";
const defaultMode: SupportedThemeMode = "light";
before(() => {
Cypress.env("density", defaultDensity);
Cypress.env("mode", defaultMode);
Cypress.env("dateLocale", undefined);
});
Loading

0 comments on commit b272497

Please sign in to comment.