@cahyo-dimas/freeday 3.6.0 → 3.8.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 +139 -0
- package/COMPONENTS.md +119 -6
- package/README.id.md +19 -6
- package/README.md +17 -5
- package/USAGE.md +42 -0
- package/adapters/blazor/FdyDatepicker.razor +3 -1
- package/adapters/blazor/FdyDatepicker.razor.cs +22 -1
- package/adapters/blazor/FdyTableFooter.razor +6 -6
- package/adapters/blazor/FdyTableFooter.razor.cs +36 -0
- package/adapters/react/components/FdyDatepicker.tsx +122 -42
- package/adapters/react/components/FdyTableFooter.tsx +37 -6
- package/adapters/react/index.d.ts +1 -1
- package/adapters/vue/components/FdyDatepicker.vue +119 -34
- package/adapters/vue/components/FdyTableFooter.vue +39 -6
- package/dist/freeday-datepicker.js +68 -14
- package/dist/freeday-menu.js +15 -1
- package/dist/freeday-popover.js +33 -11
- package/dist/freeday-tabs.js +21 -2
- package/dist/freeday.bundle.css +493 -117
- package/dist/freeday.css +190 -113
- package/dist/freeday.js +137 -28
- package/dist/freeday.tokens.css +303 -4
- package/docs/getting-started.md +1 -1
- package/package.json +2 -2
- package/src/base.css +1 -1
- package/src/components/accordion.css +1 -1
- package/src/components/alert.css +5 -5
- package/src/components/app-shell.css +2 -2
- package/src/components/appbar.css +3 -3
- package/src/components/avatar.css +15 -9
- package/src/components/badge.css +18 -14
- package/src/components/breadcrumb.css +1 -1
- package/src/components/button.css +21 -13
- package/src/components/carousel.css +3 -3
- package/src/components/cascade.css +4 -4
- package/src/components/chip.css +33 -15
- package/src/components/combo.css +5 -5
- package/src/components/datepicker.css +8 -8
- package/src/components/file-upload.css +9 -4
- package/src/components/input-group.css +2 -2
- package/src/components/input.css +2 -2
- package/src/components/menu.css +3 -3
- package/src/components/pagination.css +1 -1
- package/src/components/selection.css +2 -2
- package/src/components/skeleton.css +1 -1
- package/src/components/stepper.css +1 -1
- package/src/components/table.css +12 -7
- package/src/components/tabs.css +32 -1
- package/src/components/timepicker.css +3 -3
- package/src/components/toast.css +1 -1
- package/src/components/tree.css +1 -1
- package/tokens/tokens.json +57 -4
|
@@ -9,6 +9,12 @@ import { usePopover } from '../usePopover';
|
|
|
9
9
|
// (outside-click + focusout) mirrors the already-approved FdyCombo.tsx pattern. Range mode is
|
|
10
10
|
// out of scope (v1 = single date).
|
|
11
11
|
|
|
12
|
+
/** Which grid is on screen. The drill goes days -> months -> years and back down. */
|
|
13
|
+
type CalendarLevel = 'days' | 'months' | 'years';
|
|
14
|
+
|
|
15
|
+
/** What the picker answers with: a day (`YYYY-MM-DD`) or a whole month (`YYYY-MM`). */
|
|
16
|
+
export type FdyDatepickerMode = 'day' | 'month';
|
|
17
|
+
|
|
12
18
|
interface DayCell {
|
|
13
19
|
date: Date;
|
|
14
20
|
iso: string;
|
|
@@ -40,8 +46,11 @@ interface MonthCell {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export interface FdyDatepickerProps {
|
|
49
|
+
/** ISO `YYYY-MM-DD`, or `YYYY-MM` when `mode` is `'month'`. `''`/`null` for empty. */
|
|
43
50
|
value: string | null;
|
|
44
51
|
onChange: (value: string) => void;
|
|
52
|
+
/** `'day'` (default) answers with a date. `'month'` opens on the month grid, stops there, and answers with `YYYY-MM`. */
|
|
53
|
+
mode?: FdyDatepickerMode;
|
|
45
54
|
min?: string;
|
|
46
55
|
max?: string;
|
|
47
56
|
locale?: string;
|
|
@@ -79,6 +88,32 @@ function pad(n: number): string {
|
|
|
79
88
|
function toISO(d: Date): string {
|
|
80
89
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
81
90
|
}
|
|
91
|
+
function toISOMonth(d: Date): string {
|
|
92
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
|
|
93
|
+
}
|
|
94
|
+
/** Month mode reads BOTH shapes, and the dropped day is the point: a bound of `2026-03` lands on
|
|
95
|
+
* March 1st, which `isMonthDisabled` then compares a whole month against correctly at either end,
|
|
96
|
+
* so an app can pass the strings it already stores. A full date still works. */
|
|
97
|
+
function parseISOMonth(s: string | null | undefined): Date | null {
|
|
98
|
+
if (s === null || s === undefined || s === '') return null;
|
|
99
|
+
const parts: string[] = s.split('-');
|
|
100
|
+
if (parts.length < 2) return null;
|
|
101
|
+
const y: number = Number(parts[0]);
|
|
102
|
+
const m: number = Number(parts[1]);
|
|
103
|
+
const day: number = parts.length > 2 ? Number(parts[2]) : 1;
|
|
104
|
+
const d: Date = new Date(y, m - 1, day);
|
|
105
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
106
|
+
}
|
|
107
|
+
/* The page's own `lang`, which is what COMPONENTS.md has promised all along and what the vanilla
|
|
108
|
+
enhancer has always done. Handing `undefined` to Intl instead falls back to the BROWSER's
|
|
109
|
+
language, so an Indonesian reader saw Indonesian month names inside a kit that is English on
|
|
110
|
+
every other path, and an Indonesian PAGE read by an English browser got English ones. That is
|
|
111
|
+
the symptom this month mode was asked for, so the fallback has to be the page. */
|
|
112
|
+
function pageLocale(): string {
|
|
113
|
+
if (typeof document === 'undefined') return 'en';
|
|
114
|
+
// `||`, not `??`: an empty `lang=""` is not null and Intl throws RangeError on it.
|
|
115
|
+
return document.documentElement.getAttribute('lang') || 'en';
|
|
116
|
+
}
|
|
82
117
|
function parseISO(s: string | null | undefined): Date | null {
|
|
83
118
|
if (s === null || s === undefined || s === '') return null;
|
|
84
119
|
const parts: string[] = s.split('-');
|
|
@@ -131,10 +166,20 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
131
166
|
// don't steal DOM focus off the button, matching the Vue template's behavior.
|
|
132
167
|
const pendingFocusRef = useRef<string | null>(null);
|
|
133
168
|
|
|
169
|
+
const isMonthMode: boolean = props.mode === 'month';
|
|
170
|
+
/** Resolved once so every formatter below reads the same tag. */
|
|
171
|
+
const activeLocale: string = props.locale ?? pageLocale();
|
|
172
|
+
const parseValue = isMonthMode ? parseISOMonth : parseISO;
|
|
173
|
+
|
|
134
174
|
const [open, setOpen] = useState<boolean>(false);
|
|
135
|
-
/*
|
|
136
|
-
title. Before this the only pointer route to another month was one click per month.
|
|
137
|
-
|
|
175
|
+
/* Which grid is showing, and the calendar drills one level up instead of growing furniture beside
|
|
176
|
+
the title. Before this the only pointer route to another month was one click per month.
|
|
177
|
+
Named `level`, not `mode`: `mode` is the PROP that decides what the picker answers with.
|
|
178
|
+
|
|
179
|
+
Month mode has no level below months, so the day grid is unreachable by construction rather
|
|
180
|
+
than by every path remembering not to ask for it. */
|
|
181
|
+
const startLevel = (): CalendarLevel => (props.mode === 'month' ? 'months' : 'days');
|
|
182
|
+
const [level, setLevel] = useState<CalendarLevel>(startLevel);
|
|
138
183
|
const [focusMonth, setFocusMonth] = useState<number>(0);
|
|
139
184
|
const [focusYear, setFocusYear] = useState<number>(0);
|
|
140
185
|
|
|
@@ -157,13 +202,13 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
157
202
|
panelRef.current?.setAttribute('popover', 'manual');
|
|
158
203
|
}, []);
|
|
159
204
|
|
|
160
|
-
const selectedDate: Date | null = useMemo((): Date | null =>
|
|
161
|
-
const minDate: Date | null = useMemo((): Date | null =>
|
|
162
|
-
const maxDate: Date | null = useMemo((): Date | null =>
|
|
205
|
+
const selectedDate: Date | null = useMemo((): Date | null => parseValue(props.value), [props.value, isMonthMode]);
|
|
206
|
+
const minDate: Date | null = useMemo((): Date | null => parseValue(props.min), [props.min, isMonthMode]);
|
|
207
|
+
const maxDate: Date | null = useMemo((): Date | null => parseValue(props.max), [props.max, isMonthMode]);
|
|
163
208
|
const isDisabled: boolean = props.disabled === true;
|
|
164
209
|
const isReadonly: boolean = props.readonly === true;
|
|
165
210
|
const isInvalid: boolean = props.invalid === true;
|
|
166
|
-
const displayPlaceholder: string = props.placeholder ?? 'Select date';
|
|
211
|
+
const displayPlaceholder: string = props.placeholder ?? (isMonthMode ? 'Select month' : 'Select date');
|
|
167
212
|
const showClear: boolean = props.clearable === true && selectedDate !== null && !isDisabled && !isReadonly;
|
|
168
213
|
const chooseMonthLabelText: string = props.chooseMonthLabel ?? 'Choose month';
|
|
169
214
|
const prevYearLabelText: string = props.prevYearLabel ?? 'Previous year';
|
|
@@ -172,19 +217,19 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
172
217
|
const prevYearsLabelText: string = props.prevYearsLabel ?? 'Previous years';
|
|
173
218
|
const nextYearsLabelText: string = props.nextYearsLabel ?? 'Next years';
|
|
174
219
|
const monthCellFmt: Intl.DateTimeFormat = useMemo(
|
|
175
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
176
|
-
[
|
|
220
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { month: 'short' }),
|
|
221
|
+
[activeLocale],
|
|
177
222
|
);
|
|
178
223
|
const monthNameFmt: Intl.DateTimeFormat = useMemo(
|
|
179
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
180
|
-
[
|
|
224
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { month: 'long', year: 'numeric' }),
|
|
225
|
+
[activeLocale],
|
|
181
226
|
);
|
|
182
227
|
const prevMonthLabelText: string = props.prevMonthLabel ?? 'Previous month';
|
|
183
228
|
const nextMonthLabelText: string = props.nextMonthLabel ?? 'Next month';
|
|
184
229
|
const clearLabelText: string = props.clearLabel ?? 'Clear date';
|
|
185
230
|
|
|
186
|
-
const [viewMonth, setViewMonth] = useState<Date>((): Date => startOfMonth(
|
|
187
|
-
const [focusDate, setFocusDate] = useState<Date>((): Date =>
|
|
231
|
+
const [viewMonth, setViewMonth] = useState<Date>((): Date => startOfMonth(parseValue(props.value) ?? new Date()));
|
|
232
|
+
const [focusDate, setFocusDate] = useState<Date>((): Date => parseValue(props.value) ?? new Date());
|
|
188
233
|
|
|
189
234
|
// Keep the roving-tabindex cell inside the visible month so the grid stays keyboard-reachable
|
|
190
235
|
// after navigating with Home/End/PageUp/PageDown (mirrors freeday-datepicker.js's clamp).
|
|
@@ -195,23 +240,28 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
195
240
|
}, [focusDate, viewMonth]);
|
|
196
241
|
|
|
197
242
|
const monthFmt: Intl.DateTimeFormat = useMemo(
|
|
198
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
199
|
-
[
|
|
243
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { month: 'long', year: 'numeric' }),
|
|
244
|
+
[activeLocale],
|
|
200
245
|
);
|
|
201
246
|
const valueFmt: Intl.DateTimeFormat = useMemo(
|
|
202
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
203
|
-
[
|
|
247
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { day: '2-digit', month: 'short', year: 'numeric' }),
|
|
248
|
+
[activeLocale],
|
|
204
249
|
);
|
|
205
250
|
const dayLabelFmt: Intl.DateTimeFormat = useMemo(
|
|
206
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
207
|
-
[
|
|
251
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }),
|
|
252
|
+
[activeLocale],
|
|
208
253
|
);
|
|
209
254
|
const dowFmt: Intl.DateTimeFormat = useMemo(
|
|
210
|
-
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(
|
|
211
|
-
[
|
|
255
|
+
(): Intl.DateTimeFormat => new Intl.DateTimeFormat(activeLocale, { weekday: 'short' }),
|
|
256
|
+
[activeLocale],
|
|
212
257
|
);
|
|
213
258
|
|
|
214
|
-
|
|
259
|
+
/* Month mode shows the month through the SAME formatter the month cells and the day grid's title
|
|
260
|
+
already use, which is the whole point of the mode: <input type="month"> names the month in the
|
|
261
|
+
browser's language, this names it in the page's. */
|
|
262
|
+
const displayValue: string = selectedDate === null
|
|
263
|
+
? displayPlaceholder
|
|
264
|
+
: (isMonthMode ? monthNameFmt : valueFmt).format(selectedDate);
|
|
215
265
|
|
|
216
266
|
const weekdayHeaders: string[] = useMemo((): string[] => {
|
|
217
267
|
const monday: Date = new Date(2024, 0, 1); // a known Monday
|
|
@@ -237,7 +287,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
237
287
|
const monthCells: MonthCell[] = useMemo((): MonthCell[] => {
|
|
238
288
|
const year: number = viewMonth.getFullYear();
|
|
239
289
|
const today: Date = new Date();
|
|
240
|
-
const sel: Date | null =
|
|
290
|
+
const sel: Date | null = parseValue(props.value);
|
|
241
291
|
return Array.from({ length: 12 }, (_unused: unknown, index: number): MonthCell => {
|
|
242
292
|
const cellDate: Date = new Date(year, index, 1);
|
|
243
293
|
return {
|
|
@@ -254,6 +304,10 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
254
304
|
}, [viewMonth, props.value, focusMonth, monthCellFmt, monthNameFmt, minDate, maxDate]);
|
|
255
305
|
|
|
256
306
|
const gridCells: DayCell[] = useMemo((): DayCell[] => {
|
|
307
|
+
/* Month mode never mounts this grid, and useMemo runs whether or not its result is read, so
|
|
308
|
+
without this every arrow key in the month grid would format forty two day labels through Intl
|
|
309
|
+
for cells nobody sees. Vue needs no such guard: a computed is lazy. */
|
|
310
|
+
if (isMonthMode) return [];
|
|
257
311
|
const first: Date = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1);
|
|
258
312
|
const offset: number = (first.getDay() + 6) % 7; // Monday-first
|
|
259
313
|
const startCell: Date = addDays(first, -offset);
|
|
@@ -278,7 +332,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
278
332
|
// Deps intentionally mirror the port recipe, not every closure read: minDate/maxDate,
|
|
279
333
|
// dayLabelFmt and effectiveFocusDate all derive deterministically from the listed deps
|
|
280
334
|
// within the same render pass, so this stays in sync without listing them separately.
|
|
281
|
-
}, [viewMonth, props.value, props.min, props.max,
|
|
335
|
+
}, [viewMonth, props.value, props.min, props.max, activeLocale, isMonthMode]);
|
|
282
336
|
|
|
283
337
|
// Focus the roving-tabindex cell after a render triggered by moveFocus/openPanel (React's
|
|
284
338
|
// analogue of Vue's `nextTick(focusCell)`). Runs after every render; cheap no-op otherwise.
|
|
@@ -305,10 +359,27 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
305
359
|
|
|
306
360
|
function openMonthGrid(): void {
|
|
307
361
|
setFocusMonth(viewMonth.getMonth());
|
|
308
|
-
|
|
362
|
+
setLevel('months');
|
|
309
363
|
pendingMonthFocusRef.current = viewMonth.getMonth();
|
|
310
364
|
}
|
|
311
365
|
|
|
366
|
+
/* What a month cell DOES, decided in one place for the click handler and the Enter key alike. In
|
|
367
|
+
day mode a month is a step on the way down; in month mode it is the answer. */
|
|
368
|
+
function chooseMonth(index: number): void {
|
|
369
|
+
if (isMonthMode) pickMonth(index);
|
|
370
|
+
else openMonth(index);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function pickMonth(index: number): void {
|
|
374
|
+
const year: number = viewMonth.getFullYear();
|
|
375
|
+
if (isMonthDisabled(year, index)) return;
|
|
376
|
+
const first: Date = new Date(year, index, 1);
|
|
377
|
+
setFocusMonth(index);
|
|
378
|
+
setFocusDate(first);
|
|
379
|
+
props.onChange(toISOMonth(first));
|
|
380
|
+
closePanel(true);
|
|
381
|
+
}
|
|
382
|
+
|
|
312
383
|
/* Picking a month is NAVIGATION, not selection: back to the day grid with the roving cell clamped
|
|
313
384
|
into the new month, so nothing is committed until a day is chosen. */
|
|
314
385
|
function openMonth(index: number): void {
|
|
@@ -317,7 +388,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
317
388
|
const next: Date = new Date(year, index, Math.min(focusDate.getDate(), daysInMonth));
|
|
318
389
|
setViewMonth(new Date(year, index, 1));
|
|
319
390
|
setFocusDate(next);
|
|
320
|
-
|
|
391
|
+
setLevel('days');
|
|
321
392
|
pendingFocusRef.current = toISO(next);
|
|
322
393
|
}
|
|
323
394
|
|
|
@@ -341,7 +412,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
341
412
|
const yearCells: YearCell[] = useMemo((): YearCell[] => {
|
|
342
413
|
const start: number = yearPageStart(focusYear);
|
|
343
414
|
const thisYear: number = new Date().getFullYear();
|
|
344
|
-
const sel: Date | null =
|
|
415
|
+
const sel: Date | null = parseValue(props.value);
|
|
345
416
|
return Array.from({ length: YEARS_PER_PAGE }, (_unused: unknown, index: number): YearCell => {
|
|
346
417
|
const year: number = start + index;
|
|
347
418
|
return {
|
|
@@ -357,7 +428,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
357
428
|
|
|
358
429
|
function openYearGrid(): void {
|
|
359
430
|
setFocusYear(viewMonth.getFullYear());
|
|
360
|
-
|
|
431
|
+
setLevel('years');
|
|
361
432
|
pendingYearFocusRef.current = viewMonth.getFullYear();
|
|
362
433
|
}
|
|
363
434
|
|
|
@@ -366,7 +437,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
366
437
|
function openYear(year: number): void {
|
|
367
438
|
setViewMonth(new Date(year, viewMonth.getMonth(), 1));
|
|
368
439
|
setFocusMonth(viewMonth.getMonth());
|
|
369
|
-
|
|
440
|
+
setLevel('months');
|
|
370
441
|
pendingMonthFocusRef.current = viewMonth.getMonth();
|
|
371
442
|
}
|
|
372
443
|
|
|
@@ -407,7 +478,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
407
478
|
case 'PageUp': moveMonthFocus(focusMonth, -1); break;
|
|
408
479
|
case 'PageDown': moveMonthFocus(focusMonth, 1); break;
|
|
409
480
|
case 'Enter':
|
|
410
|
-
case ' ':
|
|
481
|
+
case ' ': chooseMonth(focusMonth); break;
|
|
411
482
|
case 'Escape': closePanel(true); break;
|
|
412
483
|
default: handled = false;
|
|
413
484
|
}
|
|
@@ -441,7 +512,16 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
441
512
|
const next: Date = selectedDate ?? focusDate ?? new Date();
|
|
442
513
|
setFocusDate(next);
|
|
443
514
|
setViewMonth(new Date(next.getFullYear(), next.getMonth(), 1));
|
|
515
|
+
/* Reopening starts at the mode's own floor. Without this a month picker that drilled to the
|
|
516
|
+
year grid and was dismissed would open on years the next time, and a keyboard user would
|
|
517
|
+
land on a grid that cannot answer. */
|
|
518
|
+
setLevel(startLevel());
|
|
444
519
|
setOpen(true);
|
|
520
|
+
if (isMonthMode) {
|
|
521
|
+
setFocusMonth(next.getMonth());
|
|
522
|
+
pendingMonthFocusRef.current = next.getMonth();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
445
525
|
pendingFocusRef.current = toISO(next);
|
|
446
526
|
}
|
|
447
527
|
|
|
@@ -575,10 +655,10 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
575
655
|
<button
|
|
576
656
|
type="button"
|
|
577
657
|
className="fdy-cal__nav"
|
|
578
|
-
aria-label={
|
|
658
|
+
aria-label={level === 'years' ? prevYearsLabelText : level === 'months' ? prevYearLabelText : prevMonthLabelText}
|
|
579
659
|
onClick={(): void => {
|
|
580
|
-
if (
|
|
581
|
-
else if (
|
|
660
|
+
if (level === 'years') moveYearFocus(focusYear - YEARS_PER_PAGE);
|
|
661
|
+
else if (level === 'months') moveMonthFocus(focusMonth, -1);
|
|
582
662
|
else setViewMonth(addMonths(viewMonth, -1));
|
|
583
663
|
}}
|
|
584
664
|
>
|
|
@@ -588,25 +668,25 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
588
668
|
id={titleId}
|
|
589
669
|
type="button"
|
|
590
670
|
className="fdy-cal__title"
|
|
591
|
-
aria-label={
|
|
592
|
-
onClick={(): void => { if (
|
|
671
|
+
aria-label={level === 'years' ? undefined : level === 'months' ? chooseYearLabelText : chooseMonthLabelText}
|
|
672
|
+
onClick={(): void => { if (level === 'months') openYearGrid(); else openMonthGrid(); }}
|
|
593
673
|
>
|
|
594
|
-
{
|
|
674
|
+
{level === 'years' ? yearRangeLabel : level === 'months' ? String(viewMonth.getFullYear()) : monthFmt.format(viewMonth)}
|
|
595
675
|
</button>
|
|
596
676
|
<button
|
|
597
677
|
type="button"
|
|
598
678
|
className="fdy-cal__nav"
|
|
599
|
-
aria-label={
|
|
679
|
+
aria-label={level === 'years' ? nextYearsLabelText : level === 'months' ? nextYearLabelText : nextMonthLabelText}
|
|
600
680
|
onClick={(): void => {
|
|
601
|
-
if (
|
|
602
|
-
else if (
|
|
681
|
+
if (level === 'years') moveYearFocus(focusYear + YEARS_PER_PAGE);
|
|
682
|
+
else if (level === 'months') moveMonthFocus(focusMonth, 1);
|
|
603
683
|
else setViewMonth(addMonths(viewMonth, 1));
|
|
604
684
|
}}
|
|
605
685
|
>
|
|
606
686
|
›
|
|
607
687
|
</button>
|
|
608
688
|
</div>
|
|
609
|
-
{
|
|
689
|
+
{level === 'years' ? (
|
|
610
690
|
<div className="fdy-cal__grid fdy-cal__grid--years" role="grid" aria-labelledby={titleId} onKeyDown={onYearKeydown}>
|
|
611
691
|
{yearCells.map((cell: YearCell) => (
|
|
612
692
|
<button
|
|
@@ -625,7 +705,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
625
705
|
</button>
|
|
626
706
|
))}
|
|
627
707
|
</div>
|
|
628
|
-
) :
|
|
708
|
+
) : level === 'months' ? (
|
|
629
709
|
<div className="fdy-cal__grid fdy-cal__grid--months" role="grid" aria-labelledby={titleId} onKeyDown={onMonthKeydown}>
|
|
630
710
|
{monthCells.map((cell: MonthCell) => (
|
|
631
711
|
<button
|
|
@@ -638,7 +718,7 @@ export function FdyDatepicker(props: FdyDatepickerProps): JSX.Element {
|
|
|
638
718
|
aria-selected={cell.selected ? true : undefined}
|
|
639
719
|
disabled={cell.disabled}
|
|
640
720
|
tabIndex={cell.focusable ? 0 : -1}
|
|
641
|
-
onClick={(): void =>
|
|
721
|
+
onClick={(): void => chooseMonth(cell.index)}
|
|
642
722
|
>
|
|
643
723
|
{cell.label}
|
|
644
724
|
</button>
|
|
@@ -25,6 +25,28 @@ export interface FdyTableFooterProps {
|
|
|
25
25
|
* the row you were looking at.
|
|
26
26
|
*/
|
|
27
27
|
pageSizes?: readonly number[];
|
|
28
|
+
/* The strings, six of them, one per thing this band says. The raw path has had a hatch since
|
|
29
|
+
* 1.39.0 (`data-fdy-text-<key>` read through `textOf()`) and this path had none, so an app that
|
|
30
|
+
* does not speak English could only rewrite the whole band by hand; one shipped eighty one lines
|
|
31
|
+
* of its own markup to change five words. Names follow the enhancer's `TEXT` keys where a key
|
|
32
|
+
* exists, `info`, `prev`, `next`, so one string is not two names across the two paths. The other
|
|
33
|
+
* three have no twin because on the raw path that markup is the app's own. Defaults stay English
|
|
34
|
+
* on every path, which COMPONENTS.md promises and the suite enforces. */
|
|
35
|
+
/**
|
|
36
|
+
* The range line. `{from}`, `{to}` and `{total}` are substituted, every occurrence, as `textOf()`
|
|
37
|
+
* does. Default `Showing {from}–{to} of {total}`, the enhancer's `info`.
|
|
38
|
+
*/
|
|
39
|
+
infoText?: string;
|
|
40
|
+
/** The visible word beside the rows-per-page control, which also names it. Default `Rows`. */
|
|
41
|
+
pageSizeLabel?: string;
|
|
42
|
+
/** Accessible name of the pager landmark. Default `Pagination`. */
|
|
43
|
+
paginationLabel?: string;
|
|
44
|
+
/** Accessible name of the previous-page arrow. Default `Previous page`. */
|
|
45
|
+
prevLabel?: string;
|
|
46
|
+
/** Accessible name of the next-page arrow. Default `Next page`. */
|
|
47
|
+
nextLabel?: string;
|
|
48
|
+
/** Accessible name of a numbered page button. `{n}` substituted. Default `Go to page {n}`. */
|
|
49
|
+
pageLabel?: string;
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
@@ -39,6 +61,15 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
39
61
|
const rangeTo: number = Math.min(total, (index + 1) * size);
|
|
40
62
|
const pages: Array<number | 'ellipsis'> = pageWindow(currentPage1, totalPages);
|
|
41
63
|
|
|
64
|
+
/* Regex with /g, not String.replace on a string pattern, which stops at the first hit and would
|
|
65
|
+
* drop a second `{total}` a translator wrote. This is what `textOf()` does on the raw path. */
|
|
66
|
+
const infoLine: string = (props.infoText ?? 'Showing {from}–{to} of {total}')
|
|
67
|
+
.replace(/\{from\}/g, String(rangeFrom))
|
|
68
|
+
.replace(/\{to\}/g, String(rangeTo))
|
|
69
|
+
.replace(/\{total\}/g, String(total));
|
|
70
|
+
const pageAriaLabel = (page1: number): string =>
|
|
71
|
+
(props.pageLabel ?? 'Go to page {n}').replace(/\{n\}/g, String(page1));
|
|
72
|
+
|
|
42
73
|
const hasPager: boolean = size > 0 && totalPages > 1;
|
|
43
74
|
const sizes: readonly number[] = props.pageSizes ?? [];
|
|
44
75
|
/* One page and no size control means there is nothing here to say, the table has always withheld
|
|
@@ -58,11 +89,11 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
58
89
|
|
|
59
90
|
return (
|
|
60
91
|
<div className="fdy-table-footer">
|
|
61
|
-
<span className="fdy-table-footer__info">
|
|
92
|
+
<span className="fdy-table-footer__info">{infoLine}</span>
|
|
62
93
|
|
|
63
94
|
{sizes.length > 0 && (
|
|
64
95
|
<div className="fdy-table-footer__size">
|
|
65
|
-
<span id={sizeLabelId}>Rows</span>
|
|
96
|
+
<span id={sizeLabelId}>{props.pageSizeLabel ?? 'Rows'}</span>
|
|
66
97
|
{/* The kit's own listbox, not a native <select>: an OS menu is unthemeable, and on macOS
|
|
67
98
|
it drops a dark panel into a light page. FdyCombo's own header says exactly that. */}
|
|
68
99
|
<FdyCombo
|
|
@@ -75,10 +106,10 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
75
106
|
)}
|
|
76
107
|
|
|
77
108
|
{hasPager && (
|
|
78
|
-
<nav className="fdy-pagination" aria-label=
|
|
109
|
+
<nav className="fdy-pagination" aria-label={props.paginationLabel ?? 'Pagination'}>
|
|
79
110
|
<ul className="fdy-pagination__list">
|
|
80
111
|
<li>
|
|
81
|
-
<button type="button" className="fdy-pagination__link" aria-label=
|
|
112
|
+
<button type="button" className="fdy-pagination__link" aria-label={props.prevLabel ?? 'Previous page'} disabled={currentPage1 === 1} onClick={(): void => goTo(currentPage1 - 1)}>‹</button>
|
|
82
113
|
</li>
|
|
83
114
|
{pages.map((p: number | 'ellipsis', i: number): JSX.Element => (
|
|
84
115
|
<li key={typeof p === 'number' ? `p-${p}` : `gap-${i}`}>
|
|
@@ -87,12 +118,12 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
87
118
|
) : p === currentPage1 ? (
|
|
88
119
|
<span className="fdy-pagination__link" aria-current="page">{p}</span>
|
|
89
120
|
) : (
|
|
90
|
-
<button type="button" className="fdy-pagination__link" aria-label={
|
|
121
|
+
<button type="button" className="fdy-pagination__link" aria-label={pageAriaLabel(p)} onClick={(): void => goTo(p)}>{p}</button>
|
|
91
122
|
)}
|
|
92
123
|
</li>
|
|
93
124
|
))}
|
|
94
125
|
<li>
|
|
95
|
-
<button type="button" className="fdy-pagination__link" aria-label=
|
|
126
|
+
<button type="button" className="fdy-pagination__link" aria-label={props.nextLabel ?? 'Next page'} disabled={currentPage1 === totalPages} onClick={(): void => goTo(currentPage1 + 1)}>›</button>
|
|
96
127
|
</li>
|
|
97
128
|
</ul>
|
|
98
129
|
</nav>
|
|
@@ -29,7 +29,7 @@ export interface FdyTableChangeDetail { page: number; sort: string | null; query
|
|
|
29
29
|
// Re-exports of React components and hooks
|
|
30
30
|
export { usePopover } from './usePopover';
|
|
31
31
|
export { FdyCombo, type FdyComboProps, type FdyComboOption } from './components/FdyCombo';
|
|
32
|
-
export { FdyDatepicker, type FdyDatepickerProps } from './components/FdyDatepicker';
|
|
32
|
+
export { FdyDatepicker, type FdyDatepickerProps, type FdyDatepickerMode } from './components/FdyDatepicker';
|
|
33
33
|
export { FdyDateRange, type FdyDateRangeProps, type DateRangeValue } from './components/FdyDateRange';
|
|
34
34
|
export { FdyAutocomplete, type FdyAutocompleteProps } from './components/FdyAutocomplete';
|
|
35
35
|
export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components/FdyCascade';
|