@cahyo-dimas/freeday 3.8.0 → 3.9.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 +104 -0
- package/COMPONENTS.md +112 -29
- package/README.id.md +4 -4
- package/README.md +4 -3
- package/adapters/blazor/FdyDatepicker.razor.cs +15 -1
- package/adapters/blazor/FdyMonthpicker.razor +29 -0
- package/adapters/blazor/FdyMonthpicker.razor.cs +102 -0
- package/adapters/blazor/FdyTable.razor +6 -1
- package/adapters/blazor/FdyTable.razor.cs +33 -0
- package/adapters/core/month-grid.d.ts +116 -0
- package/adapters/core/month-grid.js +234 -0
- package/adapters/react/components/FdyDatepicker.tsx +78 -160
- package/adapters/react/components/FdyMonthpicker.tsx +400 -0
- package/adapters/react/components/FdyTable.tsx +38 -2
- package/adapters/react/components/FdyTableFilter.tsx +13 -1
- package/adapters/react/index.d.ts +5 -1
- package/adapters/react/index.js +1 -0
- package/adapters/react/usePopover.ts +54 -9
- package/adapters/vue/components/FdyDatepicker.vue +72 -159
- package/adapters/vue/components/FdyMonthpicker.vue +387 -0
- package/adapters/vue/components/FdyTable.vue +33 -1
- package/adapters/vue/components/FdyTableFilter.vue +13 -1
- package/adapters/vue/index.d.ts +16 -0
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/usePopover.ts +53 -10
- package/dist/freeday-table.js +64 -5
- package/dist/freeday.bundle.css +19 -2
- package/dist/freeday.css +19 -2
- package/dist/freeday.js +64 -5
- package/docs/agent-onboarding.md +3 -3
- package/docs/getting-started.md +6 -6
- package/docs/integrations.md +22 -4
- package/package.json +2 -2
- package/src/components/table.css +19 -2
|
@@ -116,6 +116,39 @@ public partial class FdyTable<TRow>
|
|
|
116
116
|
/// <summary>Bulk-bar actions (Blazor equivalent of Vue's <c>bulk-actions</c> slot).</summary>
|
|
117
117
|
[Parameter] public RenderFragment? BulkContent { get; set; }
|
|
118
118
|
|
|
119
|
+
// The footer's six strings, forwarded to the FdyTableFooter this table draws itself. 3.8.0 gave
|
|
120
|
+
// the footer those parameters and this table passed none of them on, so the hatch reached every
|
|
121
|
+
// footer except the one almost every page uses, and the documented way out was to switch the
|
|
122
|
+
// footer off and render it yourself: a door that opens only if you stop using the table. Flat
|
|
123
|
+
// parameters carrying the footer's own names, so moving between the two routes moves a
|
|
124
|
+
// parameter rather than reshaping it.
|
|
125
|
+
//
|
|
126
|
+
// Nullable, unlike SelectedText and its four neighbours above, which this component renders
|
|
127
|
+
// itself and therefore defaults itself. These six are rendered by FdyTableFooter, which already
|
|
128
|
+
// holds their English; a default repeated here would be a second copy to keep in step, and the
|
|
129
|
+
// two copies would disagree the first time one of them was edited.
|
|
130
|
+
|
|
131
|
+
/// <summary>Footer range line, <c>{from}</c> <c>{to}</c> <c>{total}</c> substituted.
|
|
132
|
+
/// Null leaves <c>FdyTableFooter</c>'s default, <c>Showing {from}–{to} of {total}</c>.</summary>
|
|
133
|
+
[Parameter] public string? InfoText { get; set; }
|
|
134
|
+
|
|
135
|
+
/// <summary>Footer: the visible word beside the rows-per-page control, which also names it.
|
|
136
|
+
/// Null leaves the default, <c>Rows</c>.</summary>
|
|
137
|
+
[Parameter] public string? PageSizeLabel { get; set; }
|
|
138
|
+
|
|
139
|
+
/// <summary>Footer: accessible name of the pager landmark. Null leaves <c>Pagination</c>.</summary>
|
|
140
|
+
[Parameter] public string? PaginationLabel { get; set; }
|
|
141
|
+
|
|
142
|
+
/// <summary>Footer: accessible name of the previous-page arrow. Null leaves <c>Previous page</c>.</summary>
|
|
143
|
+
[Parameter] public string? PrevLabel { get; set; }
|
|
144
|
+
|
|
145
|
+
/// <summary>Footer: accessible name of the next-page arrow. Null leaves <c>Next page</c>.</summary>
|
|
146
|
+
[Parameter] public string? NextLabel { get; set; }
|
|
147
|
+
|
|
148
|
+
/// <summary>Footer: accessible name of a numbered page button, <c>{n}</c> substituted. Null
|
|
149
|
+
/// leaves <c>Go to page {n}</c>.</summary>
|
|
150
|
+
[Parameter] public string? PageLabel { get; set; }
|
|
151
|
+
|
|
119
152
|
private FdySortState? _internalSort;
|
|
120
153
|
private Dictionary<string, FdyColumnFilter> _internalFilters = new();
|
|
121
154
|
private int _internalPageIndex;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Types for the framework-agnostic month and year grid logic (adapters/core/month-grid.js).
|
|
2
|
+
// Shared by FdyDatepicker and FdyMonthpicker in the Vue and React adapters.
|
|
3
|
+
|
|
4
|
+
/** A whole month as ISO `YYYY-MM`, e.g. `'2026-10'`. The format a month picker answers with. */
|
|
5
|
+
export type FdyMonth = string;
|
|
6
|
+
|
|
7
|
+
/** One cell of the twelve-cell month grid, ready to render. */
|
|
8
|
+
export interface FdyMonthCell {
|
|
9
|
+
/** 0-11, the month's index in its year. */
|
|
10
|
+
index: number;
|
|
11
|
+
/** What the cell shows, a short month name in the active locale. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The cell's accessible name: the month and its year, spelled out in the active locale. */
|
|
14
|
+
ariaLabel: string;
|
|
15
|
+
selected: boolean;
|
|
16
|
+
today: boolean;
|
|
17
|
+
/** The whole month falls outside min/max, so it cannot be picked or navigated into. */
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
/** Holds the grid's single `tabindex="0"`. */
|
|
20
|
+
focusable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One cell of the twelve-cell year grid. */
|
|
24
|
+
export interface FdyYearCell {
|
|
25
|
+
year: number;
|
|
26
|
+
selected: boolean;
|
|
27
|
+
today: boolean;
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
focusable: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The two Intl formatters a month grid renders through. */
|
|
33
|
+
export interface FdyMonthFormatters {
|
|
34
|
+
/** Short month name, for the cell's text. */
|
|
35
|
+
cell: Intl.DateTimeFormat;
|
|
36
|
+
/** Month and year spelled out, for the cell's accessible name and the trigger's value. */
|
|
37
|
+
name: Intl.DateTimeFormat;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FdyMonthGridView {
|
|
41
|
+
year: number;
|
|
42
|
+
selected: Date | null;
|
|
43
|
+
min: Date | null;
|
|
44
|
+
max: Date | null;
|
|
45
|
+
/** Which month index holds the roving tabindex. */
|
|
46
|
+
focusMonth: number;
|
|
47
|
+
formatters: FdyMonthFormatters;
|
|
48
|
+
/** Overridable only so a test can pin "today"; defaults to now. */
|
|
49
|
+
today?: Date;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FdyYearGridView {
|
|
53
|
+
pageStart: number;
|
|
54
|
+
selected: Date | null;
|
|
55
|
+
min: Date | null;
|
|
56
|
+
max: Date | null;
|
|
57
|
+
focusYear: number;
|
|
58
|
+
today?: Date;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What a key press in a grid means. `choose` is the one the two components answer differently:
|
|
63
|
+
* `FdyMonthpicker` commits the month, `FdyDatepicker` walks down into its days.
|
|
64
|
+
*/
|
|
65
|
+
export type FdyGridAction = 'focus' | 'choose' | 'close' | 'none';
|
|
66
|
+
|
|
67
|
+
export interface FdyMonthKeyIntent {
|
|
68
|
+
action: FdyGridAction;
|
|
69
|
+
/** 0-11, already wrapped inside the year on screen. */
|
|
70
|
+
index: number;
|
|
71
|
+
/** Years to step the view by, which only PageUp and PageDown set. */
|
|
72
|
+
yearDelta: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FdyYearKeyIntent {
|
|
76
|
+
action: FdyGridAction;
|
|
77
|
+
year: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A year page holds twelve years, so the grid matches the month grid's shape. */
|
|
81
|
+
export declare const YEARS_PER_PAGE: number;
|
|
82
|
+
|
|
83
|
+
/** A Date as `YYYY-MM`. The day is dropped. */
|
|
84
|
+
export declare function toISOMonth(d: Date): FdyMonth;
|
|
85
|
+
|
|
86
|
+
/** Read `YYYY-MM` or `YYYY-MM-DD` as the first day of the month named; null when unparseable. */
|
|
87
|
+
export declare function parseISOMonth(s: string | null | undefined): Date | null;
|
|
88
|
+
|
|
89
|
+
/** The `lang` the page declares, then `en`. Never the browser's language, which is the whole point. */
|
|
90
|
+
export declare function pageLocale(): string;
|
|
91
|
+
|
|
92
|
+
export declare function startOfDay(d: Date): Date;
|
|
93
|
+
export declare function startOfMonth(d: Date): Date;
|
|
94
|
+
export declare function addMonths(d: Date, n: number): Date;
|
|
95
|
+
|
|
96
|
+
/** True only when the WHOLE month falls outside the bounds. */
|
|
97
|
+
export declare function monthDisabled(year: number, month: number, min: Date | null, max: Date | null): boolean;
|
|
98
|
+
|
|
99
|
+
/** True only when every month in the year falls outside the bounds. */
|
|
100
|
+
export declare function yearDisabled(year: number, min: Date | null, max: Date | null): boolean;
|
|
101
|
+
|
|
102
|
+
/** Where the aligned page holding this year starts (2016 for 2026). */
|
|
103
|
+
export declare function yearPageStart(year: number): number;
|
|
104
|
+
|
|
105
|
+
/** The year grid's title, naming the page it shows. */
|
|
106
|
+
export declare function yearRangeLabel(pageStart: number): string;
|
|
107
|
+
|
|
108
|
+
export declare function monthFormatters(locale: string): FdyMonthFormatters;
|
|
109
|
+
|
|
110
|
+
export declare function monthCells(view: FdyMonthGridView): FdyMonthCell[];
|
|
111
|
+
|
|
112
|
+
export declare function yearCells(view: FdyYearGridView): FdyYearCell[];
|
|
113
|
+
|
|
114
|
+
export declare function monthKeyIntent(key: string, focusMonth: number): FdyMonthKeyIntent;
|
|
115
|
+
|
|
116
|
+
export declare function yearKeyIntent(key: string, focusYear: number): FdyYearKeyIntent;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Freeday, month and year grid logic, framework-agnostic (pure, zero dependencies).
|
|
2
|
+
//
|
|
3
|
+
// The twelve-cell month grid and the twelve-cell year grid behind it: which cell is selected, which
|
|
4
|
+
// is out of bounds, how a year page is numbered, and what a key press means. Shared by
|
|
5
|
+
// `FdyDatepicker` (where both grids are navigation on the way to a day) and `FdyMonthpicker` (where
|
|
6
|
+
// the month grid is the answer), in Vue and in React, so four components read one copy.
|
|
7
|
+
//
|
|
8
|
+
// It is here rather than in each component because of what this release cost. The panel-position
|
|
9
|
+
// logic existed in four copies; #079 was found in one, fixed in one, and shipped broken in the
|
|
10
|
+
// other three, and three items of 3.9.0 went on cleaning that up. `FdyMonthpicker` needs the month
|
|
11
|
+
// grid, which would have made the fifth copy of it, so the rule moved out first.
|
|
12
|
+
//
|
|
13
|
+
// The vanilla enhancer (src/freeday-datepicker.js) deliberately does NOT import this, for the
|
|
14
|
+
// reason adapters/core/app-shell.js records: dist/freeday.js is a plain IIFE concatenation with no
|
|
15
|
+
// module system. That leaves two implementations, and browser/month-mode.mjs and
|
|
16
|
+
// browser/monthpicker.mjs hold both to the same observable behaviour.
|
|
17
|
+
//
|
|
18
|
+
// Everything here is a pure function over its arguments. State, refs, focus scheduling and markup
|
|
19
|
+
// stay with the caller, because those are the parts Vue and React genuinely do differently.
|
|
20
|
+
|
|
21
|
+
/** A year page holds twelve years, so the grid is the same shape as the month grid above it. */
|
|
22
|
+
export const YEARS_PER_PAGE = 12;
|
|
23
|
+
|
|
24
|
+
function pad(n) {
|
|
25
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A Date as the `YYYY-MM` a month picker reports. The day is dropped, which is the point. */
|
|
29
|
+
export function toISOMonth(d) {
|
|
30
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read `YYYY-MM` or `YYYY-MM-DD` as the first day of the month named.
|
|
35
|
+
*
|
|
36
|
+
* Both shapes on purpose, and the dropped day is why: a bound of `2026-03` lands on March 1st,
|
|
37
|
+
* which `monthDisabled` then compares a whole month against correctly at either end, so an app can
|
|
38
|
+
* pass the strings it already stores. A full date still works for a form that keeps one format
|
|
39
|
+
* across all of its bounds.
|
|
40
|
+
*
|
|
41
|
+
* @param {string | null | undefined} s
|
|
42
|
+
* @returns {Date | null} null for an empty, short or unparseable value
|
|
43
|
+
*/
|
|
44
|
+
export function parseISOMonth(s) {
|
|
45
|
+
if (s === null || s === undefined || s === '') return null;
|
|
46
|
+
const parts = s.split('-');
|
|
47
|
+
if (parts.length < 2) return null;
|
|
48
|
+
const d = new Date(Number(parts[0]), Number(parts[1]) - 1, parts.length > 2 ? Number(parts[2]) : 1);
|
|
49
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The language the page declares, which is what COMPONENTS.md has always promised and what the
|
|
54
|
+
* vanilla enhancer has always done.
|
|
55
|
+
*
|
|
56
|
+
* Handing `undefined` to Intl instead falls back to the BROWSER's language, so an Indonesian reader
|
|
57
|
+
* saw Indonesian month names inside a kit that is English on every other path, and an Indonesian
|
|
58
|
+
* page read by an English browser got English ones. That second case is the symptom the month
|
|
59
|
+
* picker was asked for, so the fallback has to be the page.
|
|
60
|
+
*/
|
|
61
|
+
export function pageLocale() {
|
|
62
|
+
if (typeof document === 'undefined') return 'en';
|
|
63
|
+
// `||`, not `??`: an empty `lang=""` is not null and Intl throws RangeError on it.
|
|
64
|
+
return document.documentElement.getAttribute('lang') || 'en';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function startOfDay(d) {
|
|
68
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function startOfMonth(d) {
|
|
72
|
+
return new Date(d.getFullYear(), d.getMonth(), 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function addMonths(d, n) {
|
|
76
|
+
const x = new Date(d);
|
|
77
|
+
x.setMonth(x.getMonth() + n);
|
|
78
|
+
return x;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Is this whole month outside the bounds?
|
|
83
|
+
*
|
|
84
|
+
* Only when the WHOLE month is, which is why it reads the bounds directly instead of asking whether
|
|
85
|
+
* both ends of the month are disabled days: a range that sits INSIDE one month disables both of its
|
|
86
|
+
* ends while the middle stays perfectly selectable.
|
|
87
|
+
*
|
|
88
|
+
* @param {number} year
|
|
89
|
+
* @param {number} month 0-11
|
|
90
|
+
* @param {Date | null} min
|
|
91
|
+
* @param {Date | null} max
|
|
92
|
+
*/
|
|
93
|
+
export function monthDisabled(year, month, min, max) {
|
|
94
|
+
const first = new Date(year, month, 1);
|
|
95
|
+
const last = new Date(year, month + 1, 0);
|
|
96
|
+
if (min !== null && startOfDay(last).getTime() < startOfDay(min).getTime()) return true;
|
|
97
|
+
if (max !== null && startOfDay(first).getTime() > startOfDay(max).getTime()) return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Unreachable only when EVERY month in the year is, the same rule one level up. */
|
|
102
|
+
export function yearDisabled(year, min, max) {
|
|
103
|
+
if (min !== null && startOfDay(new Date(year, 11, 31)).getTime() < startOfDay(min).getTime()) return true;
|
|
104
|
+
if (max !== null && startOfDay(new Date(year, 0, 1)).getTime() > startOfDay(max).getTime()) return true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Where the page holding this year starts: 2016 for 2026, so pages tile as 2016-2027, 2028-2039.
|
|
110
|
+
* Anchoring the page on the year in view would make the arrows land on overlapping windows, and the
|
|
111
|
+
* same year would sit somewhere different every step.
|
|
112
|
+
*/
|
|
113
|
+
export function yearPageStart(year) {
|
|
114
|
+
return Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The year grid's title, naming the page it shows. */
|
|
118
|
+
export function yearRangeLabel(pageStart) {
|
|
119
|
+
return `${pageStart} – ${pageStart + YEARS_PER_PAGE - 1}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The two Intl formatters a month grid needs: `cell` writes what a reader sees in the cell, `name`
|
|
124
|
+
* writes its accessible name and the value in the trigger. Built once per locale by the caller,
|
|
125
|
+
* because both frameworks memoize and `new Intl.DateTimeFormat` is not cheap.
|
|
126
|
+
*/
|
|
127
|
+
export function monthFormatters(locale) {
|
|
128
|
+
return {
|
|
129
|
+
cell: new Intl.DateTimeFormat(locale, { month: 'short' }),
|
|
130
|
+
name: new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The twelve cells of one year's month grid.
|
|
136
|
+
*
|
|
137
|
+
* @param {{ year: number, selected: Date | null, min: Date | null, max: Date | null,
|
|
138
|
+
* focusMonth: number, formatters: { cell: Intl.DateTimeFormat, name: Intl.DateTimeFormat },
|
|
139
|
+
* today?: Date }} view
|
|
140
|
+
*/
|
|
141
|
+
export function monthCells(view) {
|
|
142
|
+
const today = view.today ?? new Date();
|
|
143
|
+
return Array.from({ length: 12 }, (_unused, index) => {
|
|
144
|
+
const cellDate = new Date(view.year, index, 1);
|
|
145
|
+
return {
|
|
146
|
+
index,
|
|
147
|
+
label: view.formatters.cell.format(cellDate),
|
|
148
|
+
ariaLabel: view.formatters.name.format(cellDate),
|
|
149
|
+
selected: view.selected !== null && view.selected.getFullYear() === view.year && view.selected.getMonth() === index,
|
|
150
|
+
today: today.getFullYear() === view.year && today.getMonth() === index,
|
|
151
|
+
disabled: monthDisabled(view.year, index, view.min, view.max),
|
|
152
|
+
focusable: index === view.focusMonth,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The twelve cells of one year page.
|
|
159
|
+
*
|
|
160
|
+
* @param {{ pageStart: number, selected: Date | null, min: Date | null, max: Date | null,
|
|
161
|
+
* focusYear: number, today?: Date }} view
|
|
162
|
+
*/
|
|
163
|
+
export function yearCells(view) {
|
|
164
|
+
const thisYear = (view.today ?? new Date()).getFullYear();
|
|
165
|
+
return Array.from({ length: YEARS_PER_PAGE }, (_unused, index) => {
|
|
166
|
+
const year = view.pageStart + index;
|
|
167
|
+
return {
|
|
168
|
+
year,
|
|
169
|
+
selected: view.selected !== null && view.selected.getFullYear() === year,
|
|
170
|
+
today: thisYear === year,
|
|
171
|
+
disabled: yearDisabled(year, view.min, view.max),
|
|
172
|
+
focusable: year === view.focusYear,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What a key press in the month grid means, with the wrap already applied.
|
|
179
|
+
*
|
|
180
|
+
* `choose` is deliberately not called `select`: whether choosing a month commits a value or walks
|
|
181
|
+
* down into its days is the one thing the two components disagree about, and that decision belongs
|
|
182
|
+
* to them. `none` means the key was not ours, so the caller leaves the event alone.
|
|
183
|
+
*
|
|
184
|
+
* Arrows move one or three months and WRAP inside the year on screen, which is why `index` can come
|
|
185
|
+
* back on the far side without `yearDelta` moving. PageUp and PageDown are the keys that change the
|
|
186
|
+
* year, holding the month still (WAI-ARIA APG).
|
|
187
|
+
*
|
|
188
|
+
* @param {string} key a KeyboardEvent.key
|
|
189
|
+
* @param {number} focusMonth 0-11
|
|
190
|
+
* @returns {{ action: 'focus' | 'choose' | 'close' | 'none', index: number, yearDelta: number }}
|
|
191
|
+
*/
|
|
192
|
+
export function monthKeyIntent(key, focusMonth) {
|
|
193
|
+
const focus = (raw, yearDelta) => ({ action: 'focus', index: (raw + 12) % 12, yearDelta });
|
|
194
|
+
switch (key) {
|
|
195
|
+
case 'ArrowLeft': return focus(focusMonth - 1, 0);
|
|
196
|
+
case 'ArrowRight': return focus(focusMonth + 1, 0);
|
|
197
|
+
case 'ArrowUp': return focus(focusMonth - 3, 0);
|
|
198
|
+
case 'ArrowDown': return focus(focusMonth + 3, 0);
|
|
199
|
+
case 'Home': return focus(0, 0);
|
|
200
|
+
case 'End': return focus(11, 0);
|
|
201
|
+
case 'PageUp': return focus(focusMonth, -1);
|
|
202
|
+
case 'PageDown': return focus(focusMonth, 1);
|
|
203
|
+
case 'Enter':
|
|
204
|
+
case ' ': return { action: 'choose', index: focusMonth, yearDelta: 0 };
|
|
205
|
+
case 'Escape': return { action: 'close', index: focusMonth, yearDelta: 0 };
|
|
206
|
+
default: return { action: 'none', index: focusMonth, yearDelta: 0 };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* What a key press in the year grid means. `choose` here is always navigation, in both components:
|
|
212
|
+
* a year is never an answer, so nothing is committed from this grid.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} key a KeyboardEvent.key
|
|
215
|
+
* @param {number} focusYear
|
|
216
|
+
* @returns {{ action: 'focus' | 'choose' | 'close' | 'none', year: number }}
|
|
217
|
+
*/
|
|
218
|
+
export function yearKeyIntent(key, focusYear) {
|
|
219
|
+
const focus = (year) => ({ action: 'focus', year });
|
|
220
|
+
switch (key) {
|
|
221
|
+
case 'ArrowLeft': return focus(focusYear - 1);
|
|
222
|
+
case 'ArrowRight': return focus(focusYear + 1);
|
|
223
|
+
case 'ArrowUp': return focus(focusYear - 3);
|
|
224
|
+
case 'ArrowDown': return focus(focusYear + 3);
|
|
225
|
+
case 'Home': return focus(yearPageStart(focusYear));
|
|
226
|
+
case 'End': return focus(yearPageStart(focusYear) + YEARS_PER_PAGE - 1);
|
|
227
|
+
case 'PageUp': return focus(focusYear - YEARS_PER_PAGE);
|
|
228
|
+
case 'PageDown': return focus(focusYear + YEARS_PER_PAGE);
|
|
229
|
+
case 'Enter':
|
|
230
|
+
case ' ': return { action: 'choose', year: focusYear };
|
|
231
|
+
case 'Escape': return { action: 'close', year: focusYear };
|
|
232
|
+
default: return { action: 'none', year: focusYear };
|
|
233
|
+
}
|
|
234
|
+
}
|