feat(DateTimePicker): new component with two-step popover and presets - #55
IgorShevchik wants to merge 6 commits into
Conversation
- Adds B24DateTimePicker built on B24Popover, B24Calendar and B24Input - Two-step UX: calendar → hour/minute grid with configurable minute step - Side preset list with localized defaults (Today, Tomorrow, End of week, In a week, End of month) and full override via the `presets` prop - `dateOnly` mode forces value to CalendarDate with 00:00:00 - Works with @internationalized/date types via v-model (CalendarDateTime, ZonedDateTime, CalendarDate) - Adds theme, locale messages for all 19 locales, demo+nuxt playground pages, docs page with five examples and skill references update
…er-component-NqwmO
…er-component-NqwmO
… i18n, docs) - tests: add DateTimePicker.spec.ts (snapshots + minuteStep clamp + axe a11y) — 42 tests across nuxt + vue projects, both green - security/correctness: guard `minuteStep` against `NaN` (Number.isFinite check) - correctness: memoize resolved preset values into `resolvedPresets` so factory presets evaluate once per render instead of twice (was called by both `:active` and `:data-active`) - a11y: add `role="group"` + `aria-labelledby` on hour/minute grids and `aria-pressed` on each cell - i18n: fix "endOfWeek" mistranslations in de/fr/it/br/tr — they meant "weekend" instead of "end of work week (Friday)" - docs: drop misleading `B24Button` from intro composition, fix "Pick a time" → "Pick a date and time", add "With min/max dates" section and matching example, show `open` slot prop in custom trigger, add `name`/`required` to form-field example - skills: mention `DateTimePickerPreset` type for custom presets - theme: correct `@memo` comment (uses `B24Input`, not `B24InputDate`)
…er-component-NqwmO
Addresses all 11 review issues from the second pass:
1. Time footer (clock + value) is now a clickable button that switches
to the time step. Styled with accent color to match the reference.
2. Weekend day cells (Sat/Sun) coloured red via #day slot.
3. Two-tone highlight on time grid: solid accent for selected,
`data-now` light fill for the real-world current hour/minute.
4. Time-step header now shows date only (was date + time).
6. Preset card titles use `--b24ui-typography-label-color` instead of
default inherited text → matches the rest of the design system.
7. Time-step header colour matches the calendar heading
(`--b24ui-typography-legend-color`).
8. Mobile (`screen.isMobile`) renders the picker in a `B24Drawer`
(bottom sheet) instead of `B24Popover`. Same content tree, different
wrapper picked via `<component :is>`.
9. `Minute step` doc example placeholder fixed (covered by trigger fix).
10. `Locale` doc example switched to Hindi (`hi`).
11. **Dead-zone fix on the trigger**: clicks on padding around the input
now open the picker. Trigger is a `role=button` div wrapping the
`B24Input` (`pointer-events-none`, `tabindex=-1`, `aria-hidden`).
`$attrs` move to the wrapper so consumer-passed `aria-label`/`id`/etc
land where ARIA expects them. Adds `dateTimePicker.openPicker`
locale string as fallback `aria-label` (filled across all 20 locales).
Test snapshots regenerated; 42 tests still green.
Two axe rules (`nested-interactive`, `label`) are disabled in the a11y
test for the trigger composition — the inner input is `tabindex=-1`,
`aria-hidden`, and `pointer-events-none`, so it is not user-reachable.
|
Housekeeping note, no action requested on this PR. The follow-up issues listed in the description have been closed as not planned: #56, #57, #58, #59 (#60 was already closed back in June). They all described work inside For the record, measured today rather than recalled:
Two findings that would otherwise have been lost with the issues:
This PR stays open and the branch is untouched. If Generated by Claude Code |
|
Closing this branch, not the idea. Why it cannot be revived.
Re-applying 1935 lines and then rewriting the half that trips those rules costs more than writing the component fresh against them — and produces a worse result, because the seams stay. What is being kept from this PR. The design is sound and is the specification for the rebuild: the two-step popover (calendar → hour/minute grid), the side preset column with What is not being carried over as-is. The 386 lines of snapshots and the 19 locale files will be regenerated and rewritten rather than copied — a snapshot recorded against May's The follow-ups this PR filed — #56, #57, #58, #59, #60 — stay open and apply to the rebuild. The branch is left as it is, and remains readable. Generated by Claude Code |
Written against today's `main` rather than ported from the branch behind #55, which shares no ancestor with it. The design is that PR's: a calendar that hands over to an hour/minute grid, a preset column beside it, `dateOnly` to drop the time step, a `B24Drawer` on small screens and a `B24Popover` elsewhere, and the `default` / `presets` / `preset` / `time-header` slots. Three things are different because the tree is. **No `as unknown as DateValue` anywhere.** The old branch carried those casts throughout, and #56 was filed to clean them up. They were not needed: `ref()` deep-unwraps, which widens the `DateValue` union of class instances into a structural type that no longer matches, and `shallowRef` does not. Date values are immutable, so nothing is lost. That one change took the file from six type errors to one. The last of them is `B24Calendar` typing its emit for every mode it supports — the handler takes the wide type and narrows, rather than casting. **The preset column caps its height with `--max-height-popup-menu`**, the token the menus use, instead of a literal `20rem`. `.sync/PORTING.md` records that those caps are tokens; the side effect is that `test/utils/theme-css-compiles.spec.ts` now covers this class automatically, since it collects exactly that shape. **One fewer string to translate.** The old design had a `pickTime` label for the footer when no time was chosen. The footer shows a time either way, so it falls back to `00:00` and the key is gone — 20 locale files lighter. Every utility the theme uses was compiled through the installed engine before being written down, and every `--ui-*` / `--b24ui-*` variable it references was checked to still exist. The locale strings come from the branch behind #55. Thirteen of them are machine-translated and still want a native reader — that is #60, which stays open and applies here unchanged. Icons are direct imports, not dictionary roles: the dictionary has no calendar or clock key, and `test/utils/icon-claims.spec.ts` forbids documenting one that is not reachable. The JSDoc names the components instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
Eleven render cases and six behaviour tests. The behaviour half is the point: the minute grid is asserted at the default step, at a divisor of 60, above the cap, below the floor and at `NaN`; the selected hour and minute are asserted to be the *only* cells marked pressed; a factory preset is asserted to resolve exactly once per render; and picking a preset is asserted to keep the time in the normal case and to drop it under `dateOnly`. Writing them turned up three defects, all in code written earlier today. **The component emitted `update:open` but had no `open` prop**, so `v-model:open` could not work — a consumer could hear the picker open and never tell it to. `open` and `defaultOpen` are now props, which is also what lets the spec assert the popover content rather than reach into the instance. **Two accessibility defects, both found by `axe` rather than by reading.** The trigger was a `div` that the popover decorates with `aria-haspopup` and `aria-expanded`; `aria-allowed-attr` rejects those on an element with no role. Adding `role="button"` moved the failure rather than fixing it — the wrapper contained a real `<input>`, and `nested-interactive` rejects a control inside a control. The wrapper is gone: `B24Input` forwards fall-through attributes onto its `<input>`, so the readonly input *is* the trigger and the popover's ARIA lands on a real control. The `triggerInput` theme slot went with it rather than staying as a promise nothing keeps. That last one is inherited from the design in #55, where the same wrapper stood. It shipped no tests for the trigger, so nothing said. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
The comment claimed the deep-unwrapping problem is "what forces `as unknown as DateValue` casts elsewhere (#56)". Neither half held: no such cast exists anywhere in `src/` today, and #56 was closed as not planned when PR #55 was abandoned, so the reference points at a decision not to act. Replaced with the measurement: swapping `shallowRef` for `ref` and running `pnpm typecheck` yields seven TS2322/TS2345 errors in this file, all of the same widened-union shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
What this PR is about
A new
B24DateTimePickercomponent — a Bitrix24-style date + time picker with a two-step popover (calendar → hour/minute grid), a side preset column, and an optionaldate-onlymode (time forced to00:00:00). It fills the gap between the segmentedB24InputDate/B24InputTimeand the more "native" Bitrix24 UX.What was done
src/runtime/components/DateTimePicker.vuecomposed of built-inB24Popover,B24Calendarand a read-onlyB24Input(default trigger). Model —CalendarDateTime/ZonedDateTime(orCalendarDateindate-onlymode).src/theme/date-time-picker.ts— minimum styling, design-system classes only.dateTimePicker.*added to theMessagestype and filled in for all 19 locales.endOfWeekfor DE/FR/IT/BR/TR was fixed to actually mean "end of week" (the machine translation produced "weekend" instead).role="group"+aria-labelledbyon the hours/minutes grids,aria-pressedon each cell, unique id viauseId().Number.isFiniteguard onminute-stepto protect againstNaN.test/components/DateTimePicker.spec.ts+ 2 snapshot files: 21 cases × (nuxt + vue) = 42 green tests, including an axe a11y check.docs/content/docs/2.components/date-time-picker.md+ 6 examples (Basic,DateOnly,CustomPresets,CustomTrigger,FormField,MinMaxDates). Responsive: presets drop below the calendar on mobile (flex-col-reverse sm:flex-row).date-time-picker.vuematrix page + registration inuseNavigation.ts.b24-ui-nuxt— updatedcomponents.md,component-selection.mdand the "Date" section informs.md.theme/index.ts, key inThemeDefaults.API
Key props:
modelValue,defaultValue,dateOnly,minuteStep(5),locale,placeholder,presets,hidePresets,format,color,size,disabled,icon,timeIcon,popover,calendar,input.Slots:
default(trigger),presets,preset,time-header.Manual QA checklist
pnpm dev→/components/date-time-picker: popover opens, picking a date switches to the time step, clicking a minute closes the popoverDate only— time step disappears, model becomesCalendarDateminute-step={5|10|15|30}— correct cell count;NaN/0/9999don't break rendering (covered by tests)presets(factory function) —DateTimePickerCustomPresetsExample, factory runs once per render#default(DateTimePickerCustomTriggerExample) —openreacts; defaultB24Inputtriggerlocale="ru"/locale="en"— value format, weekday names, preset labels all changecolor,size,disabled— walk through the Matrix in the playgroundhide-presets— side column disappearsB24FormField(DateTimePickerFormFieldExample) —name/required/label/hintwork:calendar="{ minValue, maxValue }"(DateTimePickerMinMaxDatesExample) — bounds respectedaria-pressedstate on cellspnpm test— neighboring components' snapshots are not affectedWhat's next
Follow-up issues have been filed (from review):
as anyкасты вокруг @internationalized/date #56 — clean upas anycasts around@internationalized/date(also affects Calendar/InputDate)min/max/isDateDisabledpropsv-modeltodefineModel(batched across components)Nothing urgent after merge — the component is self-contained and introduces no breaking changes to the public API.
Ready for squash-merge. Green locally:
pnpm lint,pnpm typecheck,pnpm exec vitest test/components/DateTimePicker.spec.ts(42/42).🤖 Generated with Claude Code