feat(selector): native-like month/year selector mode with keyboard UX polish#1
Conversation
…ector) Add the spec-kit artifact set for feature 001-feat-native-scroll-month-year-selector: - spec.md, plan.md, research.md, data-model.md - contracts/selector-mode-contract.md - quickstart.md and tasks.md Include approved clarifications, native reference asset links, analysis-driven updates, and task progress with T001-T006 checked. This commit intentionally includes only specs/ so source implementation in src/ can be reviewed and committed separately.
…le year scroll modes and stability controls Implement the selector-mode UX end-to-end with native-like calendar<->wheel toggling, click-to-center wheel behavior, smooth month/year transitions, and opt-in rendering/styling controls for integrators. This commit also updates spec artifacts to match the delivered behavior and validation matrix. Problem: - Selector mode required several interaction and visual refinements to feel native-like in real usage: header affordance, wheel centering, smooth transitions, panel context correctness, and stable container geometry. - Integrators needed styling and behavior options (focus tint and year-scroll strategy) without forking component logic. - Existing specs/tasks needed to reflect the expanded implemented UX contract. Approach: - Rework month/year selector interactions around wheel/list centering semantics and calendar<->selector toggle flow. - Add configurable selector options (`selectorFocusTint`, `selectorYearScrollMode`) with conservative defaults. - Stabilize selector container dimensions and header behavior, then document all delivered semantics in Spec-Kit artifacts. Changes: - Add selector-mode header as a single combined month+year toggle with explicit affordance; hide side month arrows in selector mode. - Implement click-to-center behavior for month and year wheels with smooth centering motion. - Implement wheel-like continuous month scrolling and robust month/year synchronization in selector mode. - Add `selectorFocusTint` prop to enable/disable active column tint styling. - Add `selectorYearScrollMode` prop with `boundary` (default clarity-first) and `fractional` (continuous drift) variants. - Align month/year selector styling and centering behavior for visual consistency. - Add selector container width/height stabilization for single-panel selector flows to reduce toggle jitter. - Improve year fast-scroll UX with tuned virtualization window/reanchor settings and lightweight active-scroll styling safeguards. - Expand demo coverage in `src/App.vue`, including boundary vs fractional year-scroll variants. - Update `README.md` selector-mode documentation and options usage. - Update spec artifacts (`spec.md`, `plan.md`, `research.md`, `contracts/selector-mode-contract.md`, `quickstart.md`, `tasks.md`) to capture header styling, focus tint controls, stability requirements, click-to-center semantics, smooth scrolling behavior, and year-scroll mode variants. Tests: - `npm run typecheck` passed. - `npm run build` passed. Behavioral effect: - Selector mode now behaves like a native wheel-based month/year selector while remaining opt-in and backward compatible by default. - Consumers can choose clarity-focused or continuous year-wheel behavior and can disable focus tint styling to fit custom design systems.
…ar rendering and themeable styling Deliver a native-picker-inspired selector flow for month/year selection, including a canvas-based year wheel, continuous month wheel behavior, and extensive style tokens for visual parity and customization across light and dark themes. Core selector interaction changes: - Keep selector mode as a calendar/selector toggle flow driven from header interactions. - Improve year wheel edge handling and re-anchor behavior to reduce hard-stop/stall cases. - Add in-canvas edge loading indicators with animated clock icon while waiting to re-anchor. - Ensure click selection scrolls smoothly to center and preserves stable wheel behavior. Year wheel rendering and fidelity: - Replace DOM year wheel selector mode rendering with a canvas-rendered wheel. - Add configurable canvas DPR and border-width scaling to improve sharpness and match DOM visuals. - Use metric-based text centering via actual bounding box ascent/descent to correct optical drift. - Add configurable vertical text offset token for fine-grained visual alignment. - Apply pixel-aligned stroke rendering to reduce antialias bloom differences versus DOM borders. Month wheel and visual parity: - Keep month wheel in DOM with continuous scrolling, rebase behavior, and selector sync. - Align month and year wheel cell geometry and styling via shared/parallel CSS token model. - Remove unintended white/native press styling artifacts with explicit selector button resets. - Normalize wheel row height and border radius for month/year parity. Theme and customization tokens: - Add tokenized control over selector hover/selected states for month and year independently. - Add tokenized typography controls (font family, size, weight, line-height where applicable). - Add tokenized text colors for light/dark parity and per-wheel overrides. - Add separate month and year border width tokens and keep year canvas width mapping explicit. - Add light/dark-specific hover defaults so dark hover remains visible and light hover is not overly dark. Demo app improvements: - Add an in-app dark theme toggle in App.vue. - Enable class-based dark variant behavior so the local toggle applies to picker dark styles. - Add bottom page padding in App.vue to make selector overlays usable near lower demo sections. Stability fixes around selector updates: - Ensure same-year selector events can still trigger year window re-anchor when needed. - Avoid out-of-range year emissions during edge states. - Improve scroll/lock behavior at edges for thumb and momentum interactions. Validation: - npm run typecheck - npm run build
…on and stable range preview behavior Complete the selector-mode UX pass with keyboard parity, range-preview transition refinements, and token-aligned wheel geometry so the picker behaves more like a native control while remaining customizable and testable. Problem: - Selector interactions still had rough edges across keyboard focus flow, range-preview transitions, and year-wheel geometry when theming tokens were customized. - Repeated regressions were hard to guard because selector behavior lacked a dedicated unit-test harness. Approach: - Consolidate selector keyboard/focus behavior around explicit wheel and panel focus contracts. - Separate range body/edge preview rendering to reduce visual snapping artifacts during range movement. - Make year-wheel interaction math consume the same wheel-height token used for visual styling. - Add focused unit coverage and documented testing follow-ups for browser-level interaction QA. Changes: - Add and refine selector mode keyboard and focus behavior across picker, month wheel, and year wheel, including smoother month keyboard scrolling and panel-level focus cycling. - Improve range-preview rendering in calendar mode by splitting body and edge layers and keeping endpoint styling consistent while reducing transition artifacts. - Expand selector/calendar styling tokens and class semantics in `src/index.css` and component classes to support clearer theming and focus styling. - Add `closeOnRangeSelection` control and related selection-flow updates for range workflows. - Fix year-wheel lifecycle and geometry stability: disconnect `ResizeObserver` when selector mode closes, and derive scroll/pointer/canvas math from `--vtd-selector-wheel-cell-height` with robust CSS variable reads. - Introduce unit test infrastructure (Vitest + Vue Test Utils + jsdom) and selector-focused tests for keyboard navigation, focus tint behavior, year scroll mode behavior, and custom wheel-height hit-testing. - Add `tests/TODO.md` to track future browser-level interaction coverage (Playwright/Cypress/Puppeteer). Tests: - npm run typecheck - npm run test:unit - npm run build Behavioral effect: - Selector mode now provides more predictable keyboard navigation, smoother and clearer range-preview transitions, and consistent year-wheel targeting under custom theming, with automated tests covering the critical selector paths.
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in “selector mode” to vue-tailwind-datepicker, enabling native-like month/year wheel selection with improved keyboard navigation, smoother range-preview rendering, and a theming/token surface for selector styling.
Changes:
- Introduces selector-mode state + focus-cycle logic in
VueTailwindDatePicker.vue, plus updated header interactions to toggle calendar ↔ selector view. - Replaces month/year panels with wheel-style selectors (month DOM wheel, year canvas wheel) and adds CSS tokens for theming + dark-mode stability.
- Adds Vitest unit test harness and new selector-focused unit tests; updates docs/spec artifacts for traceability.
Reviewed changes
Copilot reviewed 25 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Adds Vitest + Vue plugin config (jsdom, setup, test include). |
| tests/setup.ts | Adds jsdom polyfills/mocks for RAF, scrollTo, canvas context, and DOM cleanup. |
| tests/unit/selector-wheel-keyboard.spec.ts | Unit tests for selector keyboard navigation and Tab focus cycling. |
| tests/unit/selector-focus-tint.spec.ts | Unit tests verifying selectorFocusTint styling behavior. |
| tests/unit/selector-year-scroll-mode.spec.ts | Unit tests validating boundary vs fractional year wheel sync + click hit-testing with custom cell height. |
| tests/TODO.md | Tracks future E2E/browser-level coverage needs. |
| src/index.css | Adds dark variant + selector/range-preview CSS variables and component-layer styles for selector/range preview. |
| src/components/Year.vue | Implements selector-mode year wheel with canvas rendering, scroll sync modes, and keyboard/pointer handling. |
| src/components/Month.vue | Implements selector-mode month wheel with scroll/keyboard handling and tokenized styling. |
| src/components/Header.vue | Adds selector-mode header toggle behavior and per-panel focus heuristics. |
| src/components/Calendar.vue | Refines range preview rendering + introduces keyboard focus targeting and arrow-key navigation. |
| src/VueTailwindDatePicker.vue | Adds selector-mode props/state, focus management, view toggling, range-close behavior control, and selector wiring. |
| src/App.vue | Expands demo scenarios to exercise selector-mode configs, disabled dates, and invalid/empty model seeds. |
| specs/constitution.md | Adds repository “constitution” document for spec-driven workflow expectations. |
| specs/001-feat-native-scroll-month-year-selector/spec.md | Adds feature spec with requirements, stories, and success criteria. |
| specs/001-feat-native-scroll-month-year-selector/plan.md | Adds implementation plan and verification strategy for selector mode. |
| specs/001-feat-native-scroll-month-year-selector/tasks.md | Adds task breakdown with requirement mapping and checkpoints. |
| specs/001-feat-native-scroll-month-year-selector/research.md | Adds research notes and design decisions for selector mode. |
| specs/001-feat-native-scroll-month-year-selector/quickstart.md | Adds manual verification quickstart + QA evidence table. |
| specs/001-feat-native-scroll-month-year-selector/data-model.md | Documents selector-mode state entities and invariants. |
| specs/001-feat-native-scroll-month-year-selector/contracts/selector-mode-contract.md | Documents selector-mode public contract and semantics. |
| specs/001-feat-native-scroll-month-year-selector/references/native-selector-reference.png | Adds reference image asset for selector UX. |
| package.json | Adds unit test script and test dependencies (vitest, jsdom, @vue/test-utils). |
| docs/theming-options.md | Documents selector wheel CSS token surface for theming. |
| docs/props.md | Documents selector-mode props and usage examples. |
| README.md | Adds end-user documentation section for selector mode and related options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Resolve latest PR review threads by removing implicit contiguous-year assumptions in Year selector logic and by making the shared test visibility mock closer to browser behavior. What changed: - Updated centered-year derivation to read from `props.years[index]` directly. - Updated canvas year row rendering to read each visible row value from `props.years[i]` with a guard. - Updated `HTMLElement#getClientRects` test mock to return an empty list for hidden elements (`display:none` / `visibility:hidden`) so focus/visibility tests do not pass incorrectly. Behavior impact: - No runtime behavior change for current contiguous year windows. - Improved future safety if non-contiguous year arrays are ever provided. - More realistic unit-test visibility behavior. Validation: - `npm run test:unit` - `npm run typecheck`
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 28 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… timers Address PR review feedback for selector wheel accessibility and test determinism. What changed: - Month selector row wrappers now use `--vtd-selector-wheel-cell-height` instead of fixed `h-11` so row geometry stays aligned with themed wheel cell heights. - Month selector listbox now sets `aria-activedescendant` to the active month option id. - Year selector (canvas-backed) switched from `role=listbox` to `role=spinbutton` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, and `aria-valuetext`. - Replaced fixed 320ms sleeps in selector unit tests with fake timers (`vi.useFakeTimers` + `vi.advanceTimersByTime`) and reset to real timers after each test. Notes: - Kept the in-range day non-rounded class intentionally to preserve consistent range block geometry during active range transitions. Validation: - npm run test:unit - npm run typecheck
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 28 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ual-panel wheel behavior Restore quick month navigation while selector wheels are open and fix selector state coupling in range mode so each panel can be opened and controlled independently without cross-panel drift. This also smooths step-button interactions and keeps focus styling predictable during mixed scroll/keyboard usage. Problem: - Selector mode hid header side buttons, forcing users to open/scroll wheels even for simple month stepping. - In dual-panel range mode, opening the second selector reused shared selected month/year state and visually updated the first panel. - Repeated step interactions could rubber-band due to competing sync paths, and focus auto-centering could interrupt smooth year stepping. Approach: - Add explicit selector-mode month-step events from header controls and route them to the month wheel’s internal smooth stepping API. - Track selector panel visibility per panel in dual-panel range mode and bind each panel’s month/year wheel to its own context date. - Introduce reusable wheel step-button UI and tighten month/year wheel sync logic to avoid secondary scroll corrections during programmatic motion. Changes: - Re-add left/right header side buttons in selector view with subdued styling and DRY shared class/path/label helpers in `Header.vue`. - Add `step-month` header event handling in `VueTailwindDatePicker.vue`, including wheel-ref dispatch via exposed `Month.stepBy()` and safe fallback month stepping. - Add per-panel selector-open state (`previous`/`next`) for dual-panel range mode so both selectors can remain open simultaneously. - Bind selector wheel selected month/year per panel (`previous` vs `next`) instead of a shared selection binding to prevent cross-panel updates. - Add reusable `SelectorWheelStepButton.vue` and use it in both month and year selector wheels for consistent tap targets/icons. - Improve month wheel synchronization: smooth external month updates, year-only tuple shift without recentering, and suppression of duplicate scroll-driven corrections. - Improve year wheel step-button behavior by suppressing focus auto-center immediately after step clicks to preserve smooth repeated stepping. - Keep selector focus semantics stable by not forcibly switching focus source on scroll update callbacks. Tests: - `npm run typecheck` - `npm run test:unit` - Add `tests/unit/header-selector-calendar-nav.spec.ts` for selector-mode header side-button visibility/behavior. - Expand selector wheel tests for month/year step buttons, smooth header-step behavior, year-only month-row stability, and dual-panel selector independence. - Add focus-tint regression coverage for month scroll updates while year remains focused. - Add year scroll mode regression coverage for repeated step-button smooth scrolling. Behavioral effect: - Selector mode now keeps quick header month navigation available, wheel controls are larger and consistent, repeated step interactions stay smooth, and dual-panel range selectors can be opened together without one panel mutating the other.
…dual-panel behavior Update the 001 Spec-Kit documents to match the implemented selector UX so requirements, contract language, and verification notes are consistent with current behavior. Changes: - Update `spec.md` acceptance criteria and requirements to cover selector-mode header quick-nav arrows, wheel step controls, and dual-panel simultaneous selector behavior. - Extend `contracts/selector-mode-contract.md` with explicit behavior contracts for header quick month navigation and month/year wheel step controls. - Refresh `research.md` decisions to reflect keeping subdued selector-mode side arrows and supporting both selector panels open in double-panel range mode. - Clarify `data-model.md` with `SelectorPanelState` and per-panel selected month/year ownership in dual-panel selector mode. - Update `plan.md` summary/technical context/implementation outline to include selector quick-nav and dual-panel selector independence. - Expand `quickstart.md` verification checklist and QA evidence with selector header quick-nav, wheel controls, and dual-panel independence validation. - Add completed follow-up tasks (`T039`-`T042`) in `tasks.md` and revise stale side-arrow wording.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 30 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…istic range preview styling This patch updates remaining legacy Tailwind opacity utilities to Tailwind 4-compatible slash opacity syntax so styles are emitted reliably by the compiler. What changed: - Replaced `bg-opacity-*`, `ring-opacity-*`, and `text-opacity-*` usage with slash syntax equivalents in calendar, header, and input/day-state classes. - Updated calendar range preview fill classes to use `bg-vtd-primary-100/60` and `dark:bg-vtd-secondary-700/50` directly on preview layers. - Removed CSS variable background overrides for range preview in `src/index.css` so preview coloring follows class-based Tailwind output consistently. - Added/kept a focused regression test to verify `today` remains visually distinct when it is inside an active range but not an endpoint. Why: - In Tailwind 4, legacy opacity utilities can become no-ops depending on build output. Migrating to slash syntax prevents silent style drift. - Class-driven range preview styling avoids mismatch caused by mixed variable/class color paths. Validation: - `npm run typecheck` - `npm run test:unit` - `npm run build`
…ge preview theme tokens This commit expands selector keyboard controls, improves calendar keyboard activation, and exposes range preview styling via CSS variables so consumers can tune opacity/colors without writing custom selectors. What changed: - Added year selector keyboard jump props at the public picker API level: - `selectorYearHomeJump` (default `100`) - `selectorYearEndJump` (default `100`) - `selectorYearPageJump` (default `10`) - `selectorYearPageShiftJump` (default `100`) - Wired those props through to `Year.vue` and applied them to Home/End/PageUp/PageDown key handling (including Shift variants). - Added Enter/Space/Spacebar keyboard activation in `Calendar.vue` so focused day buttons can be selected without mouse interaction. - Made range close behavior explicit with `closeOnRangeSelection` and documented that it is a no-op in `no-input` static mode (no popover callback exists to close). - Added calendar range preview theming tokens: - `--vtd-calendar-range-preview-bg` - `--vtd-calendar-range-preview-bg-dark` while preserving edge-cap tokens and default visual output. Documentation updates: - README selector options now include keyboard jump props and close-on-range-selection note. - docs/props includes sections for close-on-range-selection and selector year keyboard jumps. - docs/theming-options documents calendar range preview tokens with examples. Test coverage: - Added `tests/unit/calendar-keyboard-activation.spec.ts` for Enter/Space date activation. - Extended `tests/unit/selector-wheel-keyboard.spec.ts` for configurable Home/End and PageUp/PageDown jump behavior. Validation: - `npm run test:unit` (22 tests passing) - `npm run typecheck` - `npm run build`
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 32 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…window anchoring
This commit applies the final PR review follow-ups while preserving the intended
selector integration behavior in host applications.
What changed:
- Added defensive explicit return in calendar key activation path after handling
Enter/Space/Spacebar date selection.
- Added synchronous selector year-window inclusion guard in picker-level
`onSelectorYearUpdate` so out-of-window year jumps are anchored before applying
the year update (`ensureSelectorYearInWindow(year)`).
- Updated docs guidance for selector mode range UX:
- recommend `close-on-range-selection=false` when consumers want a native-like
keep-open flow in selector mode.
- Kept Month/Year key handlers on `preventDefault` without blanket
`stopPropagation`, to avoid suppressing parent-level host shortcuts/forms
unless a real integration conflict exists.
- Aligned selector keyboard unit tests with final behavior.
Validation:
- npm run test:unit -- tests/unit/selector-wheel-keyboard.spec.ts tests/unit/calendar-keyboard-activation.spec.ts
- npm run typecheck
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 32 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Update the demo locale control in App.vue from a native select to inline buttons with locale flags (EN/ES/DE) for clearer visual selection. Follow-up adjustments: - remove the globe icon so each option displays only flag + locale code - keep active/inactive button states and keyboard-click semantics Validation: - npm run typecheck
Summary
This PR delivers an opt-in native-like month/year selector mode and follows through on UX hardening needed for production usage: smoother wheel interactions, selector-mode quick navigation, robust dual-panel range behavior, keyboard/focus predictability, and stronger automated coverage.
Release-notes level highlights:
calendar <-> selector) for month/year selection.selector-year-home-jump/selector-year-end-jump(default100)selector-year-page-jump(default10)selector-year-page-shift-jump(default100)Specification and Traceability
specs/001-feat-native-scroll-month-year-selector/spec.mdspecs/001-feat-native-scroll-month-year-selector/plan.mdspecs/001-feat-native-scroll-month-year-selector/contracts/selector-mode-contract.mdspecs/001-feat-native-scroll-month-year-selector/tasks.mdspecs/001-feat-native-scroll-month-year-selector/quickstart.mdDelivered scope maps to FR-001 through FR-021, including:
Engineering Decisions
1) Month wheel is DOM-based; year wheel is canvas-based
2) Header interaction model
3) Dual-panel range selector state model
previous/next) so both range selectors can be open concurrently.4) Smoothness and anti-rubber-band behavior
Month.stepBy()and used it as the shared path for header quick-nav and wheel controls to keep one motion model.5) Focus and keyboard behavior
selector-year-home-jumpselector-year-end-jumpselector-year-page-jumpselector-year-page-shift-jump6) Accessibility semantics
aria-valuenow,aria-valuetext, bounds) for better assistive-technology representation of continuous year stepping.7) Theming and customization
8) Testing strategy decisions
9) Tailwind 4 opacity migration and style parity
bg-opacity-*,ring-opacity-*,text-opacity-*) with Tailwind 4 slash-opacity syntax so compiled CSS always reflects intended opacity.--vtd-calendar-range-preview-bg--vtd-calendar-range-preview-bg-darkKey Files
src/VueTailwindDatePicker.vuesrc/components/Header.vuesrc/components/Month.vuesrc/components/Year.vuesrc/components/SelectorWheelStepButton.vuesrc/components/Calendar.vuesrc/index.csstests/unit/calendar-keyboard-activation.spec.tstests/unit/calendar-today-range-style.spec.tstests/unit/header-selector-calendar-nav.spec.tstests/unit/selector-wheel-keyboard.spec.tstests/unit/selector-focus-tint.spec.tstests/unit/selector-year-scroll-mode.spec.tstests/setup.tsvitest.config.tsspecs/001-feat-native-scroll-month-year-selector/*Validation
npm run typechecknpm run test:unitnpm run buildBuild note:
5.8.2vs project TS5.9.3remains informational.