diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8b6ae3381dfee6..91bb80168e9f22 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -339,6 +339,8 @@ packages/react-components/react-calendar-preview/library @microsoft/cxe-prg packages/react-components/react-calendar-preview/stories @microsoft/cxe-prg packages/react-components/react-timepicker-preview/library @microsoft/cxe-prg packages/react-components/react-timepicker-preview/stories @microsoft/cxe-prg +packages/react-components/react-datepicker-preview/library @microsoft/cxe-prg +packages/react-components/react-datepicker-preview/stories @microsoft/cxe-prg packages/react-components/react-range-slider-preview/library @microsoft/cxe-prg packages/react-components/react-range-slider-preview/stories @microsoft/cxe-prg # <%= NX-CODEOWNER-PLACEHOLDER %> diff --git a/packages/react-components/react-datepicker-preview/library/.babelrc.json b/packages/react-components/react-datepicker-preview/library/.babelrc.json new file mode 100644 index 00000000000000..630deaf765c49f --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/.babelrc.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../.babelrc-v9.json", + "plugins": ["annotate-pure-calls", "@babel/transform-react-pure-annotations"] +} diff --git a/packages/react-components/react-datepicker-preview/library/.swcrc b/packages/react-components/react-datepicker-preview/library/.swcrc new file mode 100644 index 00000000000000..a5d9d62d2f9c8f --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/.swcrc @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "exclude": [ + "/testing", + "/**/*.cy.ts", + "/**/*.cy.tsx", + "/**/*.spec.ts", + "/**/*.spec.tsx", + "/**/*.test.ts", + "/**/*.test.tsx" + ], + "jsc": { + "baseUrl": ".", + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": false, + "dynamicImport": false + }, + "externalHelpers": true, + "transform": { + "react": { + "runtime": "classic", + "useSpread": true + } + }, + "target": "es2019" + }, + "minify": false, + "sourceMaps": true +} diff --git a/packages/react-components/react-datepicker-preview/library/LICENSE b/packages/react-components/react-datepicker-preview/library/LICENSE new file mode 100644 index 00000000000000..a1668d4cc6fdd7 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-datepicker-preview + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license diff --git a/packages/react-components/react-datepicker-preview/library/README.md b/packages/react-components/react-datepicker-preview/library/README.md new file mode 100644 index 00000000000000..5a19ad42e68317 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/README.md @@ -0,0 +1,5 @@ +# @fluentui/react-datepicker-preview + +**React Datepicker components for [Fluent UI React](https://react.fluentui.dev/)** + +These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. diff --git a/packages/react-components/react-datepicker-preview/library/config/api-extractor.json b/packages/react-components/react-datepicker-preview/library/config/api-extractor.json new file mode 100644 index 00000000000000..8d482156d10d53 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json", + "mainEntryPointFilePath": "/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts" +} diff --git a/packages/react-components/react-datepicker-preview/library/config/tests.cjs b/packages/react-components/react-datepicker-preview/library/config/tests.cjs new file mode 100644 index 00000000000000..2e211ae9e21420 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/config/tests.cjs @@ -0,0 +1 @@ +/** Jest test setup file. */ diff --git a/packages/react-components/react-datepicker-preview/library/docs/Spec.md b/packages/react-components/react-datepicker-preview/library/docs/Spec.md new file mode 100644 index 00000000000000..679e4151df123b --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/docs/Spec.md @@ -0,0 +1,624 @@ +# @fluentui/react-datepicker-preview Spec + +**Status:** Proposed. This document specifies the target component, not the current scaffold's implemented API. + +## Background + +`DatePicker` combines a date input with a popup calendar. It is the non-compat v9 replacement for +`@fluentui/react-datepicker-compat`, built on `@fluentui/react-calendar-preview`. + +The goal is **the same appearance and user-visible behavior as DatePicker Compat, with a native v9 +component contract**: slots, standard refs, controlled and uncontrolled state, event/data callbacks, +Field integration, token-based Griffel styles, and Calendar Preview's compositional APIs. + +This is not a visual redesign, a native browser date input, or a wrapper around either compat package. +The default experience remains an initially empty, read-only date input with a trailing calendar icon +and a dismissible calendar dialog. Editable input is opt-in. + +This package remains preview-only until the API and parity requirements below are verified. This spec +does not authorize production migration or declare parity achieved. + +## Prior Art + +- [Compat props and slots](../../../react-datepicker-compat/library/src/components/DatePicker/DatePicker.types.ts) +- [Compat behavior](../../../react-datepicker-compat/library/src/components/DatePicker/useDatePicker.tsx) +- [Compat styles](../../../react-datepicker-compat/library/src/components/DatePicker/useDatePickerStyles.styles.ts) +- [Compat unit tests](../../../react-datepicker-compat/library/src/components/DatePicker/DatePicker.test.tsx) + and [browser tests](../../../react-datepicker-compat/library/src/components/DatePicker/DatePicker.cy.tsx) +- [Compat examples](../../../react-datepicker-compat/stories/src/DatePicker) +- [Calendar Preview spec](../../../react-calendar-preview/library/docs/Spec.md) + and [API](../../../react-calendar-preview/library/src/components/Calendar/Calendar.types.ts) +- [Input API](../../../react-input/library/src/components/Input/Input.types.ts) +- [WAI-ARIA date picker dialog example](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) + +When compat comments and implementation disagree, observable behavior and verified examples are the +baseline. In particular, the hook defaults to no opening on focus, Sunday as the first weekday, +`FirstDay` week numbering, and `date.toDateString()` input formatting. Do not infer defaults from the +compat prop comments alone. + +## Sample Code + +Examples use the proposed API; they are not examples of the current scaffold. + +### Basic usage + +```tsx +import { Field } from '@fluentui/react-components'; +import { DatePicker } from '@fluentui/react-datepicker-preview'; + +export const Example = () => ( + + + +); +``` + +### Controlled selection and validation + +```tsx +import * as React from 'react'; +import { Field } from '@fluentui/react-components'; +import { DatePicker, defaultDatePickerErrorStrings } from '@fluentui/react-datepicker-preview'; +import type { DatePickerErrorType } from '@fluentui/react-datepicker-preview'; + +export const EditableExample = () => { + const [value, setValue] = React.useState(null); + const [error, setError] = React.useState(); + + return ( + + setValue(data.value)} + onValidationResult={(_event, data) => setError(data.error)} + /> + + ); +}; +``` + +### Calendar composition + +```tsx + +``` + +For localization, pair input `formatDate` and `parseDate` functions with Calendar's `formatters` +and localized action-slot content. `Intl.DateTimeFormat` formats dates but does not parse user input. +Use `popupSurface={{ 'aria-label': localizedCalendarLabel }}` to localize the dialog name. + +## Variants + +- **Input:** selection-only by default; editable with `allowTextInput`. +- **Appearance and size:** Input's `appearance` and `size` APIs. Defaults are `outline` and `medium`. + `underline` and `filled-lighter` reproduce compat's underlined and borderless treatments. +- **Calendar:** day/month side by side, responsive overlay, explicit overlay, day-only, week numbers, + and optional close and Go to today actions. Configure these through the `calendar` slot. +- **Selection range:** Calendar's `day`, `week`, `workWeek`, and `month` modes are supported through + `calendar.dateRangeType`. The input still displays the activated date, not a formatted range. +- **Popup:** portaled by default; inline when `inlinePopup` is true. Inline means DOM placement, + not an always-visible calendar. +- **State:** empty, selected, hovered, focused, pressed, open, disabled, required, and invalid. + Invalid appearance comes from Field or explicit input accessibility props, not an internal label. + +There is no additional clear button, time picker, multiple-date selection, or start/end-range input. + +## API + +The implementation must replace the scaffold in +[DatePicker.types.ts](../src/components/DatePicker/DatePicker.types.ts) with this contract. + +### Slots and ref + +| Slot | Type | Contract | +| -------------- | ------------------------------------ | ------------------------------------------------------------------------------------- | +| `root` | `NonNullable>` | Required Input component, preserving Input's native-prop routing and styling surface. | +| `calendar` | `NonNullable>` | Required Calendar Preview component; mounted only while the dialog is open. | +| `popupSurface` | `Slot<'div'>` | Dialog surface, rendered by default while open and absent while closed. | + +Input's `input`, `contentBefore`, and `contentAfter` shorthand props remain available through the +root Input contract. They are not duplicate DatePicker-owned slots. The default `contentAfter` is +the calendar icon and its activation affordance. Customizing its content must not remove opening +behavior or accessible trigger semantics. + +The forwarded ref is `HTMLInputElement`, matching Input and compat. The Input wrapper can be reached +through `root.ref`. Calendar's root ref is an `HTMLDivElement`; its picker-slot refs expose the +existing Calendar Preview focus handles. Do not introduce `componentRef`, `ICalendar`, or a second +imperative DatePicker API. + +### Props + +Start from `ComponentProps>`, omitting Input's string `value`, +`defaultValue`, `type`, and `readOnly`. DatePicker fixes `type="text"` and derives `readOnly` from +`allowTextInput`. Preserve other applicable Input props, including `name`, `id`, `placeholder`, +`size`, `appearance`, `disabled`, `required`, `onChange`, and native `aria-*` props. + +| Prop | Type | Default / contract | +| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- | +| `value` | `Date \| null` | Controlled committed date; `undefined` means uncontrolled and `null` means empty. | +| `defaultValue` | `Date \| null` | Initial uncontrolled committed date; defaults to `null`. | +| `onDateChange` | `EventHandler` | Reports a user date commit or clear, not each keystroke. | +| `allowTextInput` | `boolean` | `false`; input is read-only but remains enabled and focusable. | +| `formatDate` | `(date: Date) => string` | `date.toDateString()`; never called with an empty value. | +| `parseDate` | `(text: string) => Date \| null` | Uses `Date.parse`; failure returns `null`. Used only for editable input. | +| `onValidationResult` | `EventHandler` | Reports the result of a user validation attempt. | +| `open` | `boolean` | Controlled popup visibility. | +| `defaultOpen` | `boolean` | `false`; initial uncontrolled visibility. | +| `onOpenChange` | `EventHandler` | Requests a visibility transition. | +| `openOnClick` | `boolean` | `true`; controls opening from the input body, not the icon or keyboard. | +| `openOnFocus` | `boolean` | `false`; opt-in focus opening for selection-only input. | +| `today` | `Date` | Current local date; shared with Calendar. Does not select or populate the input. | +| `minDate`, `maxDate` | `Date` | Inclusive local-calendar-date boundaries for both parsing and Calendar. | +| `inlinePopup` | `boolean` | `false`; renders the positioned surface next to the trigger instead of in a Portal. | +| `mountNode` | `PortalProps['mountNode']` | Portal target; ignored for inline rendering. | +| `positioning` | `PositioningProps` | Below the input wrapper, aligned to logical start, with normal collision handling. | + +`onChange` retains Input's `(event, { value: string })` signature and observes editable draft text. +It does not commit a date. DatePicker owns the displayed string; the nested Input `value`, +`defaultValue`, `type`, and `readOnly` must not independently override DatePicker's state or input mode. +Independent controlled text state is outside this proposal. + +Use `calendar` for Calendar-only configuration: `firstDayOfWeek`, `firstWeekOfYear`, `formatters`, +`layout`, `view`, `defaultView`, `displayedDate`, `defaultDisplayedDate`, `showWeekNumbers`, +`highlightCurrentMonth`, `highlightSelectedMonth`, `allFocusable`, `restrictedDates`, `workWeekDays`, +and picker/action slots. Use Calendar Preview's string unions, not compat enums or interfaces. + +DatePicker owns Calendar's `value`, selection/dismissal wiring, and shared `today`, `minDate`, and +`maxDate`. Omit conflicting `calendar.value`, `calendar.defaultValue`, `calendar.today`, +`calendar.minDate`, and `calendar.maxDate` from the slot's accepted props. Slot event handlers and refs +are composed with internal handlers and refs, not overwritten. A consumer handler runs first; +`preventDefault()` cancels the corresponding built-in action. + +Retain Calendar's default `layout="auto"`, Sunday week start, `firstWeekOfYear="firstDay"`, day-range +selection, visible month picker, visible Go to today action, no close button, no week numbers, and no +month highlights. Do not pass compat-era visibility flags down to Calendar Preview. + +### Events and exports + +Use the repository's `EventHandler` / `EventData` types, with `(event, data)` on every component +callback. Each payload also carries the originating event and its correctly typed event discriminator. +The additional payload fields are: + +```ts +type DatePickerDateChangeData = { + value: Date | null; + selectedDateRange: Date[]; + reason: 'calendar' | 'input' | 'clear'; +}; + +type DatePickerOpenChangeData = { + open: boolean; + reason: 'input' | 'icon' | 'keyboard' | 'focus' | 'selection' | 'dismiss'; +}; + +type DatePickerErrorType = 'invalid-input' | 'out-of-bounds' | 'required-input'; + +type DatePickerValidationResultData = { + error?: DatePickerErrorType; +}; +``` + +These shapes describe payload fields, not complete type declarations. Input commits report +`selectedDateRange: [value]`; clearing reports `[]`; Calendar commits forward Calendar's actual range. +Range generation stays in Calendar, even when a consumer configures week or month selection. +Use `React.ChangeEvent`, `React.FocusEvent`, `React.KeyboardEvent`, or pointer/click events as +appropriate; outside-interaction requests can originate from native events. Do not fabricate a click +event for a blur, scroll, or controlled prop change. + +Each logical action emits at most one date-change request and one open-change request, even when +selection, blur, and dismissal occur together. External prop updates and initial render emit neither. +Controlled callbacks describe a request; they do not imply the owner accepted it. + +Export `DatePicker`, its props/slots/state and event-data types, `DatePickerErrorType`, +`defaultDatePickerErrorStrings`, `datePickerClassNames`, and the standard +`useDatePicker_unstable`, `useDatePickerStyles_unstable`, and `renderDatePicker_unstable` helpers. +The default error messages remain "Invalid date format", "Date is out of bounds", and +"Field is required". Applications can replace them with localized messages. + +## Structure + +### Component implementation + +Use `ForwardRefComponent` and `React.forwardRef`, with the standard state-hook, +style-hook, and render-function split. DatePicker owns input text, parsing, validation, popup +visibility, positioning, dismissal, and popup focus management. + +Calendar Preview owns date arithmetic, range generation, day/month/year navigation, disabled dates, +week numbers, localization of calendar content, grid semantics, selection announcements, and motion. +Do not copy Calendar Compat's implementation or fork Calendar Preview inside DatePicker. + +Compose the existing Input, Portal, positioning, Field-context, utility, and Tabster primitives. +Use `useFluent_unstable().targetDocument` and its `defaultView` for environment access. Opening, +positioning, dismissal, and focus must work in a non-default document as well as during SSR. + +### DOM + +The conceptual default structure is: + +```html + + + + + + + + + +``` + +The icon's semantic activation target is not an extra Tab stop, matching compat. The input provides +the keyboard route to the same dialog. Expose an accessible name based on the input's label and a +localized action label; preserve voice-control and touch-screen-reader activation. +The dialog and its `aria-controls` relationship exist only while open. Input's `id`, `name`, +description, required state, and invalid state belong on the native input. + +### Styling and visual parity + +Use the compat DatePicker and Calendar rendered together as the visual reference, not just Input. +Match input height, width behavior, padding, typography, icon size and placement, cursor, borders, +focus indicator, surface geometry, calendar spacing, divider, action placement, cell treatments, +and popup alignment in equivalent configurations. + +The popup uses `colorNeutralBackground1`, `colorNeutralForeground1`, `shadow16`, +`borderRadiusMedium`, `strokeWidthThin`, `colorTransparentStroke`, and `typographyStyles.body1`. +Use semantic theme tokens for colors, spacing, typography, and strokes throughout. Inline rendering +must preserve the compat stacking behavior above the positioned input. + +Calendar Preview is not automatically a visual-parity guarantee: for example, its root uses intrinsic +width while [Calendar Compat styles](../../../react-calendar-compat/library/src/components/Calendar/useCalendarStyles.styles.ts) +specify different widths for single/dual pickers and week numbers. Its Go to today spacing also differs. +Record and resolve actual screenshot differences before claiming parity. Address reusable calendar +differences in Calendar Preview, not with DatePicker descendant selectors against private DOM. +Where no existing token reproduces a required measurement, obtain a token/design decision rather +than hardcoding legacy spacing or accepting a silent visual change. + +Apply slot classes through Griffel with consumer `className` last. Preserve the static +`fui-DatePicker`, `fui-DatePicker__calendar`, and `fui-DatePicker__popupSurface` class names. +No new visual adornment or motion is introduced; Calendar Preview motion must respect reduced motion. + +## Migration + +This is an explicit API migration, not a drop-in re-export. + +| Compat API | Proposed API | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `value`, `onSelectDate(date)` | `value`, `onDateChange(event, { value })`; use `null` for empty controlled state. | +| No date `defaultValue` | `defaultValue` initializes uncontrolled selection. | +| `onOpenChange(open)` | `onOpenChange(event, { open, reason })`. | +| `onValidationResult(data)` | `onValidationResult(event, data)`. | +| `underlined` | `appearance="underline"`. | +| `borderless` | `appearance="filled-lighter"`. | +| `disableAutoFocus` | `openOnFocus` with inverted meaning; default remains no focus opening. | +| `parseDateFromString` | `parseDate`. | +| `initialPickerDate` | `calendar.defaultDisplayedDate`; initializes navigation, not the selected input value. | +| `isMonthPickerVisible={false}` | `calendar={{ monthPicker: null }}`. | +| `showMonthPickerAsOverlay` | `calendar={{ layout: 'overlay' }}`. | +| `showGoToToday={false}` | `calendar={{ goToTodayButton: null }}`. | +| `showCloseButton` | `calendar={{ dayPicker: { closeButton: {} } }}`. | +| Week settings, month highlights, `allFocusable` | Corresponding `calendar` props, using string unions rather than compat enums. | +| `strings`, `dateTimeFormatter` | Calendar `formatters` and action-slot content; input formatting/parsing remain separate. | +| `calendar.componentRef` | Standard Calendar root ref or child picker focus handles. | + +Retain `allowTextInput`, `formatDate`, boundaries, `today`, popup options, and Input shorthand +customization. Move Calendar-only settings into the slot; do not retain duplicate legacy aliases. + +V8 consumers must additionally replace `styles`, `theme`, runtime styling, label, and inline error +props with Griffel, FluentProvider, Field, and slot props. V0 has no direct compatibility contract; +map its selected-date and popup state explicitly and revalidate interaction and localization. + +Observable compat defects are not new API guarantees: duplicate close notifications, accepting an +invalid date object, treating the Unix epoch as parse failure, and leaving stale internal selection +after an uncontrolled clear must not be reproduced. Cover these corrections explicitly in tests. +Any other discovered user-visible difference requires a documented decision and regression coverage, +not an undocumented "v9 improvement". + +### Compat-to-preview migration walkthrough + +This walkthrough targets `@fluentui/react-datepicker-compat` consumers. The destination code uses +the **proposed** API in this spec and is not runnable against the current scaffold. Migrate only +after that API is implemented and parity is verified; keep production consumers on compat meanwhile. + +#### 1. Replace imports and adapt controlled callbacks + +Before: + +```tsx +import * as React from 'react'; +import { Field } from '@fluentui/react-components'; +import { DayOfWeek, FirstWeekOfYear } from '@fluentui/react-calendar-compat'; +import { DatePicker, defaultDatePickerErrorStrings } from '@fluentui/react-datepicker-compat'; +import type { DatePickerValidationResultData } from '@fluentui/react-datepicker-compat'; + +export const Example = () => { + const [value, setValue] = React.useState(null); + const [open, setOpen] = React.useState(false); + const [error, setError] = React.useState(); + + return ( + + setValue(date ?? null)} + open={open} + onOpenChange={setOpen} + onValidationResult={data => setError(data.error)} + allowTextInput + underlined + firstDayOfWeek={DayOfWeek.Monday} + firstWeekOfYear={FirstWeekOfYear.FirstFourDayWeek} + showWeekNumbers + showMonthPickerAsOverlay + showGoToToday={false} + showCloseButton + /> + + ); +}; +``` + +After: + +```tsx +import * as React from 'react'; +import { Field } from '@fluentui/react-components'; +import { DatePicker, defaultDatePickerErrorStrings } from '@fluentui/react-datepicker-preview'; +import type { DatePickerErrorType } from '@fluentui/react-datepicker-preview'; + +export const Example = () => { + const [value, setValue] = React.useState(null); + const [open, setOpen] = React.useState(false); + const [error, setError] = React.useState(); + + return ( + + setValue(data.value)} + open={open} + onOpenChange={(_event, data) => setOpen(data.open)} + onValidationResult={(_event, data) => setError(data.error)} + allowTextInput + appearance="underline" + calendar={{ + firstDayOfWeek: 'monday', + firstWeekOfYear: 'firstFourDayWeek', + showWeekNumbers: true, + layout: 'overlay', + goToTodayButton: null, + dayPicker: { closeButton: { 'aria-label': 'Close date picker' } }, + }} + /> + + ); +}; +``` + +Do not pass a state setter directly to an event/data callback: its first argument is the event. +Use `data.value`, `data.open`, or `data.error`. Update callback type imports and tests that previously +asserted a date-only or boolean-only call. Application side effects must not depend on duplicate +compat close notifications. + +#### 2. Separate selection, navigation, and draft text + +- Preserve `value={null}` for an empty controlled field. Do not replace it with `undefined`, which + selects uncontrolled mode. If the application does not need controlled selection, replace its + initial-value state plumbing with `defaultValue={initialDate}`. +- Move `initialPickerDate` to `calendar.defaultDisplayedDate`, not `defaultValue`. An initial + navigation target must not populate the field. Use `calendar.displayedDate` and + `calendar.onDisplayedDateChange` only when navigation itself needs to be controlled. +- Move date commits from compat `onSelectDate` to `onDateChange`. Retain `onChange` only for observing + raw input text; it still receives a string and must not be used as a date-selection callback. +- Rename `parseDateFromString` to `parseDate`. Keep the existing parser and formatter paired; + do not assume that switching Calendar localization also changes input parsing. +- Keep `minDate`, `maxDate`, and `today` at DatePicker level. Remove duplicate values from the + `calendar` slot so input validation and calendar availability share one source of truth. + +For example, browsing an initial month while leaving the input empty becomes: + +```tsx + +``` + +#### 3. Move Calendar customization without losing existing slot props + +Merge migrated settings into the consumer's existing `calendar` object, including nested +`dayPicker` and `monthPicker` objects. Do not replace an existing slot object and inadvertently drop +its handlers, refs, labels, or custom content. + +- Replace compat `DayOfWeek`, `FirstWeekOfYear`, and `DateRangeType` enum values with Calendar + Preview's string unions. For example, `DateRangeType.WorkWeek` becomes + `calendar={{ dateRangeType: 'workWeek' }}`. +- Move range, restricted-date, highlighting, and week-number configuration to Calendar Preview's + corresponding props. Read `data.selectedDateRange` from `onDateChange` when the application needs + the generated range; the input continues to display only `data.value`. +- Translate `strings` and `dateTimeFormatter` explicitly into Calendar `formatters` and action-slot + content. There is no one-to-one object spread between compat's strings and the new formatter API. + Include navigation labels, selection/today labels, week labels, Go to today content, and close labels. +- Preserve the popup's localized accessible name through `popupSurface['aria-label']` and preserve + Field label, description, required state, and error-message wiring. +- Keep `inlinePopup`, `mountNode`, `positioning`, and applicable Input props. Replace legacy appearance + flags rather than supplying both flags and `appearance`. +- Replace `disableAutoFocus` with `openOnFocus={!disableAutoFocus}` only when the old flag was + explicitly configured. When it was omitted, omit `openOnFocus` to retain the default. Set + `openOnClick` independently; do not rely on compat's coupling between click and focus flags. +- Keep input refs as `React.Ref`. Replace Calendar's `componentRef` only where + imperative access is actually needed, using the preview root ref or a picker focus handle. + +#### 4. Verify the migrated consumer + +Check the consumer's existing compat configuration against the acceptance matrix below, not only +the default story. Compare identical values, dates, locale, theme, dimensions, and viewport. +Specifically verify opening and return focus, editable validation, required clearing, range +selection, localization, and controlled-owner acceptance/rejection. + +Remove compat imports and direct dependencies only when no remaining consumer needs them. Do not +remove a shared dependency as part of migrating one DatePicker. A successful type check alone does +not establish visual or behavioral parity. + +## Behaviors + +### State and date model + +- Maintain committed date, editable draft string, and popup visibility separately. Browsing a month + or year, changing the displayed date, and using Go to today do not populate or change the input. +- Uncontrolled selection starts from `defaultValue ?? null`, never implicitly from today. Pass + `value={null}` to Calendar when empty so its uncontrolled today default cannot select a date. +- The initial displayed date resolves from `calendar.defaultDisplayedDate`, the selected date, + then the current local date. `calendar.displayedDate` controls navigation independently. + Reopening with a selected date reveals it unless navigation is explicitly controlled; reopening + empty uses the initial displayed-date rule. Bound navigation without committing a boundary date. +- `value` and `open` are independently controlled when defined; do not switch either between controlled + and uncontrolled during the component's lifetime. Use `useControllableState`. +- An external `value` update reformats the input without interaction callbacks. A formatter change + updates committed-date text. Do not overwrite a user's draft on unrelated rerenders. + A controlled owner that rejects a commit remains authoritative. +- Compare boundaries and restricted dates by local year/month/day, not timestamps. Do not convert + date-only values through UTC, mutate consumer dates, or change days around daylight-saving transitions. +- Out-of-range supplied values must not be silently clamped into a new committed value. Calendar + cannot select them; validation reports the problem on the next user validation attempt. +- `disabled` prevents editing, activation, and a usable popup, including with `open={true}`. Disabling + an already-open picker removes the popup without committing a draft or emitting a synthetic user event. + +### Opening and dismissal + +- Clicking/tapping the input opens the calendar when `openOnClick` is true. Clicking the icon opens + it independently. In editable mode, activating the already-open input or icon dismisses it for editing; + in selection-only mode, repeated trigger activation leaves it open. +- Input focus alone does not open by default. `openOnFocus` applies only to selection-only mode. + Focus restoration after dismissal must never immediately reopen the popup. +- A committed Calendar selection updates or requests the date, formats the input, validates, and + closes or requests closure. Preserve `selectedDateRange` for non-day Calendar modes. +- Escape, Calendar's close action, an outside pointer interaction, or scrolling outside the + trigger/surface dismisses the popup. Scrolling within the popup must not dismiss it. +- A dismissal validates editable text using the same rules as blur. Escape does not introduce a + new rollback-to-open-time-value behavior. Navigation alone never emits `onDateChange`. +- Editing text while the popup is open closes it and retains the new draft. Do not parse every + keystroke or accidentally commit the old draft as part of that edit. +- Closing unmounts the surface and Calendar. Opening focuses the selected/navigated day, or an + available bounded fallback. If no day is selectable, focus an enabled navigation/close action or + the dialog itself. Never leave focus in unmounted content. + +### Editing, commits, and validation + +Validate on input blur, Enter, and popup dismissal; coalesce overlapping events from one action. +Do not reparse unchanged text that already equals `formatDate(committedDate)`, since a custom format +may be intentionally ambiguous. Nevertheless, validate required state and availability. + +| Input at validation | Result | +| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| Valid parse, in bounds and not restricted | Commit/request the date and normalize the displayed text with `formatDate`. | +| Empty text, optional | Commit/request `null`, display empty text, and report no error. | +| Empty text, required | Commit/request `null` and report `required-input`; do not resurrect the previous selection. | +| Parse failure or invalid `Date` | Report `invalid-input`, retain the committed date, and restore its formatted text (or empty text), matching compat's invalid-format treatment. | +| Parsed date outside boundaries or in `calendar.restrictedDates` | Report `out-of-bounds`, retain the committed date, and leave the draft visible for correction. | +| Selection-only input | Validate required state and committed-date availability without parsing. | + +Use finite timestamp checks, not truthiness, for default parsing. Empty handling happens before +`parseDate`; `null` or an invalid `Date` result means invalid input. A consumer parser's thrown +exception is not silently converted into success or an empty value. + +Field's required state takes precedence as in compat. Use Field context to inherit input ID, +label/description relationships, invalid state, and validation presentation. DatePicker reports +validation but does not render an error message or automatically choose a localized message for Field. +The default `type="text"` submits the displayed string under `name`; do not add a hidden ISO-value input. + +### Keyboard, pointer, and touch + +| Focus area | Input | Behavior | +| -------------------- | ------------------------------------- | ----------------------------------------------------------------------------------- | +| Closed input | `Enter` | Validate and request opening; consume the key rather than submitting the form. | +| Closed input | `ArrowDown` | Request opening and move focus into Calendar. | +| Open editable input | `Enter` | Validate and request dismissal. | +| Open picker | `Escape` | Dismiss, consume the key, and restore input focus. | +| Closed input | `Escape` | Leave the event available to the surrounding UI. | +| Selection-only input | Text-editing keys | Do not edit the text; retain ordinary focus and selection behavior. | +| Editable input | Text-editing keys | Preserve native editing, including caret movement and clipboard actions. | +| Calendar grids | Arrows, paging keys, `Enter`, `Space` | Delegate to Calendar Preview's documented day/month/year navigation and activation. | +| Open dialog | `Tab`, `Shift+Tab` | Cycle through enabled dialog controls using its focus trap. | + +Input and icon share the same mouse, touch, voice-control, and assistive-technology opening path. +Use pointer cursor treatment on the selection affordance, ordinary editing behavior where applicable, +and default cursors for disabled content. Unavailable dates can be focused with `allFocusable` but +cannot be selected. Consumer-canceled keyboard or activation events must not trigger internal actions. + +## Accessibility + +- Use a native text input, not `input[type=date]`: browser-native date controls cannot guarantee the + specified styling, parsing, Calendar Preview composition, or interaction parity. +- The input exposes `role="combobox"`, `aria-haspopup="dialog"`, current `aria-expanded`, and an + `aria-controls` reference while open. Read-only mode must not be exposed as disabled. +- The popup is a named `role="dialog"` with default name "Calendar". Match compat's modal focus + treatment using Tabster; `aria-modal="true"` is valid only while focus containment and assistive + technology isolation are actually active. Calendar itself is not a modal and must not own the trap. +- Opening transfers focus into Calendar using merged picker handles, not compat `componentRef`, + global document access, CSS-class queries, or an arbitrary timeout. +- Selection, Escape, and the close action return focus to the input without reopening. Outside + pointer dismissal must not steal focus from the outside control the user activated. If the picker + is disabled or unmounted, do not attempt to restore focus to it. +- Calendar owns roving grid focus, selected/current/unavailable semantics, view-switch focus, + localized grid names, navigation announcements, and selection announcements. Do not add duplicate + DatePicker live-region announcements. Field owns error-message association and presentation. +- Localize dialog/trigger/close names, Calendar labels, Go to today content, input formatting/parsing, + and validation messages independently. Locale formatting does not change Gregorian arithmetic. +- Support RTL, keyboard-only operation, touch screen readers, voice control, forced colors, and + reduced motion. Keep selected, current, disabled, and focused states distinguishable without color alone. +- At 400% zoom and narrow viewports, keep all controls usable through Calendar's overlay layout and + popup collision handling. Test both inline and portaled rendering with VoiceOver, NVDA, and JAWS. + +## Acceptance Criteria and Verification + +Parity is a release requirement, not an assumption based on reusing Calendar Preview. + +| Area | Required evidence | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default configuration | Initially empty read-only input; click/Enter/ArrowDown opening; no default focus opening; matching popup and default Calendar configuration. | +| Visual parity | Paired compat/preview fixtures for default, editable, disabled, focused, invalid, all matching appearances/sizes, day-only, side-by-side, overlay, week numbers, month/year views, and range states. | +| Visual environments | Light, dark, brand theme, RTL, forced colors, narrow/wide viewport, zoom, reduced motion, inline/portal, and custom mount node. | +| Selection | Controlled and uncontrolled select, clear, repeat selection, same-date activation, external update, rejected controlled request, and every supported Calendar range mode. | +| Editing | Draft changes versus date commits; blur/Enter/dismiss validation; ambiguous formatter round trips; invalid/empty/out-of-bounds/restricted dates; no duplicate callbacks. | +| Date arithmetic | Inclusive boundaries, month/year rollover, leap day, non-midnight values, Unix epoch, and time zones with daylight-saving transitions. | +| Popup | Every open/dismiss path, controlled requests, focus entry/return, outside-target focus, internal versus external scroll, default-open, disabling while open, and no reopen loop. | +| Composition | Input native props, forwarded ref, nested refs, slot handler ordering/cancellation, consumer classes last, Calendar display/view control, and Field required/invalid/description wiring. | +| Accessibility | Combobox/dialog relationships, localized names, grid announcements, focus trapping, keyboard navigation, disabled-but-focusable dates, and screen-reader checks. | +| Environment | SSR/hydration without global DOM access; correct document, portal, focus, and outside-interaction handling in an iframe. | + +Use frozen `today`, explicit dates/locales/time zones, equal container widths, and settled motion in +paired fixtures. Compare matching states rather than unrelated default screenshots. Require zero +unexplained screenshot differences under the same rendering environment; any approved rasterization +tolerance must not hide geometry, token, or interaction differences. Test actual popup and grid +measurements as well as screenshots. + +Port the compat stories and behavioral assertions to the new API, retaining the originals as the +reference. Add focused unit tests for state/events/validation, browser tests for actual focus and +outside interactions, v9 conformance tests, and visual-regression fixtures for the matrix above. +API extraction and type tests must verify slot restrictions, event/data signatures, string unions, +and ref targets. Run relevant Nx targets for the new package and any Calendar Preview changes. + +Implementation is ready for review only when every compat configuration has a migration path, +paired visual/behavioral evidence is recorded, and any remaining +difference has an explicit decision. Until then, retain the preview designation and do not claim the +component is an exact replacement. diff --git a/packages/react-components/react-datepicker-preview/library/eslint.config.cjs b/packages/react-components/react-datepicker-preview/library/eslint.config.cjs new file mode 100644 index 00000000000000..ec2e7cb1fc479f --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/eslint.config.cjs @@ -0,0 +1,5 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [...fluentPlugin.configs['flat/react']]; diff --git a/packages/react-components/react-datepicker-preview/library/etc/react-datepicker-preview.api.md b/packages/react-components/react-datepicker-preview/library/etc/react-datepicker-preview.api.md new file mode 100644 index 00000000000000..e2b71f8801117e --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/etc/react-datepicker-preview.api.md @@ -0,0 +1,38 @@ +## API Report File for "@fluentui/react-datepicker-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import type * as React_2 from 'react'; +import type { Slot } from '@fluentui/react-utilities'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +// @public +export const DatePicker: ForwardRefComponent; + +// @public (undocumented) +export const datePickerClassNames: SlotClassNames; + +// @public +export type DatePickerProps = ComponentProps & {}; + +// @public +export type DatePickerState = ComponentState; + +// @public +export const renderDatePicker_unstable: (state: DatePickerState) => JSXElement; + +// @public +export const useDatePicker_unstable: (props: DatePickerProps, ref: React_2.Ref) => DatePickerState; + +// @public +export const useDatePickerStyles_unstable: (state: DatePickerState) => DatePickerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-datepicker-preview/library/jest.config.cjs b/packages/react-components/react-datepicker-preview/library/jest.config.cjs new file mode 100644 index 00000000000000..7dabe1f81b2be0 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/jest.config.cjs @@ -0,0 +1,34 @@ +// @ts-check +/* eslint-disable */ + +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(join(__dirname, '.swcrc'), 'utf-8')); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-datepicker-preview', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.cjs'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-datepicker-preview/library/package.json b/packages/react-components/react-datepicker-preview/library/package.json new file mode 100644 index 00000000000000..fa428df69af764 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/package.json @@ -0,0 +1,59 @@ +{ + "name": "@fluentui/react-datepicker-preview", + "version": "0.0.0", + "private": true, + "description": "New fluentui react package", + "type": "module", + "main": "lib-commonjs/index.cjs", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "dist/*.d.cts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.4.6", + "@fluentui/react-shared-contexts": "^9.26.4", + "@fluentui/react-theme": "^9.2.2", + "@fluentui/react-utilities": "^9.26.7", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./lib/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./lib-commonjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-datepicker-preview/library/project.json b/packages/react-components/react-datepicker-preview/library/project.json new file mode 100644 index 00000000000000..3dab67782c8577 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-datepicker-preview", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-datepicker-preview/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-datepicker-preview/library/src/DatePicker.ts b/packages/react-components/react-datepicker-preview/library/src/DatePicker.ts new file mode 100644 index 00000000000000..bd248a994b0e0b --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/DatePicker.ts @@ -0,0 +1,8 @@ +export { + datePickerClassNames, + DatePicker, + renderDatePicker_unstable, + useDatePicker_unstable, + useDatePickerStyles_unstable, +} from './components/DatePicker/index'; +export type { DatePickerProps, DatePickerState } from './components/DatePicker/index'; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.test.tsx b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.test.tsx new file mode 100644 index 00000000000000..60bcff06a3cd90 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { DatePicker } from './DatePicker'; + +describe('DatePicker', () => { + isConformant({ + Component: DatePicker, + displayName: 'DatePicker', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default DatePicker); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.tsx b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.tsx new file mode 100644 index 00000000000000..e283a2da05fbc5 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.tsx @@ -0,0 +1,30 @@ +'use client'; +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useDatePicker_unstable } from './useDatePicker'; +import { renderDatePicker_unstable } from './renderDatePicker'; +import { useDatePickerStyles_unstable } from './useDatePickerStyles.styles'; +import type { DatePickerProps } from './DatePicker.types'; + +/** + * DatePicker component - TODO: add more docs + */ +export const DatePicker: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useDatePicker_unstable(props, ref); + + useDatePickerStyles_unstable(state); + + /** + * @see https://github.com/microsoft/fluentui/blob/master/docs/react-v9/contributing/rfcs/react-components/convergence/custom-styling.md + * + * TODO: 💡 once package will become stable (PR which will be part of promoting PREVIEW package to STABLE), + * - uncomment this line + * - update types {@link file://./../../../../../../../packages/react-components/react-shared-contexts/library/src/CustomStyleHooksContext/CustomStyleHooksContext.ts#CustomStyleHooksContextValue} + * - verify that custom global style override works for your component + */ + // useCustomStyleHook_unstable('useDatePickerStyles_unstable')(state); + + return renderDatePicker_unstable(state); +}); + +DatePicker.displayName = 'DatePicker'; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.types.ts b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.types.ts new file mode 100644 index 00000000000000..a140322c309349 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/DatePicker.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type DatePickerSlots = { + root: Slot<'div'>; +}; + +/** + * DatePicker Props + */ +export type DatePickerProps = ComponentProps & {}; + +/** + * State used in rendering DatePicker + */ +export type DatePickerState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from DatePickerProps. +// & Required> diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap new file mode 100644 index 00000000000000..e0db7d1cbf9a40 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`DatePicker renders a default state 1`] = ` +
+
+ Default DatePicker +
+
+`; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/index.ts b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/index.ts new file mode 100644 index 00000000000000..11adf7404e60dd --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/index.ts @@ -0,0 +1,5 @@ +export { DatePicker } from './DatePicker'; +export type { DatePickerProps, DatePickerState } from './DatePicker.types'; +export { renderDatePicker_unstable } from './renderDatePicker'; +export { useDatePicker_unstable } from './useDatePicker'; +export { datePickerClassNames, useDatePickerStyles_unstable } from './useDatePickerStyles.styles'; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/renderDatePicker.tsx b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/renderDatePicker.tsx new file mode 100644 index 00000000000000..24b75a9f1b1ce8 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/renderDatePicker.tsx @@ -0,0 +1,16 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import type { DatePickerState, DatePickerSlots } from './DatePicker.types'; + +/** + * Render the final JSX of DatePicker + */ +export const renderDatePicker_unstable = (state: DatePickerState): JSXElement => { + assertSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePicker.ts b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePicker.ts new file mode 100644 index 00000000000000..9c5851e42033ec --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePicker.ts @@ -0,0 +1,31 @@ +import type * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import type { DatePickerProps, DatePickerState } from './DatePicker.types'; + +/** + * Create the state required to render DatePicker. + * + * The returned state can be modified with hooks such as useDatePickerStyles_unstable, + * before being passed to renderDatePicker_unstable. + * + * @param props - props from this instance of DatePicker + * @param ref - reference to root HTMLDivElement of DatePicker + */ +export const useDatePicker_unstable = (props: DatePickerProps, ref: React.Ref): DatePickerState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: slot.always( + getIntrinsicElementProps('div', { + ref, + ...props, + }), + { elementType: 'div' }, + ), + }; +}; diff --git a/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePickerStyles.styles.ts b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePickerStyles.styles.ts new file mode 100644 index 00000000000000..639181655e72ba --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/components/DatePicker/useDatePickerStyles.styles.ts @@ -0,0 +1,35 @@ +'use client'; +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { DatePickerSlots, DatePickerState } from './DatePicker.types'; + +export const datePickerClassNames: SlotClassNames = { + root: 'fui-DatePicker', + // TODO: add class names for all slots on DatePickerSlots. + // Should be of the form `: 'fui-DatePicker__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the DatePicker slots based on the state + */ +export const useDatePickerStyles_unstable = (state: DatePickerState): DatePickerState => { + const styles = useStyles(); + // eslint-disable-next-line react-hooks/immutability + state.root.className = mergeClasses(datePickerClassNames.root, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-datepicker-preview/library/src/index.ts b/packages/react-components/react-datepicker-preview/library/src/index.ts new file mode 100644 index 00000000000000..ae4771023b1f03 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/index.ts @@ -0,0 +1,8 @@ +export { + datePickerClassNames, + DatePicker, + renderDatePicker_unstable, + useDatePicker_unstable, + useDatePickerStyles_unstable, +} from './DatePicker'; +export type { DatePickerProps, DatePickerState } from './DatePicker'; diff --git a/packages/react-components/react-datepicker-preview/library/src/testing/isConformant.ts b/packages/react-components/react-datepicker-preview/library/src/testing/isConformant.ts new file mode 100644 index 00000000000000..8ed2da0f925135 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/src/testing/isConformant.ts @@ -0,0 +1,15 @@ +import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; +import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; +import griffelTests from '@fluentui/react-conformance-griffel'; + +export function isConformant( + testInfo: Omit, 'componentPath'> & { componentPath?: string }, +): void { + const defaultOptions: Partial> = { + tsConfig: { configName: 'tsconfig.spec.json' }, + componentPath: require.main?.filename.replace('.test', ''), + extraTests: griffelTests as TestObject, + }; + + baseIsConformant(defaultOptions, testInfo); +} diff --git a/packages/react-components/react-datepicker-preview/library/tsconfig.json b/packages/react-components/react-datepicker-preview/library/tsconfig.json new file mode 100644 index 00000000000000..32bdbdf1ac26f0 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/react-components/react-datepicker-preview/library/tsconfig.lib.json b/packages/react-components/react-datepicker-preview/library/tsconfig.lib.json new file mode 100644 index 00000000000000..53066fdd11fff0 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../../dist/out-tsc/types", + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-datepicker-preview/library/tsconfig.spec.json b/packages/react-components/react-datepicker-preview/library/tsconfig.spec.json new file mode 100644 index 00000000000000..911456fe4b4d91 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/library/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/packages/react-components/react-datepicker-preview/stories/.storybook/main.cjs b/packages/react-components/react-datepicker-preview/stories/.storybook/main.cjs new file mode 100644 index 00000000000000..67905c6bfe15f2 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/.storybook/main.cjs @@ -0,0 +1,14 @@ +const rootMain = require('../../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.mdx', '../src/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-datepicker-preview/stories/.storybook/preview.js b/packages/react-components/react-datepicker-preview/stories/.storybook/preview.js new file mode 100644 index 00000000000000..98274ed0b8095f --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/.storybook/preview.js @@ -0,0 +1,9 @@ +import * as rootPreview from '../../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; + +export const tags = ['autodocs']; diff --git a/packages/react-components/react-datepicker-preview/stories/.storybook/tsconfig.json b/packages/react-components/react-datepicker-preview/stories/.storybook/tsconfig.json new file mode 100644 index 00000000000000..434150d0bd347e --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment"] + }, + "include": ["*.js", "*.cjs"] +} diff --git a/packages/react-components/react-datepicker-preview/stories/README.md b/packages/react-components/react-datepicker-preview/stories/README.md new file mode 100644 index 00000000000000..75ff094e1128b3 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/README.md @@ -0,0 +1,17 @@ +# @fluentui/react-datepicker-preview-stories + +Storybook stories for packages/react-components/react-datepicker-preview + +## Usage + +To include within storybook specify stories globs: + +\`\`\`js +module.exports = { +stories: ['../packages/react-components/react-datepicker-preview/stories/src/**/*.mdx', '../packages/react-components/react-datepicker-preview/stories/src/**/index.stories.@(ts|tsx)'], +} +\`\`\` + +## API + +no public API available diff --git a/packages/react-components/react-datepicker-preview/stories/eslint.config.js b/packages/react-components/react-datepicker-preview/stories/eslint.config.js new file mode 100644 index 00000000000000..f8362c3e413031 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [ + ...fluentPlugin.configs['flat/react'], + { + rules: {}, + }, +]; diff --git a/packages/react-components/react-datepicker-preview/stories/package.json b/packages/react-components/react-datepicker-preview/stories/package.json new file mode 100644 index 00000000000000..85c8e4784fc0a2 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/package.json @@ -0,0 +1,5 @@ +{ + "name": "@fluentui/react-datepicker-preview-stories", + "version": "0.0.0", + "private": true +} diff --git a/packages/react-components/react-datepicker-preview/stories/project.json b/packages/react-components/react-datepicker-preview/stories/project.json new file mode 100644 index 00000000000000..d36b023a0f9e44 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-datepicker-preview-stories", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-datepicker-preview/stories/src", + "tags": ["vNext", "platform:web", "type:stories"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-datepicker-preview/stories/src/.gitkeep b/packages/react-components/react-datepicker-preview/stories/src/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerBestPractices.md b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerBestPractices.md new file mode 100644 index 00000000000000..08ff8ddeeb5f86 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerBestPractices.md @@ -0,0 +1,5 @@ +## Best practices + +### Do + +### Don't diff --git a/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerDefault.stories.tsx b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerDefault.stories.tsx new file mode 100644 index 00000000000000..3e3c9b0214ebbc --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerDefault.stories.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; +import { DatePicker } from '@fluentui/react-datepicker-preview'; +import type { DatePickerProps } from '@fluentui/react-datepicker-preview'; + +export const Default = (props: Partial): React.ReactElement => ; diff --git a/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerDescription.md b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/DatePickerDescription.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-datepicker-preview/stories/src/DatePicker/index.stories.tsx b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/index.stories.tsx new file mode 100644 index 00000000000000..a66fea00edb804 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/src/DatePicker/index.stories.tsx @@ -0,0 +1,18 @@ +import { DatePicker } from '@fluentui/react-datepicker-preview'; + +import descriptionMd from './DatePickerDescription.md'; +import bestPracticesMd from './DatePickerBestPractices.md'; + +export { Default } from './DatePickerDefault.stories'; + +export default { + title: 'Preview Components/DatePicker', + component: DatePicker, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + }, + }, + }, +}; diff --git a/packages/react-components/react-datepicker-preview/stories/src/index.ts b/packages/react-components/react-datepicker-preview/stories/src/index.ts new file mode 100644 index 00000000000000..cb0ff5c3b541f6 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-components/react-datepicker-preview/stories/tsconfig.json b/packages/react-components/react-datepicker-preview/stories/tsconfig.json new file mode 100644 index 00000000000000..efc50169d1df18 --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/react-datepicker-preview/stories/tsconfig.lib.json b/packages/react-components/react-datepicker-preview/stories/tsconfig.lib.json new file mode 100644 index 00000000000000..9486b224643d9f --- /dev/null +++ b/packages/react-components/react-datepicker-preview/stories/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2019", "dom"], + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index fce57dc575a101..9df601f1dedc10 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -118,6 +118,7 @@ "@fluentui/react-datepicker-compat-stories": [ "packages/react-components/react-datepicker-compat/stories/src/index.ts" ], + "@fluentui/react-datepicker-preview": ["packages/react-components/react-datepicker-preview/library/src/index.ts"], "@fluentui/react-dialog": ["packages/react-components/react-dialog/library/src/index.ts"], "@fluentui/react-dialog-stories": ["packages/react-components/react-dialog/stories/src/index.ts"], "@fluentui/react-divider": ["packages/react-components/react-divider/library/src/index.ts"], @@ -276,6 +277,9 @@ "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], + "@fluentui/react-datepicker-preview-stories": [ + "packages/react-components/react-datepicker-preview/stories/src/index.ts" + ], "@fluentui/react-range-slider-preview-stories": [ "packages/react-components/react-range-slider-preview/stories/src/index.ts" ] diff --git a/tsconfig.base.json b/tsconfig.base.json index 1e34d8afc4980a..e9bee952957809 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -73,6 +73,10 @@ "@fluentui/react-datepicker-compat-stories": [ "packages/react-components/react-datepicker-compat/stories/src/index.ts" ], + "@fluentui/react-datepicker-preview": ["packages/react-components/react-datepicker-preview/library/src/index.ts"], + "@fluentui/react-datepicker-preview-stories": [ + "packages/react-components/react-datepicker-preview/stories/src/index.ts" + ], "@fluentui/react-dialog": ["packages/react-components/react-dialog/library/src/index.ts"], "@fluentui/react-dialog-stories": ["packages/react-components/react-dialog/stories/src/index.ts"], "@fluentui/react-divider": ["packages/react-components/react-divider/library/src/index.ts"], diff --git a/yarn.lock b/yarn.lock index 9ad37cfeeb584b..94afce8e88e49e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4148,6 +4148,31 @@ __metadata: languageName: unknown linkType: soft +"@fluentui/react-datepicker-preview-stories@workspace:packages/react-components/react-datepicker-preview/stories": + version: 0.0.0-use.local + resolution: "@fluentui/react-datepicker-preview-stories@workspace:packages/react-components/react-datepicker-preview/stories" + languageName: unknown + linkType: soft + +"@fluentui/react-datepicker-preview@workspace:packages/react-components/react-datepicker-preview/library": + version: 0.0.0-use.local + resolution: "@fluentui/react-datepicker-preview@workspace:packages/react-components/react-datepicker-preview/library" + dependencies: + "@fluentui/react-jsx-runtime": "npm:^9.4.6" + "@fluentui/react-shared-contexts": "npm:^9.26.4" + "@fluentui/react-theme": "npm:^9.2.2" + "@fluentui/react-utilities": "npm:^9.26.7" + "@griffel/react": "npm:^1.5.32" + "@swc/helpers": "npm:^0.5.1" + peerDependencies: + "@types/react": ">=16.14.0 <20.0.0" + react: ">=16.14.0 <20.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@fluentui/react-dialog-stories@workspace:packages/react-components/react-dialog/stories": version: 0.0.0-use.local resolution: "@fluentui/react-dialog-stories@workspace:packages/react-components/react-dialog/stories" @@ -4717,7 +4742,7 @@ __metadata: languageName: unknown linkType: soft -"@fluentui/react-jsx-runtime@npm:^9.4.7, @fluentui/react-jsx-runtime@workspace:packages/react-components/react-jsx-runtime": +"@fluentui/react-jsx-runtime@npm:^9.4.6, @fluentui/react-jsx-runtime@npm:^9.4.7, @fluentui/react-jsx-runtime@workspace:packages/react-components/react-jsx-runtime": version: 0.0.0-use.local resolution: "@fluentui/react-jsx-runtime@workspace:packages/react-components/react-jsx-runtime" dependencies: @@ -6232,7 +6257,7 @@ __metadata: languageName: unknown linkType: soft -"@fluentui/react-utilities@npm:*, @fluentui/react-utilities@npm:^9.27.0, @fluentui/react-utilities@workspace:packages/react-components/react-utilities": +"@fluentui/react-utilities@npm:*, @fluentui/react-utilities@npm:^9.26.7, @fluentui/react-utilities@npm:^9.27.0, @fluentui/react-utilities@workspace:packages/react-components/react-utilities": version: 0.0.0-use.local resolution: "@fluentui/react-utilities@workspace:packages/react-components/react-utilities" dependencies: