diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/CHANGELOG.md b/packages/pluggableWidgets/datagrid-date-filter-web/CHANGELOG.md index 47f9e63f71..f110b9152b 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/CHANGELOG.md +++ b/packages/pluggableWidgets/datagrid-date-filter-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We updated the calendar picker, improving how screen readers announce the day-name row and the open calendar. + ## [3.10.0] - 2026-05-06 ### Fixed diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js b/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js index 650e6984dc..245024882f 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js +++ b/packages/pluggableWidgets/datagrid-date-filter-web/e2e/DataGridDateFilter.spec.js @@ -58,8 +58,8 @@ test.describe("datagrid-date-filter-web", () => { test.describe("with Default value", () => { test("set initial condition (apply filter right after load)", async ({ page }) => { - await page.goto("/#/filter_init_condition", { timeout: 1000 }); - await page.reload(); + await page.goto("/p/filter_init_condition"); + await waitForMendixApp(page); const row1 = page.locator(".mx-name-dataGrid22 [role=row]").nth(1); await expect(row1).toHaveText("Chester2/20/2003"); const row7 = page.locator(".mx-name-dataGrid22 [role=row]").nth(7); @@ -71,8 +71,8 @@ test.describe("datagrid-date-filter-web", () => { test.describe("with Default start and Default end dates", () => { test("set initial condition (apply filter right after load)", async ({ page }) => { - await page.goto("/#/filter_init_condition", { timeout: 1000 }); - await page.reload(); + await page.goto("/p/filter_init_condition"); + await waitForMendixApp(page); const row1 = page.locator(".mx-name-dataGrid21 [role=row]").nth(1); await expect(row1).toHaveText("Jayden4/21/1993"); const row10 = page.locator(".mx-name-dataGrid21 [role=row]").nth(10); diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/.openspec.yaml b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/.openspec.yaml new file mode 100644 index 0000000000..1c37182ed6 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-05 diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/design.md b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/design.md new file mode 100644 index 0000000000..78bda915bf --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/design.md @@ -0,0 +1,115 @@ +## Context + +`datagrid-date-filter-web@3.11.3` depends on `react-datepicker@^6.6.0` plus the community `@types/react-datepicker@^6.2.0`. Upstream is at `9.1.0`. Three majors of drift bring: + +- **v7.0.0** — the library was rewritten in TypeScript and began shipping `dist/index.d.ts`. The community types package became redundant and diverges from reality. +- **v7.4.0** — `react-onclickoutside` was dropped (commit `31ff0f8`) in favour of an internal `ClickOutsideWrapper`, removing a transitive dependency. +- **v8.0.0** — `date-fns` bumped to `^4.1.0` (commit `9484932`); React 19 added to the peer range. +- **v9.x** — current line; `DatePicker` is a non-generic class and `DatePickerProps` is a discriminated union keyed on `selectsRange` / `selectsMultiple`. + +Two things that would normally make this jump risky do **not** apply: + +- **No popper.js migration.** `@floating-ui/react` landed in v5.0.0, so v6.6.0 is already on floating-ui. `popperPlacement`, `popperProps`, `popperContainer`, and `showPopperArrow` all survive in v9; only their underlying option types changed (`popperProps` is now `Omit`). +- **No CSS class churn.** Diffing `src/stylesheets/datepicker.scss` between `v6.6.0` and `v9.1.0` shows exactly one removed class (`.react-datepicker__week-number--keyboard-selected`, unused here) and eight added. Every selector the widget, the Atlas theme file, and the E2E spec rely on still exists, and `popper_component.tsx` still emits `data-placement`. + +The real work is therefore concentrated in TypeScript types across two files. + +A close in-repo reference exists: `date-time-picker-web` already runs `react-datepicker@^8.9.0` and shows the target import shape (`import ReactDatePicker, { DatePickerProps, DatePicker } from "react-datepicker"`), the non-generic `createRef()`, and the `MouseEvent | KeyboardEvent` shape of `onChangeRaw`. It is not a drop-in template — it uses `makeAutoObservable`, holds dates in a local array instead of a filter store, and has no `selectsRange`-vs-single duality driven by a filter function — but it de-risks the API questions. + +Constraints: + +- Repo pins React to `>=18.0.0 <19.0.0`; v9 peers on `^16.9.0 || ^17 || ^18 || ^19`, so React 18 stays valid. +- `date-fns` versions already vary across the monorepo (`^2.30.0` in two chart widgets, `^3.6.0` here and in `widget-plugin-filtering`, `^4.1.0` in `calendar-web`), so pnpm already resolves multiple majors side by side. +- The widget ships inside the `data-widgets` module; the Atlas theme override lives in that module, not in this package. + +## Goals / Non-Goals + +**Goals:** + +- `react-datepicker@^9.1.0` with the widget's user-visible behavior unchanged. +- Drop `@types/react-datepicker`; consume the library's own types. +- Align `date-fns` on `^4.1.0` for this widget, and widen the `widget-plugin-filtering` peer range to admit it. +- Capture the picker integration contract as a spec so the next bump has a regression baseline. +- Keep the type surface honest — no blanket `any` or `@ts-expect-error` to paper over the union. + +**Non-Goals:** + +- Bumping `date-time-picker-web` from `^8.9.0` to `^9.1.0`. Separate change; this one is scoped to the widget that is actually behind. +- Refactoring `DatePickerController`, the mobx wiring, or the `withLinkedDateStore` / `withParentProvidedDateStore` HOCs. +- Adopting new v7-v9 features (`selectsMultiple`, `swapRange`, `showIcon`, the `timezone` prop and its optional `date-fns-tz` peer). +- Migrating the other six `widget-plugin-filtering` consumers to date-fns 4. +- Changing the widget's XML property contract. + +## Decisions + +### Go to 9.1.0 in one step, not 6 → 7 → 8 → 9 + +The intermediate majors add no value as separate commits: v7's break is the types rename, v8's is date-fns, and none of the removed APIs are reintroduced later. Staging would mean three rounds of snapshot churn for the same end state. Verification comes from the type checker plus the existing test suites rather than from intermediate stops. + +_Alternative considered:_ land v8.10.0 first to match `date-time-picker-web`, then v9. Rejected — it defers rather than removes the v9 type work and needs two release cycles for one dependency refresh. + +### Replace the type imports rather than shim the old names + +v6's community types exported `ReactDatePickerProps` and a generic `declare class ReactDatePicker<...>`. v9 exports `DatePickerProps` (a union) and a non-generic `DatePicker` class, with the class also available as the default export. The two usage sites change like this: + +- `DatePicker.tsx`: `Pick, ...>` → `Pick`; `RefObject | null>` → `RefObject`. +- `DatePickerController.ts`: `interface DatePickerBackendProps extends ReactDatePickerProps, ClassAttributes` → a type alias intersecting `DatePickerProps` with `ClassAttributes`, mirroring what `date-time-picker-web` does. This must become a `type` rather than an `interface`, because an interface cannot extend a union. +- `createRef>()` → `createRef()`. + +A local alias re-exporting the new types under the old names was considered and rejected: it would hide the union from `Pick<>`, which is exactly where the compiler needs to see it. + +Note the widget's own component is _also_ named `DatePicker` (`src/components/DatePicker.tsx`), so importing the library's `DatePicker` class into that file would collide. Import it aliased there — e.g. `DatePicker as ReactDatePickerClass` — or keep using the default import's instance type. `DatePickerController.ts` has no such collision. + +### Cast `onChange` at the props boundary, keep the controller handler polymorphic + +v9's `DatePickerProps` is a union whose `onChange` signature depends on `selectsRange`: `(date: Date | null, ...)` when false/absent, `(date: [Date | null, Date | null], ...)` when true. This widget flips `selectsRange` at runtime from the filter function, so one handler must serve both arms — `handlePickerChange` already accepts `Date | [Date | null, Date | null] | null` and branches on `isDate`. + +TypeScript cannot verify that a single handler satisfies a runtime-selected union arm, so the assignment needs one narrow cast where the handler is passed to the picker element. `date-time-picker-web` solves the same problem by casting its whole returned props object (`as DatePickerProps` in `useSetupProps.ts`). Prefer the narrower fix here: cast only `onChange` at the JSX site, leaving every other prop type-checked. Document it with a short comment explaining the runtime-selected-arm reason. + +_Alternative considered:_ split into two elements, one per mode. Rejected — it would remount the picker on filter-function change, losing focus and input state, and the widget deliberately refocuses instead. + +### Narrow `UNSAFE_handleChangeRaw` to the v9 event type + +v6 typed `onChangeRaw` loosely enough that `BaseSyntheticEvent` worked. v9 declares `(event?: React.MouseEvent | React.KeyboardEvent, selectionMeta?: { date, formattedDate }) => void`. Change the parameter to the optional union and keep the `event?.type === "change"` guard — the ignore-in-range-mode behavior is unchanged. `date-time-picker-web` already uses this exact signature. + +### Drop `useWeekdaysShort` from the passed props + +It is no longer part of the public `DatePickerProps` surface in v9 — `Calendar` still accepts it internally, but `DatePickerProps` omits its way around it, so passing it is a type error. The widget passes `useWeekdaysShort={false}`, which is also the library default (day names come from `weekdaysMin`), so removing it is behavior-neutral. Confirm via the day-name row in the refreshed snapshots. + +### Bump the widget's date-fns to ^4.1.0; widen the plugin peer range only + +`react-datepicker@9` depends on `date-fns@^4.1.0` internally. The widget separately imports `date-fns/locale`, `date-fns/isDate`, and the `Locale` type, and passes locale objects _into_ the picker via `registerLocale` — so a v3/v4 split across that boundary risks `Locale` shape mismatches. Bumping the widget to `^4.1.0` keeps one copy on the path that matters. + +`widget-plugin-filtering` declares `date-fns` as a peer _and_ dev dependency but has **zero** `date-fns` imports in `src/` (verified by grep). Widening the peer to `^3.6.0 || ^4.1.0` is enough to keep pnpm quiet without forcing the other six filter widgets to move. date-fns 4's breaking changes are principally the timezone/`TZDate` rework and the removal of the sub-path `esm/` builds; the three call sites here (`locale`, `isDate`, `Locale`) are unaffected. + +_Alternative considered:_ let the nested `date-fns@4` copy inside `react-datepicker` serve the picker while the widget stays on v3. Rejected — locale objects would cross versions. + +### Refresh snapshots by inspection, not blind `-u` + +The three snapshot files total ~560 lines and encode the picker's rendered DOM. Regenerate with `pnpm run test -u`, then read the diff for anything that is a real behavior change — missing `aria-*` attributes on input or button, a changed `role`, a vanished `data-placement`, a portal container that no longer receives children — as opposed to benign class-order or wrapper-nesting churn. Accepting the diff unread is how an accessibility regression ships. + +## Risks / Trade-offs + +- **The `onChange` cast could mask a genuine mode/handler mismatch** → Keep it scoped to that single prop, never the whole props object, and cover both modes in unit tests (single-date sets arg1; range sets arg1+arg2; null clears both). +- **Snapshot churn hides an accessibility regression** → Assert the a11y contract explicitly rather than trusting snapshots: `aria-expanded` flipping with calendar state, `aria-controls` matching the portal id, and the configured screen-reader captions. The E2E axe scan (`wcag21aa`) is the backstop. +- **Calendar chrome shifts enough to break the E2E screenshot baseline** → The baseline is Linux/CI-generated and cannot be regenerated faithfully on macOS; if `dataGridDateFilter.png` fails, regenerate it in CI rather than locally. Note the second picker-specific screenshot test is already `test.fixme`, so it is not a signal. +- **Popup positioning regresses under the grid's overflow** → `popperProps: { strategy: "fixed" }` still type-checks against `UseFloatingOptions`, but `date-time-picker-web` additionally sets `transform: false` and a `computeStyles`/`gpuAcceleration: false` middleware entry, suggesting it hit positioning artifacts on v8. If the calendar mispositions, that is the known remedy. Verify with the filter inside a scrolled Data Grid header, not just in isolation. +- **date-fns 4 changes locale object shape** → The `Locale` type and `date-fns/locale` entry points are stable across v3→v4; `calendar-web` already runs `^4.1.0` in this repo. Type-check plus the locale unit tests cover it. +- **`widget-plugin-filtering` peer widening ripples to six other filter widgets** → The package has no `date-fns` imports, so widening cannot change its behavior. Still, build the dependent widgets once to confirm no peer-resolution warnings. + +## Migration Plan + +1. Update `package.json`: `react-datepicker` → `^9.1.0`, `date-fns` → `^4.1.0`, remove `@types/react-datepicker`. Widen the `date-fns` peer in `widget-plugin-filtering`. Install. +2. Fix types in `DatePickerController.ts`, then `DatePicker.tsx` — controller first, since its type alias is the one `DatePicker.tsx`'s props extend. Let `tsc` drive the sequence. +3. Run unit tests, review and refresh snapshots, add explicit a11y and both-modes assertions where snapshots were doing that work implicitly. +4. Build the widget and the dependent filter widgets; confirm no peer warnings and no `react-onclickoutside` in the tree. +5. Verify in a Mendix project: calendar opens via mouse and keyboard, popup escapes grid overflow and survives scroll, both filter modes filter correctly, locale/format behavior holds, Atlas theme still applies. +6. Run E2E; regenerate the screenshot baseline in CI only if it fails for benign chrome reasons. +7. CHANGELOG entry and version bump. + +**Rollback:** the change is confined to two source files plus dependency declarations, so reverting the commit and reinstalling restores v6 fully. No data migration, no persisted state, no XML contract change. + +## Open Questions + +- Does the v9 calendar need the `transform: false` / `gpuAcceleration: false` popper treatment that `date-time-picker-web` carries, or was that specific to that widget's layout? Resolve by testing the unmodified `strategy: "fixed"` config inside a scrolled grid first. +- Should `widget-plugin-filtering` eventually move to `date-fns@^4` outright and drop the dual peer range? Out of scope here; worth a follow-up once the other filter widgets are surveyed. diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/proposal.md b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/proposal.md new file mode 100644 index 0000000000..3e2c1e8a9b --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/proposal.md @@ -0,0 +1,57 @@ +## Why + +The Date Filter widget is pinned to `react-datepicker@^6.6.0`, three major versions behind the current `9.1.0`. Staying on v6 keeps a `react-onclickoutside` transitive dependency that upstream removed in v7.4.0, blocks the React 19 compatibility that v8+ declares, and forces the widget to carry `@types/react-datepicker` because v6 shipped no bundled types. The sibling `date-time-picker-web` widget already runs `^8.9.0`, so the repo maintains two divergent picker APIs. + +## What Changes + +- Upgrade `react-datepicker` from `^6.6.0` to `^9.1.0` in `datagrid-date-filter-web`. +- Remove the `@types/react-datepicker` devDependency — v7.0.0 onward ships its own `dist/index.d.ts`. +- **BREAKING (internal API only)** Replace the `ReactDatePickerProps` type import with `DatePickerProps`, and the default-export-as-type `ReactDatePicker` instance type with the named `DatePicker` class export. The v9 `DatePicker` class is non-generic, so the `ReactDatePicker` / `ReactDatePickerProps` type arguments used in `DatePicker.tsx` and `DatePickerController.ts` must be dropped. +- Adapt `onChange` to the v9 discriminated union: the props type now keys `onChange`'s signature off `selectsRange`/`selectsMultiple`, so the single controller handler that accepts `Date | [Date | null, Date | null] | null` needs an explicit cast at the assignment boundary. +- Adapt `onChangeRaw` to its narrowed v9 signature — `(event?: MouseEvent | KeyboardEvent, selectionMeta?) => void` instead of v6's `BaseSyntheticEvent`. +- Remove the `useWeekdaysShort` prop from the picker element: it is no longer part of the public `DatePickerProps` surface in v9. +- Bump `date-fns` from `^3.6.0` to `^4.1.0` in the widget, matching the version `react-datepicker@8+` depends on, and widen the `date-fns` peer range in `@mendix/widget-plugin-filtering` to admit v4. +- Refresh the three Jest snapshots and re-verify the Playwright E2E selectors against v9's rendered DOM. +- Add a user-facing CHANGELOG entry describing the dependency refresh. + +## Capabilities + +### New Capabilities + +- `date-picker-integration`: How the Date Filter widget binds to the third-party `react-datepicker` component — the props contract it passes, the calendar popup/portal and accessibility behavior it depends on, the CSS class names it and the Atlas theme rely on, and the date-library version constraints. Captures behavior that must hold across picker upgrades so future bumps have a regression baseline. + +### Modified Capabilities + +None. This change alters the widget's third-party integration and internal types; the widget's own filtering behavior, XML property contract, and user-visible interaction model stay the same. + +## Impact + +**Widget source (`datagrid-date-filter-web`)** + +- `src/components/DatePicker.tsx` — type imports, `InheritedProps` pick, `pickerRef` type, removal of `useWeekdaysShort`, `StaticProps` alias. +- `src/helpers/DatePickerController.ts` — `DatePickerBackendProps` alias, `pickerRef` `createRef` type argument, `handlePickerChange` and `UNSAFE_handleChangeRaw` signatures. +- `src/utils/date-utils.ts` — `registerLocale` import still valid; verify `date-fns/locale` `Locale` shape under v4. +- `src/helpers/useSetup.ts` — imports `Locale` from `date-fns`; confirm under v4. +- `src/DatagridDateFilter.editorPreview.tsx` — `require("react-datepicker/dist/react-datepicker.css")`; v9 still publishes this path via its `./dist/` export. +- Three snapshot files under `src/components/__tests__/__snapshots__/`. + +**Dependencies** + +- `react-datepicker` `^6.6.0` → `^9.1.0`; drops the `react-onclickoutside` transitive (removed upstream in v7.4.0, replaced by an internal `ClickOutsideWrapper`). +- `@types/react-datepicker@^6.2.0` removed. +- `date-fns` `^3.6.0` → `^4.1.0` in the widget; peer range widened in `@mendix/widget-plugin-filtering` (declared as peer + dev only, with zero `src/` imports, so no code change is required there and the other six filter widgets are unaffected). +- `@floating-ui/react` and `clsx` remain bundled deps of `react-datepicker`; v6 already used floating-ui, so no popper.js migration is involved. +- React 18 stays supported — v9 peers on `^16.9.0 || ^17 || ^18 || ^19`, satisfying the repo's `>=18.0.0 <19.0.0` pin. + +**Styling** + +- `packages/modules/data-widgets/src/themesource/datawidgets/web/_date-picker.scss` targets `.react-datepicker-popper[data-placement^="bottom"]`; v9 still emits `data-placement` from `popper_component.tsx`, so the selector holds. Between v6 and v9 only `.react-datepicker__week-number--keyboard-selected` was dropped and eight class names added (including `__sr-only`, `__header-wrapper`, `__month-select`, `__year-select`), none of which the theme file overrides. Requires visual verification rather than code change. + +**Tests** + +- `e2e/DataGridDateFilter.spec.js` depends on `.react-datepicker__month-select`, `.react-datepicker__year-select`, and `.react-datepicker__day--0NN` — all present in v9. +- One E2E screenshot baseline (`dataGridDateFilter-chromium-linux.png`) may need regeneration if v9 changes calendar chrome. + +**Release** + +- Widget version bump from `3.11.3` plus a CHANGELOG entry; the widget is distributed inside the `data-widgets` module. diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/specs/date-picker-integration/spec.md b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/specs/date-picker-integration/spec.md new file mode 100644 index 0000000000..2312e1acb4 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/specs/date-picker-integration/spec.md @@ -0,0 +1,201 @@ +## ADDED Requirements + +### Requirement: Calendar popup renders into a widget-owned portal + +The widget SHALL render the calendar popup into a portal container it owns and identifies, so the popup escapes the Data Grid's clipping and scrolling ancestors. The portal container element SHALL carry the same id that the calendar toggle button references via `aria-controls`. + +#### Scenario: Popup escapes grid overflow + +- **WHEN** the filter is placed in a Data Grid header and the user opens the calendar +- **THEN** the calendar is rendered inside the widget's portal container rather than inline in the filter cell +- **AND** the full calendar is visible without being clipped by the grid's overflow + +#### Scenario: Popup positioning survives page scroll + +- **WHEN** the calendar is open and the surrounding page scrolls +- **THEN** the calendar stays anchored below the start of the filter input + +#### Scenario: Popup content is only mounted while expanded + +- **WHEN** the calendar is closed +- **THEN** no calendar content is present in the portal container + +### Requirement: Calendar toggle button exposes accessible expanded state + +The widget SHALL render a calendar toggle button that announces its popup relationship and current expanded state to assistive technology. The button SHALL reflect `aria-expanded` matching whether the calendar is open, reference the portal container through `aria-controls`, declare `aria-haspopup`, and carry a configurable accessible label. + +#### Scenario: Expanded state tracks calendar visibility + +- **WHEN** the user opens the calendar +- **THEN** the toggle button reports `aria-expanded` as true +- **AND** when the calendar closes the button reports `aria-expanded` as false + +#### Scenario: Author-configured screen reader captions are used + +- **WHEN** the widget is configured with a screen reader calendar caption and input caption +- **THEN** the toggle button uses the calendar caption as its accessible name +- **AND** the picker input is labelled by the input caption + +#### Scenario: Default captions when unconfigured + +- **WHEN** no screen reader captions are configured +- **THEN** the toggle button falls back to "Show calendar" +- **AND** the input falls back to a "date filter" label + +### Requirement: Keyboard and pointer activation of the calendar + +The widget SHALL open the calendar and move focus into the picker when the toggle button is activated by pointer or by keyboard. Pointer activation SHALL be handled on mouse-down rather than click to avoid racing the calendar's outside-click dismissal. Keyboard activation SHALL respond to Enter and Space and SHALL suppress the default browser action. + +#### Scenario: Mouse activation opens the calendar + +- **WHEN** the user presses the mouse down on the calendar toggle button while the calendar is closed +- **THEN** the picker receives focus and the calendar opens + +#### Scenario: Mouse-down while already open does not re-focus + +- **WHEN** the user presses the mouse down on the toggle button while the calendar is already open +- **THEN** the widget does not re-issue a focus request + +#### Scenario: Enter and Space open the calendar + +- **WHEN** the toggle button has focus and the user presses Enter or Space +- **THEN** the default action and propagation are suppressed +- **AND** the picker receives focus and the calendar opens + +### Requirement: Single-date and range filter modes drive the picker + +The widget SHALL configure the picker for range selection when the active filter function is `between`, and for single-date selection otherwise. In range mode the picker SHALL receive start and end dates and SHALL offer a clear affordance; in single-date mode it SHALL receive one selected date. When the filter function is `empty` or `notEmpty` the picker SHALL be disabled. + +#### Scenario: Between filter selects a range + +- **WHEN** the active filter function is `between` +- **THEN** the picker operates in range mode with the filter's first and second arguments as start and end dates +- **AND** a clear affordance is available + +#### Scenario: Non-range filter selects a single date + +- **WHEN** the active filter function is any comparison other than `between`, `empty`, or `notEmpty` +- **THEN** the picker operates in single-date mode with the filter's first argument as the selected date + +#### Scenario: Emptiness filters disable the picker + +- **WHEN** the active filter function is `empty` or `notEmpty` +- **THEN** the picker input is disabled + +#### Scenario: Switching filter function refocuses the picker + +- **WHEN** the user changes the filter function from the filter selector +- **THEN** the picker receives focus + +### Requirement: Picker change events map onto filter arguments + +The widget SHALL translate the picker's selection callback into filter argument updates for both selection modes. A single `Date` SHALL set the first filter argument. A start/end pair SHALL set the first and second arguments, mapping absent endpoints to undefined. A null selection SHALL clear both arguments. + +#### Scenario: Single date selection sets one argument + +- **WHEN** the picker reports a single `Date` selection +- **THEN** the filter's first argument is set to that date + +#### Scenario: Range selection sets both arguments + +- **WHEN** the picker reports a start/end pair +- **THEN** the filter's first argument is the start date and the second is the end date +- **AND** a null endpoint becomes undefined + +#### Scenario: Cleared selection resets both arguments + +- **WHEN** the picker reports a null selection +- **THEN** both filter arguments are cleared + +#### Scenario: Calendar stays open after picking a date + +- **WHEN** the user selects a date in the calendar +- **THEN** the calendar remains open so a range can be completed + +### Requirement: Range mode restricts direct text entry + +In range selection mode the widget SHALL suppress raw text input changes on the picker input, and SHALL clear the filter when Backspace is pressed while the input has focus. + +#### Scenario: Typing is suppressed in range mode + +- **WHEN** the picker is in range mode and a raw input change event occurs +- **THEN** the change is prevented + +#### Scenario: Backspace clears a range + +- **WHEN** the picker is in range mode and the user presses Backspace with the input focused +- **THEN** the filter is cleared + +#### Scenario: Typing is allowed in single-date mode + +- **WHEN** the picker is in single-date mode +- **THEN** raw text entry into the input is not suppressed + +### Requirement: Session locale and date format configure the calendar + +The widget SHALL derive the calendar's locale, first day of week, and accepted date formats from the active Mendix session locale. Short day and month tokens SHALL be widened so values with leading zeros parse, uppercase day-of-week tokens SHALL be lowercased to match the picker's format standard, and both the widened and original patterns SHALL be accepted when they differ. Date parsing SHALL be strict. The month and year selectors SHALL be rendered as dropdowns. + +#### Scenario: Session locale registers the calendar locale + +- **WHEN** the session locale language tag matches an available date library locale +- **THEN** that locale is registered and passed to the picker +- **AND** the calendar's first day of week comes from the session locale + +#### Scenario: Short format tokens accept padded input + +- **WHEN** the session date pattern uses single `d` or `M` tokens +- **THEN** the picker accepts both the widened pattern and the original session pattern + +#### Scenario: Uppercase day-of-week token is normalized + +- **WHEN** the session date pattern contains an uppercase `E` +- **THEN** the pattern passed to the picker uses a lowercase `e` + +#### Scenario: Month and year are chosen from dropdowns + +- **WHEN** the calendar is open +- **THEN** month and year are selectable via dropdown selects rather than scroll lists + +### Requirement: Picker stylesheet and class names remain available to the theme + +The widget SHALL depend on the picker's published stylesheet entry point, and the calendar markup SHALL keep the class names that the Data Widgets Atlas theme and the widget's own styles target. The popup wrapper SHALL continue to expose its resolved placement as a data attribute so placement-conditional theme rules apply. + +#### Scenario: Stylesheet import path resolves + +- **WHEN** the widget or its editor preview imports the picker's distributed stylesheet +- **THEN** the import resolves against the installed picker package + +#### Scenario: Theme-targeted class names are present + +- **WHEN** the calendar is rendered +- **THEN** the month select, year select, day, day-name, week, month container, header, and popper wrapper class names targeted by the theme are present + +#### Scenario: Placement is exposed for conditional styling + +- **WHEN** the calendar opens below the input +- **THEN** the popup wrapper exposes a placement data attribute beginning with `bottom` + +### Requirement: Picker dependency stays on a supported, current major + +The widget SHALL depend on a `react-datepicker` major that is actively supported, SHALL rely on the picker's bundled TypeScript types rather than a separate community types package, and SHALL declare a `date-fns` version compatible with the one the picker itself depends on. The picker SHALL NOT pull in a `react-onclickoutside` transitive dependency. + +#### Scenario: Types come from the picker package + +- **WHEN** the widget imports the picker's prop and instance types +- **THEN** those types resolve from the picker package's own type declarations +- **AND** no separate community types package is installed for it + +#### Scenario: Date library versions agree + +- **WHEN** dependencies are installed +- **THEN** the `date-fns` major the widget declares matches the major the picker depends on + +#### Scenario: Removed transitive dependency is absent + +- **WHEN** the dependency tree is inspected +- **THEN** `react-onclickoutside` is not present via the picker + +#### Scenario: React version support is retained + +- **WHEN** the widget is built against the repository's supported React version +- **THEN** the picker's peer range admits that version diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/tasks.md b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/tasks.md new file mode 100644 index 0000000000..04a8ed8280 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-date-filter-web/openspec/changes/update-react-datepicker-v9/tasks.md @@ -0,0 +1,113 @@ +## 1. Dependencies + +- [x] 1.1 In `packages/pluggableWidgets/datagrid-date-filter-web/package.json`, set `react-datepicker` to `^9.1.0` and `date-fns` to `^4.1.0` +- [x] 1.2 Remove the `@types/react-datepicker` devDependency from the same file +- [x] 1.3 In `packages/shared/widget-plugin-filtering/package.json`, widen the `date-fns` peerDependency to `^3.6.0 || ^4.1.0` and bump its devDependency to `^4.1.0` +- [x] 1.4 Run `pnpm install` from the repo root and confirm no peer-dependency warnings for the touched packages +- [x] 1.5 Confirm `react-onclickoutside` is no longer reachable via `react-datepicker` (`pnpm why react-onclickoutside`) + +## 2. Type migration — controller + +- [x] 2.1 In `src/helpers/DatePickerController.ts`, change the import to `import ReactDatePicker, { DatePickerProps, DatePicker } from "react-datepicker"` +- [x] 2.2 Convert `DatePickerBackendProps` from an `interface ... extends` to a `type` alias intersecting `DatePickerProps` with `ClassAttributes` (an interface cannot extend the v9 union) +- [x] 2.3 Change `pickerRef` to `createRef()`, dropping the `` type arguments +- [x] 2.4 Retype `UNSAFE_handleChangeRaw` to accept `event?: MouseEvent | KeyboardEvent` and keep the `event?.type === "change"` range-mode guard +- [x] 2.5 Verify `handlePickerChange` still accepts `Date | [Date | null, Date | null] | null` and that its `isDate` / null / tuple branches are unchanged + +## 3. Type migration — component + +- [x] 3.1 In `src/components/DatePicker.tsx`, import the library types, aliasing the library's `DatePicker` class (the local component shares that name) +- [x] 3.2 Change `InheritedProps` to `Pick`, dropping the `` type argument +- [x] 3.3 Retype `pickerRef` to `RefObject<`_aliased library class_`| null>` +- [x] 3.4 Update the `StaticProps` alias to derive from `DatePickerProps` +- [x] 3.5 Remove the `useWeekdaysShort={false}` prop from the rendered picker (no longer in the public props surface; `false` was already the default) +- [x] 3.6 ~~Cast only the `onChange` prop at the JSX site~~ — superseded: a cast cannot work here. The union is + discriminated on `selectsRange`, and our `selectsRange` is a runtime `boolean`, which matches no single arm. + Resolved with zero casts instead: narrow the discriminant at the JSX site with + `selectsRange={props.selectsRange || undefined}` (type `true | undefined`, so the range arm is selected; + the picker treats an absent value as `false`), and declare our own `onChange` as `PickerChangeHandler`, + which accepts `Date | [Date | null, Date | null] | null` and is therefore assignable to either arm. +- [x] 3.7 Run `pnpm run lint` and a type-check; resolve remaining errors without introducing `any` or `@ts-expect-error` + — `tsc --noEmit` is clean and lint reports 0 errors, with no suppressions and no casts + +## 4. Verify date-fns 4 compatibility + +- [x] 4.1 Confirm `src/utils/date-utils.ts` still type-checks — `date-fns/locale` namespace import, the `Locale` type, and `registerLocale` +- [x] 4.2 Confirm `src/helpers/useSetup.ts`'s `Locale` import from `date-fns` still resolves +- [x] 4.3 Confirm `src/helpers/DatePickerController.ts`'s `date-fns/isDate` sub-path import still resolves under v4 + +## 5. Unit tests + +- [x] 5.1 Run `pnpm run test` and catalogue failures before changing anything +- [x] 5.2 Regenerate snapshots with `pnpm run test -u`, then read the full diff — **no snapshot changes at all**: the + closed-picker markup is byte-identical to v6. The 5 failures were the locale weekday tests, and the cause was + _not_ locale registration (month names localise correctly under date-fns 4). Two v9 DOM changes broke the + tests' structural navigation: + (a) `.react-datepicker__day-names` moved out of `.react-datepicker__header` into a new + `role="table"` > `role="rowgroup"` wrapper, so `header.lastChild` is now the month/year dropdown container + (hence the concatenated month names and years in the received value); + (b) each day name now renders a visually hidden full weekday name (`.react-datepicker__sr-only`, e.g. "Sunday") + next to an `aria-hidden` abbreviation ("Su"), so the row's `textContent` is `"SundaySuMondayMo…"`. +- [x] 5.3 In the snapshot diff, confirm no loss of `aria-expanded`, `aria-controls`, `aria-haspopup`, or the screen-reader label wiring on input and toggle button — all retained; snapshots unchanged +- [x] 5.4 Add or strengthen explicit assertions for the toggle button's `aria-expanded` tracking calendar state and `aria-controls` matching the portal container id (per spec: _Calendar toggle button exposes accessible expanded state_) +- [x] 5.5 Add or strengthen assertions covering both selection modes: single date sets arg1, range sets arg1 and arg2, null clears both (per spec: _Picker change events map onto filter arguments_) — new `src/helpers/__tests__/DatePickerController.spec.ts` +- [x] 5.6 Assert the day-name row is unchanged after removing `useWeekdaysShort` — the locale tests now read the + `aria-hidden` abbreviations per column, plus a new test asserting the columns' accessible names +- [x] 5.7 Confirm the full unit suite passes — 68 passed, 9 snapshots passed +- [x] 5.8 Keep the `calendar toggle button` block last in `DatagridDateFilter.spec.tsx`: rendering the widget advances + React's `useId` counter, which the filter-selector snapshots capture verbatim, so inserting renders earlier in + the file churns unrelated snapshots + +## 6. Build and downstream check + +- [x] 6.1 Run `pnpm turbo build` in the widget package and confirm a clean build +- [x] 6.2 Confirm the editor preview's `require("react-datepicker/dist/react-datepicker.css")` still resolves against v9 + — it does, via the bundler. Verified in the output rather than by assumption: the widget CSS contains 278 + `react-datepicker` rules including v9's new `.react-datepicker__sr-only`, and the editor-preview bundle inlines + the stylesheet. + **Caveat worth knowing:** v9 added an `exports` map whose subpath entry uses the legacy _trailing-slash_ form + (`"./dist/": "./dist/"`). Node 17+ dropped support for that form, so `require.resolve` of the CSS fails under + plain Node with `ERR_PACKAGE_PATH_NOT_EXPORTED`. Rollup's resolver still honours it, so our build and Jest are + fine — but any future tool that strictly follows Node resolution would break on this import. +- [x] 6.3 Build the other `widget-plugin-filtering` consumers (`datagrid-web`, `datagrid-text-filter-web`, `datagrid-number-filter-web`, `datagrid-dropdown-filter-web`, `gallery-web`, `dropdown-sort-web`) to confirm the peer widening broke nothing — all built clean + +## 7. Manual verification in Studio Pro + +> **Blocked — needs a workstation with Studio Pro.** `MX_PROJECT_PATH` is unset and no Mendix test project for this +> widget is checked out locally, so none of 7.1–7.9 could be executed. The widget does build and deploy-package +> cleanly (`pnpm turbo build`, `pnpm run verify`), so this section is purely the interactive pass. + +- [ ] 7.1 Deploy to a Mendix test project via `MX_PROJECT_PATH` and `pnpm start` +- [ ] 7.2 Verify calendar opens on mouse-down and on Enter/Space from the toggle button, and that focus lands in the picker +- [ ] 7.3 Verify the popup escapes the Data Grid's overflow and stays anchored while the page scrolls (per spec: _Calendar popup renders into a widget-owned portal_) +- [ ] 7.4 If positioning is wrong, add `transform: false` to the existing `popperProps` (which already sets + `strategy: "fixed"`). **Do not copy `date-time-picker-web`'s `popperModifiers` entry**: v9 replaced Popper.js + with Floating UI (`@floating-ui/react`), so `popperProps` is `Omit` and + `popperModifiers` is a Floating UI `Middleware[]`. `computeStyles` / `gpuAcceleration` are Popper-only options; + that sibling widget's entry is a Popper-era leftover with `fn: () => ({})`, i.e. a no-op. `transform: false` + is the option that actually changes positioning (top/left instead of a `translate()` transform). +- [ ] 7.5 Verify single-date filtering by typed input and by calendar click +- [ ] 7.6 Verify `between` mode: range selection, calendar staying open between endpoints, clear affordance, Backspace clearing, and typing suppressed +- [ ] 7.7 Verify `empty` / `notEmpty` disable the input +- [ ] 7.8 Verify month/year dropdowns, first-day-of-week, and date parsing under a non-`en-US` session locale +- [ ] 7.9 Verify the Atlas theme still applies — including the `.react-datepicker-popper[data-placement^="bottom"]` rule in `data-widgets`' `_date-picker.scss` + +## 8. E2E + +> **8.1 / 8.3 / 8.4 blocked — the Docker daemon is not running**, and `run-e2e` needs it to bring up the Mendix app. + +- [ ] 8.1 Run the E2E suite for the widget +- [x] 8.2 Confirm the `.react-datepicker__month-select`, `.react-datepicker__year-select`, and `.react-datepicker__day--0NN` selectors still resolve — verified against the actual v9 DOM (dumped from a jsdom render of the widget with the calendar open), not just from the changelog: all four selectors the spec uses are present and unchanged +- [ ] 8.3 Confirm the axe `wcag21aa` scan reports no violations. + Note: the scan runs on `/` with the calendar **closed**, so it does not cover the calendar markup — which is + exactly where v9 changed roles (it now emits `role="table"` / `rowgroup` / `row` / `columnheader` / `gridcell` + and a `role="dialog" aria-modal="true"` wrapper). Consider opening the calendar before the scan so this pass + actually exercises the new structure. +- [ ] 8.4 If `dataGridDateFilter-chromium-linux.png` fails only from benign calendar-chrome changes, regenerate the baseline in CI — not locally on macOS + +## 9. Release prep + +- [x] 9.1 Add a user-facing CHANGELOG entry describing the picker dependency update (behavior, not implementation detail) +- [x] 9.2 Leave the version bump for release time per repo convention — version left at 3.11.3 +- [x] 9.3 Run `pnpm run lint` and `pnpm run verify` for a final check — lint: 0 errors (3 pre-existing import/order + warnings in `DatagridDateFilter.spec.tsx`); verify: success diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/package.json b/packages/pluggableWidgets/datagrid-date-filter-web/package.json index 5cb7d0f3a9..256a424d95 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/package.json +++ b/packages/pluggableWidgets/datagrid-date-filter-web/package.json @@ -44,8 +44,8 @@ "@mendix/widget-plugin-external-events": "workspace:*", "@mendix/widget-plugin-filtering": "workspace:*", "classnames": "^2.5.1", - "date-fns": "^3.6.0", - "react-datepicker": "^6.6.0" + "date-fns": "^4.1.0", + "react-datepicker": "^9.1.0" }, "devDependencies": { "@mendix/automation-utils": "workspace:*", @@ -56,7 +56,6 @@ "@mendix/widget-plugin-component-kit": "workspace:*", "@mendix/widget-plugin-hooks": "workspace:*", "@mendix/widget-plugin-platform": "workspace:*", - "@mendix/widget-plugin-test-utils": "workspace:*", - "@types/react-datepicker": "^6.2.0" + "@mendix/widget-plugin-test-utils": "workspace:*" } } diff --git a/packages/pluggableWidgets/datagrid-date-filter-web/src/components/DatePicker.tsx b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/DatePicker.tsx index d548c476b0..cf65cf8715 100644 --- a/packages/pluggableWidgets/datagrid-date-filter-web/src/components/DatePicker.tsx +++ b/packages/pluggableWidgets/datagrid-date-filter-web/src/components/DatePicker.tsx @@ -1,11 +1,20 @@ import classNames from "classnames"; -import { Fragment, KeyboardEventHandler, MouseEventHandler, ReactElement, RefObject, useState } from "react"; -import ReactDatePicker, { ReactDatePickerProps } from "react-datepicker"; +import { + Fragment, + KeyboardEvent, + KeyboardEventHandler, + MouseEvent, + MouseEventHandler, + ReactElement, + RefObject, + useState +} from "react"; +import ReactDatePicker, { DatePickerProps as RdpDatePickerProps, DatePicker as RdpDatePicker } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import CalendarIcon from "./CalendarIcon"; type InheritedProps = Pick< - ReactDatePickerProps, + RdpDatePickerProps, | "calendarStartDay" | "dateFormat" | "disabled" @@ -13,20 +22,31 @@ type InheritedProps = Pick< | "locale" | "onCalendarClose" | "onCalendarOpen" - | "onChange" | "onChangeRaw" | "onKeyDown" | "selected" | "selectsRange" | "startDate" >; + +/** + * The picker props are a union discriminated on `selectsRange`: the single-date arm + * hands `onChange` a `Date | null`, the range arm a `[start, end]` tuple. We accept + * both so one handler stays assignable to whichever arm the filter mode selects. + */ +export type PickerChangeHandler = ( + value: Date | [Date | null, Date | null] | null, + event?: MouseEvent | KeyboardEvent +) => void; + export interface DatePickerProps extends InheritedProps { adjustable: boolean; id?: string; + onChange?: PickerChangeHandler; placeholder?: string; screenReaderCalendarCaption?: string; screenReaderInputCaption?: string; - pickerRef?: RefObject | null>; + pickerRef?: RefObject; expanded: boolean; onButtonMouseDown?: MouseEventHandler; onButtonKeyDown?: KeyboardEventHandler; @@ -64,14 +84,15 @@ export function DatePicker(props: DatePickerProps): ReactElement { placeholderText={props.placeholder} ref={props.pickerRef} selected={props.selected} - selectsRange={props.selectsRange} + // Narrow `boolean` to `true | undefined` so the props union resolves to its + // range arm; the picker treats an absent `selectsRange` as `false` anyway. + selectsRange={props.selectsRange || undefined} shouldCloseOnSelect={false} showMonthDropdown showPopperArrow={false} showYearDropdown startDate={props.startDate} strictParsing - useWeekdaysShort={false} />