Skip to content

feat(DateTimePicker): new component with two-step popover and presets - #55

Closed
IgorShevchik wants to merge 6 commits into
mainfrom
claude/date-time-picker-component-NqwmO
Closed

IgorShevchik wants to merge 6 commits into
mainfrom
claude/date-time-picker-component-NqwmO

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented May 27, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR is about

A new B24DateTimePicker component — a Bitrix24-style date + time picker with a two-step popover (calendar → hour/minute grid), a side preset column, and an optional date-only mode (time forced to 00:00:00). It fills the gap between the segmented B24InputDate / B24InputTime and the more "native" Bitrix24 UX.

What was done

  • Component src/runtime/components/DateTimePicker.vue composed of built-in B24Popover, B24Calendar and a read-only B24Input (default trigger). Model — CalendarDateTime / ZonedDateTime (or CalendarDate in date-only mode).
  • Theme src/theme/date-time-picker.ts — minimum styling, design-system classes only.
  • Localization — dateTimePicker.* added to the Messages type and filled in for all 19 locales. endOfWeek for DE/FR/IT/BR/TR was fixed to actually mean "end of week" (the machine translation produced "weekend" instead).
  • Accessibility — role="group" + aria-labelledby on the hours/minutes grids, aria-pressed on each cell, unique id via useId().
  • Correctness — preset resolution is memoized (factory functions now run once per render instead of twice), Number.isFinite guard on minute-step to protect against NaN.
  • Tests — test/components/DateTimePicker.spec.ts + 2 snapshot files: 21 cases × (nuxt + vue) = 42 green tests, including an axe a11y check.
  • Documentation — 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).
  • Playgrounds (nuxt + demo, in sync) — date-time-picker.vue matrix page + registration in useNavigation.ts.
  • Skill b24-ui-nuxt — updated components.md, component-selection.md and the "Date" section in forms.md.
  • Registration — type exports, theme entry in theme/index.ts, key in ThemeDefaults.
  • Multi-reviewer feedback addressed (docs / engineering / tests / security / CTO).

API

<B24DateTimePicker
  v-model="value"
  :minute-step="5"
  :presets="customPresets"
  date-only
  hide-presets
  locale="en"
  placeholder="Pick a date and time"
  :calendar="{ minValue, maxValue, isDateDisabled }"
/>

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 popover
  • Toggling Date only — time step disappears, model becomes CalendarDate
  • minute-step={5|10|15|30} — correct cell count; NaN/0/9999 don't break rendering (covered by tests)
  • Default presets "Today" / "Tomorrow" / "End of week" / "In a week" / "End of month" set the correct date; active one is highlighted
  • Custom presets (factory function) — DateTimePickerCustomPresetsExample, factory runs once per render
  • Custom trigger via #default (DateTimePickerCustomTriggerExample) — open reacts; default B24Input trigger
  • locale="ru" / locale="en" — value format, weekday names, preset labels all change
  • Responsive: on narrow viewport presets drop below and scroll horizontally
  • color, size, disabled — walk through the Matrix in the playground
  • hide-presets — side column disappears
  • B24FormField (DateTimePickerFormFieldExample) — name/required/label/hint work
  • :calendar="{ minValue, maxValue }" (DateTimePickerMinMaxDatesExample) — bounds respected
  • A11y: screen reader announces "group: hours / group: minutes" and the aria-pressed state on cells
  • Manual translation review for AR/IN/TH/KZ/JA/SC/TC/VN/ID/MS/LA/PL/UA — machine-generated (tracked in i18n(DateTimePicker): native-speaker review of machine-translated presets #60)
  • pnpm test — neighboring components' snapshots are not affected

What's next

Follow-up issues have been filed (from review):

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

claude added 4 commits May 7, 2026 19:02
- 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
… 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`)
claude added 2 commits May 28, 2026 11:05
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.
@IgorShevchik
IgorShevchik marked this pull request as draft August 24, 2026 10:49

Copy link
Copy Markdown
Collaborator Author

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 src/runtime/components/DateTimePicker.vue, which exists only on this branch — so none of them could be opened against main, and they were sitting in the backlog looking actionable when they were not.

For the record, measured today rather than recalled:

  • git merge-base --is-ancestor 31d331bf origin/main → not in main
  • git rev-list --count ae14a5c3..origin/main → 129 commits behind
  • mergeable_state: "dirty" — the branch already conflicts with main
  • last commit carrying code: 05f1865e, 28 May 2026

Two findings that would otherwise have been lost with the issues:

This PR stays open and the branch is untouched. If B24DateTimePicker is picked up again, the sane path is a fresh branch off current main using this one as the reference — rebasing 129 commits would also mean absorbing the test-harness changes that landed since (attached mounts, enableAutoUnmount, the console-gate register), which is more work than rewriting the component against what the library looks like now.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Closing this branch, not the idea. B24DateTimePicker is being rebuilt on current main, with this PR as the reference.

Why it cannot be revived. main was re-rooted on 2026-07-10, and this branch predates that: git merge-base origin/main <head> returns nothing, so there is no ancestor to rebase onto or merge from. The same was true of #27–#35; those were small enough to re-apply by hand. This one is 43 files and 1935 insertions, and re-applying it would land May code in a tree that has moved a long way since — several of the rules it would now have to satisfy did not exist when it was written:

  • test/utils/console-gate.ts fails any test that renders while Vue logs a warning, and its debt register may only shrink. A 21-case spec written before that gate has to be re-verified case by case, not carried over.
  • test/utils/indistinguishable-snapshots.spec.ts rejects a snapshot byte-identical to a sibling. The two 193-line snapshot files here were recorded without it.
  • src/theme/card.ts now expresses its size scale in --spacing-* utilities rather than raw pixels, and the theme in this branch predates that convention.
  • Components that embed a B24Avatar now bind :color="props.avatar?.color ?? props.color" after the v-bind, which is a rule this branch cannot know about.

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 label/hint/value where value may be a factory, dateOnly forcing 00:00:00, minuteStep with a guard, B24Drawer on mobile against B24Popover on desktop, the slot surface (default trigger, presets, preset, time-header), and the dateTimePicker.* locale keys. The decision to memoise preset resolution so factories run once per render is kept too, and so is the observation that endOfWeek in the DE/FR/IT/BR/TR locales had been machine-translated as "weekend".

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 B24Calendar and B24Popover describes markup that no longer exists, and carrying one forward would assert nothing while looking like coverage.

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

IgorShevchik pushed a commit that referenced this pull request Sep 14, 2026
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
IgorShevchik pushed a commit that referenced this pull request Sep 14, 2026
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
IgorShevchik pushed a commit that referenced this pull request Sep 14, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants