Skip to content

Commit 0b9b980

Browse files
committed
refactor(date-picker): add data attr to month/year cells
1 parent d636365 commit 0b9b980

File tree

4 files changed

+98
-48
lines changed

4 files changed

+98
-48
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@zag-js/date-picker": minor
3+
---
4+
5+
Add missing range data attributes to month and year cell triggers for range picker mode.
6+
7+
- `data-range-start`, `data-range-end`, `data-in-hover-range`, `data-hover-range-start`, `data-hover-range-end` now
8+
render on month and year cell triggers (previously only on day cells).
9+
10+
- `TableCellState` now includes `firstInRange`, `lastInRange`, `inHoveredRange`, `firstInHoveredRange`,
11+
`lastInHoveredRange`, and `outsideRange`.
12+
13+
- **Fixed:** Year cell `selectable` state was inverted, causing years outside the visible decade or min/max range to
14+
appear selectable.
15+
16+
- **Improved:** Range boundary dates now announce "Starting range from {date}" and "Range ending at {date}" for better
17+
screen reader context.
18+
19+
- **Changed:** `DayTableCellState.formattedDate` removed — use `valueText` instead (inherited from `TableCellState`).

packages/machines/date-picker/src/date-picker.connect.ts

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,30 @@ export function connect<T extends PropTypes>(
144144

145145
const decadeYears = getDecadeRange(startValue.year, { strict: true })
146146
const isOutsideVisibleRange = !decadeYears.includes(value)
147-
const isOutsideRange = isValueWithinRange(value, min?.year, max?.year)
147+
const isWithinMinMax = isValueWithinRange(value, min?.year, max?.year)
148+
149+
const isInSelectedRange = isRangePicker && isDateWithinRange(dateValue, selectedValue)
150+
const isFirstInSelectedRange = isRangePicker && selectedValue[0] && isEqualYear(dateValue, selectedValue[0])
151+
const isLastInSelectedRange = isRangePicker && selectedValue[1] && isEqualYear(dateValue, selectedValue[1])
152+
153+
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0
154+
const isInHoveredRange = hasHoveredRange && isDateWithinRange(dateValue, hoveredRangeValue)
155+
const isFirstInHoveredRange =
156+
hasHoveredRange && hoveredRangeValue[0] && isEqualYear(dateValue, hoveredRangeValue[0])
157+
const isLastInHoveredRange = hasHoveredRange && hoveredRangeValue[1] && isEqualYear(dateValue, hoveredRangeValue[1])
148158

149159
const cellState = {
150160
focused: focusedValue.year === props.value,
151-
selectable: isOutsideVisibleRange || isOutsideRange,
161+
selectable: !isOutsideVisibleRange && isWithinMinMax,
152162
outsideRange: isOutsideVisibleRange,
153163
selected: !!selectedValue.find((date) => date && date.year === value),
154164
valueText: value.toString(),
155-
inRange:
156-
isRangePicker &&
157-
(isDateWithinRange(dateValue, selectedValue) || isDateWithinRange(dateValue, hoveredRangeValue)),
165+
inRange: isInSelectedRange || isInHoveredRange,
166+
firstInRange: !!isFirstInSelectedRange,
167+
lastInRange: !!isLastInSelectedRange,
168+
inHoveredRange: !!isInHoveredRange,
169+
firstInHoveredRange: !!isFirstInHoveredRange,
170+
lastInHoveredRange: !!isLastInHoveredRange,
158171
value: dateValue,
159172
get disabled() {
160173
return disabled || !cellState.selectable
@@ -167,14 +180,30 @@ export function connect<T extends PropTypes>(
167180
const { value, disabled } = props
168181
const dateValue = focusedValue.set({ month: value })
169182
const formatter = getMonthFormatter(locale, timeZone, focusedValue)
183+
184+
const isInSelectedRange = isRangePicker && isDateWithinRange(dateValue, selectedValue)
185+
const isFirstInSelectedRange = isRangePicker && selectedValue[0] && isEqualMonth(dateValue, selectedValue[0])
186+
const isLastInSelectedRange = isRangePicker && selectedValue[1] && isEqualMonth(dateValue, selectedValue[1])
187+
188+
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0
189+
const isInHoveredRange = hasHoveredRange && isDateWithinRange(dateValue, hoveredRangeValue)
190+
const isFirstInHoveredRange =
191+
hasHoveredRange && hoveredRangeValue[0] && isEqualMonth(dateValue, hoveredRangeValue[0])
192+
const isLastInHoveredRange =
193+
hasHoveredRange && hoveredRangeValue[1] && isEqualMonth(dateValue, hoveredRangeValue[1])
194+
170195
const cellState = {
171196
focused: focusedValue.month === props.value,
172197
selectable: !isDateOutsideRange(dateValue, min, max),
173198
selected: !!selectedValue.find((date) => date && date.month === value && date.year === focusedValue.year),
174199
valueText: formatter.format(dateValue.toDate(timeZone)),
175-
inRange:
176-
isRangePicker &&
177-
(isDateWithinRange(dateValue, selectedValue) || isDateWithinRange(dateValue, hoveredRangeValue)),
200+
inRange: isInSelectedRange || isInHoveredRange,
201+
firstInRange: !!isFirstInSelectedRange,
202+
lastInRange: !!isLastInSelectedRange,
203+
inHoveredRange: !!isInHoveredRange,
204+
firstInHoveredRange: !!isFirstInHoveredRange,
205+
lastInHoveredRange: !!isLastInHoveredRange,
206+
outsideRange: false,
178207
value: dateValue,
179208
get disabled() {
180209
return disabled || !cellState.selectable
@@ -220,13 +249,11 @@ export function connect<T extends PropTypes>(
220249
outsideRange: isOutsideRange,
221250
today: isToday(value, timeZone),
222251
weekend: isWeekend(value, locale),
223-
formattedDate: formatter.format(value.toDate(timeZone)),
252+
value,
253+
valueText: formatter.format(value.toDate(timeZone)),
224254
get focused() {
225255
return isDateEqual(value, focusedValue) && (!cellState.outsideRange || outsideDaySelectable)
226256
},
227-
get ariaLabel(): string {
228-
return translations.dayCell(cellState)
229-
},
230257
get selectable() {
231258
return !cellState.disabled && !cellState.unavailable
232259
},
@@ -611,7 +638,7 @@ export function connect<T extends PropTypes>(
611638
role: "button",
612639
dir: prop("dir"),
613640
tabIndex: cellState.focused ? 0 : -1,
614-
"aria-label": cellState.ariaLabel,
641+
"aria-label": translations.dayCell(cellState),
615642
"aria-disabled": ariaAttr(!cellState.selectable),
616643
"aria-invalid": ariaAttr(cellState.invalid),
617644
"data-disabled": dataAttr(!cellState.selectable),
@@ -668,19 +695,24 @@ export function connect<T extends PropTypes>(
668695
const cellState = getMonthTableCellState(props)
669696
return normalize.element({
670697
...parts.tableCellTrigger.attrs,
671-
dir: prop("dir"),
672-
role: "button",
673698
id: dom.getCellTriggerId(scope, value.toString()),
674-
"data-selected": dataAttr(cellState.selected),
699+
role: "button",
700+
dir: prop("dir"),
701+
tabIndex: cellState.focused ? 0 : -1,
702+
"aria-label": cellState.valueText,
675703
"aria-disabled": ariaAttr(!cellState.selectable),
676704
"data-disabled": dataAttr(!cellState.selectable),
705+
"data-selected": dataAttr(cellState.selected),
706+
"data-value": value,
707+
"data-view": "month",
677708
"data-focus": dataAttr(cellState.focused),
678-
"data-in-range": dataAttr(cellState.inRange),
679709
"data-outside-range": dataAttr(cellState.outsideRange),
680-
"aria-label": cellState.valueText,
681-
"data-view": "month",
682-
"data-value": value,
683-
tabIndex: cellState.focused ? 0 : -1,
710+
"data-range-start": dataAttr(cellState.firstInRange),
711+
"data-range-end": dataAttr(cellState.lastInRange),
712+
"data-in-range": dataAttr(cellState.inRange),
713+
"data-in-hover-range": dataAttr(cellState.inHoveredRange),
714+
"data-hover-range-start": dataAttr(cellState.firstInHoveredRange),
715+
"data-hover-range-end": dataAttr(cellState.lastInHoveredRange),
684716
onClick(event) {
685717
if (event.defaultPrevented) return
686718
if (!cellState.selectable) return
@@ -708,7 +740,7 @@ export function connect<T extends PropTypes>(
708740
dir: prop("dir"),
709741
colSpan: columns,
710742
role: "gridcell",
711-
"aria-selected": ariaAttr(cellState.selected),
743+
"aria-selected": ariaAttr(cellState.selected || cellState.inRange),
712744
"data-selected": dataAttr(cellState.selected),
713745
"aria-disabled": ariaAttr(!cellState.selectable),
714746
"data-value": value,
@@ -720,19 +752,24 @@ export function connect<T extends PropTypes>(
720752
const cellState = getYearTableCellState(props)
721753
return normalize.element({
722754
...parts.tableCellTrigger.attrs,
723-
dir: prop("dir"),
724-
role: "button",
725755
id: dom.getCellTriggerId(scope, value.toString()),
726-
"data-selected": dataAttr(cellState.selected),
727-
"data-focus": dataAttr(cellState.focused),
728-
"data-in-range": dataAttr(cellState.inRange),
756+
role: "button",
757+
dir: prop("dir"),
758+
tabIndex: cellState.focused ? 0 : -1,
759+
"aria-label": cellState.valueText,
729760
"aria-disabled": ariaAttr(!cellState.selectable),
730761
"data-disabled": dataAttr(!cellState.selectable),
731-
"aria-label": cellState.valueText,
732-
"data-outside-range": dataAttr(cellState.outsideRange),
762+
"data-selected": dataAttr(cellState.selected),
733763
"data-value": value,
734764
"data-view": "year",
735-
tabIndex: cellState.focused ? 0 : -1,
765+
"data-focus": dataAttr(cellState.focused),
766+
"data-outside-range": dataAttr(cellState.outsideRange),
767+
"data-range-start": dataAttr(cellState.firstInRange),
768+
"data-range-end": dataAttr(cellState.lastInRange),
769+
"data-in-range": dataAttr(cellState.inRange),
770+
"data-in-hover-range": dataAttr(cellState.inHoveredRange),
771+
"data-hover-range-start": dataAttr(cellState.firstInHoveredRange),
772+
"data-hover-range-end": dataAttr(cellState.lastInHoveredRange),
736773
onClick(event) {
737774
if (event.defaultPrevented) return
738775
if (!cellState.selectable) return

packages/machines/date-picker/src/date-picker.types.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,13 @@ export interface TableCellState {
448448
selected: boolean
449449
valueText: string
450450
inRange: boolean
451+
firstInRange: boolean
452+
lastInRange: boolean
453+
inHoveredRange: boolean
454+
firstInHoveredRange: boolean
455+
lastInHoveredRange: boolean
451456
value: DateValue
452-
outsideRange?: boolean | undefined
457+
outsideRange: boolean
453458
readonly disabled: boolean
454459
}
455460

@@ -464,24 +469,11 @@ export interface WeekNumberCellProps {
464469
week: DateValue[]
465470
}
466471

467-
export interface DayTableCellState {
472+
export interface DayTableCellState extends TableCellState {
468473
invalid: boolean
469-
disabled: boolean
470-
selected: boolean
471474
unavailable: boolean
472-
outsideRange: boolean
473-
inRange: boolean
474-
firstInRange: boolean
475-
lastInRange: boolean
476475
today: boolean
477476
weekend: boolean
478-
formattedDate: string
479-
readonly focused: boolean
480-
readonly ariaLabel: string
481-
readonly selectable: boolean
482-
inHoveredRange: boolean
483-
firstInHoveredRange: boolean
484-
lastInHoveredRange: boolean
485477
}
486478

487479
export interface TableProps {

packages/machines/date-picker/src/date-picker.utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,11 @@ export function getLocaleSeparator(locale: string) {
7272

7373
export const defaultTranslations: IntlTranslations = {
7474
dayCell(state) {
75-
if (state.unavailable) return `Not available. ${state.formattedDate}`
76-
if (state.selected) return `Selected date. ${state.formattedDate}`
77-
return `Choose ${state.formattedDate}`
75+
if (state.unavailable) return `Not available. ${state.valueText}`
76+
if (state.firstInRange) return `Starting range from ${state.valueText}`
77+
if (state.lastInRange) return `Range ending at ${state.valueText}`
78+
if (state.selected) return `Selected date. ${state.valueText}`
79+
return `Choose ${state.valueText}`
7880
},
7981
trigger(open) {
8082
return open ? "Close calendar" : "Open calendar"

0 commit comments

Comments
 (0)