From 2b0a8e2ba85153639ae4dac82283ecbf65fd4b53 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 18 Oct 2026 09:55:46 +0200 Subject: [PATCH 1/2] docs(react-headless-components-preview): prepare blocked DatePicker integration Record verified DatePicker base exports, existing headless Input support, the missing headless Calendar dependency, and requirements for the final headless DatePicker layer. Explicitly defer executable API, stories and packaging changes until the public headless Calendar contract is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../stories/README.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/react-components/react-headless-components-preview/stories/README.md b/packages/react-components/react-headless-components-preview/stories/README.md index 48fffabba3d2dc..7ea2e5b7b3a9c9 100644 --- a/packages/react-components/react-headless-components-preview/stories/README.md +++ b/packages/react-components/react-headless-components-preview/stories/README.md @@ -25,6 +25,51 @@ module.exports = { No public API — this package only ships stories. +## DatePicker integration status + +Headless DatePicker is **blocked**, not implemented or exported. The final DatePicker stack layer +reserves this integration after the styled implementation and its separate public-preview +publication layer. Do not use the styled Calendar as a temporary default for an unstyled DatePicker. + +Prerequisites: + +- [Headless Calendar, microsoft/fluentui#36752](https://github.com/microsoft/fluentui/pull/36752) + must provide the unstyled Calendar family and its public composition/focus contract. +- [Calendar popup focus and geometry fixes, microsoft/fluentui#36917](https://github.com/microsoft/fluentui/pull/36917) + must merge independently before rebasing the DatePicker stack. These changes intentionally + remain outside the DatePicker GitHub PR stack. + +### Verified upstream inventory + +| Surface | Available foundation | Remaining headless integration | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Date state and validation | [`useDatePickerBase_unstable`](../../react-datepicker-preview/library/src/components/DatePicker/useDatePickerBase.ts) shares controlled/uncontrolled selection, editable drafts, parsing/formatting, required/boundary/restriction validation, and popup requests. | Reuse this behavior; do not duplicate date arithmetic or callback/validation rules. | +| Props and state | [`DatePickerBaseProps` and `DatePickerBaseState`](../../react-datepicker-preview/library/src/components/DatePicker/DatePicker.types.ts) are exported. The base hook does not produce DOM slots or accept a ref. | Define native/headless slots and preserve the native input ref. Do not alias the styled Input/Calendar slot types or claim the base state is renderable. | +| Input | The headless [Input family](../library/src/components/Input) already exists. | Preserve Field naming, required/invalid associations, text editing, and consumer slot forwarding without appearance/size defaults or icons. | +| Calendar | The current base does not export a headless Calendar subpath. | Integrate the public unstyled Calendar family from the prerequisite PR; verify selection/range events, restricted dates, view switching, and focus entry. | +| Popup and rendering | The [styled integration](../../react-datepicker-preview/library/src/components/DatePicker/useDatePicker.tsx) documents and tests open/dismiss/focus behavior; its renderer composes styled Input and Calendar and cannot be aliased. | Use headless popup/positioning primitives, with correct document ownership, SSR/hydration, Escape/outside dismissal, and input-focus restoration. Do not copy Tabster or Griffel into the headless implementation. | + +### Completion requirements after the prerequisite lands + +- Export stable `DatePicker`, `useDatePicker`, `renderDatePicker`, and related props/slots/state/event + types through the public `date-picker` subpath. Keep the package root-export policy unchanged. +- Define and test reserved root presence attributes for behavioral state (`data-open`, + `data-disabled`, and `data-invalid`), including their absent states and consumer-override + protection. These are proposed selectors, not an API currently available to consumers. +- Cover conformance, controlled/uncontrolled selection and visibility, validation callbacks, + accessible input/dialog relationships, and consumer slot/ref forwarding. +- Add browser coverage for open/select/close and inverse actions, Escape/outside dismissal, focus + entry/return, Calendar keyboard/view navigation, RTL, iframe ownership, and hydration. +- Add token-based CSS Module stories demonstrating default, editable, controlled, invalid, and + disabled states, with light/dark themes, visible focus, forced-colors support, and complete + TSX/CSS "Show code" output. +- Add the package dependency/export-map entry, subpath API report, AllComponents bundle fixture, + and package-scoped Beachball change file only when the implementation exists. Verify the + headless bundle does not introduce styled Calendar, default icons, Griffel, or Tabster. + +This readiness record adds no executable DatePicker API, stories, package dependencies, or release +claim. Keep the final PR in draft until the dependency and implementation requirements are met. + --- ## Authoring a new component story From b569c278165f545e88b4ba980b9bb02280ff1ef1 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Sun, 18 Oct 2026 10:38:23 +0200 Subject: [PATCH 2/2] feat(react-headless-components-preview): implement DatePicker Compose shared date state with headless Input, pending Calendar, and native modal popup behavior. Keep Calendar prerequisite patches outside the DatePicker stack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-e74c0e8e-4c60-49b2-b401-c01b375fbc31.json | 6 + .../bundle-size/AllComponents.fixture.js | 4 + .../library/bundle-size/DatePicker.fixture.js | 7 + .../library/etc/date-picker.api.md | 80 +++++ .../library/etc/utils.api.md | 3 + .../library/package.json | 12 + .../components/DatePicker/DatePicker.cy.tsx | 219 ++++++++++++++ .../components/DatePicker/DatePicker.test.tsx | 257 ++++++++++++++++ .../src/components/DatePicker/DatePicker.tsx | 14 + .../components/DatePicker/DatePicker.types.ts | 54 ++++ .../src/components/DatePicker/index.ts | 12 + .../DatePicker/renderDatePicker.tsx | 21 ++ .../components/DatePicker/useDatePicker.tsx | 280 ++++++++++++++++++ .../library/src/date-picker.ts | 10 + .../library/src/utils/toDataAttributeValue.ts | 2 + .../stories/README.md | 55 ++-- .../DatePickerControlled.stories.tsx | 24 ++ .../DatePicker/DatePickerDefault.stories.tsx | 14 + .../src/DatePicker/DatePickerDescription.md | 34 +++ .../DatePicker/DatePickerDisabled.stories.tsx | 12 + .../DatePicker/DatePickerEditable.stories.tsx | 27 ++ .../DatePickerInitiallyOpen.stories.tsx | 14 + .../src/DatePicker/date-picker.module.css | 92 ++++++ .../stories/src/DatePicker/index.stories.tsx | 15 + yarn.lock | 2 + 25 files changed, 1237 insertions(+), 33 deletions(-) create mode 100644 change/@fluentui-react-headless-components-preview-e74c0e8e-4c60-49b2-b401-c01b375fbc31.json create mode 100644 packages/react-components/react-headless-components-preview/library/bundle-size/DatePicker.fixture.js create mode 100644 packages/react-components/react-headless-components-preview/library/etc/date-picker.api.md create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.cy.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.test.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/renderDatePicker.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/DatePicker/useDatePicker.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/date-picker.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerControlled.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerDefault.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerDescription.md create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerDisabled.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerEditable.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/DatePickerInitiallyOpen.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/date-picker.module.css create mode 100644 packages/react-components/react-headless-components-preview/stories/src/DatePicker/index.stories.tsx diff --git a/change/@fluentui-react-headless-components-preview-e74c0e8e-4c60-49b2-b401-c01b375fbc31.json b/change/@fluentui-react-headless-components-preview-e74c0e8e-4c60-49b2-b401-c01b375fbc31.json new file mode 100644 index 00000000000000..6ab6652c441829 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-e74c0e8e-4c60-49b2-b401-c01b375fbc31.json @@ -0,0 +1,6 @@ +{ + "type": "minor", + "comment": "Add an unstyled DatePicker with shared date validation, headless Calendar composition, and native modal-dialog behavior.", + "packageName": "@fluentui/react-headless-components-preview", + "email": "dmytrokirpa@microsoft.com" +} diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js index 6d2171cf5db2b6..f5af7b43f56d93 100644 --- a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js @@ -130,6 +130,7 @@ import { renderCompoundButton, useCompoundButton, } from '@fluentui/react-headless-components-preview/compound-button'; +import { DatePicker, renderDatePicker, useDatePicker } from '@fluentui/react-headless-components-preview/date-picker'; import { Dialog, DialogActions, @@ -733,6 +734,9 @@ console.log( CompoundButton, renderCompoundButton, useCompoundButton, + DatePicker, + renderDatePicker, + useDatePicker, Dialog, DialogActions, DialogBody, diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/DatePicker.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/DatePicker.fixture.js new file mode 100644 index 00000000000000..c86bac915270e0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/DatePicker.fixture.js @@ -0,0 +1,7 @@ +import { DatePicker } from '@fluentui/react-headless-components-preview/date-picker'; + +console.log(DatePicker); + +export default { + name: 'DatePicker', +}; diff --git a/packages/react-components/react-headless-components-preview/library/etc/date-picker.api.md b/packages/react-components/react-headless-components-preview/library/etc/date-picker.api.md new file mode 100644 index 00000000000000..1290e96bdfdc99 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/etc/date-picker.api.md @@ -0,0 +1,80 @@ +## API Report File for "@fluentui/react-headless-components-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { CalendarBaseProps } from '@fluentui/react-calendar-preview'; +import { CalendarDayBaseProps } from '@fluentui/react-calendar-preview'; +import { CalendarDayHandle } from '@fluentui/react-calendar-preview'; +import type { CalendarMonthBaseProps } from '@fluentui/react-calendar-preview'; +import { CalendarMonthHandle } from '@fluentui/react-calendar-preview'; +import type { CalendarMonthSlots as CalendarMonthSlots_2 } from '@fluentui/react-calendar-preview'; +import type { CalendarSlots as CalendarSlots_2 } from '@fluentui/react-calendar-preview'; +import { CalendarYearBaseProps } from '@fluentui/react-calendar-preview'; +import { CalendarYearHandle } from '@fluentui/react-calendar-preview'; +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { DatePickerBaseProps } from '@fluentui/react-datepicker-preview'; +import type { DatePickerBaseState } from '@fluentui/react-datepicker-preview'; +import { DatePickerDateChangeData } from '@fluentui/react-datepicker-preview'; +import { DatePickerErrorType } from '@fluentui/react-datepicker-preview'; +import { DatePickerOpenChangeData } from '@fluentui/react-datepicker-preview'; +import { DatePickerValidationResultData } from '@fluentui/react-datepicker-preview'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { InputBaseProps } from '@fluentui/react-input'; +import type { JSXElement } from '@fluentui/react-utilities'; +import type { PositioningProps as PositioningProps_2 } from '@fluentui/react-positioning'; +import type { PositioningShorthandValue } from '@fluentui/react-positioning'; +import * as React_2 from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; +import type { Slot } from '@fluentui/react-utilities'; + +// @public +export const DatePicker: ForwardRefComponent; + +export { DatePickerDateChangeData } + +export { DatePickerErrorType } + +export { DatePickerOpenChangeData } + +// @public +export type DatePickerProps = Omit & { + calendar?: DatePickerSlots['calendar']; + trigger?: DatePickerSlots['trigger']; + popupSurface?: DatePickerSlots['popupSurface']; + positioning?: PositioningShorthand; +}; + +// @public +export type DatePickerSlots = { + root: NonNullable>; + calendar: NonNullable & RefAttributes>>; + trigger: NonNullable>; + popupSurface: NonNullable>; +}; + +// @public (undocumented) +export type DatePickerState = ComponentState & Pick & { + renderSurface: boolean; + root: { + root: { + 'data-open'?: ''; + 'data-disabled'?: ''; + 'data-invalid'?: ''; + }; + }; +}; + +export { DatePickerValidationResultData } + +// @public +export const renderDatePicker: (state: DatePickerState) => JSXElement; + +// @public +export const useDatePicker: (props: DatePickerProps, ref: React_2.Ref) => DatePickerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless-components-preview/library/etc/utils.api.md b/packages/react-components/react-headless-components-preview/library/etc/utils.api.md index 71fa91e7c2abe8..8af5cc2306bda9 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/utils.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/utils.api.md @@ -5,6 +5,9 @@ ```ts // @public +export function toDataAttributeValue(value: boolean | undefined): '' | undefined; + +// @public (undocumented) export function toDataAttributeValue(value: boolean | string | number | undefined): string | undefined; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index d8df5de297cff5..9366c152fc6560 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -26,11 +26,13 @@ "@fluentui/react-badge": "^9.5.6", "@fluentui/react-breadcrumb": "^9.4.6", "@fluentui/react-button": "^9.11.1", + "@fluentui/react-calendar-preview": "workspace:^", "@fluentui/react-card": "^9.7.3", "@fluentui/react-checkbox": "^9.6.5", "@fluentui/react-color-picker": "^9.3.1", "@fluentui/react-combobox": "^9.17.7", "@fluentui/react-context-selector": "^9.2.20", + "@fluentui/react-datepicker-preview": "workspace:^", "@fluentui/react-dialog": "^9.18.5", "@fluentui/react-divider": "^9.7.5", "@fluentui/react-drawer": "^9.13.4", @@ -202,6 +204,16 @@ "default": "./lib-commonjs/compound-button.cjs" } }, + "./date-picker": { + "import": { + "types": "./dist/date-picker.d.ts", + "default": "./lib/date-picker.js" + }, + "require": { + "types": "./dist/date-picker.d.cts", + "default": "./lib-commonjs/date-picker.cjs" + } + }, "./dialog": { "import": { "types": "./dist/dialog.d.ts", diff --git a/packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.cy.tsx b/packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.cy.tsx new file mode 100644 index 00000000000000..8ff79c242a5e89 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/DatePicker/DatePicker.cy.tsx @@ -0,0 +1,219 @@ +import * as React from 'react'; +import { createPortal } from 'react-dom'; +import { mount } from '@fluentui/scripts-cypress'; +import { polyfillBodyAndObserve } from '@microsoft/focusgroup-polyfill'; +import { Provider } from '../Provider'; +import { DatePicker } from './DatePicker'; + +polyfillBodyAndObserve(); +const today = new Date(2030, 0, 15); +const day = (label: string) => `td[role="gridcell"]:has(button[aria-label="${label}"])`; + +describe('headless DatePicker', () => { + it('opens from the keyboard, navigates and selects, then restores input focus without reopening', () => { + const onDateChange = cy.stub().as('dateChange'); + const onOpenChange = cy.stub().as('openChange'); + mount( + + + , + ); + cy.get('input').focus(); + cy.get('dialog').should('be.visible').and('have.attr', 'open'); + cy.get(day('January 15, 2030')).should('be.focused').realPress('ArrowRight'); + cy.get(day('January 16, 2030')).should('be.focused').realPress('Enter'); + cy.get('dialog').should('not.exist'); + cy.get('input').should('be.focused').and('have.value', 'Wed Jan 16 2030'); + cy.get('@dateChange').should('have.been.calledOnce'); + cy.get('@openChange').should('have.been.calledTwice'); + cy.get('input').realPress('ArrowDown'); + cy.get(day('January 16, 2030')).should('be.focused').realPress('Escape'); + cy.get('dialog').should('not.exist'); + cy.get('input').should('be.focused'); + }); + + it('makes outside content inert while modal and dismisses with Escape', () => { + mount( + + + + , + ); + cy.get('input').focus().realPress('Enter'); + cy.get('dialog').should('be.visible'); + cy.get('dialog').should(surface => expect(surface[0].matches(':modal')).to.equal(true)); + cy.get(day('January 15, 2030')).focus(); + cy.contains('button', 'Outside').then(button => { + button[0].focus(); + expect(button[0].ownerDocument.activeElement).not.to.equal(button[0]); + }); + cy.get(day('January 15, 2030')).focus().realPress('Escape'); + cy.get('dialog').should('not.exist'); + cy.get('input').should('be.focused'); + }); + + it('dismisses a backdrop click and does not immediately reopen', () => { + mount( + + + , + ); + cy.get('input').focus(); + cy.get('dialog').should('be.visible'); + cy.get('dialog').click(-10, -10, { force: true }); + cy.get('dialog').should('not.exist'); + cy.get('input').should('be.focused'); + }); + + it('keeps rejected controlled close requests modal', () => { + const onOpenChange = cy.stub().as('openChange'); + mount( + + + , + ); + cy.get('dialog').should('be.visible'); + cy.focused().realPress('Escape'); + cy.get('dialog').should('be.visible').and('have.attr', 'open'); + cy.get('@openChange').should('have.been.calledOnce'); + }); + + it('renders, selects, and restores focus in the input owner document', () => { + const Example = () => { + const [frame, setFrame] = React.useState(null); + const targetDocument = frame?.contentDocument; + return ( + <> +