@cahyo-dimas/freeday 1.34.0 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/COMPONENTS.md +35 -8
- package/adapters/react/components/FdyDatepicker.tsx +135 -9
- package/adapters/vue/components/FdyDatepicker.vue +134 -9
- package/dist/freeday-datepicker.js +115 -10
- package/dist/freeday.bundle.css +31 -7
- package/dist/freeday.css +31 -7
- package/dist/freeday.js +115 -10
- package/package.json +1 -1
- package/src/components/datepicker.css +7 -7
- package/src/components/table.css +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
Semua perubahan penting dicatat di sini. Format longgar mengikuti
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
|
|
5
5
|
|
|
6
|
+
## [1.36.0] — 2026-08-19
|
|
7
|
+
One note from the back-office app (IDU_EMATE_APPL_WEB, #013).
|
|
8
|
+
### Added
|
|
9
|
+
- **A year grid in the calendar** (vanilla · Vue · React · Blazor). 1.32.0's month grid killed
|
|
10
|
+
one-click-per-month and left one-click-per-year behind it: the month grid's arrows step a year
|
|
11
|
+
each, so 2026 → 1998 was **28 clicks**. The title now drills twice — days → months → years — with
|
|
12
|
+
the arrows following the level shown, and picking a year drops to that year's months. Every level
|
|
13
|
+
above the day grid stays **navigation**: nothing is committed until a day is picked. Year pages
|
|
14
|
+
are aligned (2016–2027, 2028–2039) rather than centred on the year in view, so the pages tile
|
|
15
|
+
instead of sliding over each other. New classes `.fdy-cal__grid--years` / `.fdy-cal__year`; new
|
|
16
|
+
aria-label props `chooseYearLabel`, `prevYearsLabel`, `nextYearsLabel`.
|
|
17
|
+
### Changed
|
|
18
|
+
- **The month grid's title now drills UP to years rather than back down to days.** Getting back to
|
|
19
|
+
days is what picking a month already does. Nothing in the kit's own tests or in a consuming app's
|
|
20
|
+
helpers clicked the title from the month grid, so this is not expected to be breaking.
|
|
21
|
+
### Fixed
|
|
22
|
+
- **`freeday-datepicker.js` spoke Indonesian to the user.** Its placeholder and every nav
|
|
23
|
+
`aria-label` were Indonesian, and because `FdyDatepicker.razor` delegates to the vanilla
|
|
24
|
+
enhancer, **every Blazor app inherited them** — while #009's language guard, which scans
|
|
25
|
+
`adapters/` only, passed. The datepicker is English now. The rest of `src/` is NOT audited: see
|
|
26
|
+
improvement note #013 §2, which lists the enhancers still affected.
|
|
27
|
+
|
|
28
|
+
## [1.35.0] — 2026-08-19
|
|
29
|
+
One note from the back-office app (IDU_EMATE_APPL_WEB, #012).
|
|
30
|
+
### Added
|
|
31
|
+
- **`.fdy-table--sticky` + `.fdy-table-scroll--frozen`** — a table frozen on one axis or both, for
|
|
32
|
+
a grid read against two axes at once (a rate matrix, a timetable). This is a kit class rather
|
|
33
|
+
than four lines in an app because `position:sticky` on its own produces a **visibly broken**
|
|
34
|
+
table: `.fdy-table` collapses its borders, a collapsed border is painted by the table rather than
|
|
35
|
+
by either cell, and so a frozen cell scrolls out from under its own rule — the frozen column
|
|
36
|
+
arrives with nothing separating it from the data. The modifier separates the borders and puts
|
|
37
|
+
each one back on the cell that sticks. `<thead>` freezes at the top, `<th scope="row">` at the
|
|
38
|
+
left, the first header cell is the corner, and the row header paints the **body** surface so a
|
|
39
|
+
hovered row stays one band across the freeze line. `.fdy-table-scroll--frozen` is the scrollport
|
|
40
|
+
the freeze resolves against (the base `.fdy-table-scroll` scrolls one axis, so `top:0` never
|
|
41
|
+
engages); size it with `--fdy-table-frozen-h`, default `30rem`.
|
|
42
|
+
|
|
6
43
|
## [1.34.0] — 2026-08-19
|
|
7
44
|
One note from the back-office app (IDU_EMATE_APPL_WEB, #008), and the second half of #005 it
|
|
8
45
|
finally makes answerable.
|
package/COMPONENTS.md
CHANGED
|
@@ -516,20 +516,27 @@ Input-styled trigger + calendar popover. Needs `freeday-datepicker.js`. **Author
|
|
|
516
516
|
— the enhancer builds everything.
|
|
517
517
|
|
|
518
518
|
- `.fdy-datepicker` (+`--error`) · `__trigger` `__value` (+`--placeholder`) `__icon` `__clear`
|
|
519
|
-
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months`
|
|
520
|
-
`__day` `__month`
|
|
519
|
+
`__panel`; calendar internals `.fdy-cal__head` `__nav` `__title` `__grid` (+`--months` `--years`)
|
|
520
|
+
`__dow` `__day` `__month` `__year`
|
|
521
521
|
- Range: wrap two pickers in `.fdy-daterange` + `data-fdy-daterange` (`role="group"`), children
|
|
522
522
|
get `data-role="from"` / `"to"`; separator `.fdy-daterange__sep`. The end can't precede the start.
|
|
523
523
|
**Typed wrapper: `<FdyDateRange>`** (Vue/React/Blazor) — use it in those stacks.
|
|
524
|
-
**Getting to a distant
|
|
525
|
-
12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`)
|
|
526
|
-
arrows
|
|
527
|
-
|
|
528
|
-
|
|
524
|
+
**Getting to a distant date.** The title is a **button**, and it drills up one level each press:
|
|
525
|
+
day grid → 12-cell month grid (`.fdy-cal__grid--months` of `.fdy-cal__month`) → 12-cell year grid
|
|
526
|
+
(`.fdy-cal__grid--years` of `.fdy-cal__year`). The arrows follow the level shown — months, then
|
|
527
|
+
years, then pages of twelve years. Picking a year drops to that year's months, picking a month
|
|
528
|
+
drops to its days: every level above the day grid is **navigation**, so nothing is committed until a
|
|
529
|
+
day is selected. From the year grid the title steps back down to months.
|
|
530
|
+
|
|
531
|
+
Year pages are **aligned**, not centred on the year in view — 2016–2027, then 2028–2039 — so the
|
|
532
|
+
pages tile and a given year always sits in the same place. From August 2026 to March 2022 that is 7
|
|
533
|
+
clicks; walking month by month is 53. To 1998 it is 8; walking is 336.
|
|
529
534
|
|
|
530
535
|
Keyboard, in the day grid: arrows move a day, `Home`/`End` the week, `PageUp`/`PageDown` a month,
|
|
531
536
|
and **`Shift`+`PageUp`/`PageDown` a year** (WAI-ARIA APG). The month grid mirrors it: arrows ±1/±3
|
|
532
|
-
months, `Home`/`End` January/December, `PageUp`/`PageDown` a year, `Enter`/`Space` to choose.
|
|
537
|
+
months, `Home`/`End` January/December, `PageUp`/`PageDown` a year, `Enter`/`Space` to choose. The
|
|
538
|
+
year grid likewise: arrows ±1/±3 years, `Home`/`End` the ends of the page, `PageUp`/`PageDown` a
|
|
539
|
+
page of twelve.
|
|
533
540
|
|
|
534
541
|
- Attributes: `data-value="YYYY-MM-DD"`, `data-placeholder`, `data-label`, `data-fdy-no-icon`
|
|
535
542
|
|
|
@@ -687,6 +694,26 @@ Semantic static table. Wrap in `.fdy-table-wrap` (bordered surface) or `.fdy-tab
|
|
|
687
694
|
</div>
|
|
688
695
|
```
|
|
689
696
|
|
|
697
|
+
**Frozen axes — `.fdy-table--sticky`.** For a grid read against two axes at once (a rate
|
|
698
|
+
matrix, a timetable). Put `.fdy-table-scroll--frozen` on the wrapper — it scrolls both ways
|
|
699
|
+
and is the scrollport the frozen cells stick to — and size it with `--fdy-table-frozen-h`
|
|
700
|
+
(default `30rem`). `<thead>` freezes at the top, `<th scope="row">` freezes at the left, and
|
|
701
|
+
the first header cell is the corner. Do not reach for `position:sticky` directly: the base
|
|
702
|
+
table collapses its borders, and a collapsed border belongs to the table rather than the
|
|
703
|
+
cell, so it scrolls out from under whatever you froze.
|
|
704
|
+
|
|
705
|
+
```html
|
|
706
|
+
<div class="fdy-table-scroll fdy-table-scroll--frozen" style="--fdy-table-frozen-h:26rem">
|
|
707
|
+
<table class="fdy-table fdy-table--sticky">
|
|
708
|
+
<caption class="fdy-visually-hidden">Exchange rates, August 2026</caption>
|
|
709
|
+
<thead><tr><th scope="col">Date</th><th scope="col" class="fdy-table__num">USD</th></tr></thead>
|
|
710
|
+
<tbody>
|
|
711
|
+
<tr><th scope="row">1 Aug</th><td class="fdy-table__num">16,240</td></tr>
|
|
712
|
+
</tbody>
|
|
713
|
+
</table>
|
|
714
|
+
</div>
|
|
715
|
+
```
|
|
716
|
+
|
|
690
717
|
## Data table — `.fdy-datatable`
|
|
691
718
|
> **Typed wrapper: `<FdyTable>`** — controlled: `columns` + `rows`, with sort/filter/page events (`update:pageIndex` · `onPageIndexChange` · `PageIndexChanged`) and `process` for driving a card list off the same processed set. The markup below is the raw enhancer path.
|
|
692
719
|
|
|
@@ -21,6 +21,14 @@ interface DayCell {
|
|
|
21
21
|
focusable: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface YearCell {
|
|
25
|
+
year: number;
|
|
26
|
+
selected: boolean;
|
|
27
|
+
today: boolean;
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
focusable: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
interface MonthCell {
|
|
25
33
|
index: number;
|
|
26
34
|
label: string;
|
|
@@ -53,6 +61,11 @@ export interface FdyDatepickerProps {
|
|
|
53
61
|
/** aria-labels for the year arrows shown in the month grid. Defaults 'Previous year' / 'Next year'. */
|
|
54
62
|
prevYearLabel?: string;
|
|
55
63
|
nextYearLabel?: string;
|
|
64
|
+
/** aria-label for the title button that drills from the month grid to the year grid. Default 'Choose year'. */
|
|
65
|
+
chooseYearLabel?: string;
|
|
66
|
+
/** aria-labels for the page arrows shown in the year grid. Defaults 'Previous years' / 'Next years'. */
|
|
67
|
+
prevYearsLabel?: string;
|
|
68
|
+
nextYearsLabel?: string;
|
|
56
69
|
prevMonthLabel?: string;
|
|
57
70
|
/** aria-label for the next-month nav button. Default 'Next month'. */
|
|
58
71
|
nextMonthLabel?: string;
|
|
@@ -121,10 +134,19 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
121
134
|
const [open, setOpen] = useState<boolean>(false);
|
|
122
135
|
/* 'days' | 'months' — the calendar drills one level up instead of growing furniture beside the
|
|
123
136
|
title. Before this the only pointer route to another month was one click per month. */
|
|
124
|
-
const [mode, setMode] = useState<'days' | 'months'>('days');
|
|
137
|
+
const [mode, setMode] = useState<'days' | 'months' | 'years'>('days');
|
|
125
138
|
const [focusMonth, setFocusMonth] = useState<number>(0);
|
|
139
|
+
const [focusYear, setFocusYear] = useState<number>(0);
|
|
140
|
+
|
|
141
|
+
/* A page of twelve years, ALIGNED so pages tile: 2016-2027, 2028-2039. Anchoring on the year in
|
|
142
|
+
view would make the arrows land on overlapping windows, and the same year would sit somewhere
|
|
143
|
+
different every step. */
|
|
144
|
+
const YEARS_PER_PAGE = 12;
|
|
145
|
+
const yearPageStart = (year: number): number => Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
126
146
|
const monthRefs = useRef<Record<number, HTMLButtonElement | null>>({});
|
|
127
147
|
const pendingMonthFocusRef = useRef<number | null>(null);
|
|
148
|
+
const yearRefs = useRef<Record<number, HTMLButtonElement | null>>({});
|
|
149
|
+
const pendingYearFocusRef = useRef<number | null>(null);
|
|
128
150
|
|
|
129
151
|
// Render the calendar panel in the top layer (popover) so it never gets clipped by a card or
|
|
130
152
|
// scroll container; positions against the trigger.
|
|
@@ -146,6 +168,9 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
146
168
|
const chooseMonthLabelText: string = props.chooseMonthLabel ?? 'Choose month';
|
|
147
169
|
const prevYearLabelText: string = props.prevYearLabel ?? 'Previous year';
|
|
148
170
|
const nextYearLabelText: string = props.nextYearLabel ?? 'Next year';
|
|
171
|
+
const chooseYearLabelText: string = props.chooseYearLabel ?? 'Choose year';
|
|
172
|
+
const prevYearsLabelText: string = props.prevYearsLabel ?? 'Previous years';
|
|
173
|
+
const nextYearsLabelText: string = props.nextYearsLabel ?? 'Next years';
|
|
149
174
|
const monthCellFmt: Intl.DateTimeFormat = useMemo(
|
|
150
175
|
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { month: 'short' }),
|
|
151
176
|
[props.locale],
|
|
@@ -271,6 +296,13 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
271
296
|
monthRefs.current[index]?.focus();
|
|
272
297
|
});
|
|
273
298
|
|
|
299
|
+
useEffect((): void => {
|
|
300
|
+
if (pendingYearFocusRef.current === null) return;
|
|
301
|
+
const year: number = pendingYearFocusRef.current;
|
|
302
|
+
pendingYearFocusRef.current = null;
|
|
303
|
+
yearRefs.current[year]?.focus();
|
|
304
|
+
});
|
|
305
|
+
|
|
274
306
|
function openMonthGrid(): void {
|
|
275
307
|
setFocusMonth(viewMonth.getMonth());
|
|
276
308
|
setMode('months');
|
|
@@ -296,6 +328,73 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
296
328
|
pendingMonthFocusRef.current = next;
|
|
297
329
|
}
|
|
298
330
|
|
|
331
|
+
/** A year is unreachable only when EVERY month in it falls outside min/max. */
|
|
332
|
+
function isYearDisabled(year: number): boolean {
|
|
333
|
+
if (minDate !== null && startOfDay(new Date(year, 11, 31)).getTime() < startOfDay(minDate).getTime()) return true;
|
|
334
|
+
if (maxDate !== null && startOfDay(new Date(year, 0, 1)).getTime() > startOfDay(maxDate).getTime()) return true;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const yearPage: number = yearPageStart(focusYear);
|
|
339
|
+
const yearRangeLabel: string = `${yearPage} – ${yearPage + YEARS_PER_PAGE - 1}`;
|
|
340
|
+
|
|
341
|
+
const yearCells: YearCell[] = useMemo((): YearCell[] => {
|
|
342
|
+
const start: number = yearPageStart(focusYear);
|
|
343
|
+
const thisYear: number = new Date().getFullYear();
|
|
344
|
+
const sel: Date | null = parseISO(props.value);
|
|
345
|
+
return Array.from({ length: YEARS_PER_PAGE }, (_unused: unknown, index: number): YearCell => {
|
|
346
|
+
const year: number = start + index;
|
|
347
|
+
return {
|
|
348
|
+
year,
|
|
349
|
+
selected: sel !== null && sel.getFullYear() === year,
|
|
350
|
+
today: thisYear === year,
|
|
351
|
+
disabled: isYearDisabled(year),
|
|
352
|
+
focusable: year === focusYear,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
356
|
+
}, [focusYear, props.value, minDate, maxDate]);
|
|
357
|
+
|
|
358
|
+
function openYearGrid(): void {
|
|
359
|
+
setFocusYear(viewMonth.getFullYear());
|
|
360
|
+
setMode('years');
|
|
361
|
+
pendingYearFocusRef.current = viewMonth.getFullYear();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Picking a year is NAVIGATION, exactly like picking a month: it drops to that year's month grid
|
|
365
|
+
and commits nothing. */
|
|
366
|
+
function openYear(year: number): void {
|
|
367
|
+
setViewMonth(new Date(year, viewMonth.getMonth(), 1));
|
|
368
|
+
setFocusMonth(viewMonth.getMonth());
|
|
369
|
+
setMode('months');
|
|
370
|
+
pendingMonthFocusRef.current = viewMonth.getMonth();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function moveYearFocus(year: number): void {
|
|
374
|
+
setFocusYear(year);
|
|
375
|
+
setViewMonth(new Date(year, viewMonth.getMonth(), 1));
|
|
376
|
+
pendingYearFocusRef.current = year;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function onYearKeydown(e: React.KeyboardEvent): void {
|
|
380
|
+
let handled: boolean = true;
|
|
381
|
+
switch (e.key) {
|
|
382
|
+
case 'ArrowLeft': moveYearFocus(focusYear - 1); break;
|
|
383
|
+
case 'ArrowRight': moveYearFocus(focusYear + 1); break;
|
|
384
|
+
case 'ArrowUp': moveYearFocus(focusYear - 3); break;
|
|
385
|
+
case 'ArrowDown': moveYearFocus(focusYear + 3); break;
|
|
386
|
+
case 'Home': moveYearFocus(yearPageStart(focusYear)); break;
|
|
387
|
+
case 'End': moveYearFocus(yearPageStart(focusYear) + YEARS_PER_PAGE - 1); break;
|
|
388
|
+
case 'PageUp': moveYearFocus(focusYear - YEARS_PER_PAGE); break;
|
|
389
|
+
case 'PageDown': moveYearFocus(focusYear + YEARS_PER_PAGE); break;
|
|
390
|
+
case 'Enter':
|
|
391
|
+
case ' ': openYear(focusYear); break;
|
|
392
|
+
case 'Escape': closePanel(true); break;
|
|
393
|
+
default: handled = false;
|
|
394
|
+
}
|
|
395
|
+
if (handled) { e.preventDefault(); e.stopPropagation(); }
|
|
396
|
+
}
|
|
397
|
+
|
|
299
398
|
function onMonthKeydown(e: React.KeyboardEvent): void {
|
|
300
399
|
let handled: boolean = true;
|
|
301
400
|
switch (e.key) {
|
|
@@ -476,8 +575,12 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
476
575
|
<button
|
|
477
576
|
type="button"
|
|
478
577
|
className="fdy-cal__nav"
|
|
479
|
-
aria-label={mode === 'months' ? prevYearLabelText : prevMonthLabelText}
|
|
480
|
-
onClick={(): void => {
|
|
578
|
+
aria-label={mode === 'years' ? prevYearsLabelText : mode === 'months' ? prevYearLabelText : prevMonthLabelText}
|
|
579
|
+
onClick={(): void => {
|
|
580
|
+
if (mode === 'years') moveYearFocus(focusYear - YEARS_PER_PAGE);
|
|
581
|
+
else if (mode === 'months') moveMonthFocus(focusMonth, -1);
|
|
582
|
+
else setViewMonth(addMonths(viewMonth, -1));
|
|
583
|
+
}}
|
|
481
584
|
>
|
|
482
585
|
‹
|
|
483
586
|
</button>
|
|
@@ -485,21 +588,44 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
485
588
|
id={titleId}
|
|
486
589
|
type="button"
|
|
487
590
|
className="fdy-cal__title"
|
|
488
|
-
aria-label={mode === '
|
|
489
|
-
onClick={(): void => { if (mode === 'months')
|
|
591
|
+
aria-label={mode === 'years' ? undefined : mode === 'months' ? chooseYearLabelText : chooseMonthLabelText}
|
|
592
|
+
onClick={(): void => { if (mode === 'months') openYearGrid(); else openMonthGrid(); }}
|
|
490
593
|
>
|
|
491
|
-
{mode === 'months' ? String(viewMonth.getFullYear()) : monthFmt.format(viewMonth)}
|
|
594
|
+
{mode === 'years' ? yearRangeLabel : mode === 'months' ? String(viewMonth.getFullYear()) : monthFmt.format(viewMonth)}
|
|
492
595
|
</button>
|
|
493
596
|
<button
|
|
494
597
|
type="button"
|
|
495
598
|
className="fdy-cal__nav"
|
|
496
|
-
aria-label={mode === 'months' ? nextYearLabelText : nextMonthLabelText}
|
|
497
|
-
onClick={(): void => {
|
|
599
|
+
aria-label={mode === 'years' ? nextYearsLabelText : mode === 'months' ? nextYearLabelText : nextMonthLabelText}
|
|
600
|
+
onClick={(): void => {
|
|
601
|
+
if (mode === 'years') moveYearFocus(focusYear + YEARS_PER_PAGE);
|
|
602
|
+
else if (mode === 'months') moveMonthFocus(focusMonth, 1);
|
|
603
|
+
else setViewMonth(addMonths(viewMonth, 1));
|
|
604
|
+
}}
|
|
498
605
|
>
|
|
499
606
|
›
|
|
500
607
|
</button>
|
|
501
608
|
</div>
|
|
502
|
-
{mode === '
|
|
609
|
+
{mode === 'years' ? (
|
|
610
|
+
<div className="fdy-cal__grid fdy-cal__grid--years" role="grid" aria-labelledby={titleId} onKeyDown={onYearKeydown}>
|
|
611
|
+
{yearCells.map((cell: YearCell) => (
|
|
612
|
+
<button
|
|
613
|
+
key={cell.year}
|
|
614
|
+
ref={(el: HTMLButtonElement | null): void => { yearRefs.current[cell.year] = el; }}
|
|
615
|
+
type="button"
|
|
616
|
+
className={['fdy-cal__year', cell.today ? 'is-today' : '', cell.selected ? 'is-selected' : ''].filter(Boolean).join(' ')}
|
|
617
|
+
role="gridcell"
|
|
618
|
+
aria-label={String(cell.year)}
|
|
619
|
+
aria-selected={cell.selected ? true : undefined}
|
|
620
|
+
disabled={cell.disabled}
|
|
621
|
+
tabIndex={cell.focusable ? 0 : -1}
|
|
622
|
+
onClick={(): void => openYear(cell.year)}
|
|
623
|
+
>
|
|
624
|
+
{cell.year}
|
|
625
|
+
</button>
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
) : mode === 'months' ? (
|
|
503
629
|
<div className="fdy-cal__grid fdy-cal__grid--months" role="grid" aria-labelledby={titleId} onKeyDown={onMonthKeydown}>
|
|
504
630
|
{monthCells.map((cell: MonthCell) => (
|
|
505
631
|
<button
|
|
@@ -50,6 +50,11 @@ const props = defineProps<{
|
|
|
50
50
|
/** aria-labels for the year arrows shown in the month grid. Defaults 'Previous year' / 'Next year'. */
|
|
51
51
|
prevYearLabel?: string;
|
|
52
52
|
nextYearLabel?: string;
|
|
53
|
+
/** aria-label for the title button that drills from the month grid to the year grid. Default 'Choose year'. */
|
|
54
|
+
chooseYearLabel?: string;
|
|
55
|
+
/** aria-labels for the page arrows shown in the year grid. Defaults 'Previous years' / 'Next years'. */
|
|
56
|
+
prevYearsLabel?: string;
|
|
57
|
+
nextYearsLabel?: string;
|
|
53
58
|
}>();
|
|
54
59
|
|
|
55
60
|
const emit = defineEmits<{
|
|
@@ -124,6 +129,9 @@ const clearLabelText: ComputedRef<string> = computed((): string => props.clearLa
|
|
|
124
129
|
const chooseMonthLabelText: ComputedRef<string> = computed((): string => props.chooseMonthLabel ?? 'Choose month');
|
|
125
130
|
const prevYearLabelText: ComputedRef<string> = computed((): string => props.prevYearLabel ?? 'Previous year');
|
|
126
131
|
const nextYearLabelText: ComputedRef<string> = computed((): string => props.nextYearLabel ?? 'Next year');
|
|
132
|
+
const chooseYearLabelText: ComputedRef<string> = computed((): string => props.chooseYearLabel ?? 'Choose year');
|
|
133
|
+
const prevYearsLabelText: ComputedRef<string> = computed((): string => props.prevYearsLabel ?? 'Previous years');
|
|
134
|
+
const nextYearsLabelText: ComputedRef<string> = computed((): string => props.nextYearsLabel ?? 'Next years');
|
|
127
135
|
|
|
128
136
|
const view: Ref<Date> = ref(startOfMonth(selectedDate.value ?? new Date()));
|
|
129
137
|
const focusDate: Ref<Date> = ref(selectedDate.value ?? new Date());
|
|
@@ -140,8 +148,15 @@ const effectiveFocusDate: ComputedRef<Date> = computed((): Date => {
|
|
|
140
148
|
/* 'days' | 'months' — the calendar drills one level up instead of growing furniture beside the title.
|
|
141
149
|
Before this the only pointer route to another month was one click per month: August 2026 to March
|
|
142
150
|
2022 is fifty-three of them, and the Shift+PageUp jump had no affordance at all. */
|
|
143
|
-
const mode: Ref<'days' | 'months'> = ref('days');
|
|
151
|
+
const mode: Ref<'days' | 'months' | 'years'> = ref('days');
|
|
144
152
|
const focusMonth: Ref<number> = ref(0);
|
|
153
|
+
const focusYear: Ref<number> = ref(0);
|
|
154
|
+
|
|
155
|
+
/* A page of twelve years, ALIGNED so pages tile: 2016-2027, 2028-2039. Anchoring the page on the
|
|
156
|
+
year in view would make the arrows land on overlapping windows, and the same year would sit at a
|
|
157
|
+
different spot every time you stepped. */
|
|
158
|
+
const YEARS_PER_PAGE = 12;
|
|
159
|
+
const yearPageStart = (year: number): number => Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
145
160
|
|
|
146
161
|
const monthCellFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
147
162
|
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { month: 'short' }),
|
|
@@ -189,6 +204,44 @@ const monthCells: ComputedRef<MonthCell[]> = computed((): MonthCell[] => {
|
|
|
189
204
|
});
|
|
190
205
|
});
|
|
191
206
|
|
|
207
|
+
interface YearCell {
|
|
208
|
+
year: number;
|
|
209
|
+
selected: boolean;
|
|
210
|
+
today: boolean;
|
|
211
|
+
disabled: boolean;
|
|
212
|
+
focusable: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A year is unreachable only when EVERY month in it falls outside min/max — the same rule
|
|
216
|
+
* isMonthDisabled applies one level down, and for the same reason. */
|
|
217
|
+
function isYearDisabled(year: number): boolean {
|
|
218
|
+
if (minDate.value !== null && startOfDay(new Date(year, 11, 31)).getTime() < startOfDay(minDate.value).getTime()) return true;
|
|
219
|
+
if (maxDate.value !== null && startOfDay(new Date(year, 0, 1)).getTime() > startOfDay(maxDate.value).getTime()) return true;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const yearPage: ComputedRef<number> = computed((): number => yearPageStart(focusYear.value));
|
|
224
|
+
|
|
225
|
+
const yearCells: ComputedRef<YearCell[]> = computed((): YearCell[] => {
|
|
226
|
+
const start: number = yearPage.value;
|
|
227
|
+
const thisYear: number = new Date().getFullYear();
|
|
228
|
+
const sel: Date | null = selectedDate.value;
|
|
229
|
+
return Array.from({ length: YEARS_PER_PAGE }, (_unused: unknown, index: number): YearCell => {
|
|
230
|
+
const year: number = start + index;
|
|
231
|
+
return {
|
|
232
|
+
year,
|
|
233
|
+
selected: sel !== null && sel.getFullYear() === year,
|
|
234
|
+
today: thisYear === year,
|
|
235
|
+
disabled: isYearDisabled(year),
|
|
236
|
+
focusable: year === focusYear.value,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const yearRangeLabel: ComputedRef<string> = computed(
|
|
242
|
+
(): string => `${yearPage.value} – ${yearPage.value + YEARS_PER_PAGE - 1}`,
|
|
243
|
+
);
|
|
244
|
+
|
|
192
245
|
const monthFmt: ComputedRef<Intl.DateTimeFormat> = computed(
|
|
193
246
|
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(props.locale, { month: 'long', year: 'numeric' }),
|
|
194
247
|
);
|
|
@@ -278,6 +331,51 @@ function openMonth(index: number): void {
|
|
|
278
331
|
void nextTick((): void => focusCell());
|
|
279
332
|
}
|
|
280
333
|
|
|
334
|
+
function openYearGrid(): void {
|
|
335
|
+
focusYear.value = view.value.getFullYear();
|
|
336
|
+
mode.value = 'years';
|
|
337
|
+
void nextTick((): void => focusYearCell());
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function focusYearCell(): void {
|
|
341
|
+
const cell: HTMLElement | null = panelEl.value?.querySelector('.fdy-cal__year[tabindex="0"]') ?? null;
|
|
342
|
+
cell?.focus();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Picking a year is NAVIGATION, exactly like picking a month: it drops to that year's month grid
|
|
346
|
+
and commits nothing. */
|
|
347
|
+
function openYear(year: number): void {
|
|
348
|
+
view.value = new Date(year, view.value.getMonth(), 1);
|
|
349
|
+
focusMonth.value = view.value.getMonth();
|
|
350
|
+
mode.value = 'months';
|
|
351
|
+
void nextTick((): void => focusMonthCell());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function moveYearFocus(year: number): void {
|
|
355
|
+
focusYear.value = year;
|
|
356
|
+
view.value = new Date(year, view.value.getMonth(), 1);
|
|
357
|
+
void nextTick((): void => focusYearCell());
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function onYearKeydown(e: KeyboardEvent): void {
|
|
361
|
+
let handled: boolean = true;
|
|
362
|
+
switch (e.key) {
|
|
363
|
+
case 'ArrowLeft': moveYearFocus(focusYear.value - 1); break;
|
|
364
|
+
case 'ArrowRight': moveYearFocus(focusYear.value + 1); break;
|
|
365
|
+
case 'ArrowUp': moveYearFocus(focusYear.value - 3); break;
|
|
366
|
+
case 'ArrowDown': moveYearFocus(focusYear.value + 3); break;
|
|
367
|
+
case 'Home': moveYearFocus(yearPageStart(focusYear.value)); break;
|
|
368
|
+
case 'End': moveYearFocus(yearPageStart(focusYear.value) + YEARS_PER_PAGE - 1); break;
|
|
369
|
+
case 'PageUp': moveYearFocus(focusYear.value - YEARS_PER_PAGE); break;
|
|
370
|
+
case 'PageDown': moveYearFocus(focusYear.value + YEARS_PER_PAGE); break;
|
|
371
|
+
case 'Enter':
|
|
372
|
+
case ' ': openYear(focusYear.value); break;
|
|
373
|
+
case 'Escape': closePanel(true); break;
|
|
374
|
+
default: handled = false;
|
|
375
|
+
}
|
|
376
|
+
if (handled) { e.preventDefault(); e.stopPropagation(); }
|
|
377
|
+
}
|
|
378
|
+
|
|
281
379
|
function moveMonthFocus(index: number, yearDelta: number): void {
|
|
282
380
|
if (yearDelta !== 0) view.value = addMonths(view.value, yearDelta * 12);
|
|
283
381
|
focusMonth.value = (index + 12) % 12;
|
|
@@ -465,24 +563,51 @@ onBeforeUnmount((): void => {
|
|
|
465
563
|
<button
|
|
466
564
|
type="button"
|
|
467
565
|
class="fdy-cal__nav"
|
|
468
|
-
:aria-label="mode === 'months' ? prevYearLabelText : prevMonthLabelText"
|
|
469
|
-
@click="
|
|
566
|
+
:aria-label="mode === 'years' ? prevYearsLabelText : mode === 'months' ? prevYearLabelText : prevMonthLabelText"
|
|
567
|
+
@click="
|
|
568
|
+
mode === 'years'
|
|
569
|
+
? moveYearFocus(focusYear - 12)
|
|
570
|
+
: mode === 'months'
|
|
571
|
+
? moveMonthFocus(focusMonth, -1)
|
|
572
|
+
: (view = addMonths(view, -1))
|
|
573
|
+
"
|
|
470
574
|
>‹</button>
|
|
471
575
|
<button
|
|
472
576
|
:id="titleId"
|
|
473
577
|
type="button"
|
|
474
578
|
class="fdy-cal__title"
|
|
475
|
-
:aria-label="mode === '
|
|
476
|
-
@click="mode === 'months' ? (
|
|
477
|
-
>{{ mode === 'months' ? String(view.getFullYear()) : monthFmt.format(view) }}</button>
|
|
579
|
+
:aria-label="mode === 'years' ? undefined : mode === 'months' ? chooseYearLabelText : chooseMonthLabelText"
|
|
580
|
+
@click="mode === 'months' ? openYearGrid() : openMonthGrid()"
|
|
581
|
+
>{{ mode === 'years' ? yearRangeLabel : mode === 'months' ? String(view.getFullYear()) : monthFmt.format(view) }}</button>
|
|
478
582
|
<button
|
|
479
583
|
type="button"
|
|
480
584
|
class="fdy-cal__nav"
|
|
481
|
-
:aria-label="mode === 'months' ? nextYearLabelText : nextMonthLabelText"
|
|
482
|
-
@click="
|
|
585
|
+
:aria-label="mode === 'years' ? nextYearsLabelText : mode === 'months' ? nextYearLabelText : nextMonthLabelText"
|
|
586
|
+
@click="
|
|
587
|
+
mode === 'years'
|
|
588
|
+
? moveYearFocus(focusYear + 12)
|
|
589
|
+
: mode === 'months'
|
|
590
|
+
? moveMonthFocus(focusMonth, 1)
|
|
591
|
+
: (view = addMonths(view, 1))
|
|
592
|
+
"
|
|
483
593
|
>›</button>
|
|
484
594
|
</div>
|
|
485
|
-
<div v-if="mode === '
|
|
595
|
+
<div v-if="mode === 'years'" class="fdy-cal__grid fdy-cal__grid--years" role="grid" :aria-labelledby="titleId" @keydown="onYearKeydown">
|
|
596
|
+
<button
|
|
597
|
+
v-for="cell in yearCells"
|
|
598
|
+
:key="cell.year"
|
|
599
|
+
type="button"
|
|
600
|
+
class="fdy-cal__year"
|
|
601
|
+
:class="{ 'is-today': cell.today, 'is-selected': cell.selected }"
|
|
602
|
+
role="gridcell"
|
|
603
|
+
:aria-label="String(cell.year)"
|
|
604
|
+
:aria-selected="cell.selected ? 'true' : undefined"
|
|
605
|
+
:disabled="cell.disabled"
|
|
606
|
+
:tabindex="cell.focusable ? 0 : -1"
|
|
607
|
+
@click="openYear(cell.year)"
|
|
608
|
+
>{{ cell.year }}</button>
|
|
609
|
+
</div>
|
|
610
|
+
<div v-else-if="mode === 'months'" class="fdy-cal__grid fdy-cal__grid--months" role="grid" :aria-labelledby="titleId" @keydown="onMonthKeydown">
|
|
486
611
|
<button
|
|
487
612
|
v-for="cell in monthCells"
|
|
488
613
|
:key="cell.index"
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
wrap.dataset.fdyDpReady = '1';
|
|
58
58
|
wrap.classList.add('fdy-datepicker');
|
|
59
59
|
|
|
60
|
-
var placeholder = wrap.getAttribute('data-placeholder') || '
|
|
61
|
-
var label = wrap.getAttribute('data-label') || '
|
|
60
|
+
var placeholder = wrap.getAttribute('data-placeholder') || 'Choose a date';
|
|
61
|
+
var label = wrap.getAttribute('data-label') || 'Date';
|
|
62
62
|
var selected = parseISO(wrap.getAttribute('data-value'));
|
|
63
63
|
var minDate = parseISO(wrap.getAttribute('data-min'));
|
|
64
64
|
var maxDate = parseISO(wrap.getAttribute('data-max'));
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
month" twenty-four times to reach a date two years back. */
|
|
74
74
|
var mode = 'days';
|
|
75
75
|
var focusMonth = null;
|
|
76
|
+
var focusYear = null;
|
|
76
77
|
|
|
77
78
|
var trigger = document.createElement('button');
|
|
78
79
|
trigger.type = 'button';
|
|
@@ -150,20 +151,21 @@
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
function render() {
|
|
154
|
+
if (mode === 'years') { renderYears(); return; }
|
|
153
155
|
if (mode === 'months') { renderMonths(); return; }
|
|
154
156
|
panel.innerHTML = '';
|
|
155
157
|
var head = document.createElement('div');
|
|
156
158
|
head.className = 'fdy-cal__head';
|
|
157
|
-
var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ',
|
|
159
|
+
var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ', choose month', function () {
|
|
158
160
|
mode = 'months';
|
|
159
161
|
focusMonth = view.getMonth();
|
|
160
162
|
render();
|
|
161
163
|
focusMonthCell();
|
|
162
164
|
});
|
|
163
165
|
panel.setAttribute('aria-labelledby', title.id);
|
|
164
|
-
head.appendChild(navButton('‹', '
|
|
166
|
+
head.appendChild(navButton('‹', 'Previous month', function () { view = addMonths(view, -1); render(); }));
|
|
165
167
|
head.appendChild(title);
|
|
166
|
-
head.appendChild(navButton('›', '
|
|
168
|
+
head.appendChild(navButton('›', 'Next month', function () { view = addMonths(view, 1); render(); }));
|
|
167
169
|
panel.appendChild(head);
|
|
168
170
|
|
|
169
171
|
var grid = document.createElement('div');
|
|
@@ -220,15 +222,16 @@
|
|
|
220
222
|
var year = view.getFullYear();
|
|
221
223
|
var head = document.createElement('div');
|
|
222
224
|
head.className = 'fdy-cal__head';
|
|
223
|
-
var title = titleButton(String(year), year + ',
|
|
224
|
-
mode = '
|
|
225
|
+
var title = titleButton(String(year), year + ', choose year', function () {
|
|
226
|
+
mode = 'years';
|
|
227
|
+
focusYear = year;
|
|
225
228
|
render();
|
|
226
|
-
|
|
229
|
+
focusYearCell();
|
|
227
230
|
});
|
|
228
231
|
panel.setAttribute('aria-labelledby', title.id);
|
|
229
|
-
head.appendChild(navButton('‹', '
|
|
232
|
+
head.appendChild(navButton('‹', 'Previous year', function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
|
|
230
233
|
head.appendChild(title);
|
|
231
|
-
head.appendChild(navButton('›', '
|
|
234
|
+
head.appendChild(navButton('›', 'Next year', function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
|
|
232
235
|
panel.appendChild(head);
|
|
233
236
|
|
|
234
237
|
var grid = document.createElement('div');
|
|
@@ -259,6 +262,108 @@
|
|
|
259
262
|
panel.appendChild(grid);
|
|
260
263
|
}
|
|
261
264
|
|
|
265
|
+
/* A page of twelve years, aligned so pages TILE: 2016-2027, 2028-2039. Anchoring the page on
|
|
266
|
+
the year in view instead would make ‹ and › land on overlapping windows, and the same year
|
|
267
|
+
would appear at a different spot every time you stepped. */
|
|
268
|
+
var YEARS_PER_PAGE = 12;
|
|
269
|
+
function yearPageStart(y) { return Math.floor(y / YEARS_PER_PAGE) * YEARS_PER_PAGE; }
|
|
270
|
+
|
|
271
|
+
/* A year is only unreachable when EVERY month in it falls outside min/max — the same rule
|
|
272
|
+
monthDisabled applies one level down, for the same reason: a bound that sits inside the year
|
|
273
|
+
disables neither end. */
|
|
274
|
+
function yearDisabled(y) {
|
|
275
|
+
if (minDate && new Date(y, 11, 31) < startOfDay(minDate)) return true;
|
|
276
|
+
if (maxDate && new Date(y, 0, 1) > startOfDay(maxDate)) return true;
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderYears() {
|
|
281
|
+
panel.innerHTML = '';
|
|
282
|
+
var start = yearPageStart(view.getFullYear());
|
|
283
|
+
var end = start + YEARS_PER_PAGE - 1;
|
|
284
|
+
var head = document.createElement('div');
|
|
285
|
+
head.className = 'fdy-cal__head';
|
|
286
|
+
var title = titleButton(start + ' – ' + end, start + ' to ' + end + ', back to months', function () {
|
|
287
|
+
mode = 'months';
|
|
288
|
+
focusMonth = view.getMonth();
|
|
289
|
+
render();
|
|
290
|
+
focusMonthCell();
|
|
291
|
+
});
|
|
292
|
+
panel.setAttribute('aria-labelledby', title.id);
|
|
293
|
+
head.appendChild(navButton('‹', 'Previous years', function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
|
|
294
|
+
head.appendChild(title);
|
|
295
|
+
head.appendChild(navButton('›', 'Next years', function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
|
|
296
|
+
panel.appendChild(head);
|
|
297
|
+
|
|
298
|
+
var grid = document.createElement('div');
|
|
299
|
+
grid.className = 'fdy-cal__grid fdy-cal__grid--years';
|
|
300
|
+
grid.setAttribute('role', 'grid');
|
|
301
|
+
grid.setAttribute('aria-labelledby', title.id);
|
|
302
|
+
var thisYear = new Date().getFullYear();
|
|
303
|
+
if (focusYear === null) focusYear = view.getFullYear();
|
|
304
|
+
for (var i = 0; i < YEARS_PER_PAGE; i++) {
|
|
305
|
+
var y = start + i;
|
|
306
|
+
var btn = document.createElement('button');
|
|
307
|
+
btn.type = 'button';
|
|
308
|
+
btn.className = 'fdy-cal__year';
|
|
309
|
+
btn.setAttribute('role', 'gridcell');
|
|
310
|
+
btn.setAttribute('aria-label', String(y));
|
|
311
|
+
btn.textContent = String(y);
|
|
312
|
+
if (y === thisYear) btn.classList.add('is-today');
|
|
313
|
+
if (selected && selected.getFullYear() === y) {
|
|
314
|
+
btn.classList.add('is-selected');
|
|
315
|
+
btn.setAttribute('aria-selected', 'true');
|
|
316
|
+
}
|
|
317
|
+
if (yearDisabled(y)) btn.disabled = true;
|
|
318
|
+
btn.tabIndex = y === focusYear ? 0 : -1;
|
|
319
|
+
btn.addEventListener('click', (function (yy) { return function () { openYear(yy); }; })(y));
|
|
320
|
+
grid.appendChild(btn);
|
|
321
|
+
}
|
|
322
|
+
grid.addEventListener('keydown', onYearKey);
|
|
323
|
+
panel.appendChild(grid);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function focusYearCell() {
|
|
327
|
+
var cell = panel.querySelector('.fdy-cal__year[tabindex="0"]');
|
|
328
|
+
if (cell) cell.focus();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Picking a year is navigation, exactly like picking a month: it drops to the month grid of
|
|
332
|
+
that year. Nothing is committed until a DAY is chosen. */
|
|
333
|
+
function openYear(y) {
|
|
334
|
+
view = new Date(y, view.getMonth(), 1);
|
|
335
|
+
focusMonth = view.getMonth();
|
|
336
|
+
mode = 'months';
|
|
337
|
+
render();
|
|
338
|
+
focusMonthCell();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function moveYearFocus(y) {
|
|
342
|
+
focusYear = y;
|
|
343
|
+
view = new Date(y, view.getMonth(), 1);
|
|
344
|
+
render();
|
|
345
|
+
focusYearCell();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function onYearKey(e) {
|
|
349
|
+
var handled = true;
|
|
350
|
+
switch (e.key) {
|
|
351
|
+
case 'ArrowLeft': moveYearFocus(focusYear - 1); break;
|
|
352
|
+
case 'ArrowRight': moveYearFocus(focusYear + 1); break;
|
|
353
|
+
case 'ArrowUp': moveYearFocus(focusYear - 3); break;
|
|
354
|
+
case 'ArrowDown': moveYearFocus(focusYear + 3); break;
|
|
355
|
+
case 'Home': moveYearFocus(yearPageStart(focusYear)); break;
|
|
356
|
+
case 'End': moveYearFocus(yearPageStart(focusYear) + YEARS_PER_PAGE - 1); break;
|
|
357
|
+
case 'PageUp': moveYearFocus(focusYear - YEARS_PER_PAGE); break;
|
|
358
|
+
case 'PageDown': moveYearFocus(focusYear + YEARS_PER_PAGE); break;
|
|
359
|
+
case 'Enter':
|
|
360
|
+
case ' ': openYear(focusYear); break;
|
|
361
|
+
case 'Escape': close(true); break;
|
|
362
|
+
default: handled = false;
|
|
363
|
+
}
|
|
364
|
+
if (handled) { e.preventDefault(); e.stopPropagation(); }
|
|
365
|
+
}
|
|
366
|
+
|
|
262
367
|
function focusMonthCell() {
|
|
263
368
|
var cell = panel.querySelector('.fdy-cal__month[tabindex="0"]');
|
|
264
369
|
if (cell) cell.focus();
|
package/dist/freeday.bundle.css
CHANGED
|
@@ -1145,12 +1145,12 @@ a { color: var(--color-primary); }
|
|
|
1145
1145
|
.fdy-cal__title{flex:1;appearance:none;border:0;background:transparent;margin:0;padding:var(--space-1) var(--space-2);border-radius:var(--radius-sm);text-align:center;font-family:inherit;font-weight:var(--weight-semibold);font-size:var(--text-sm);color:var(--color-text);text-transform:capitalize;cursor:pointer;}
|
|
1146
1146
|
.fdy-cal__title:hover{background:var(--color-surface-2);}
|
|
1147
1147
|
.fdy-cal__title:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1148
|
-
.fdy-cal__month{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
1149
|
-
.fdy-cal__month:hover{background:var(--color-surface-2);}
|
|
1150
|
-
.fdy-cal__month:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1151
|
-
.fdy-cal__month:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
1152
|
-
.fdy-cal__month.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
1153
|
-
.fdy-cal__month.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
1148
|
+
.fdy-cal__month,.fdy-cal__year{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
1149
|
+
.fdy-cal__month:hover,.fdy-cal__year:hover{background:var(--color-surface-2);}
|
|
1150
|
+
.fdy-cal__month:focus-visible,.fdy-cal__year:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
1151
|
+
.fdy-cal__month:disabled,.fdy-cal__year:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
1152
|
+
.fdy-cal__month.is-today,.fdy-cal__year.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
1153
|
+
.fdy-cal__month.is-selected,.fdy-cal__year.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
1154
1154
|
.fdy-cal__nav{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-muted);font-size:var(--text-lg);line-height:1;cursor:pointer;}
|
|
1155
1155
|
.fdy-cal__nav:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
1156
1156
|
.fdy-cal__nav:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
@@ -1161,7 +1161,7 @@ a { color: var(--color-primary); }
|
|
|
1161
1161
|
AFTER `.fdy-cal__grid`, and that is the whole rule: the element carries both classes, they weigh
|
|
1162
1162
|
the same, so source order decides. Declared earlier, the modifier lost to the base it modifies and
|
|
1163
1163
|
twelve months rendered seven across — the layout the comment above described, never once applied. */
|
|
1164
|
-
.fdy-cal__grid--months{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
1164
|
+
.fdy-cal__grid--months,.fdy-cal__grid--years{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
1165
1165
|
.fdy-cal__dow{display:flex;align-items:center;justify-content:center;height:1.75rem;font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--color-text-muted);text-transform:capitalize;}
|
|
1166
1166
|
.fdy-cal__day{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-size:var(--text-sm);font-variant-numeric:tabular-nums;cursor:pointer;}
|
|
1167
1167
|
.fdy-cal__day:hover{background:var(--color-surface-2);}
|
|
@@ -1657,6 +1657,30 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1657
1657
|
.fdy-table__selcol{width:1px;white-space:nowrap;}
|
|
1658
1658
|
.fdy-table tbody tr[aria-selected="true"],.fdy-table tbody tr[aria-selected="true"]:hover{background:var(--color-primary-soft);}
|
|
1659
1659
|
|
|
1660
|
+
/* Frozen header row and/or first column — a rate matrix, a timetable, any grid read
|
|
1661
|
+
against two axes at once.
|
|
1662
|
+
|
|
1663
|
+
This modifier exists because `position:sticky` alone is NOT enough. With
|
|
1664
|
+
`border-collapse:collapse` the shared border belongs to the TABLE, not to either
|
|
1665
|
+
cell, so a frozen cell scrolls out from under its own rule and the frozen column
|
|
1666
|
+
loses the line that separates it from the data. Separate borders put each border
|
|
1667
|
+
back on the cell that sticks. */
|
|
1668
|
+
.fdy-table--sticky{border-collapse:separate;border-spacing:0;}
|
|
1669
|
+
.fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
1670
|
+
.fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
|
|
1671
|
+
/* The row header freezes sideways over the data cells, so it needs an opaque
|
|
1672
|
+
background of its own — and it takes the BODY surface, not the header one, so a
|
|
1673
|
+
hovered row stays one continuous band across the freeze line. Losing that is
|
|
1674
|
+
losing the only way to track a row across a wide grid. */
|
|
1675
|
+
.fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
|
|
1676
|
+
.fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
|
|
1677
|
+
/* The corner cell is frozen on both axes, so it has to outrank both. */
|
|
1678
|
+
.fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
|
|
1679
|
+
|
|
1680
|
+
/* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
|
|
1681
|
+
custom property so a page can size it without out-specifying this rule. */
|
|
1682
|
+
.fdy-table-scroll--frozen{position:relative;overflow:auto;max-height:var(--fdy-table-frozen-h,30rem);}
|
|
1683
|
+
|
|
1660
1684
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1661
1685
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1662
1686
|
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
package/dist/freeday.css
CHANGED
|
@@ -762,12 +762,12 @@ a { color: var(--color-primary); }
|
|
|
762
762
|
.fdy-cal__title{flex:1;appearance:none;border:0;background:transparent;margin:0;padding:var(--space-1) var(--space-2);border-radius:var(--radius-sm);text-align:center;font-family:inherit;font-weight:var(--weight-semibold);font-size:var(--text-sm);color:var(--color-text);text-transform:capitalize;cursor:pointer;}
|
|
763
763
|
.fdy-cal__title:hover{background:var(--color-surface-2);}
|
|
764
764
|
.fdy-cal__title:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
765
|
-
.fdy-cal__month{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
766
|
-
.fdy-cal__month:hover{background:var(--color-surface-2);}
|
|
767
|
-
.fdy-cal__month:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
768
|
-
.fdy-cal__month:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
769
|
-
.fdy-cal__month.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
770
|
-
.fdy-cal__month.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
765
|
+
.fdy-cal__month,.fdy-cal__year{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
766
|
+
.fdy-cal__month:hover,.fdy-cal__year:hover{background:var(--color-surface-2);}
|
|
767
|
+
.fdy-cal__month:focus-visible,.fdy-cal__year:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
768
|
+
.fdy-cal__month:disabled,.fdy-cal__year:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
769
|
+
.fdy-cal__month.is-today,.fdy-cal__year.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
770
|
+
.fdy-cal__month.is-selected,.fdy-cal__year.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
771
771
|
.fdy-cal__nav{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-muted);font-size:var(--text-lg);line-height:1;cursor:pointer;}
|
|
772
772
|
.fdy-cal__nav:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
773
773
|
.fdy-cal__nav:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
@@ -778,7 +778,7 @@ a { color: var(--color-primary); }
|
|
|
778
778
|
AFTER `.fdy-cal__grid`, and that is the whole rule: the element carries both classes, they weigh
|
|
779
779
|
the same, so source order decides. Declared earlier, the modifier lost to the base it modifies and
|
|
780
780
|
twelve months rendered seven across — the layout the comment above described, never once applied. */
|
|
781
|
-
.fdy-cal__grid--months{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
781
|
+
.fdy-cal__grid--months,.fdy-cal__grid--years{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
782
782
|
.fdy-cal__dow{display:flex;align-items:center;justify-content:center;height:1.75rem;font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--color-text-muted);text-transform:capitalize;}
|
|
783
783
|
.fdy-cal__day{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-size:var(--text-sm);font-variant-numeric:tabular-nums;cursor:pointer;}
|
|
784
784
|
.fdy-cal__day:hover{background:var(--color-surface-2);}
|
|
@@ -1274,6 +1274,30 @@ fieldset.fdy-field>legend{padding:0;float:none;margin-bottom:var(--space-2);}
|
|
|
1274
1274
|
.fdy-table__selcol{width:1px;white-space:nowrap;}
|
|
1275
1275
|
.fdy-table tbody tr[aria-selected="true"],.fdy-table tbody tr[aria-selected="true"]:hover{background:var(--color-primary-soft);}
|
|
1276
1276
|
|
|
1277
|
+
/* Frozen header row and/or first column — a rate matrix, a timetable, any grid read
|
|
1278
|
+
against two axes at once.
|
|
1279
|
+
|
|
1280
|
+
This modifier exists because `position:sticky` alone is NOT enough. With
|
|
1281
|
+
`border-collapse:collapse` the shared border belongs to the TABLE, not to either
|
|
1282
|
+
cell, so a frozen cell scrolls out from under its own rule and the frozen column
|
|
1283
|
+
loses the line that separates it from the data. Separate borders put each border
|
|
1284
|
+
back on the cell that sticks. */
|
|
1285
|
+
.fdy-table--sticky{border-collapse:separate;border-spacing:0;}
|
|
1286
|
+
.fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
1287
|
+
.fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
|
|
1288
|
+
/* The row header freezes sideways over the data cells, so it needs an opaque
|
|
1289
|
+
background of its own — and it takes the BODY surface, not the header one, so a
|
|
1290
|
+
hovered row stays one continuous band across the freeze line. Losing that is
|
|
1291
|
+
losing the only way to track a row across a wide grid. */
|
|
1292
|
+
.fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
|
|
1293
|
+
.fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
|
|
1294
|
+
/* The corner cell is frozen on both axes, so it has to outrank both. */
|
|
1295
|
+
.fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
|
|
1296
|
+
|
|
1297
|
+
/* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
|
|
1298
|
+
custom property so a page can size it without out-specifying this rule. */
|
|
1299
|
+
.fdy-table-scroll--frozen{position:relative;overflow:auto;max-height:var(--fdy-table-frozen-h,30rem);}
|
|
1300
|
+
|
|
1277
1301
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
1278
1302
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
1279
1303
|
.fdy-table-scroll{position:relative;overflow-x:auto;}
|
package/dist/freeday.js
CHANGED
|
@@ -1364,8 +1364,8 @@
|
|
|
1364
1364
|
wrap.dataset.fdyDpReady = '1';
|
|
1365
1365
|
wrap.classList.add('fdy-datepicker');
|
|
1366
1366
|
|
|
1367
|
-
var placeholder = wrap.getAttribute('data-placeholder') || '
|
|
1368
|
-
var label = wrap.getAttribute('data-label') || '
|
|
1367
|
+
var placeholder = wrap.getAttribute('data-placeholder') || 'Choose a date';
|
|
1368
|
+
var label = wrap.getAttribute('data-label') || 'Date';
|
|
1369
1369
|
var selected = parseISO(wrap.getAttribute('data-value'));
|
|
1370
1370
|
var minDate = parseISO(wrap.getAttribute('data-min'));
|
|
1371
1371
|
var maxDate = parseISO(wrap.getAttribute('data-max'));
|
|
@@ -1380,6 +1380,7 @@
|
|
|
1380
1380
|
month" twenty-four times to reach a date two years back. */
|
|
1381
1381
|
var mode = 'days';
|
|
1382
1382
|
var focusMonth = null;
|
|
1383
|
+
var focusYear = null;
|
|
1383
1384
|
|
|
1384
1385
|
var trigger = document.createElement('button');
|
|
1385
1386
|
trigger.type = 'button';
|
|
@@ -1457,20 +1458,21 @@
|
|
|
1457
1458
|
}
|
|
1458
1459
|
|
|
1459
1460
|
function render() {
|
|
1461
|
+
if (mode === 'years') { renderYears(); return; }
|
|
1460
1462
|
if (mode === 'months') { renderMonths(); return; }
|
|
1461
1463
|
panel.innerHTML = '';
|
|
1462
1464
|
var head = document.createElement('div');
|
|
1463
1465
|
head.className = 'fdy-cal__head';
|
|
1464
|
-
var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ',
|
|
1466
|
+
var title = titleButton(monthFmt.format(view), monthFmt.format(view) + ', choose month', function () {
|
|
1465
1467
|
mode = 'months';
|
|
1466
1468
|
focusMonth = view.getMonth();
|
|
1467
1469
|
render();
|
|
1468
1470
|
focusMonthCell();
|
|
1469
1471
|
});
|
|
1470
1472
|
panel.setAttribute('aria-labelledby', title.id);
|
|
1471
|
-
head.appendChild(navButton('‹', '
|
|
1473
|
+
head.appendChild(navButton('‹', 'Previous month', function () { view = addMonths(view, -1); render(); }));
|
|
1472
1474
|
head.appendChild(title);
|
|
1473
|
-
head.appendChild(navButton('›', '
|
|
1475
|
+
head.appendChild(navButton('›', 'Next month', function () { view = addMonths(view, 1); render(); }));
|
|
1474
1476
|
panel.appendChild(head);
|
|
1475
1477
|
|
|
1476
1478
|
var grid = document.createElement('div');
|
|
@@ -1527,15 +1529,16 @@
|
|
|
1527
1529
|
var year = view.getFullYear();
|
|
1528
1530
|
var head = document.createElement('div');
|
|
1529
1531
|
head.className = 'fdy-cal__head';
|
|
1530
|
-
var title = titleButton(String(year), year + ',
|
|
1531
|
-
mode = '
|
|
1532
|
+
var title = titleButton(String(year), year + ', choose year', function () {
|
|
1533
|
+
mode = 'years';
|
|
1534
|
+
focusYear = year;
|
|
1532
1535
|
render();
|
|
1533
|
-
|
|
1536
|
+
focusYearCell();
|
|
1534
1537
|
});
|
|
1535
1538
|
panel.setAttribute('aria-labelledby', title.id);
|
|
1536
|
-
head.appendChild(navButton('‹', '
|
|
1539
|
+
head.appendChild(navButton('‹', 'Previous year', function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
|
|
1537
1540
|
head.appendChild(title);
|
|
1538
|
-
head.appendChild(navButton('›', '
|
|
1541
|
+
head.appendChild(navButton('›', 'Next year', function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
|
|
1539
1542
|
panel.appendChild(head);
|
|
1540
1543
|
|
|
1541
1544
|
var grid = document.createElement('div');
|
|
@@ -1566,6 +1569,108 @@
|
|
|
1566
1569
|
panel.appendChild(grid);
|
|
1567
1570
|
}
|
|
1568
1571
|
|
|
1572
|
+
/* A page of twelve years, aligned so pages TILE: 2016-2027, 2028-2039. Anchoring the page on
|
|
1573
|
+
the year in view instead would make ‹ and › land on overlapping windows, and the same year
|
|
1574
|
+
would appear at a different spot every time you stepped. */
|
|
1575
|
+
var YEARS_PER_PAGE = 12;
|
|
1576
|
+
function yearPageStart(y) { return Math.floor(y / YEARS_PER_PAGE) * YEARS_PER_PAGE; }
|
|
1577
|
+
|
|
1578
|
+
/* A year is only unreachable when EVERY month in it falls outside min/max — the same rule
|
|
1579
|
+
monthDisabled applies one level down, for the same reason: a bound that sits inside the year
|
|
1580
|
+
disables neither end. */
|
|
1581
|
+
function yearDisabled(y) {
|
|
1582
|
+
if (minDate && new Date(y, 11, 31) < startOfDay(minDate)) return true;
|
|
1583
|
+
if (maxDate && new Date(y, 0, 1) > startOfDay(maxDate)) return true;
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function renderYears() {
|
|
1588
|
+
panel.innerHTML = '';
|
|
1589
|
+
var start = yearPageStart(view.getFullYear());
|
|
1590
|
+
var end = start + YEARS_PER_PAGE - 1;
|
|
1591
|
+
var head = document.createElement('div');
|
|
1592
|
+
head.className = 'fdy-cal__head';
|
|
1593
|
+
var title = titleButton(start + ' – ' + end, start + ' to ' + end + ', back to months', function () {
|
|
1594
|
+
mode = 'months';
|
|
1595
|
+
focusMonth = view.getMonth();
|
|
1596
|
+
render();
|
|
1597
|
+
focusMonthCell();
|
|
1598
|
+
});
|
|
1599
|
+
panel.setAttribute('aria-labelledby', title.id);
|
|
1600
|
+
head.appendChild(navButton('‹', 'Previous years', function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
|
|
1601
|
+
head.appendChild(title);
|
|
1602
|
+
head.appendChild(navButton('›', 'Next years', function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
|
|
1603
|
+
panel.appendChild(head);
|
|
1604
|
+
|
|
1605
|
+
var grid = document.createElement('div');
|
|
1606
|
+
grid.className = 'fdy-cal__grid fdy-cal__grid--years';
|
|
1607
|
+
grid.setAttribute('role', 'grid');
|
|
1608
|
+
grid.setAttribute('aria-labelledby', title.id);
|
|
1609
|
+
var thisYear = new Date().getFullYear();
|
|
1610
|
+
if (focusYear === null) focusYear = view.getFullYear();
|
|
1611
|
+
for (var i = 0; i < YEARS_PER_PAGE; i++) {
|
|
1612
|
+
var y = start + i;
|
|
1613
|
+
var btn = document.createElement('button');
|
|
1614
|
+
btn.type = 'button';
|
|
1615
|
+
btn.className = 'fdy-cal__year';
|
|
1616
|
+
btn.setAttribute('role', 'gridcell');
|
|
1617
|
+
btn.setAttribute('aria-label', String(y));
|
|
1618
|
+
btn.textContent = String(y);
|
|
1619
|
+
if (y === thisYear) btn.classList.add('is-today');
|
|
1620
|
+
if (selected && selected.getFullYear() === y) {
|
|
1621
|
+
btn.classList.add('is-selected');
|
|
1622
|
+
btn.setAttribute('aria-selected', 'true');
|
|
1623
|
+
}
|
|
1624
|
+
if (yearDisabled(y)) btn.disabled = true;
|
|
1625
|
+
btn.tabIndex = y === focusYear ? 0 : -1;
|
|
1626
|
+
btn.addEventListener('click', (function (yy) { return function () { openYear(yy); }; })(y));
|
|
1627
|
+
grid.appendChild(btn);
|
|
1628
|
+
}
|
|
1629
|
+
grid.addEventListener('keydown', onYearKey);
|
|
1630
|
+
panel.appendChild(grid);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function focusYearCell() {
|
|
1634
|
+
var cell = panel.querySelector('.fdy-cal__year[tabindex="0"]');
|
|
1635
|
+
if (cell) cell.focus();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/* Picking a year is navigation, exactly like picking a month: it drops to the month grid of
|
|
1639
|
+
that year. Nothing is committed until a DAY is chosen. */
|
|
1640
|
+
function openYear(y) {
|
|
1641
|
+
view = new Date(y, view.getMonth(), 1);
|
|
1642
|
+
focusMonth = view.getMonth();
|
|
1643
|
+
mode = 'months';
|
|
1644
|
+
render();
|
|
1645
|
+
focusMonthCell();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function moveYearFocus(y) {
|
|
1649
|
+
focusYear = y;
|
|
1650
|
+
view = new Date(y, view.getMonth(), 1);
|
|
1651
|
+
render();
|
|
1652
|
+
focusYearCell();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function onYearKey(e) {
|
|
1656
|
+
var handled = true;
|
|
1657
|
+
switch (e.key) {
|
|
1658
|
+
case 'ArrowLeft': moveYearFocus(focusYear - 1); break;
|
|
1659
|
+
case 'ArrowRight': moveYearFocus(focusYear + 1); break;
|
|
1660
|
+
case 'ArrowUp': moveYearFocus(focusYear - 3); break;
|
|
1661
|
+
case 'ArrowDown': moveYearFocus(focusYear + 3); break;
|
|
1662
|
+
case 'Home': moveYearFocus(yearPageStart(focusYear)); break;
|
|
1663
|
+
case 'End': moveYearFocus(yearPageStart(focusYear) + YEARS_PER_PAGE - 1); break;
|
|
1664
|
+
case 'PageUp': moveYearFocus(focusYear - YEARS_PER_PAGE); break;
|
|
1665
|
+
case 'PageDown': moveYearFocus(focusYear + YEARS_PER_PAGE); break;
|
|
1666
|
+
case 'Enter':
|
|
1667
|
+
case ' ': openYear(focusYear); break;
|
|
1668
|
+
case 'Escape': close(true); break;
|
|
1669
|
+
default: handled = false;
|
|
1670
|
+
}
|
|
1671
|
+
if (handled) { e.preventDefault(); e.stopPropagation(); }
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1569
1674
|
function focusMonthCell() {
|
|
1570
1675
|
var cell = panel.querySelector('.fdy-cal__month[tabindex="0"]');
|
|
1571
1676
|
if (cell) cell.focus();
|
package/package.json
CHANGED
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
.fdy-cal__title{flex:1;appearance:none;border:0;background:transparent;margin:0;padding:var(--space-1) var(--space-2);border-radius:var(--radius-sm);text-align:center;font-family:inherit;font-weight:var(--weight-semibold);font-size:var(--text-sm);color:var(--color-text);text-transform:capitalize;cursor:pointer;}
|
|
35
35
|
.fdy-cal__title:hover{background:var(--color-surface-2);}
|
|
36
36
|
.fdy-cal__title:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
37
|
-
.fdy-cal__month{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
38
|
-
.fdy-cal__month:hover{background:var(--color-surface-2);}
|
|
39
|
-
.fdy-cal__month:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
40
|
-
.fdy-cal__month:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
41
|
-
.fdy-cal__month.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
42
|
-
.fdy-cal__month.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
37
|
+
.fdy-cal__month,.fdy-cal__year{display:inline-flex;align-items:center;justify-content:center;height:2.25rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-family:inherit;font-size:var(--text-sm);text-transform:capitalize;cursor:pointer;}
|
|
38
|
+
.fdy-cal__month:hover,.fdy-cal__year:hover{background:var(--color-surface-2);}
|
|
39
|
+
.fdy-cal__month:focus-visible,.fdy-cal__year:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
40
|
+
.fdy-cal__month:disabled,.fdy-cal__year:disabled{opacity:.4;cursor:not-allowed;background:transparent;}
|
|
41
|
+
.fdy-cal__month.is-today,.fdy-cal__year.is-today{font-weight:var(--weight-bold);box-shadow:inset 0 0 0 1.5px var(--color-border-strong);}
|
|
42
|
+
.fdy-cal__month.is-selected,.fdy-cal__year.is-selected{background:var(--color-primary);color:var(--color-on-primary);font-weight:var(--weight-semibold);}
|
|
43
43
|
.fdy-cal__nav{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;flex:none;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text-muted);font-size:var(--text-lg);line-height:1;cursor:pointer;}
|
|
44
44
|
.fdy-cal__nav:hover{background:var(--color-surface-2);color:var(--color-text);}
|
|
45
45
|
.fdy-cal__nav:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary) 26%,transparent);}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
AFTER `.fdy-cal__grid`, and that is the whole rule: the element carries both classes, they weigh
|
|
51
51
|
the same, so source order decides. Declared earlier, the modifier lost to the base it modifies and
|
|
52
52
|
twelve months rendered seven across — the layout the comment above described, never once applied. */
|
|
53
|
-
.fdy-cal__grid--months{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
53
|
+
.fdy-cal__grid--months,.fdy-cal__grid--years{grid-template-columns:repeat(3,1fr);gap:var(--space-1);}
|
|
54
54
|
.fdy-cal__dow{display:flex;align-items:center;justify-content:center;height:1.75rem;font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--color-text-muted);text-transform:capitalize;}
|
|
55
55
|
.fdy-cal__day{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--color-text);font-size:var(--text-sm);font-variant-numeric:tabular-nums;cursor:pointer;}
|
|
56
56
|
.fdy-cal__day:hover{background:var(--color-surface-2);}
|
package/src/components/table.css
CHANGED
|
@@ -37,6 +37,30 @@
|
|
|
37
37
|
.fdy-table__selcol{width:1px;white-space:nowrap;}
|
|
38
38
|
.fdy-table tbody tr[aria-selected="true"],.fdy-table tbody tr[aria-selected="true"]:hover{background:var(--color-primary-soft);}
|
|
39
39
|
|
|
40
|
+
/* Frozen header row and/or first column — a rate matrix, a timetable, any grid read
|
|
41
|
+
against two axes at once.
|
|
42
|
+
|
|
43
|
+
This modifier exists because `position:sticky` alone is NOT enough. With
|
|
44
|
+
`border-collapse:collapse` the shared border belongs to the TABLE, not to either
|
|
45
|
+
cell, so a frozen cell scrolls out from under its own rule and the frozen column
|
|
46
|
+
loses the line that separates it from the data. Separate borders put each border
|
|
47
|
+
back on the cell that sticks. */
|
|
48
|
+
.fdy-table--sticky{border-collapse:separate;border-spacing:0;}
|
|
49
|
+
.fdy-table--sticky th,.fdy-table--sticky td{border-bottom:var(--bw) solid var(--color-border-muted);}
|
|
50
|
+
.fdy-table--sticky thead th{position:sticky;top:0;z-index:2;border-bottom:var(--bw) solid var(--color-border);}
|
|
51
|
+
/* The row header freezes sideways over the data cells, so it needs an opaque
|
|
52
|
+
background of its own — and it takes the BODY surface, not the header one, so a
|
|
53
|
+
hovered row stays one continuous band across the freeze line. Losing that is
|
|
54
|
+
losing the only way to track a row across a wide grid. */
|
|
55
|
+
.fdy-table--sticky tbody th{position:sticky;left:0;z-index:1;background:var(--color-surface);border-right:var(--bw) solid var(--color-border);}
|
|
56
|
+
.fdy-table--sticky tbody tr:hover th{background:var(--color-surface-2);}
|
|
57
|
+
/* The corner cell is frozen on both axes, so it has to outrank both. */
|
|
58
|
+
.fdy-table--sticky thead th:first-child{left:0;z-index:3;border-right:var(--bw) solid var(--color-border);}
|
|
59
|
+
|
|
60
|
+
/* The scrollport a sticky cell sticks to. Both axes scroll here, and the height is a
|
|
61
|
+
custom property so a page can size it without out-specifying this rule. */
|
|
62
|
+
.fdy-table-scroll--frozen{position:relative;overflow:auto;max-height:var(--fdy-table-frozen-h,30rem);}
|
|
63
|
+
|
|
40
64
|
/* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
|
|
41
65
|
.fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
|
|
42
66
|
.fdy-table-scroll{position:relative;overflow-x:auto;}
|