From aef7c1857b9f31de0e81a652319f51dfc4958af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:13:10 +0200 Subject: [PATCH] DataViews: Vendor DateCalendar and DateRangeCalendar from components Move the private DateCalendar / DateRangeCalendar components (and their TZDate re-export) from @wordpress/components into @wordpress/dataviews as internal components, and delete them from the components package. DataViews was their only consumer, and unlocking them via cross-package private APIs breaks npm-bundled copies of the package (see #81230). - Move packages/components/src/calendar to packages/dataviews/src/components/calendar, including tests; drop the Storybook stories, READMEs, and their docs manifest entries. - Copy the internal useControlledValue hook, the only dependency on components internals. - Rename the CSS class prefix from components-calendar to dataviews-calendar so stale wp-components stylesheets cannot double-style the calendar. - Replace the components-internal theme variables with the design-system tokens they resolve to, per the stylelint policy banning --wp-components-color-* outside the components package. - Move the react-day-picker dependency from components to dataviews. - Import the calendar directly in the date and datetime dataform controls; date.tsx no longer needs the private APIs unlock at all. Co-Authored-By: Claude Fable 5 --- docs/manifest.json | 12 - package-lock.json | 2 +- packages/components/CHANGELOG.md | 1 + packages/components/package.json | 1 - .../src/calendar/date-calendar/README.md | 261 --------------- .../calendar/date-range-calendar/README.md | 298 ------------------ .../calendar/stories/date-calendar.story.tsx | 232 -------------- .../stories/date-range-calendar.story.tsx | 242 -------------- .../src/calendar/utils/constants.ts | 63 ---- packages/components/src/private-apis.ts | 4 - packages/components/src/style.scss | 3 +- packages/dataviews/CHANGELOG.md | 1 + packages/dataviews/package.json | 1 + .../calendar/date-calendar/index.tsx | 2 +- .../calendar/date-range-calendar/index.tsx | 2 +- .../src/components}/calendar/index.tsx | 0 .../src/components}/calendar/style.scss | 204 ++++++------ .../calendar/test/__utils__/index.ts | 0 .../calendar/test/date-calendar.tsx | 0 .../calendar/test/date-range-calendar.tsx | 0 .../src/components}/calendar/types.ts | 4 +- .../components/calendar/utils/constants.ts | 63 ++++ .../components}/calendar/utils/day-cell.tsx | 0 .../src/components}/calendar/utils/misc.ts | 0 .../calendar/utils/use-controlled-value.ts | 52 +++ .../calendar/utils/use-localization-props.ts | 0 .../src/components/dataform-controls/date.tsx | 5 +- .../components/dataform-controls/datetime.tsx | 3 +- packages/dataviews/src/style.scss | 1 + 29 files changed, 241 insertions(+), 1216 deletions(-) delete mode 100644 packages/components/src/calendar/date-calendar/README.md delete mode 100644 packages/components/src/calendar/date-range-calendar/README.md delete mode 100644 packages/components/src/calendar/stories/date-calendar.story.tsx delete mode 100644 packages/components/src/calendar/stories/date-range-calendar.story.tsx delete mode 100644 packages/components/src/calendar/utils/constants.ts rename packages/{components/src => dataviews/src/components}/calendar/date-calendar/index.tsx (96%) rename packages/{components/src => dataviews/src/components}/calendar/date-range-calendar/index.tsx (98%) rename packages/{components/src => dataviews/src/components}/calendar/index.tsx (100%) rename packages/{components/src => dataviews/src/components}/calendar/style.scss (57%) rename packages/{components/src => dataviews/src/components}/calendar/test/__utils__/index.ts (100%) rename packages/{components/src => dataviews/src/components}/calendar/test/date-calendar.tsx (100%) rename packages/{components/src => dataviews/src/components}/calendar/test/date-range-calendar.tsx (100%) rename packages/{components/src => dataviews/src/components}/calendar/types.ts (98%) create mode 100644 packages/dataviews/src/components/calendar/utils/constants.ts rename packages/{components/src => dataviews/src/components}/calendar/utils/day-cell.tsx (100%) rename packages/{components/src => dataviews/src/components}/calendar/utils/misc.ts (100%) create mode 100644 packages/dataviews/src/components/calendar/utils/use-controlled-value.ts rename packages/{components/src => dataviews/src/components}/calendar/utils/use-localization-props.ts (100%) diff --git a/docs/manifest.json b/docs/manifest.json index ce41daad14bf8d..be54879e102d31 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1517,18 +1517,6 @@ "markdown_source": "../packages/components/src/button/README.md", "parent": "components" }, - { - "title": "DateCalendar", - "slug": "date-calendar", - "markdown_source": "../packages/components/src/calendar/date-calendar/README.md", - "parent": "components" - }, - { - "title": "DateRangeCalendar", - "slug": "date-range-calendar", - "markdown_source": "../packages/components/src/calendar/date-range-calendar/README.md", - "parent": "components" - }, { "title": "CardBody", "slug": "card-body", diff --git a/package-lock.json b/package-lock.json index 6da7f9d3e1e59c..79a960441822f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50963,7 +50963,6 @@ "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.6.1", - "react-day-picker": "^9.14.0", "remove-accents": "^0.5.0", "uuid": "^14.0.0" }, @@ -51474,6 +51473,7 @@ "date-fns": "^4.4.0", "deepmerge": "^4.3.1", "fast-deep-equal": "^3.1.3", + "react-day-picker": "^9.14.0", "remove-accents": "^0.5.0" }, "devDependencies": { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index a7d804ec9d9370..58595918d83a67 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -49,6 +49,7 @@ ### Internal +- Remove the private `DateCalendar` and `DateRangeCalendar` components (and the `TZDate` re-export). They have moved to the `@wordpress/dataviews` package, their only consumer. The `react-day-picker` dependency moves with them ([#XXXXX](https://github.com/WordPress/gutenberg/pull/XXXXX)). - `Button`: Expand the Storybook e2e `VariantStates` matrix with compact, small, and with-icon rows ([#80793](https://github.com/WordPress/gutenberg/pull/80793)). - Update `react-day-picker` to 9.14.0 ([#80792](https://github.com/WordPress/gutenberg/pull/80792)). - Update `date-fns` to 4.4.0 ([#80763](https://github.com/WordPress/gutenberg/pull/80763)). diff --git a/packages/components/package.json b/packages/components/package.json index 612ad42668d7d5..fc32b623e36e93 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -97,7 +97,6 @@ "path-to-regexp": "^6.2.1", "re-resizable": "^6.4.0", "react-colorful": "^5.6.1", - "react-day-picker": "^9.14.0", "remove-accents": "^0.5.0", "uuid": "^14.0.0" }, diff --git a/packages/components/src/calendar/date-calendar/README.md b/packages/components/src/calendar/date-calendar/README.md deleted file mode 100644 index a62ea3e49accff..00000000000000 --- a/packages/components/src/calendar/date-calendar/README.md +++ /dev/null @@ -1,261 +0,0 @@ -# `DateCalendar` - -🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project. - -`DateCalendar` is a React component that provides a customizable calendar interface for **single date** selection. - -The component is built with accessibility in mind and follows ARIA best practices for calendar widgets. It provides keyboard navigation, screen reader support, and customizable labels for internationalization. - -## Usage example - -```tsx -import { DateCalendar } from '@wordpress/components'; - -function MyComponent() { - const [ selected, setSelected ] = useState< Date >( new Date() ); - - return ; -} -``` - -## Props - -These props are shared between both single date and date range calendar modes. - -### `required` - -- Type: `boolean` -- Required: No -- Default: `false` - -Whether the selection is required. When `true`, there always needs to be a date selected. - -### `selected` - -- Type: `Date | undefined | null` -- Required: No - -The selected date. - -### `onSelect` - -- Type: `(selected: Date | undefined, triggerDate: Date, modifiers: Modifiers, e: React.MouseEvent | React.KeyboardEvent) => void` -- Required: No - -Event handler when a day is selected. - -### `defaultSelected` - -- Type: `Date` -- Required: No - -The default selected date (for uncontrolled usage). - -### `defaultMonth` - -- Type: `Date` -- Required: No -- Default: Current month - -The initial month to show in the calendar view (uncontrolled). - -### `month` - -- Type: `Date` -- Required: No - -The month displayed in the calendar view (controlled). Use together with `onMonthChange` to change the month programmatically. - -### `numberOfMonths` - -- Type: `number` -- Required: No -- Default: `1` - -The number of months displayed at once. - -### `startMonth` - -- Type: `Date` -- Required: No - -The earliest month to start the month navigation. - -### `endMonth` - -- Type: `Date` -- Required: No - -The latest month to end the month navigation. - -### `autoFocus` - -- Type: `boolean` -- Required: No - -Focus the first selected day (if set) or today's date (if not disabled). Use this prop when you need to focus the calendar after a user action (e.g. opening the dialog with the calendar). - -### `disabled` - -- Type: `Matcher | Matcher[] | undefined` -- Required: No - -Specify which days are disabled. Using `true` will disable all dates. See the [Matcher Types](#matcher-types) section for more details. - -### `disableNavigation` - -- Type: `boolean` -- Required: No - -Disable the navigation buttons. - -### `labels` - -- Type: `object` -- Required: No - -Use custom labels for internationalization. All labels are optional and have sensible defaults: - -```typescript -{ - labelNav?: () => string; // Navigation toolbar label - labelGrid?: (date: Date) => string; // Month grid label (default: "LLLL y") - labelGridcell?: (date: Date, modifiers?: Modifiers) => string; // Grid cell label - labelNext?: (month: Date | undefined) => string; // Next month button label - labelPrevious?: (month: Date | undefined) => string; // Previous month button label - labelDayButton?: (date: Date, modifiers?: Modifiers) => string; // Day button label - labelWeekday?: (date: Date) => string; // Weekday label -} -``` - -**Important: For a correct localized experience, consumers should make sure the locale used for the translated labels and `locale` prop are consistent.** - -### `locale` - -- Type: `Locale` -- Required: No -- Default: `enUS` from `@date-fns/locale` - -The locale object used to localize dates. Pass a locale from `@date-fns/locale` to localize the calendar. - -**Important: For a correct localized experience, consumers should make sure the locale used for the translated labels and `locale` prop are consistent.** - -### `weekStartsOn` - -- Type: `0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined` -- Required: No -- Default: Based on the `locale` prop - -The index of the first day of the week (0 - Sunday). Overrides the locale's setting. - -### `onMonthChange` - -- Type: `(month: Date) => void` -- Required: No - -Event fired when the user navigates between months. - -### `timeZone` - -- Type: `string` -- Required: No - -The time zone (IANA or UTC offset) to use in the calendar. See [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for possible values. - -When working with time zones, use the `TZDate` object exported by this package instead of the native `Date` object. - -```tsx -import { DateCalendar, TZDate } from '@wordpress/components'; - -export function WithTimeZone() { - const timeZone = 'America/New_York'; - const [ selected, setSelected ] = useState< Date | undefined >( - new TZDate( 2024, 12, 10, timeZone ) // Use `TZDate` instead of `Date` - ); - return ( - - ); -} -``` - -### `role` - -- Type: `'application' | 'dialog' | undefined` -- Required: No -- Default: `'application'` - -The role attribute to add to the container element. - -## Matcher Types - -The calendar component uses a flexible matching system to determine which days should be disabled or have specific modifiers. Here are the available matcher types: - -### Boolean Matcher - -```typescript -const booleanMatcher: Matcher = true; // Will always match the day -``` - -### Date Matcher - -```typescript -const dateMatcher: Matcher = new Date(); // Will match today's date -``` - -### Array Matcher - -```typescript -const arrayMatcher: Matcher = [ - new Date( 2019, 1, 2 ), - new Date( 2019, 1, 4 ), -]; // Will match the days in the array -``` - -### Date After Matcher - -```typescript -const afterMatcher: DateAfter = { after: new Date( 2019, 1, 2 ) }; // Will match days after the 2nd of February 2019 -``` - -### Date Before Matcher - -```typescript -const beforeMatcher: DateBefore = { before: new Date( 2019, 1, 2 ) }; // Will match days before the 2nd of February 2019 -``` - -### Date Interval Matcher - -```typescript -const intervalMatcher: DateInterval = { - after: new Date( 2019, 1, 2 ), - before: new Date( 2019, 1, 5 ), -}; // Will match the days between the 2nd and the 5th of February 2019 (exclusive) -``` - -### Date Range Matcher - -```typescript -const rangeMatcher: DateRange = { - from: new Date( 2019, 1, 2 ), - to: new Date( 2019, 1, 5 ), -}; // Will match the days between the 2nd and the 5th of February 2019 (inclusive) -``` - -### Day of Week Matcher - -```typescript -const dayOfWeekMatcher: DayOfWeek = { dayOfWeek: 0 }; // Will match Sundays -const weekendMatcher: DayOfWeek = { dayOfWeek: [ 0, 6 ] }; // Will match weekends -``` - -### Function Matcher - -```typescript -const functionMatcher: Matcher = ( day: Date ) => { - return day.getMonth() === 2; // Will match when month is March -}; -``` diff --git a/packages/components/src/calendar/date-range-calendar/README.md b/packages/components/src/calendar/date-range-calendar/README.md deleted file mode 100644 index 3bfc9560a1c870..00000000000000 --- a/packages/components/src/calendar/date-range-calendar/README.md +++ /dev/null @@ -1,298 +0,0 @@ -# `DateRangeCalendar` - -🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project. - -`DateRangeCalendar` is a React component that provides a customizable calendar interface for **date range** selection. - -The component is built with accessibility in mind and follows ARIA best practices for calendar widgets. It provides keyboard navigation, screen reader support, and customizable labels for internationalization. - -## Usage example - -```tsx -import { DateRangeCalendar } from '@wordpress/components'; - -type DateRange = { - from: Date | undefined; - to?: Date | undefined; -}; - -function MyComponent() { - const [ selected, setSelected ] = useState< DateRange >( { - from: new Date( date.getFullYear(), date.getMonth(), 1 ), - to: new Date(), - } ); - - return ; -} -``` - -## Props - -These props are shared between both single date and date range calendar modes. - -### `required` - -- Type: `boolean` -- Required: No -- Default: `false` - -Whether the selection is required. When `true`, there always needs to be a date selected. - -### `selected` - -- Type: `DateRange | undefined | null` -- Required: No - -The selected date range. A `DateRange` object has the following shape: - -```typescript -{ - from: Date | undefined; - to?: Date | undefined; -} -``` - -### `onSelect` - -- Type: `(selected: DateRange | undefined, triggerDate: Date, modifiers: Modifiers, e: React.MouseEvent | React.KeyboardEvent) => void` -- Required: No - -Event handler when the selection changes. The `selected` parameter will contain the new date range. - -### `defaultSelected` - -- Type: `DateRange` -- Required: No - -The default selected range (for uncontrolled usage). - -### `excludeDisabled` - -- Type: `boolean` -- Required: No - -When `true`, the range will reset when including a disabled day. This is useful to prevent users from selecting ranges that include unavailable dates. - -### `min` - -- Type: `number` -- Required: No - -The minimum number of days to include in the range. If a user tries to select a range shorter than this, the selection will be adjusted to meet the minimum requirement. - -### `max` - -- Type: `number` -- Required: No - -The maximum number of days to include in the range. If a user tries to select a range longer than this, the selection will be adjusted to meet the maximum requirement. - -### `defaultMonth` - -- Type: `Date` -- Required: No -- Default: Current month - -The initial month to show in the calendar view (uncontrolled). - -### `month` - -- Type: `Date` -- Required: No - -The month displayed in the calendar view (controlled). Use together with `onMonthChange` to change the month programmatically. - -### `numberOfMonths` - -- Type: `number` -- Required: No -- Default: `1` - -The number of months displayed at once. - -### `startMonth` - -- Type: `Date` -- Required: No - -The earliest month to start the month navigation. - -### `endMonth` - -- Type: `Date` -- Required: No - -The latest month to end the month navigation. - -### `autoFocus` - -- Type: `boolean` -- Required: No - -Focus the first selected day (if set) or today's date (if not disabled). Use this prop when you need to focus the calendar after a user action (e.g. opening the dialog with the calendar). - -### `disabled` - -- Type: `Matcher | Matcher[] | undefined` -- Required: No - -Specify which days are disabled. Using `true` will disable all dates. See the [Matcher Types](#matcher-types) section for more details. - -### `disableNavigation` - -- Type: `boolean` -- Required: No - -Disable the navigation buttons. - -### `labels` - -- Type: `object` -- Required: No - -Use custom labels for internationalization. All labels are optional and have sensible defaults: - -```typescript -{ - labelNav?: () => string; // Navigation toolbar label - labelGrid?: (date: Date) => string; // Month grid label (default: "LLLL y") - labelGridcell?: (date: Date, modifiers?: Modifiers) => string; // Grid cell label - labelNext?: (month: Date | undefined) => string; // Next month button label - labelPrevious?: (month: Date | undefined) => string; // Previous month button label - labelDayButton?: (date: Date, modifiers?: Modifiers) => string; // Day button label - labelWeekday?: (date: Date) => string; // Weekday label -} -``` - -**Important: For a correct localized experience, consumers should make sure the locale used for the translated labels and `locale` prop are consistent.** - -### `locale` - -- Type: `Locale` -- Required: No -- Default: `enUS` from `@date-fns/locale` - -The locale object used to localize dates. Pass a locale from `@date-fns/locale` to localize the calendar. - -**Important: For a correct localized experience, consumers should make sure the locale used for the translated labels and `locale` prop are consistent.** - -### `weekStartsOn` - -- Type: `0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined` -- Required: No -- Default: Based on the `locale` prop - -The index of the first day of the week (0 - Sunday). Overrides the locale's setting. - -### `onMonthChange` - -- Type: `(month: Date) => void` -- Required: No - -Event fired when the user navigates between months. - -### `timeZone` - -- Type: `string` -- Required: No - -The time zone (IANA or UTC offset) to use in the calendar. See [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for possible values. - -When working with time zones, use the `TZDate` object exported by this package instead of the native `Date` object. - -```tsx -import { DateRangeCalendar, TZDate } from '@wordpress/components'; - -export function WithTimeZone() { - const timeZone = 'America/New_York'; - const [ selected, setSelected ] = useState< Date | undefined >( { - from: new TZDate( 2024, 12, 10, timeZone ), // Use `TZDate` instead of `Date` - to: new TZDate( 2024, 12, 8, timeZone ), // Use `TZDate` instead of `Date` - } ); - return ( - - ); -} -``` - -### `role` - -- Type: `'application' | 'dialog' | undefined` -- Required: No -- Default: `'application'` - -The role attribute to add to the container element. - -## Matcher Types - -The calendar component uses a flexible matching system to determine which days should be disabled or have specific modifiers. Here are the available matcher types: - -### Boolean Matcher - -```typescript -const booleanMatcher: Matcher = true; // Will always match the day -``` - -### Date Matcher - -```typescript -const dateMatcher: Matcher = new Date(); // Will match today's date -``` - -### Array Matcher - -```typescript -const arrayMatcher: Matcher = [ - new Date( 2019, 1, 2 ), - new Date( 2019, 1, 4 ), -]; // Will match the days in the array -``` - -### Date After Matcher - -```typescript -const afterMatcher: DateAfter = { after: new Date( 2019, 1, 2 ) }; // Will match days after the 2nd of February 2019 -``` - -### Date Before Matcher - -```typescript -const beforeMatcher: DateBefore = { before: new Date( 2019, 1, 2 ) }; // Will match days before the 2nd of February 2019 -``` - -### Date Interval Matcher - -```typescript -const intervalMatcher: DateInterval = { - after: new Date( 2019, 1, 2 ), - before: new Date( 2019, 1, 5 ), -}; // Will match the days between the 2nd and the 5th of February 2019 (exclusive) -``` - -### Date Range Matcher - -```typescript -const rangeMatcher: DateRange = { - from: new Date( 2019, 1, 2 ), - to: new Date( 2019, 1, 5 ), -}; // Will match the days between the 2nd and the 5th of February 2019 (inclusive) -``` - -### Day of Week Matcher - -```typescript -const dayOfWeekMatcher: DayOfWeek = { dayOfWeek: 0 }; // Will match Sundays -const weekendMatcher: DayOfWeek = { dayOfWeek: [ 0, 6 ] }; // Will match weekends -``` - -### Function Matcher - -```typescript -const functionMatcher: Matcher = ( day: Date ) => { - return day.getMonth() === 2; // Will match when month is March -}; -``` diff --git a/packages/components/src/calendar/stories/date-calendar.story.tsx b/packages/components/src/calendar/stories/date-calendar.story.tsx deleted file mode 100644 index 917f2b7122b6b6..00000000000000 --- a/packages/components/src/calendar/stories/date-calendar.story.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { fn } from 'storybook/test'; -import { - enUS, - fr, - es, - de, - it, - he, - ru, - ja, - ptBR, - nl, - ko, - tr, - id, - zhCN, - zhTW, - ar, - sv, -} from 'date-fns/locale'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { useState, useEffect } from '@wordpress/element'; -import { DateCalendar, TZDate } from '..'; - -// Storybook date controls pass a number, but react-day-picker expects a Date for `endMonth`. -function toDate( value: Date | number | undefined ): Date | undefined { - return value === undefined ? undefined : new Date( value ); -} - -const meta: Meta< typeof DateCalendar > = { - title: 'Components/Selection & Input/Time & Date/DateCalendar', - component: DateCalendar, - tags: [ 'status-private' ], - render: ( { endMonth, ...args } ) => ( - - ), - argTypes: { - locale: { - options: [ - 'English (US)', - 'French', - 'Spanish', - 'German', - 'Italian', - 'Hebrew', - 'Russian', - 'Japanese', - 'Portuguese (Brazil)', - 'Dutch', - 'Korean', - 'Turkish', - 'Indonesian', - 'Chinese (Simplified)', - 'Chinese (Traditional)', - 'Arabic', - 'Swedish', - ], - mapping: { - 'English (US)': enUS, - French: fr, - Spanish: es, - German: de, - Italian: it, - Hebrew: he, - Russian: ru, - Japanese: ja, - 'Portuguese (Brazil)': ptBR, - Dutch: nl, - Korean: ko, - Turkish: tr, - Indonesian: id, - 'Chinese (Simplified)': zhCN, - 'Chinese (Traditional)': zhTW, - Arabic: ar, - Swedish: sv, - }, - control: 'select', - }, - timeZone: { - options: [ - 'Pacific/Honolulu', - 'America/New_York', - 'Europe/London', - 'Asia/Tokyo', - 'Pacific/Auckland', - ], - control: 'select', - }, - labels: { - control: false, - }, - defaultSelected: { control: 'date' }, - selected: { control: 'date' }, - onSelect: { - control: false, - }, - defaultMonth: { control: 'date' }, - month: { control: 'date' }, - onMonthChange: { - control: false, - }, - endMonth: { control: 'date' }, - startMonth: { control: 'date' }, - }, - args: { - onMonthChange: fn(), - onSelect: fn(), - }, -}; -export default meta; - -type Story = StoryObj< typeof DateCalendar >; - -export const Default: Story = {}; - -export const DisabledDates: Story = { - args: { - disabled: [ - // Disable tomorrow (single date) - new Date( new Date().setDate( new Date().getDate() + 1 ) ), - // Disable all dates after Feb 1st of next year - { after: new Date( new Date().getFullYear() + 1, 1, 1 ) }, - // Disable all dates before Dec 1st of last year - { before: new Date( new Date().getFullYear() - 1, 11, 1 ) }, - // Disable all dates between 12th and 14th of August of this year - { - after: new Date( new Date().getFullYear(), 7, 11 ), - before: new Date( new Date().getFullYear(), 7, 15 ), - }, - // Disable all dates between 21st and 26th of October of this year - { - from: new Date( new Date().getFullYear(), 9, 21 ), - to: new Date( new Date().getFullYear(), 9, 26 ), - }, - // Disable all Wednesdays - { dayOfWeek: 3 }, - // Disable all prime day numbers - function isPrimeDate( date: Date ) { - return [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 ].includes( - date.getDate() - ); - }, - ], - }, -}; - -const nextMonth = new Date().getMonth() === 11 ? 0 : new Date().getMonth() + 1; -const nextMonthYear = - new Date().getMonth() === 11 - ? new Date().getFullYear() + 1 - : new Date().getFullYear(); -const firstDayOfNextMonth = new Date( nextMonthYear, nextMonth, 1 ); -export const WithSelectedDateAndMonth: Story = { - args: { - defaultSelected: firstDayOfNextMonth, - defaultMonth: firstDayOfNextMonth, - }, -}; - -/** - * Shows days from adjacent months in the grid. Outside days use a lighter style - * and are still interactive. Use `fixedWeeks` to keep the grid height constant. - */ -export const WithOutsideDays: Story = { - args: { - showOutsideDays: true, - fixedWeeks: true, - }, -}; - -/** - * When working with time zones, use the `TZDate` object exported by this package instead of the native `Date` object. - */ -export const WithTimeZone: Story = { - render: function DateCalendarWithTimeZone( { endMonth, ...args } ) { - const [ selected, setSelected ] = useState< TZDate | null >( null ); - - useEffect( () => { - setSelected( - // Select one week from today every time the time zone changes. - new TZDate( - new Date().setDate( new Date().getDate() + 7 ), - args.timeZone - ) - ); - }, [ args.timeZone ] ); - - return ( - <> - { - setSelected( - selectedDate - ? new TZDate( selectedDate, args.timeZone ) - : null - ); - args.onSelect?.( selectedDate, ...rest ); - } } - disabled={ [ - { - // Disable any date before today - before: new TZDate( new Date(), args.timeZone ), - }, - ] } - /> - -

- Calendar set to { args.timeZone ?? 'current' } timezone, - disabling selection for all dates before today, and starting - with a default date of 1 week from today. -

- - ); - }, - args: { - timeZone: 'Pacific/Auckland', - }, - argTypes: { - selected: { - control: false, - }, - defaultSelected: { - control: false, - }, - disabled: { - control: false, - }, - }, -}; diff --git a/packages/components/src/calendar/stories/date-range-calendar.story.tsx b/packages/components/src/calendar/stories/date-range-calendar.story.tsx deleted file mode 100644 index b51fcb131400c2..00000000000000 --- a/packages/components/src/calendar/stories/date-range-calendar.story.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { fn } from 'storybook/test'; -import { - enUS, - fr, - es, - de, - it, - he, - ru, - ja, - ptBR, - nl, - ko, - tr, - id, - zhCN, - zhTW, - ar, - sv, -} from 'date-fns/locale'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { useState, useEffect } from '@wordpress/element'; -import { DateRangeCalendar, TZDate } from '..'; - -// Storybook date controls pass a number, but react-day-picker expects a Date for `endMonth`. -function toDate( value: Date | number | undefined ): Date | undefined { - return value === undefined ? undefined : new Date( value ); -} - -const meta: Meta< typeof DateRangeCalendar > = { - title: 'Components/Selection & Input/Time & Date/DateRangeCalendar', - component: DateRangeCalendar, - tags: [ 'status-private' ], - render: ( { endMonth, ...args } ) => ( - - ), - argTypes: { - locale: { - options: [ - 'English (US)', - 'French', - 'Spanish', - 'German', - 'Italian', - 'Hebrew', - 'Russian', - 'Japanese', - 'Portuguese (Brazil)', - 'Dutch', - 'Korean', - 'Turkish', - 'Indonesian', - 'Chinese (Simplified)', - 'Chinese (Traditional)', - 'Arabic', - 'Swedish', - ], - mapping: { - 'English (US)': enUS, - French: fr, - Spanish: es, - German: de, - Italian: it, - Hebrew: he, - Russian: ru, - Japanese: ja, - 'Portuguese (Brazil)': ptBR, - Dutch: nl, - Korean: ko, - Turkish: tr, - Indonesian: id, - 'Chinese (Simplified)': zhCN, - 'Chinese (Traditional)': zhTW, - Arabic: ar, - Swedish: sv, - }, - control: 'select', - }, - timeZone: { - options: [ - 'Pacific/Honolulu', - 'America/New_York', - 'Europe/London', - 'Asia/Tokyo', - 'Pacific/Auckland', - ], - control: 'select', - }, - labels: { - control: false, - }, - defaultSelected: { control: false }, - selected: { control: false }, - onSelect: { - control: false, - }, - defaultMonth: { control: 'date' }, - month: { control: 'date' }, - onMonthChange: { - control: false, - }, - endMonth: { control: 'date' }, - startMonth: { control: 'date' }, - }, - args: { - onMonthChange: fn(), - onSelect: fn(), - }, -}; -export default meta; - -type Story = StoryObj< typeof DateRangeCalendar >; - -export const Default: Story = {}; - -export const DisabledDates: Story = { - args: { - disabled: [ - // Disable tomorrow (single date) - new Date( new Date().setDate( new Date().getDate() + 1 ) ), - // Disable all dates after Feb 1st of next year - { after: new Date( new Date().getFullYear() + 1, 1, 1 ) }, - // Disable all dates before Dec 1st of last year - { before: new Date( new Date().getFullYear() - 1, 11, 1 ) }, - // Disable all dates between 12th and 14th of August of this year - { - after: new Date( new Date().getFullYear(), 7, 11 ), - before: new Date( new Date().getFullYear(), 7, 15 ), - }, - // Disable all dates between 21st and 26th of October of this year - { - from: new Date( new Date().getFullYear(), 9, 21 ), - to: new Date( new Date().getFullYear(), 9, 26 ), - }, - // Disable all Wednesdays - { dayOfWeek: 3 }, - // Disable all prime day numbers - function isPrimeDate( date: Date ) { - return [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 ].includes( - date.getDate() - ); - }, - ], - }, -}; - -const nextMonth = new Date().getMonth() === 11 ? 0 : new Date().getMonth() + 1; -const nextMonthYear = - new Date().getMonth() === 11 - ? new Date().getFullYear() + 1 - : new Date().getFullYear(); -const firstDayOfNextMonth = new Date( nextMonthYear, nextMonth, 1 ); -const fourthDayOfNextMonth = new Date( nextMonthYear, nextMonth, 4 ); -export const WithSelectedRangeAndMonth: Story = { - args: { - defaultSelected: { - from: firstDayOfNextMonth, - to: fourthDayOfNextMonth, - }, - defaultMonth: firstDayOfNextMonth, - }, -}; - -/** - * Shows days from adjacent months in the grid. Outside days use a lighter style - * and are still interactive. Use `fixedWeeks` to keep the grid height constant. - */ -export const WithOutsideDays: Story = { - args: { - showOutsideDays: true, - fixedWeeks: true, - }, -}; - -/** - * When working with time zones, use the `TZDate` object exported by this package instead of the native `Date` object. - */ -export const WithTimeZone: Story = { - render: function DateCalendarWithTimeZone( { endMonth, ...args } ) { - const [ range, setRange ] = useState< typeof args.selected | null >( - null - ); - - useEffect( () => { - setRange( - // Select from one week from today to two weeks from today - // every time the timezone changes. - { - from: new TZDate( - new Date().setDate( new Date().getDate() + 7 ), - args.timeZone - ), - to: new TZDate( - new Date().setDate( new Date().getDate() + 14 ), - args.timeZone - ), - } - ); - }, [ args.timeZone ] ); - - return ( - <> - { - setRange( - // Set controlled state to null if there's no selection - ! selectedDate || - ( selectedDate.from === undefined && - selectedDate.to === undefined ) - ? null - : selectedDate - ); - args.onSelect?.( selectedDate, ...rest ); - } } - disabled={ [ - { - // Disable any date before today - before: new TZDate( new Date(), args.timeZone ), - }, - ] } - /> -

- Calendar set to { args.timeZone ?? 'current' } timezone, - disabling selection for all dates before today, and starting - with a default date range of 1 week from today to 2 weeks - from today. -

- - ); - }, - args: { - timeZone: 'Pacific/Auckland', - }, - argTypes: { - disabled: { - control: false, - }, - }, -}; diff --git a/packages/components/src/calendar/utils/constants.ts b/packages/components/src/calendar/utils/constants.ts deleted file mode 100644 index 9e32797cdf9969..00000000000000 --- a/packages/components/src/calendar/utils/constants.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Internal dependencies - */ -import { Day } from './day-cell'; - -const CLASSNAMES = { - root: 'components-calendar', - day: 'components-calendar__day', - day_button: 'components-calendar__day-button', - outside: 'components-calendar__day--outside', - caption_label: 'components-calendar__caption-label', - button_next: 'components-calendar__button-next', - button_previous: 'components-calendar__button-previous', - chevron: 'components-calendar__chevron', - nav: 'components-calendar__nav', - month_caption: 'components-calendar__month-caption', - months: 'components-calendar__months', - month_grid: 'components-calendar__month-grid', - weekday: 'components-calendar__weekday', - today: 'components-calendar__day--today', - selected: 'components-calendar__day--selected', - disabled: 'components-calendar__day--disabled', - hidden: 'components-calendar__day--hidden', - range_start: 'components-calendar__range-start', - range_end: 'components-calendar__range-end', - range_middle: 'components-calendar__range-middle', - weeks_before_enter: 'components-calendar__weeks-before-enter', - weeks_before_exit: 'components-calendar__weeks-before-exit', - weeks_after_enter: 'components-calendar__weeks-after-enter', - weeks_after_exit: 'components-calendar__weeks-after-exit', - caption_after_enter: 'components-calendar__caption-after-enter', - caption_after_exit: 'components-calendar__caption-after-exit', - caption_before_enter: 'components-calendar__caption-before-enter', - caption_before_exit: 'components-calendar__caption-before-exit', -}; -export const MODIFIER_CLASSNAMES = { - preview: 'components-calendar__day--preview', - preview_start: 'components-calendar__day--preview-start', - preview_end: 'components-calendar__day--preview-end', -}; - -export const COMMON_PROPS = { - animate: true, - // Only show days in the current month - showOutsideDays: false, - // Hide week number column - showWeekNumber: false, - // Show weekdays row - hideWeekdays: false, - // Month and year caption are not interactive - captionLayout: 'label', - // Show a variable number of weeks depending on the month - fixedWeeks: false, - // Show navigation buttons - hideNavigation: false, - // Class names - classNames: CLASSNAMES, - // Default role - role: 'application', - components: { - Day, - }, -} as const; diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 82cd12db36c5e6..b29fa1162817fc 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -15,7 +15,6 @@ import { withIgnoreIMEEvents } from './utils/with-ignore-ime-events'; import { lock } from './lock-unlock'; import Badge from './badge'; -import { DateCalendar, DateRangeCalendar, TZDate } from './calendar'; import { ValidatedCheckboxControl, ValidatedComboboxControl, @@ -43,9 +42,6 @@ lock( privateApis, { withIgnoreIMEEvents, Badge, normalizeTextString, - DateCalendar, - DateRangeCalendar, - TZDate, useDrag, ValidatedInputControl, ValidatedCheckboxControl, diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 6d85fd28f7ac60..97d92248de16f0 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -4,7 +4,6 @@ @use "./badge/styles.scss" as *; @use "./button-group/style.scss" as *; @use "./button/style.scss" as *; -@use "./calendar/style.scss" as *; @use "./checkbox-control/style.scss" as *; @use "./circular-option-picker/style.scss" as *; @use "./palette-edit/style.scss" as *; @@ -50,5 +49,5 @@ // Include the default WP Components color variables. // TODO: Remove this once all admin-scheme variables are accounted for in the wp-components fallback values. :root { - @include admin-scheme( #3858e9 ); + @include admin-scheme(#3858e9); } diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index a6b46354c1adaa..722c56b6b98a03 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -27,6 +27,7 @@ ### Internal +- DataViews: Move the private `DateCalendar` and `DateRangeCalendar` components from `@wordpress/components` into the package as internal components, instead of unlocking them via private cross-package APIs ([#81230](https://github.com/WordPress/gutenberg/issues/81230)). Adds a direct `react-day-picker` dependency; the calendar CSS class prefix changes from `components-calendar` to `dataviews-calendar`; no behavior change. ([#XXXXX](https://github.com/WordPress/gutenberg/pull/XXXXX)) - DataViews: Inline a verbatim copy of the `kebabCase` utility instead of unlocking the private one from `@wordpress/components`, as part of removing the package's reliance on private cross-package APIs ([#81230](https://github.com/WordPress/gutenberg/issues/81230)). Adds a direct `change-case` dependency; no behavior change. ([#81284](https://github.com/WordPress/gutenberg/pull/81284)) - Update `date-fns` to 4.4.0 ([#80763](https://github.com/WordPress/gutenberg/pull/80763)). - Update Jest type definitions to v30 ([#80767](https://github.com/WordPress/gutenberg/pull/80767)). diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 43393bfc3ce3b4..eb59cbcf7a6f0c 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -74,6 +74,7 @@ "date-fns": "^4.4.0", "deepmerge": "^4.3.1", "fast-deep-equal": "^3.1.3", + "react-day-picker": "^9.14.0", "remove-accents": "^0.5.0" }, "devDependencies": { diff --git a/packages/components/src/calendar/date-calendar/index.tsx b/packages/dataviews/src/components/calendar/date-calendar/index.tsx similarity index 96% rename from packages/components/src/calendar/date-calendar/index.tsx rename to packages/dataviews/src/components/calendar/date-calendar/index.tsx index f70807d4d38457..72b0ed439ad0ff 100644 --- a/packages/components/src/calendar/date-calendar/index.tsx +++ b/packages/dataviews/src/components/calendar/date-calendar/index.tsx @@ -12,7 +12,7 @@ import { useCallback } from '@wordpress/element'; */ import { COMMON_PROPS } from '../utils/constants'; import { clampNumberOfMonths } from '../utils/misc'; -import { useControlledValue } from '../../utils/hooks'; +import { useControlledValue } from '../utils/use-controlled-value'; import { useLocalizationProps } from '../utils/use-localization-props'; import type { DateCalendarProps, OnSelectHandler } from '../types'; diff --git a/packages/components/src/calendar/date-range-calendar/index.tsx b/packages/dataviews/src/components/calendar/date-range-calendar/index.tsx similarity index 98% rename from packages/components/src/calendar/date-range-calendar/index.tsx rename to packages/dataviews/src/components/calendar/date-range-calendar/index.tsx index c67c50aadd784f..7f3d7302d078cf 100644 --- a/packages/components/src/calendar/date-range-calendar/index.tsx +++ b/packages/dataviews/src/components/calendar/date-range-calendar/index.tsx @@ -13,7 +13,7 @@ import { useMemo, useState, useCallback } from '@wordpress/element'; */ import { COMMON_PROPS, MODIFIER_CLASSNAMES } from '../utils/constants'; import { clampNumberOfMonths } from '../utils/misc'; -import { useControlledValue } from '../../utils/hooks'; +import { useControlledValue } from '../utils/use-controlled-value'; import { useLocalizationProps } from '../utils/use-localization-props'; import type { DateRangeCalendarProps, diff --git a/packages/components/src/calendar/index.tsx b/packages/dataviews/src/components/calendar/index.tsx similarity index 100% rename from packages/components/src/calendar/index.tsx rename to packages/dataviews/src/components/calendar/index.tsx diff --git a/packages/components/src/calendar/style.scss b/packages/dataviews/src/components/calendar/style.scss similarity index 57% rename from packages/components/src/calendar/style.scss rename to packages/dataviews/src/components/calendar/style.scss index 3d38c8eb3c8763..63a66fe518bf6c 100644 --- a/packages/components/src/calendar/style.scss +++ b/packages/dataviews/src/components/calendar/style.scss @@ -1,25 +1,47 @@ -@use "@wordpress/base-styles/colors" as *; @use "@wordpress/base-styles/variables" as *; -@use "../utils/theme-variables" as *; + +// Color tokens, mapped from the theme variables internal to the +// `@wordpress/components` package (where this component previously lived) to +// the design-system tokens those variables resolve to outside of a themed +// components subtree, so the calendar keeps the same appearance it had there. +$calendar-color-accent: var(--wp-admin-theme-color, #3858e9); +$calendar-color-background: var(--wpds-color-background-surface-neutral-strong); +$calendar-color-foreground: var(--wpds-color-foreground-content-neutral); +$calendar-color-foreground-inverted: var( + --wpds-color-background-surface-neutral +); +$calendar-color-gray-400: var(--wpds-color-stroke-interactive-neutral); +$calendar-color-gray-600: var(--wpds-color-stroke-interactive-neutral); +$calendar-color-gray-700: var(--wpds-color-foreground-content-neutral-weak); +$calendar-color-gray-800: var(--wpds-color-foreground-content-neutral); +$calendar-color-disabled: $calendar-color-gray-600; /* Root of the component. */ // Internal variables -$wp-components-calendar-outline-focus: var(--wp-admin-border-width-focus) - solid $components-color-accent; -$wp-components-calendar-button-height: $grid-unit-40; -$wp-components-calendar-button-width: $grid-unit-40; -$wp-components-calendar-nav-height: $grid-unit-40; -$wp-components-calendar-range-middle-background-color: color-mix(in srgb, $components-color-accent 4%, transparent); -$wp-components-calendar-preview-border-color: color-mix(in srgb, $components-color-accent 16%, transparent); - -.components-calendar { +$wp-dataviews-calendar-outline-focus: var(--wp-admin-border-width-focus) solid + $calendar-color-accent; +$wp-dataviews-calendar-button-height: $grid-unit-40; +$wp-dataviews-calendar-button-width: $grid-unit-40; +$wp-dataviews-calendar-nav-height: $grid-unit-40; +$wp-dataviews-calendar-range-middle-background-color: color-mix( + in srgb, + $calendar-color-accent 4%, + transparent +); +$wp-dataviews-calendar-preview-border-color: color-mix( + in srgb, + $calendar-color-accent 16%, + transparent +); + +.dataviews-calendar { // TODO: add font family rule when Theme is ready position: relative; /* Required to position the navigation toolbar. */ box-sizing: border-box; display: inline flow-root; - color: $components-color-foreground; - background-color: $components-color-background; + color: $calendar-color-foreground; + background-color: $calendar-color-background; font-size: $font-size-medium; font-weight: var(--wpds-typography-font-weight-default); z-index: 0; // Create a stacking context and render on top of the background. @@ -31,7 +53,7 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } } -.components-calendar__day { +.dataviews-calendar__day { padding: 0; position: relative; @@ -39,17 +61,17 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col // day button to ensure that the color of the dot used to indicate today's // date follows the same color as the button's text, since the button // inherits its text color. - &:has(.components-calendar__day-button:disabled) { + &:has(.dataviews-calendar__day-button:disabled) { // Use a lighter shade of gray for less visual prominence. - color: $components-color-gray-400; + color: $calendar-color-gray-400; } - &:has(.components-calendar__day-button:hover:not(:disabled)), - &:has(.components-calendar__day-button:focus-visible) { - color: $components-color-accent; + &:has(.dataviews-calendar__day-button:hover:not(:disabled)), + &:has(.dataviews-calendar__day-button:focus-visible) { + color: $calendar-color-accent; } } -.components-calendar__day-button { +.dataviews-calendar__day-button { background: none; padding: 0; margin: 0; @@ -59,8 +81,8 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col display: flex; position: relative; - width: $wp-components-calendar-button-width; - height: $wp-components-calendar-button-height; + width: $wp-dataviews-calendar-button-width; + height: $wp-dataviews-calendar-button-height; border: none; border-radius: $radius-small; @@ -94,7 +116,7 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col &:disabled { cursor: revert; - @media ( forced-colors: active ) { + @media (forced-colors: active) { // As an extra visual cue, show a line-through on disabled days // in forced-colors (high-contrast) mode. text-decoration: line-through; @@ -102,12 +124,12 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } &:focus-visible { - outline: $wp-components-calendar-outline-focus; + outline: $wp-dataviews-calendar-outline-focus; outline-offset: 1px; } } -.components-calendar__caption-label { +.dataviews-calendar__caption-label { z-index: 1; position: relative; @@ -120,8 +142,8 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col text-transform: capitalize; } -.components-calendar__button-next, -.components-calendar__button-previous { +.dataviews-calendar__button-next, +.dataviews-calendar__button-previous { border: none; border-radius: $radius-small; background: none; @@ -134,8 +156,8 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col justify-content: center; position: relative; - width: $wp-components-calendar-button-width; - height: $wp-components-calendar-button-height; + width: $wp-dataviews-calendar-button-width; + height: $wp-dataviews-calendar-button-height; color: inherit; @@ -143,38 +165,38 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col &[aria-disabled="true"] { cursor: revert; - color: $components-color-disabled; + color: $calendar-color-disabled; } &:focus-visible { - outline: $wp-components-calendar-outline-focus; + outline: $wp-dataviews-calendar-outline-focus; } } -.components-calendar__chevron { +.dataviews-calendar__chevron { display: inline-block; fill: currentColor; width: $grid-unit-20; height: $grid-unit-20; } -.components-calendar[dir="rtl"] -.components-calendar__nav -.components-calendar__chevron { +.dataviews-calendar[dir="rtl"] + .dataviews-calendar__nav + .dataviews-calendar__chevron { transform: rotate(180deg); transform-origin: 50%; } -.components-calendar__month-caption { +.dataviews-calendar__month-caption { display: flex; justify-content: center; align-content: center; - height: $wp-components-calendar-nav-height; + height: $wp-dataviews-calendar-nav-height; margin-bottom: $grid-unit-15; } -.components-calendar__months { +.dataviews-calendar__months { position: relative; display: flex; justify-content: center; @@ -183,12 +205,12 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col max-width: fit-content; } -.components-calendar__month-grid { +.dataviews-calendar__month-grid { border-collapse: separate; border-spacing: 0 $grid-unit-05; } -.components-calendar__nav { +.dataviews-calendar__nav { position: absolute; inset-block-start: 0; inset-inline-start: 0; @@ -198,22 +220,22 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col align-items: center; justify-content: space-between; - height: $wp-components-calendar-nav-height; + height: $wp-dataviews-calendar-nav-height; } -.components-calendar__weekday { - width: $wp-components-calendar-button-width; - height: $wp-components-calendar-button-height; +.dataviews-calendar__weekday { + width: $wp-dataviews-calendar-button-width; + height: $wp-dataviews-calendar-button-height; padding: 0; - color: $components-color-gray-700; + color: $calendar-color-gray-700; text-align: center; text-transform: uppercase; } /* DAY MODIFIERS */ // Today's date: show a dot in the top-right corner of the button -.components-calendar__day--today::after { +.dataviews-calendar__day--today::after { content: ""; position: absolute; z-index: 1; @@ -226,17 +248,17 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } // Selected date button (individual date, range start, and range end) -.components-calendar__day--selected:not(.components-calendar__range-middle) { +.dataviews-calendar__day--selected:not(.dataviews-calendar__range-middle) { &:has( - .components-calendar__day-button, - .components-calendar__day-button:hover:not(:disabled) -) { - color: $components-color-foreground-inverted; + .dataviews-calendar__day-button, + .dataviews-calendar__day-button:hover:not(:disabled) + ) { + color: $calendar-color-foreground-inverted; } - .components-calendar__day-button { + .dataviews-calendar__day-button { &::before { - background-color: $components-color-foreground; + background-color: $calendar-color-foreground; // Render a transparent border to highlight the selected day in // forced-colors (high-contrast) mode, since the background is not // visible. @@ -244,28 +266,28 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } &:disabled::before { - background-color: $components-color-gray-400; + background-color: $calendar-color-gray-400; } &:hover:not(:disabled)::before { - background-color: $components-color-gray-800; + background-color: $calendar-color-gray-800; } } } // Outside month days (visible when showOutsideDays is true): use a lighter text color. -.components-calendar__day--outside { - color: $components-color-gray-600; +.dataviews-calendar__day--outside { + color: $calendar-color-gray-600; } // Hidden button (ie. outside current month but still rendered) -.components-calendar__day--hidden { +.dataviews-calendar__day--hidden { visibility: hidden; } // Range start button, but not when start and end are the same day. -.components-calendar__range-start:not(.components-calendar__range-end) -.components-calendar__day-button { +.dataviews-calendar__range-start:not(.dataviews-calendar__range-end) + .dataviews-calendar__day-button { // Apply border-radius changes to the button itself too, so that the focus // ring follows the same shape as the button's background. &, @@ -276,9 +298,9 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } // Middle of date range -.components-calendar__range-middle .components-calendar__day-button { +.dataviews-calendar__range-middle .dataviews-calendar__day-button { &::before { - background-color: $wp-components-calendar-range-middle-background-color; + background-color: $wp-dataviews-calendar-range-middle-background-color; border-radius: 0; // Render a top and bottom transparent border to highlight the selected @@ -291,8 +313,8 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } // Range end button, but not when start and end are the same day. -.components-calendar__range-end:not(.components-calendar__range-start) -.components-calendar__day-button { +.dataviews-calendar__range-end:not(.dataviews-calendar__range-start) + .dataviews-calendar__day-button { // Apply border-radius changes to the button itself too, so that the focus // ring follows the same shape as the button's background. &, @@ -314,23 +336,23 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col * CSS variables, we're using the SVGs as masks, and using `background-color` * to consume the accent color variable. */ -.components-calendar__day--preview svg { +.dataviews-calendar__day--preview svg { position: absolute; inset: 0; pointer-events: none; - color: $wp-components-calendar-preview-border-color; + color: $wp-dataviews-calendar-preview-border-color; - @media ( forced-colors: active ) { + @media (forced-colors: active) { color: inherit; } - .components-calendar[dir="rtl"] & { + .dataviews-calendar[dir="rtl"] & { transform: scaleX(-1); } } -.components-calendar__day--preview.components-calendar__range-middle -.components-calendar__day-button::before { +.dataviews-calendar__day--preview.dataviews-calendar__range-middle + .dataviews-calendar__day-button::before { // Remove the transparent border shown on the middle of the range // in forced-colors (high-contrast) mode, to allow for the dashed border // to be visible. @@ -392,49 +414,49 @@ $wp-components-calendar-preview-border-color: color-mix(in srgb, $components-col } } -.components-calendar__weeks-before-enter, -.components-calendar__weeks-before-exit, -.components-calendar__weeks-after-enter, -.components-calendar__weeks-after-exit, -.components-calendar__caption-after-enter, -.components-calendar__caption-after-exit, -.components-calendar__caption-before-enter, -.components-calendar__caption-before-exit { +.dataviews-calendar__weeks-before-enter, +.dataviews-calendar__weeks-before-exit, +.dataviews-calendar__weeks-after-enter, +.dataviews-calendar__weeks-after-exit, +.dataviews-calendar__caption-after-enter, +.dataviews-calendar__caption-after-exit, +.dataviews-calendar__caption-before-enter, +.dataviews-calendar__caption-before-exit { animation-duration: 0s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); animation-fill-mode: forwards; - @media not ( prefers-reduced-motion ) { + @media not (prefers-reduced-motion) { animation-duration: 0.3s; } } -.components-calendar__weeks-before-enter, -.components-calendar[dir="rtl"] .components-calendar__weeks-after-enter { +.dataviews-calendar__weeks-before-enter, +.dataviews-calendar[dir="rtl"] .dataviews-calendar__weeks-after-enter { animation-name: slide-in-left; } -.components-calendar__weeks-before-exit, -.components-calendar[dir="rtl"] .components-calendar__weeks-after-exit { +.dataviews-calendar__weeks-before-exit, +.dataviews-calendar[dir="rtl"] .dataviews-calendar__weeks-after-exit { animation-name: slide-out-left; } -.components-calendar__weeks-after-enter, -.components-calendar[dir="rtl"] .components-calendar__weeks-before-enter { +.dataviews-calendar__weeks-after-enter, +.dataviews-calendar[dir="rtl"] .dataviews-calendar__weeks-before-enter { animation-name: slide-in-right; } -.components-calendar__weeks-after-exit, -.components-calendar[dir="rtl"] .components-calendar__weeks-before-exit { +.dataviews-calendar__weeks-after-exit, +.dataviews-calendar[dir="rtl"] .dataviews-calendar__weeks-before-exit { animation-name: slide-out-right; } -.components-calendar__caption-after-enter { +.dataviews-calendar__caption-after-enter { animation-name: fade-in; } -.components-calendar__caption-after-exit { +.dataviews-calendar__caption-after-exit { animation-name: fade-out; } -.components-calendar__caption-before-enter { +.dataviews-calendar__caption-before-enter { animation-name: fade-in; } -.components-calendar__caption-before-exit { +.dataviews-calendar__caption-before-exit { animation-name: fade-out; } diff --git a/packages/components/src/calendar/test/__utils__/index.ts b/packages/dataviews/src/components/calendar/test/__utils__/index.ts similarity index 100% rename from packages/components/src/calendar/test/__utils__/index.ts rename to packages/dataviews/src/components/calendar/test/__utils__/index.ts diff --git a/packages/components/src/calendar/test/date-calendar.tsx b/packages/dataviews/src/components/calendar/test/date-calendar.tsx similarity index 100% rename from packages/components/src/calendar/test/date-calendar.tsx rename to packages/dataviews/src/components/calendar/test/date-calendar.tsx diff --git a/packages/components/src/calendar/test/date-range-calendar.tsx b/packages/dataviews/src/components/calendar/test/date-range-calendar.tsx similarity index 100% rename from packages/components/src/calendar/test/date-range-calendar.tsx rename to packages/dataviews/src/components/calendar/test/date-range-calendar.tsx diff --git a/packages/components/src/calendar/types.ts b/packages/dataviews/src/components/calendar/types.ts similarity index 98% rename from packages/components/src/calendar/types.ts rename to packages/dataviews/src/components/calendar/types.ts index 36934922e90a40..b41bc11f6e787c 100644 --- a/packages/components/src/calendar/types.ts +++ b/packages/dataviews/src/components/calendar/types.ts @@ -294,9 +294,9 @@ export interface BaseProps * for the possible values. * * When working with time zones, use the `TZDate` object exported by this - * package instead of the native `Date` object. + * component's module instead of the native `Date` object. * @example - * import { DateCalendar, TZDate } from "@wordpress/components"; + * import { DateCalendar, TZDate } from '../calendar'; * * export function WithTimeZone() { * const timeZone = "America/New_York"; diff --git a/packages/dataviews/src/components/calendar/utils/constants.ts b/packages/dataviews/src/components/calendar/utils/constants.ts new file mode 100644 index 00000000000000..2090a19c47a159 --- /dev/null +++ b/packages/dataviews/src/components/calendar/utils/constants.ts @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import { Day } from './day-cell'; + +const CLASSNAMES = { + root: 'dataviews-calendar', + day: 'dataviews-calendar__day', + day_button: 'dataviews-calendar__day-button', + outside: 'dataviews-calendar__day--outside', + caption_label: 'dataviews-calendar__caption-label', + button_next: 'dataviews-calendar__button-next', + button_previous: 'dataviews-calendar__button-previous', + chevron: 'dataviews-calendar__chevron', + nav: 'dataviews-calendar__nav', + month_caption: 'dataviews-calendar__month-caption', + months: 'dataviews-calendar__months', + month_grid: 'dataviews-calendar__month-grid', + weekday: 'dataviews-calendar__weekday', + today: 'dataviews-calendar__day--today', + selected: 'dataviews-calendar__day--selected', + disabled: 'dataviews-calendar__day--disabled', + hidden: 'dataviews-calendar__day--hidden', + range_start: 'dataviews-calendar__range-start', + range_end: 'dataviews-calendar__range-end', + range_middle: 'dataviews-calendar__range-middle', + weeks_before_enter: 'dataviews-calendar__weeks-before-enter', + weeks_before_exit: 'dataviews-calendar__weeks-before-exit', + weeks_after_enter: 'dataviews-calendar__weeks-after-enter', + weeks_after_exit: 'dataviews-calendar__weeks-after-exit', + caption_after_enter: 'dataviews-calendar__caption-after-enter', + caption_after_exit: 'dataviews-calendar__caption-after-exit', + caption_before_enter: 'dataviews-calendar__caption-before-enter', + caption_before_exit: 'dataviews-calendar__caption-before-exit', +}; +export const MODIFIER_CLASSNAMES = { + preview: 'dataviews-calendar__day--preview', + preview_start: 'dataviews-calendar__day--preview-start', + preview_end: 'dataviews-calendar__day--preview-end', +}; + +export const COMMON_PROPS = { + animate: true, + // Only show days in the current month + showOutsideDays: false, + // Hide week number column + showWeekNumber: false, + // Show weekdays row + hideWeekdays: false, + // Month and year caption are not interactive + captionLayout: 'label', + // Show a variable number of weeks depending on the month + fixedWeeks: false, + // Show navigation buttons + hideNavigation: false, + // Class names + classNames: CLASSNAMES, + // Default role + role: 'application', + components: { + Day, + }, +} as const; diff --git a/packages/components/src/calendar/utils/day-cell.tsx b/packages/dataviews/src/components/calendar/utils/day-cell.tsx similarity index 100% rename from packages/components/src/calendar/utils/day-cell.tsx rename to packages/dataviews/src/components/calendar/utils/day-cell.tsx diff --git a/packages/components/src/calendar/utils/misc.ts b/packages/dataviews/src/components/calendar/utils/misc.ts similarity index 100% rename from packages/components/src/calendar/utils/misc.ts rename to packages/dataviews/src/components/calendar/utils/misc.ts diff --git a/packages/dataviews/src/components/calendar/utils/use-controlled-value.ts b/packages/dataviews/src/components/calendar/utils/use-controlled-value.ts new file mode 100644 index 00000000000000..725283736f5026 --- /dev/null +++ b/packages/dataviews/src/components/calendar/utils/use-controlled-value.ts @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { useCallback, useState } from '@wordpress/element'; + +type Props< T > = { + defaultValue?: T; + value?: T; + onChange?: ( value: T, ...args: any[] ) => void; +}; + +/** + * Simplified and improved implementation of useControlledState. + * + * @param props + * @param props.defaultValue + * @param props.value + * @param props.onChange + * @return The controlled value and the value setter. + */ +export function useControlledValue< T >( { + defaultValue, + onChange, + value: valueProp, +}: Props< T > ) { + const hasValue = typeof valueProp !== 'undefined'; + const initialValue = hasValue ? valueProp : defaultValue; + const [ state, setState ] = useState( initialValue ); + const value = hasValue ? valueProp : state; + + const uncontrolledSetValue = useCallback( + ( nextValue: T, ...args: any[] ) => { + setState( nextValue ); + onChange?.( nextValue, ...args ); + }, + [ onChange ] + ); + + let setValue: typeof onChange; + if ( hasValue && typeof onChange === 'function' ) { + // Controlled mode. + setValue = onChange; + } else if ( ! hasValue && typeof onChange === 'function' ) { + // Uncontrolled mode, plus forwarding to the onChange prop. + setValue = uncontrolledSetValue; + } else { + // Uncontrolled mode, only update internal state. + setValue = setState; + } + + return [ value, setValue ] as const; +} diff --git a/packages/components/src/calendar/utils/use-localization-props.ts b/packages/dataviews/src/components/calendar/utils/use-localization-props.ts similarity index 100% rename from packages/components/src/calendar/utils/use-localization-props.ts rename to packages/dataviews/src/components/calendar/utils/use-localization-props.ts diff --git a/packages/dataviews/src/components/dataform-controls/date.tsx b/packages/dataviews/src/components/dataform-controls/date.tsx index bc3b24db3cc974..e7b3acd5d64484 100644 --- a/packages/dataviews/src/components/dataform-controls/date.tsx +++ b/packages/dataviews/src/components/dataform-controls/date.tsx @@ -19,7 +19,6 @@ import { BaseControl, Button, Icon as WCIcon, - privateApis as componentsPrivateApis, __experimentalInputControl as InputControl, } from '@wordpress/components'; import { speak } from '@wordpress/a11y'; @@ -45,7 +44,7 @@ import { OPERATOR_OVER, OPERATOR_BETWEEN, } from '../../constants'; -import { unlock } from '../../lock-unlock'; +import { DateCalendar, DateRangeCalendar } from '../calendar'; import type { DataFormControlProps, FieldValidity, @@ -54,8 +53,6 @@ import type { } from '../../types'; import getCustomValidity from './utils/get-custom-validity'; -const { DateCalendar, DateRangeCalendar } = unlock( componentsPrivateApis ); - type DateRange = [ string, string ] | undefined; const DATE_PRESETS: { diff --git a/packages/dataviews/src/components/dataform-controls/datetime.tsx b/packages/dataviews/src/components/dataform-controls/datetime.tsx index 3c8b5ff6227791..3ea2ef4cd277ae 100644 --- a/packages/dataviews/src/components/dataform-controls/datetime.tsx +++ b/packages/dataviews/src/components/dataform-controls/datetime.tsx @@ -19,9 +19,10 @@ import RelativeDateControl from './utils/relative-date-control'; import useDisabledDateMatchers from './utils/use-disabled-date-matchers'; import getCustomValidity from './utils/get-custom-validity'; import parseDateTime from '../../field-types/utils/parse-date-time'; +import { DateCalendar } from '../calendar'; import { unlock } from '../../lock-unlock'; -const { DateCalendar, ValidatedInputControl } = unlock( componentsPrivateApis ); +const { ValidatedInputControl } = unlock( componentsPrivateApis ); const formatDateTime = ( value?: string ): string => { if ( ! value ) { diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 3ceb2935e1250f..bc307464db7c06 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -16,6 +16,7 @@ @use "./components/dataviews-layouts/picker-activity/style.scss" as *; @use "./components/dataviews-picker-footer/style.scss" as *; +@use "./components/calendar/style.scss" as *; @use "./components/dataform-controls/style.scss" as *; @use "./components/dataform-layouts/panel/style.scss" as *; @use "./components/dataform-layouts/regular/style.scss" as *;