@konce-pt/react 0.9.1 → 0.10.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/dist/accordion/accordion.js +7 -1
- package/dist/accordion/llms.txt +8 -4
- package/dist/alert/alert.d.ts +10 -4
- package/dist/alert/alert.js +13 -6
- package/dist/alert/llms.txt +8 -6
- package/dist/app-shell/app-shell.js +28 -2
- package/dist/app-shell/llms.txt +3 -0
- package/dist/autocomplete/autocomplete.d.ts +7 -2
- package/dist/autocomplete/autocomplete.js +46 -9
- package/dist/autocomplete/llms.txt +12 -0
- package/dist/avatar/avatar.d.ts +4 -2
- package/dist/avatar/avatar.js +8 -3
- package/dist/avatar/llms.txt +9 -5
- package/dist/avatar-group/avatar-group.d.ts +5 -2
- package/dist/avatar-group/avatar-group.js +10 -2
- package/dist/avatar-group/llms.txt +7 -4
- package/dist/badge/badge.d.ts +8 -1
- package/dist/badge/badge.js +3 -2
- package/dist/badge/llms.txt +11 -5
- package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
- package/dist/bottom-sheet/bottom-sheet.js +8 -2
- package/dist/bottom-sheet/llms.txt +8 -2
- package/dist/breadcrumb/breadcrumb.js +4 -4
- package/dist/breadcrumb/llms.txt +12 -5
- package/dist/card/card.d.ts +9 -1
- package/dist/card/card.js +2 -2
- package/dist/card/llms.txt +6 -2
- package/dist/carousel/carousel.d.ts +8 -1
- package/dist/carousel/carousel.js +9 -2
- package/dist/carousel/llms.txt +24 -10
- package/dist/checkbox/checkbox.d.ts +6 -1
- package/dist/checkbox/checkbox.js +11 -3
- package/dist/checkbox/llms.txt +11 -0
- package/dist/chip/chip.js +10 -1
- package/dist/chip/llms.txt +7 -4
- package/dist/chips-input/chips-input.d.ts +7 -1
- package/dist/chips-input/chips-input.js +21 -5
- package/dist/chips-input/llms.txt +12 -0
- package/dist/color-picker/color-picker.d.ts +18 -5
- package/dist/color-picker/color-picker.js +48 -14
- package/dist/color-picker/llms.txt +14 -0
- package/dist/confirm/confirm-container.d.ts +5 -1
- package/dist/confirm/confirm-container.js +15 -1
- package/dist/confirm/llms.txt +4 -2
- package/dist/context-menu/context-menu.d.ts +5 -0
- package/dist/context-menu/context-menu.js +48 -4
- package/dist/context-menu/llms.txt +11 -4
- package/dist/date-range/date-range.d.ts +14 -1
- package/dist/date-range/date-range.js +133 -17
- package/dist/date-range/llms.txt +26 -3
- package/dist/datepicker/datepicker.d.ts +10 -1
- package/dist/datepicker/datepicker.js +147 -14
- package/dist/datepicker/llms.txt +27 -3
- package/dist/dialog/dialog.d.ts +4 -0
- package/dist/dialog/dialog.js +7 -1
- package/dist/dialog/llms.txt +2 -0
- package/dist/drawer/drawer.js +7 -2
- package/dist/drawer/llms.txt +10 -5
- package/dist/empty/empty.d.ts +6 -1
- package/dist/empty/empty.js +2 -2
- package/dist/empty/llms.txt +4 -3
- package/dist/fab/llms.txt +2 -1
- package/dist/file-upload/file-upload.d.ts +5 -0
- package/dist/file-upload/file-upload.js +24 -3
- package/dist/file-upload/llms.txt +15 -4
- package/dist/form-field/form-field-context.d.ts +30 -1
- package/dist/form-field/form-field-context.js +27 -0
- package/dist/form-field/form-field.js +16 -2
- package/dist/form-field/llms.txt +10 -1
- package/dist/galleria/galleria.d.ts +6 -1
- package/dist/galleria/galleria.js +40 -1
- package/dist/galleria/llms.txt +17 -8
- package/dist/icon/icon.d.ts +7 -1
- package/dist/icon/icon.js +18 -4
- package/dist/icon/llms.txt +5 -2
- package/dist/image/image.d.ts +5 -0
- package/dist/image/image.js +38 -2
- package/dist/image/llms.txt +24 -17
- package/dist/input/input.d.ts +1 -1
- package/dist/input/input.js +4 -6
- package/dist/input/llms.txt +5 -0
- package/dist/input-mask/input-mask.d.ts +1 -1
- package/dist/input-mask/input-mask.js +8 -6
- package/dist/input-mask/llms.txt +11 -0
- package/dist/input-number/input-number.d.ts +13 -1
- package/dist/input-number/input-number.js +49 -9
- package/dist/input-number/llms.txt +21 -3
- package/dist/input-otp/input-otp.d.ts +7 -2
- package/dist/input-otp/input-otp.js +36 -7
- package/dist/input-otp/llms.txt +14 -0
- package/dist/internal/calendar-focus.d.ts +8 -0
- package/dist/internal/calendar-focus.js +38 -0
- package/dist/internal/option-nav.d.ts +24 -0
- package/dist/internal/option-nav.js +80 -0
- package/dist/internal/roving.d.ts +9 -0
- package/dist/internal/roving.js +43 -0
- package/dist/internal/use-modal-focus.d.ts +10 -0
- package/dist/internal/use-modal-focus.js +57 -0
- package/dist/knob/knob.d.ts +5 -1
- package/dist/knob/knob.js +4 -2
- package/dist/knob/llms.txt +8 -0
- package/dist/listbox/listbox.d.ts +13 -1
- package/dist/listbox/listbox.js +89 -6
- package/dist/listbox/llms.txt +21 -5
- package/dist/megamenu/llms.txt +28 -10
- package/dist/megamenu/megamenu.d.ts +10 -1
- package/dist/megamenu/megamenu.js +116 -5
- package/dist/menu/llms.txt +12 -1
- package/dist/menu/menu-keys.d.ts +15 -0
- package/dist/menu/menu-keys.js +53 -0
- package/dist/menu/menu-rows.js +1 -1
- package/dist/menu/menu.js +52 -4
- package/dist/menubar/llms.txt +24 -7
- package/dist/menubar/menubar.d.ts +8 -1
- package/dist/menubar/menubar.js +92 -6
- package/dist/meter-group/llms.txt +10 -3
- package/dist/meter-group/meter-group.d.ts +9 -2
- package/dist/meter-group/meter-group.js +8 -2
- package/dist/order-list/llms.txt +11 -3
- package/dist/order-list/order-list.d.ts +5 -1
- package/dist/order-list/order-list.js +46 -8
- package/dist/panel/llms.txt +9 -6
- package/dist/panel/panel.js +3 -1
- package/dist/password/llms.txt +8 -0
- package/dist/password/password.d.ts +6 -1
- package/dist/password/password.js +6 -5
- package/dist/pick-list/llms.txt +12 -3
- package/dist/pick-list/pick-list.d.ts +5 -1
- package/dist/pick-list/pick-list.js +52 -5
- package/dist/popover/llms.txt +9 -2
- package/dist/popover/popover.d.ts +9 -3
- package/dist/popover/popover.js +41 -6
- package/dist/public-api.d.ts +6 -6
- package/dist/public-api.js +1 -1
- package/dist/radio-group/llms.txt +11 -0
- package/dist/radio-group/radio-group.d.ts +9 -1
- package/dist/radio-group/radio-group.js +5 -2
- package/dist/rating/llms.txt +18 -5
- package/dist/rating/rating.d.ts +21 -3
- package/dist/rating/rating.js +87 -12
- package/dist/rich-text/llms.txt +20 -0
- package/dist/rich-text/rich-text.d.ts +7 -1
- package/dist/rich-text/rich-text.js +155 -28
- package/dist/scroll-top/llms.txt +5 -1
- package/dist/scroll-top/scroll-top.js +3 -1
- package/dist/select/llms.txt +21 -7
- package/dist/select/select.d.ts +12 -1
- package/dist/select/select.js +110 -38
- package/dist/sidenav/llms.txt +6 -0
- package/dist/skeleton/llms.txt +3 -2
- package/dist/skeleton/skeleton.d.ts +1 -1
- package/dist/skeleton/skeleton.js +2 -2
- package/dist/slider/llms.txt +14 -1
- package/dist/slider/slider.d.ts +11 -2
- package/dist/slider/slider.js +15 -2
- package/dist/speed-dial/llms.txt +6 -2
- package/dist/speed-dial/speed-dial.d.ts +1 -1
- package/dist/speed-dial/speed-dial.js +14 -3
- package/dist/spinner/llms.txt +12 -8
- package/dist/spinner/spinner.d.ts +5 -1
- package/dist/spinner/spinner.js +12 -2
- package/dist/split-button/llms.txt +11 -3
- package/dist/split-button/split-button.d.ts +3 -1
- package/dist/split-button/split-button.js +43 -3
- package/dist/splitter/llms.txt +14 -4
- package/dist/splitter/splitter.d.ts +9 -2
- package/dist/splitter/splitter.js +15 -2
- package/dist/stepper/llms.txt +8 -4
- package/dist/stepper/stepper.d.ts +3 -2
- package/dist/stepper/stepper.js +7 -2
- package/dist/switch/llms.txt +10 -0
- package/dist/switch/switch.d.ts +1 -1
- package/dist/switch/switch.js +4 -2
- package/dist/switch-group/llms.txt +11 -0
- package/dist/switch-group/switch-group.d.ts +5 -1
- package/dist/switch-group/switch-group.js +7 -2
- package/dist/tabs/llms.txt +10 -4
- package/dist/tabs/tabs.d.ts +4 -0
- package/dist/tabs/tabs.js +21 -2
- package/dist/textarea/llms.txt +10 -0
- package/dist/textarea/textarea.d.ts +3 -1
- package/dist/textarea/textarea.js +13 -8
- package/dist/toast/llms.txt +21 -7
- package/dist/toast/toast-container.d.ts +6 -1
- package/dist/toast/toast-container.js +52 -1
- package/dist/toast/toast-store.d.ts +12 -0
- package/dist/toast/toast-store.js +46 -7
- package/dist/tooltip/llms.txt +10 -3
- package/dist/tooltip/tooltip.d.ts +5 -0
- package/dist/tooltip/tooltip.js +80 -7
- package/dist/tree/llms.txt +20 -8
- package/dist/tree/tree-keys.d.ts +27 -0
- package/dist/tree/tree-keys.js +39 -0
- package/dist/tree/tree.d.ts +12 -5
- package/dist/tree/tree.js +75 -29
- package/package.json +12 -12
|
@@ -23,6 +23,8 @@ export interface KptDatepickerProps extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
23
23
|
onOpenChange?: (open: boolean) => void;
|
|
24
24
|
/** Nazwa dostępna, gdy pola nie opisuje `<label for>`. */
|
|
25
25
|
ariaLabel?: string | null;
|
|
26
|
+
/** Id pola (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
27
|
+
id?: string;
|
|
26
28
|
disabled?: boolean;
|
|
27
29
|
invalid?: boolean;
|
|
28
30
|
touched?: boolean;
|
|
@@ -32,8 +34,15 @@ export interface KptDatepickerProps extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
32
34
|
* Wybór daty i godziny z własnym kalendarzem. Panel jest stosem widoków
|
|
33
35
|
* dekady → lata → miesiące → dni: klik w komórkę schodzi poziom niżej, klik w tytuł wchodzi wyżej.
|
|
34
36
|
*
|
|
37
|
+
* Klawiatura według wzorca WAI-ARIA „date picker dialog”: `Enter`/`Spacja`/`↓` na polu otwierają
|
|
38
|
+
* kalendarz i przenoszą fokus na wybrany (albo dzisiejszy) dzień. W siatce dni strzałki chodzą
|
|
39
|
+
* o dzień i tydzień, `Home`/`End` na początek i koniec tygodnia, `PageUp`/`PageDown` o miesiąc
|
|
40
|
+
* (z `Shift` — o rok); w siatkach miesięcy, lat i dekad strzałki chodzą po komórkach. `Tab` krąży
|
|
41
|
+
* w panelu, `Escape` zamyka go, a wybór i zamknięcie oddają fokus polu. `Delete`/`Backspace` na polu
|
|
42
|
+
* czyści wartość.
|
|
43
|
+
*
|
|
35
44
|
* @example
|
|
36
45
|
* <KptDatepicker value={date} onValueChange={setDate} startView="decade" minDate="1920-01-01" />
|
|
37
46
|
* <KptDatepicker selectionMode="datetime" hourFormat={12} value={at} onValueChange={setAt} />
|
|
38
47
|
*/
|
|
39
|
-
export declare function KptDatepicker({ value, defaultValue, onValueChange, selectionMode, startView, minDate, maxDate, placeholder, monthDisplay, displayFormat, hourFormat, minuteStep, open, onOpenChange, ariaLabel, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDatepickerProps>): import("react").JSX.Element;
|
|
48
|
+
export declare function KptDatepicker({ value, defaultValue, onValueChange, selectionMode, startView, minDate, maxDate, placeholder, monthDisplay, displayFormat, hourFormat, minuteStep, open, onOpenChange, ariaLabel, id, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDatepickerProps>): import("react").JSX.Element;
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { KPT_CALENDAR_LEVELS, buildDayGrid, buildDecadeCells, buildMonthCells, buildYearCells, canPageBack, canPageForward, centuryStart, commitLevelFor, decadeStart, formatDate, formatTime, initialPage, pad, pageBy, parseDateTime, parseTime, resolveStartView, selectedYearMonth, toIso, } from '@konce-pt/datetime';
|
|
2
|
+
import { useEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { KPT_CALENDAR_LEVELS, buildDayGrid, buildDecadeCells, buildMonthCells, buildYearCells, canPageBack, canPageForward, centuryStart, commitLevelFor, decadeStart, formatDate, formatTime, initialFocusDate, initialPage, moveCellIndex, moveFocusDate, pad, pageBy, parseDate, parseDateTime, parseTime, resolveStartView, selectedYearMonth, toIso, } from '@konce-pt/datetime';
|
|
4
4
|
import { monthNames, weekdayNames } from '@konce-pt/i18n';
|
|
5
5
|
import { cn } from "../internal/cn.js";
|
|
6
6
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
7
7
|
import { KptOverlay } from "../internal/overlay.js";
|
|
8
8
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
9
|
-
import {
|
|
9
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
10
10
|
import { KptButton } from "../button/button.js";
|
|
11
11
|
import { KptIcon } from "../icon/icon.js";
|
|
12
12
|
import { KptClock } from "../clock/clock.js";
|
|
13
|
+
import { useKptId } from "../internal/use-id.js";
|
|
14
|
+
import { focusCalendarActive, trapTab } from "../internal/calendar-focus.js";
|
|
13
15
|
/**
|
|
14
16
|
* Wybór daty i godziny z własnym kalendarzem. Panel jest stosem widoków
|
|
15
17
|
* dekady → lata → miesiące → dni: klik w komórkę schodzi poziom niżej, klik w tytuł wchodzi wyżej.
|
|
16
18
|
*
|
|
19
|
+
* Klawiatura według wzorca WAI-ARIA „date picker dialog”: `Enter`/`Spacja`/`↓` na polu otwierają
|
|
20
|
+
* kalendarz i przenoszą fokus na wybrany (albo dzisiejszy) dzień. W siatce dni strzałki chodzą
|
|
21
|
+
* o dzień i tydzień, `Home`/`End` na początek i koniec tygodnia, `PageUp`/`PageDown` o miesiąc
|
|
22
|
+
* (z `Shift` — o rok); w siatkach miesięcy, lat i dekad strzałki chodzą po komórkach. `Tab` krąży
|
|
23
|
+
* w panelu, `Escape` zamyka go, a wybór i zamknięcie oddają fokus polu. `Delete`/`Backspace` na polu
|
|
24
|
+
* czyści wartość.
|
|
25
|
+
*
|
|
17
26
|
* @example
|
|
18
27
|
* <KptDatepicker value={date} onValueChange={setDate} startView="decade" minDate="1920-01-01" />
|
|
19
28
|
* <KptDatepicker selectionMode="datetime" hourFormat={12} value={at} onValueChange={setAt} />
|
|
20
29
|
*/
|
|
21
|
-
export function KptDatepicker({ value, defaultValue = '', onValueChange, selectionMode = 'date', startView = null, minDate = null, maxDate = null, placeholder = '', monthDisplay = 'long', displayFormat = 'pl', hourFormat = 24, minuteStep = 1, open, onOpenChange, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
30
|
+
export function KptDatepicker({ value, defaultValue = '', onValueChange, selectionMode = 'date', startView = null, minDate = null, maxDate = null, placeholder = '', monthDisplay = 'long', displayFormat = 'pl', hourFormat = 24, minuteStep = 1, open, onOpenChange, ariaLabel = null, id, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
22
31
|
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
23
|
-
|
|
32
|
+
/** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
33
|
+
const field = useKptFieldA11y(id, 'kpt-datepicker');
|
|
24
34
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
25
35
|
const [isOpen, setOpen] = useControllableState({ value: open, defaultValue: false, onChange: onOpenChange });
|
|
26
36
|
const [anchor, setAnchor] = useState(null);
|
|
@@ -33,6 +43,14 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
33
43
|
const [timeStr, setTimeStr] = useState('');
|
|
34
44
|
const [pendingDate, setPendingDate] = useState('');
|
|
35
45
|
const [dtView, setDtView] = useState('calendar');
|
|
46
|
+
/** Dzień z fokusem w siatce dni — jedyny przystanek Tab siatki (roving tabindex). */
|
|
47
|
+
const [focusIso, setFocusIso] = useState('');
|
|
48
|
+
/** Komórka z fokusem w siatkach miesięcy, lat i dekad. */
|
|
49
|
+
const [focusCell, setFocusCell] = useState(0);
|
|
50
|
+
const panelId = useKptId(undefined, 'kpt-datepicker-panel');
|
|
51
|
+
const panelRef = useRef(null);
|
|
52
|
+
/** Prośba o przeniesienie fokusu po najbliższym renderze (otwarcie, ruch klawiszem, zmiana poziomu). */
|
|
53
|
+
const wantFocus = useRef(false);
|
|
36
54
|
const months = monthNames(activeLocale);
|
|
37
55
|
const weekdays = weekdayNames(activeLocale);
|
|
38
56
|
const commitLevel = commitLevelFor(selectionMode);
|
|
@@ -53,6 +71,16 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
53
71
|
setTimeStr(parsed ? `${pad(parsed.h)}:${pad(parsed.m)}` : '00:00');
|
|
54
72
|
setPendingDate(parsed?.date ?? '');
|
|
55
73
|
}
|
|
74
|
+
const startLevel = resolveStartView(selectionMode, startView);
|
|
75
|
+
const selected = selectionMode === 'datetime' ? (parseDateTime(current)?.date ?? '') : current;
|
|
76
|
+
const focus = initialFocusDate(selected, todayIso, minDate, maxDate);
|
|
77
|
+
setFocusIso(focus);
|
|
78
|
+
if (startLevel === 'day' && selectionMode !== 'time') {
|
|
79
|
+
const date = parseDate(focus);
|
|
80
|
+
setPage({ y: date.getFullYear(), m: date.getMonth() });
|
|
81
|
+
}
|
|
82
|
+
setFocusCell(-1); // -1 = domyślna komórka nowej siatki, wyliczana przy renderze
|
|
83
|
+
wantFocus.current = true;
|
|
56
84
|
// Zależność wyłącznie od `isOpen`: to otwarcie ma ustawić stan początkowy, a późniejsze
|
|
57
85
|
// zmiany wartości w otwartym panelu nie mogą go zresetować pod palcami użytkownika.
|
|
58
86
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -66,6 +94,21 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
66
94
|
setOpen(false);
|
|
67
95
|
onTouch?.();
|
|
68
96
|
};
|
|
97
|
+
/** Zamknięcie z powrotem fokusu na pole — po wyborze z klawiatury, `Escape`. */
|
|
98
|
+
const closeAndFocus = () => {
|
|
99
|
+
close();
|
|
100
|
+
triggerRef.current?.focus();
|
|
101
|
+
};
|
|
102
|
+
// Fokus po renderze. Panel w nakładce jest niewidoczny do pierwszego pomiaru — stąd rAF.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!isOpen || !wantFocus.current)
|
|
105
|
+
return;
|
|
106
|
+
const frame = requestAnimationFrame(() => {
|
|
107
|
+
wantFocus.current = false;
|
|
108
|
+
focusCalendarActive(panelRef.current, focusIso);
|
|
109
|
+
});
|
|
110
|
+
return () => cancelAnimationFrame(frame);
|
|
111
|
+
});
|
|
69
112
|
// ── Prezentacja w triggerze ──
|
|
70
113
|
const effectivePlaceholder = (() => {
|
|
71
114
|
if (placeholder)
|
|
@@ -140,6 +183,65 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
140
183
|
minDate,
|
|
141
184
|
maxDate,
|
|
142
185
|
});
|
|
186
|
+
const defaultCell = (() => {
|
|
187
|
+
const pick = levelCells.findIndex((c) => c.selected);
|
|
188
|
+
if (pick >= 0)
|
|
189
|
+
return pick;
|
|
190
|
+
const now = levelCells.findIndex((c) => c.current && !c.disabled);
|
|
191
|
+
return now >= 0 ? now : Math.max(0, levelCells.findIndex((c) => !c.disabled));
|
|
192
|
+
})();
|
|
193
|
+
const activeCell = focusCell >= 0 && focusCell < levelCells.length ? focusCell : defaultCell;
|
|
194
|
+
/** Pełna nazwa dnia dla czytnika („czwartek, 8 października 2026”) — sama liczba nic nie mówi. */
|
|
195
|
+
const dayFormat = new Intl.DateTimeFormat(activeLocale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
196
|
+
const dayName = (iso) => {
|
|
197
|
+
const date = parseDate(iso);
|
|
198
|
+
return date ? dayFormat.format(date) : iso;
|
|
199
|
+
};
|
|
200
|
+
const isRtl = () => !!panelRef.current && getComputedStyle(panelRef.current).direction === 'rtl';
|
|
201
|
+
const onDayKeyDown = (event) => {
|
|
202
|
+
const next = moveFocusDate(focusIso, event.key, { shift: event.shiftKey, rtl: isRtl(), minDate, maxDate });
|
|
203
|
+
if (next === null)
|
|
204
|
+
return;
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
setFocusIso(next);
|
|
207
|
+
const date = parseDate(next);
|
|
208
|
+
if (date.getFullYear() !== page.y || date.getMonth() !== page.m)
|
|
209
|
+
setPage({ y: date.getFullYear(), m: date.getMonth() });
|
|
210
|
+
wantFocus.current = true;
|
|
211
|
+
};
|
|
212
|
+
const onCellKeyDown = (event) => {
|
|
213
|
+
const next = moveCellIndex(levelCells.map((c) => c.disabled), activeCell, event.key, 3, isRtl());
|
|
214
|
+
if (next === null)
|
|
215
|
+
return;
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setFocusCell(next);
|
|
218
|
+
wantFocus.current = true;
|
|
219
|
+
};
|
|
220
|
+
/** Tytuł, który kliknięto, znika razem z poziomem — fokus idzie do nowej siatki. */
|
|
221
|
+
const goTo = (level) => {
|
|
222
|
+
setPanelView(level);
|
|
223
|
+
setFocusCell(-1);
|
|
224
|
+
wantFocus.current = true;
|
|
225
|
+
};
|
|
226
|
+
/** Strzałki ‹ ›: fokus zostaje na strzałce, ale przystanek siatki dni idzie za stroną. */
|
|
227
|
+
const turnPage = (dir) => {
|
|
228
|
+
const next = pageBy(page, panelView, dir);
|
|
229
|
+
setPage(next);
|
|
230
|
+
if (panelView === 'day') {
|
|
231
|
+
const moved = moveFocusDate(focusIso, dir === 1 ? 'PageDown' : 'PageUp', { minDate, maxDate });
|
|
232
|
+
// Dzień przycięty do granicy może leżeć poza nową stroną — wtedy pierwszy dozwolony dzień miesiąca.
|
|
233
|
+
const inPage = moved?.startsWith(`${next.y}-${pad(next.m + 1)}`);
|
|
234
|
+
const fallback = buildDayGrid({ page: next, selectedIso: selectedDateIso, todayIso, minDate, maxDate })
|
|
235
|
+
.flat()
|
|
236
|
+
.find((c) => c.inMonth && !c.disabled)?.iso;
|
|
237
|
+
const target = inPage ? moved : fallback;
|
|
238
|
+
if (target)
|
|
239
|
+
setFocusIso(target);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
setFocusCell(-1);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
143
245
|
const navLabel = (dir) => {
|
|
144
246
|
switch (panelView) {
|
|
145
247
|
case 'month':
|
|
@@ -161,7 +263,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
161
263
|
return;
|
|
162
264
|
}
|
|
163
265
|
setCurrent(iso);
|
|
164
|
-
|
|
266
|
+
closeAndFocus();
|
|
165
267
|
};
|
|
166
268
|
const selectCell = (cell) => {
|
|
167
269
|
if (cell.disabled)
|
|
@@ -169,11 +271,22 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
169
271
|
if (panelView === commitLevel) {
|
|
170
272
|
const mm = pad(cell.m + 1);
|
|
171
273
|
setCurrent(panelView === 'year' ? String(cell.y) : selectionMode === 'month' ? mm : `${cell.y}-${mm}`);
|
|
172
|
-
|
|
274
|
+
closeAndFocus();
|
|
173
275
|
return;
|
|
174
276
|
}
|
|
175
|
-
|
|
176
|
-
|
|
277
|
+
const m = panelView === 'month' ? cell.m : page.m;
|
|
278
|
+
setPage({ y: cell.y, m });
|
|
279
|
+
const lower = KPT_CALENDAR_LEVELS[KPT_CALENDAR_LEVELS.indexOf(panelView) - 1];
|
|
280
|
+
setPanelView(lower);
|
|
281
|
+
// Zejście poziom niżej: fokus na pierwszą sensowną komórkę nowej siatki (albo dzień miesiąca).
|
|
282
|
+
if (lower === 'day') {
|
|
283
|
+
const day = Math.min(parseDate(focusIso)?.getDate() ?? 1, new Date(cell.y, m + 1, 0).getDate());
|
|
284
|
+
setFocusIso(initialFocusDate(`${cell.y}-${pad(m + 1)}-${pad(day)}`, todayIso, minDate, maxDate));
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
setFocusCell(-1);
|
|
288
|
+
}
|
|
289
|
+
wantFocus.current = true;
|
|
177
290
|
};
|
|
178
291
|
const commit = () => {
|
|
179
292
|
const time = timeStr || '00:00';
|
|
@@ -184,7 +297,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
184
297
|
const parsed = parseDateTime(current);
|
|
185
298
|
setCurrent(`${pendingDate || parsed?.date || todayIso}T${time}`);
|
|
186
299
|
}
|
|
187
|
-
|
|
300
|
+
closeAndFocus();
|
|
188
301
|
};
|
|
189
302
|
const clear = (event) => {
|
|
190
303
|
event.stopPropagation();
|
|
@@ -195,8 +308,28 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
|
|
|
195
308
|
return (_jsxs("div", { className: cn('kpt-datepicker-host', className), ...rest, children: [_jsxs("div", { ref: (el) => {
|
|
196
309
|
triggerRef.current = el;
|
|
197
310
|
setAnchor(el);
|
|
198
|
-
}, className: cn('kpt-datepicker', isOpen && 'kpt-datepicker--open', disabled && 'kpt-datepicker--disabled', showInvalid && 'kpt-datepicker--invalid'), role: "button", tabIndex: 0, id: field
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
311
|
+
}, className: cn('kpt-datepicker', isOpen && 'kpt-datepicker--open', disabled && 'kpt-datepicker--disabled', showInvalid && 'kpt-datepicker--invalid'), role: "button", tabIndex: 0, "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, id: field.id, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? undefined, onClick: () => !disabled && setOpen(!isOpen), onKeyDown: (event) => {
|
|
312
|
+
if (disabled)
|
|
313
|
+
return;
|
|
314
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
if (!isOpen)
|
|
317
|
+
setOpen(true);
|
|
318
|
+
}
|
|
319
|
+
else if ((event.key === 'Delete' || event.key === 'Backspace') && current) {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
clear(event);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
// Przy otwartym panelu fokus idzie do kalendarza — „dotknięcie” wyśle dopiero zamknięcie.
|
|
325
|
+
onBlur: () => !isOpen && onTouch?.(), children: [display ? (_jsx("span", { className: "kpt-datepicker__value", children: display })) : (_jsx("span", { className: "kpt-datepicker__placeholder", children: effectivePlaceholder })), _jsx("span", { className: "kpt-datepicker__spacer" }), display ? (_jsx("button", { type: "button", className: "kpt-datepicker__clear", tabIndex: -1, onClick: clear, "aria-label": t('common.clear'), children: _jsx(KptIcon, { name: "x" }) })) : null, _jsx(KptIcon, { className: "kpt-datepicker__icon", name: selectionMode === 'time' ? 'clock' : 'calendar' })] }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 4, children: _jsxs("div", { ref: panelRef, id: panelId, role: "dialog", "aria-label": t('datepicker.dialog'), className: cn('kpt-calendar', showClock && 'kpt-calendar--clock'), onKeyDown: (event) => {
|
|
326
|
+
if (event.key === 'Escape') {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
closeAndFocus();
|
|
330
|
+
}
|
|
331
|
+
else if (event.key === 'Tab') {
|
|
332
|
+
trapTab(event.currentTarget, event.nativeEvent);
|
|
333
|
+
}
|
|
334
|
+
}, children: [showClock ? (_jsxs(_Fragment, { children: [selectionMode === 'datetime' ? (_jsxs("div", { className: "kpt-calendar__header", children: [_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => setDtView('calendar'), "aria-label": t('datepicker.calendar'), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsx("span", { className: "kpt-calendar__title", children: t('datepicker.time') })] })) : null, _jsx(KptClock, { value: timeStr, onValueChange: setTimeStr, hourFormat: hourFormat, minuteStep: minuteStep })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-calendar__header", children: [hasNav ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", disabled: !canPageBack(page, panelView, minDate), onClick: () => turnPage(-1), "aria-label": navLabel('prev'), children: _jsx(KptIcon, { name: "chevron-left" }) })) : null, _jsxs("span", { className: "kpt-calendar__titles", children: [panelView === 'day' ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('month'), "aria-label": t('datepicker.chooseMonth'), children: months[page.m] }), _jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('year'), "aria-label": t('datepicker.chooseYear'), children: page.y })] })) : null, panelView === 'month' ? (selectionMode === 'month' ? (_jsx("span", { className: "kpt-calendar__title", children: t('datepicker.month') })) : (_jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('year'), "aria-label": t('datepicker.chooseYear'), children: page.y }))) : null, panelView === 'year' ? (_jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('decade'), "aria-label": t('datepicker.chooseDecade'), children: `${decadeStart(page.y)} – ${decadeStart(page.y) + 9}` })) : null, panelView === 'decade' ? (_jsx("span", { className: "kpt-calendar__title", children: `${centuryStart(page.y)} – ${centuryStart(page.y) + 119}` })) : null] }), hasNav ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", disabled: !canPageForward(page, panelView, maxDate), onClick: () => turnPage(1), "aria-label": navLabel('next'), children: _jsx(KptIcon, { name: "chevron-right" }) })) : null] }), panelView === 'day' ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-calendar__weekdays", children: weekdays.map((weekday) => (_jsx("span", { className: "kpt-calendar__weekday", children: weekday }, weekday))) }), _jsx("div", { className: "kpt-calendar__grid", onKeyDown: onDayKeyDown, children: weeks.flat().map((cell) => (_jsx("button", { type: "button", className: cn('kpt-calendar__day', !cell.inMonth && 'kpt-calendar__day--muted', cell.today && 'kpt-calendar__day--today', cell.selected && 'kpt-calendar__day--selected'), "data-iso": cell.iso, tabIndex: cell.iso === focusIso ? 0 : -1, "aria-label": dayName(cell.iso), "aria-current": cell.today ? 'date' : undefined, disabled: cell.disabled, "aria-pressed": cell.selected, onClick: () => selectDay(cell.iso, cell.disabled), children: cell.day }, cell.iso))) })] })) : (_jsx("div", { className: cn('kpt-calendar__grid kpt-calendar__grid--cells', panelView === 'decade' && 'kpt-calendar__grid--decades'), onKeyDown: onCellKeyDown, children: levelCells.map((cell, index) => (_jsx("button", { type: "button", className: cn('kpt-calendar__cell', cell.selected && 'kpt-calendar__cell--selected', cell.current && 'kpt-calendar__cell--current'), tabIndex: index === activeCell ? 0 : -1, disabled: cell.disabled, "aria-pressed": cell.selected, onClick: () => selectCell(cell), children: cell.label }, cell.label))) })), selectionMode === 'datetime' ? (_jsxs("button", { type: "button", className: "kpt-calendar__timebar", onClick: () => setDtView('clock'), children: [_jsx(KptIcon, { name: "clock" }), _jsx("span", { children: formatTime(timeParts?.h ?? 0, timeParts?.m ?? 0, hourFormat) })] })) : null] })), hasFooter ? (_jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })) : null] }) })] }));
|
|
202
335
|
}
|
package/dist/datepicker/llms.txt
CHANGED
|
@@ -26,9 +26,25 @@ Import: `import { KptDatepicker } from '@konce-pt/react';`
|
|
|
26
26
|
- `hourFormat`: 12 | 24 (default 24) — the dial; the stored value is always 24h
|
|
27
27
|
- `minuteStep`: number (default 1) — minute snapping on the dial
|
|
28
28
|
- `open` / `onOpenChange` — the panel state, so it can be opened from outside
|
|
29
|
-
- `placeholder`, `ariaLabel`, `disabled`, `invalid`, `touched
|
|
29
|
+
- `placeholder`, `ariaLabel`, `disabled`, `invalid`, `touched`
|
|
30
|
+
- `onTouch()` — fires when the panel closes or focus leaves the closed trigger
|
|
30
31
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
31
32
|
|
|
33
|
+
## Keyboard
|
|
34
|
+
The date picker dialog pattern. The trigger has `aria-haspopup="dialog"` and `aria-expanded`;
|
|
35
|
+
`Enter`, `Space` and `↓` open the panel — a `role="dialog"` named "Choose a date" — and move focus to
|
|
36
|
+
the selected day, or today, clamped to `minDate`/`maxDate`.
|
|
37
|
+
- Day grid: `←`/`→` one day, `↑`/`↓` one week, `Home`/`End` Monday and Sunday of the week,
|
|
38
|
+
`PageUp`/`PageDown` one month, with `Shift` one year (the day is clamped to the month length).
|
|
39
|
+
Crossing a month edge turns the page. Each day is named with the full date from `Intl`
|
|
40
|
+
("Thursday, October 8, 2026"), today has `aria-current="date"`, and only the focused day is
|
|
41
|
+
a Tab stop. In RTL the horizontal arrows are mirrored.
|
|
42
|
+
- Month, year and decade grids: the arrows move between cells (three columns), `Home`/`End` to the ends.
|
|
43
|
+
Clicking a title moves focus to the new grid.
|
|
44
|
+
- `Tab` cycles inside the panel; `Escape` closes it. Picking a value (or "Done") closes the panel
|
|
45
|
+
and returns focus to the trigger. `Delete`/`Backspace` on the trigger clears the value; the "×" button
|
|
46
|
+
sits outside the Tab order.
|
|
47
|
+
|
|
32
48
|
## The view stack
|
|
33
49
|
The header is always `‹ title ›`: the arrows page the current level, the title goes one level up.
|
|
34
50
|
|
|
@@ -65,10 +81,18 @@ Weekday and month names come from `Intl` for the active locale, not from the dic
|
|
|
65
81
|
navigation labels and "Done" come from the `datepicker` and `common` namespaces; `locale` and
|
|
66
82
|
`dictionary` override them per instance.
|
|
67
83
|
|
|
84
|
+
## Label and description
|
|
85
|
+
`id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
|
|
86
|
+
generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
|
|
87
|
+
`aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
|
|
88
|
+
is added to `aria-describedby`. New: `id`; the trigger is the element that carries it.
|
|
89
|
+
|
|
68
90
|
## Tokens
|
|
69
91
|
`--kpt-form-field-{bg,text,border,border-focus,border-error,radius}`; the panel
|
|
70
92
|
`--kpt-color-surface-raised`, the selection `--kpt-color-primary`.
|
|
71
93
|
|
|
72
94
|
## Accessibility
|
|
73
|
-
The trigger is `role="button"` with `
|
|
74
|
-
`
|
|
95
|
+
The trigger is `role="button"` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`;
|
|
96
|
+
the panel is a named `role="dialog"`; every cell is a real `<button>` with `aria-pressed`, days carry
|
|
97
|
+
the full date and `aria-current="date"`; `ariaLabel` names the field when no `<label for>` describes
|
|
98
|
+
it. The keyboard is described above.
|
package/dist/dialog/dialog.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface KptDialogProps extends Omit<ComponentPropsWithRef<'dialog'>, 'o
|
|
|
20
20
|
* Okno modalne na natywnym `<dialog>` — pułapka fokusu, Escape i backdrop pochodzą z platformy,
|
|
21
21
|
* nie z naszego kodu. Stopkę wstaw przez `<KptDialog.Footer>`.
|
|
22
22
|
*
|
|
23
|
+
* Z `header` okno bierze nazwę z nagłówka (`aria-labelledby`); bez niego podaj `aria-label`.
|
|
24
|
+
* Okno wymagające odpowiedzi dostaje `role="alertdialog"` i `aria-describedby` wprost z propsów
|
|
25
|
+
* `<dialog>` — tak robi `KptConfirmContainer`.
|
|
26
|
+
*
|
|
23
27
|
* @example
|
|
24
28
|
* <KptDialog open={open} onOpenChange={setOpen} header="Usunąć?">
|
|
25
29
|
* Tej operacji nie można cofnąć.
|
package/dist/dialog/dialog.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Children, isValidElement, useEffect, useRef, useState, } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { composeRefs } from "../internal/compose-refs.js";
|
|
5
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
6
|
import { KptOverlayContainerContext } from "../internal/overlay-container.js";
|
|
6
7
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
8
|
import { KptIcon } from "../icon/icon.js";
|
|
@@ -9,6 +10,10 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
9
10
|
* Okno modalne na natywnym `<dialog>` — pułapka fokusu, Escape i backdrop pochodzą z platformy,
|
|
10
11
|
* nie z naszego kodu. Stopkę wstaw przez `<KptDialog.Footer>`.
|
|
11
12
|
*
|
|
13
|
+
* Z `header` okno bierze nazwę z nagłówka (`aria-labelledby`); bez niego podaj `aria-label`.
|
|
14
|
+
* Okno wymagające odpowiedzi dostaje `role="alertdialog"` i `aria-describedby` wprost z propsów
|
|
15
|
+
* `<dialog>` — tak robi `KptConfirmContainer`.
|
|
16
|
+
*
|
|
12
17
|
* @example
|
|
13
18
|
* <KptDialog open={open} onOpenChange={setOpen} header="Usunąć?">
|
|
14
19
|
* Tej operacji nie można cofnąć.
|
|
@@ -21,6 +26,7 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
21
26
|
export function KptDialog({ open, onOpenChange, header = '', size = 'md', dismissible = true, locale, dictionary, className, children, ref, ...rest }) {
|
|
22
27
|
const { t } = useKptMessages({ locale, dictionary });
|
|
23
28
|
const dialogRef = useRef(null);
|
|
29
|
+
const titleId = useKptId(undefined, 'kpt-dialog-title');
|
|
24
30
|
// Element trafia do kontekstu, więc musi wywołać render — sam `ref` by nie wystarczył.
|
|
25
31
|
const [dialogEl, setDialogEl] = useState(null);
|
|
26
32
|
useEffect(() => {
|
|
@@ -46,7 +52,7 @@ export function KptDialog({ open, onOpenChange, header = '', size = 'md', dismis
|
|
|
46
52
|
}
|
|
47
53
|
onOpenChange(false);
|
|
48
54
|
};
|
|
49
|
-
return (_jsx(KptOverlayContainerContext.Provider, { value: open ? dialogEl : null, children: _jsxs("dialog", { ref: composeRefs(dialogRef, setDialogEl, ref), className: cn('kpt-dialog', className), "data-size": size, onClose: () => onOpenChange(false), onCancel: onCancel, ...rest, children: [header ? (_jsxs("div", { className: "kpt-dialog__header", children: [_jsx("h2", { className: "kpt-dialog__title", children: header }), dismissible ? (_jsx("button", { className: "kpt-dialog__close", type: "button", onClick: () => onOpenChange(false), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] })) : null, _jsx("div", { className: "kpt-dialog__body", children: body }), footers] }) }));
|
|
55
|
+
return (_jsx(KptOverlayContainerContext.Provider, { value: open ? dialogEl : null, children: _jsxs("dialog", { ref: composeRefs(dialogRef, setDialogEl, ref), className: cn('kpt-dialog', className), "data-size": size, "aria-labelledby": header ? titleId : undefined, onClose: () => onOpenChange(false), onCancel: onCancel, ...rest, children: [header ? (_jsxs("div", { className: "kpt-dialog__header", children: [_jsx("h2", { className: "kpt-dialog__title", id: titleId, children: header }), dismissible ? (_jsx("button", { className: "kpt-dialog__close", type: "button", onClick: () => onOpenChange(false), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] })) : null, _jsx("div", { className: "kpt-dialog__body", children: body }), footers] }) }));
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Stopka okna. Nosi atrybut `kptdialogfooter`, w który celuje wspólny SCSS (nazwy atrybutów
|
package/dist/dialog/llms.txt
CHANGED
|
@@ -49,3 +49,5 @@ dialog; the mechanism differs, the result does not.
|
|
|
49
49
|
A native `<dialog>` opened with `showModal()`: focus moves inside and is trapped, the rest of the
|
|
50
50
|
page becomes inert, and Escape closes it — all from the browser. With `dismissible={false}` the
|
|
51
51
|
`cancel` event is prevented, so Escape stops working; give the user another way out.
|
|
52
|
+
With `header` the dialog is named by its title (`aria-labelledby`); without one pass `aria-label`.
|
|
53
|
+
`role="alertdialog"` and `aria-describedby` go straight through as `<dialog>` props.
|
package/dist/drawer/drawer.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback } from 'react';
|
|
2
|
+
import { useCallback, useRef } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
|
+
import { useModalFocus } from "../internal/use-modal-focus.js";
|
|
4
6
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
5
7
|
import { KptIcon } from "../icon/icon.js";
|
|
6
8
|
/**
|
|
@@ -17,9 +19,12 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
17
19
|
export function KptDrawer({ open, onOpenChange, position = 'left', header = '', dismissible = true, closeOnBackdrop = true, locale, dictionary, className, children, ...rest }) {
|
|
18
20
|
const { t } = useKptMessages({ locale, dictionary });
|
|
19
21
|
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
22
|
+
const panelRef = useRef(null);
|
|
23
|
+
const titleId = useKptId(undefined, 'kpt-drawer-title');
|
|
24
|
+
useModalFocus(open, panelRef);
|
|
20
25
|
const onKeyDown = (event) => {
|
|
21
26
|
if (event.key === 'Escape' && dismissible)
|
|
22
27
|
close();
|
|
23
28
|
};
|
|
24
|
-
return (_jsxs("div", { className: cn('kpt-drawer-host', className), ...rest, children: [_jsx("div", { className: cn('kpt-drawer__backdrop', open && 'is-open'), onClick: () => closeOnBackdrop && close() }), _jsxs("aside", { className: cn('kpt-drawer__panel', open && 'is-open'), "data-position": position, role: "dialog", "aria-modal": "true", tabIndex: -1, onKeyDown: onKeyDown, children: [header || dismissible ? (_jsxs("div", { className: "kpt-drawer__header", children: [_jsx("span", { className: "kpt-drawer__title", children: header }), dismissible ? (_jsx("button", { type: "button", className: "kpt-drawer__close", onClick: close, "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] })) : null, _jsx("div", { className: "kpt-drawer__body", children: children })] })] }));
|
|
29
|
+
return (_jsxs("div", { className: cn('kpt-drawer-host', className), ...rest, children: [_jsx("div", { className: cn('kpt-drawer__backdrop', open && 'is-open'), onClick: () => closeOnBackdrop && close() }), _jsxs("aside", { ref: panelRef, className: cn('kpt-drawer__panel', open && 'is-open'), "data-position": position, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, onKeyDown: onKeyDown, children: [header || dismissible ? (_jsxs("div", { className: "kpt-drawer__header", children: [_jsx("span", { className: "kpt-drawer__title", id: titleId, children: header }), dismissible ? (_jsx("button", { type: "button", className: "kpt-drawer__close", onClick: close, "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] })) : null, _jsx("div", { className: "kpt-drawer__body", children: children })] })] }));
|
|
25
30
|
}
|
package/dist/drawer/llms.txt
CHANGED
|
@@ -29,9 +29,14 @@ That is what makes the slide animatable — mounting the panel on open would giv
|
|
|
29
29
|
nothing to animate from.
|
|
30
30
|
|
|
31
31
|
## Not a native modal
|
|
32
|
-
Unlike `KptDialog` this is a plain `<aside role="dialog" aria-modal="true">`,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Unlike `KptDialog` this is a plain `<aside role="dialog" aria-modal="true">`, not a native
|
|
33
|
+
`<dialog>` — the focus handling below is done by the component, not by the browser.
|
|
34
|
+
|
|
35
|
+
## Focus and keyboard
|
|
36
|
+
Opening moves focus into the panel (its first focusable element, else the panel
|
|
37
|
+
itself) and traps it there — `Tab` / `Shift+Tab` wrap inside, as `aria-modal="true"` promises.
|
|
38
|
+
Closing returns focus to the element that had it before. `Escape` closes the panel — only when `dismissible`, like the `×` button. The panel
|
|
39
|
+
is named by its `header` through `aria-labelledby`. The shared helper is `internal/use-modal-focus.ts` (the Angular port has a twin on the CDK focus trap).
|
|
35
40
|
|
|
36
41
|
## Examples
|
|
37
42
|
<KptDrawer open={open} onOpenChange={setOpen} position="right" header="Filters">
|
|
@@ -43,5 +48,5 @@ the panel. For a hard modal reach for `KptDialog`.
|
|
|
43
48
|
`--kpt-color-overlay-scrim`, `--kpt-motion-duration-base`, `--kpt-z-overlay`.
|
|
44
49
|
|
|
45
50
|
## Accessibility
|
|
46
|
-
`role="dialog"
|
|
47
|
-
|
|
51
|
+
`role="dialog"`, `aria-modal="true"` and `aria-labelledby` pointing at the title; the close button gets
|
|
52
|
+
its label from `common.close`. Focus handling — see "Focus and keyboard" above.
|
package/dist/empty/empty.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export interface KptEmptyProps extends Omit<ComponentPropsWithRef<'div'>, 'title
|
|
|
4
4
|
icon?: string | null;
|
|
5
5
|
title?: string;
|
|
6
6
|
description?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Robi z tytułu nagłówek strony (`role="heading"` z `aria-level`), żeby pusty stan pojawił się
|
|
9
|
+
* w strukturze dokumentu — np. gdy zastępuje całą sekcję. Bez tego tytuł to sam tekst.
|
|
10
|
+
*/
|
|
11
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
7
12
|
/** Akcje pod opisem. */
|
|
8
13
|
children?: ReactNode;
|
|
9
14
|
}
|
|
@@ -15,4 +20,4 @@ export interface KptEmptyProps extends Omit<ComponentPropsWithRef<'div'>, 'title
|
|
|
15
20
|
* <KptButton>Wyczyść filtry</KptButton>
|
|
16
21
|
* </KptEmpty>
|
|
17
22
|
*/
|
|
18
|
-
export declare function KptEmpty({ icon, title, description, className, children, ...rest }: Readonly<KptEmptyProps>): import("react").JSX.Element;
|
|
23
|
+
export declare function KptEmpty({ icon, title, description, headingLevel, className, children, ...rest }: Readonly<KptEmptyProps>): import("react").JSX.Element;
|
package/dist/empty/empty.js
CHANGED
|
@@ -9,6 +9,6 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
9
9
|
* <KptButton>Wyczyść filtry</KptButton>
|
|
10
10
|
* </KptEmpty>
|
|
11
11
|
*/
|
|
12
|
-
export function KptEmpty({ icon = 'info-circle', title = '', description = '', className, children, ...rest }) {
|
|
13
|
-
return (_jsx("div", { className: cn('kpt-empty-host', className), ...rest, children: _jsxs("div", { className: "kpt-empty", children: [icon ? (_jsx("div", { className: "kpt-empty__icon", children: _jsx(KptIcon, { name: icon }) })) : null, title ? _jsx("p", { className: "kpt-empty__title", children: title }) : null, description ? _jsx("p", { className: "kpt-empty__desc", children: description }) : null, _jsx("div", { className: "kpt-empty__actions", children: children })] }) }));
|
|
12
|
+
export function KptEmpty({ icon = 'info-circle', title = '', description = '', headingLevel, className, children, ...rest }) {
|
|
13
|
+
return (_jsx("div", { className: cn('kpt-empty-host', className), ...rest, children: _jsxs("div", { className: "kpt-empty", children: [icon ? (_jsx("div", { className: "kpt-empty__icon", children: _jsx(KptIcon, { name: icon }) })) : null, title ? (_jsx("p", { className: "kpt-empty__title", role: headingLevel ? 'heading' : undefined, "aria-level": headingLevel, children: title })) : null, description ? _jsx("p", { className: "kpt-empty__desc", children: description }) : null, _jsx("div", { className: "kpt-empty__actions", children: children })] }) }));
|
|
14
14
|
}
|
package/dist/empty/llms.txt
CHANGED
|
@@ -16,6 +16,7 @@ Import: `import { KptEmpty } from '@konce-pt/react';`
|
|
|
16
16
|
## Props
|
|
17
17
|
- `icon`: string | null (default 'info-circle') — a registry icon name; `null` hides it
|
|
18
18
|
- `title`: string, `description`: string — each row disappears when its text is empty
|
|
19
|
+
- `headingLevel`: 1–6 | null (default null) — makes the title a heading (`role="heading"` + `aria-level`)
|
|
19
20
|
- `children` — the actions under the description
|
|
20
21
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
21
22
|
|
|
@@ -30,6 +31,6 @@ Import: `import { KptEmpty } from '@konce-pt/react';`
|
|
|
30
31
|
`--kpt-color-on-surface-muted` (the description), `--kpt-space-*`.
|
|
31
32
|
|
|
32
33
|
## Accessibility
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
The icon is decorative. The title is a styled paragraph unless `headingLevel` makes it a heading —
|
|
35
|
+
use that when the empty state stands in for a whole section. Replacing results with an empty state
|
|
36
|
+
is not announced; report “0 results” in a live region of your own.
|
package/dist/fab/llms.txt
CHANGED
|
@@ -31,7 +31,8 @@ pins it wherever the design wants (usually `position: fixed` in a corner).
|
|
|
31
31
|
|
|
32
32
|
## Tokens
|
|
33
33
|
`--kpt-color-primary` / `--kpt-color-on-primary` (filled), `--kpt-color-muted` /
|
|
34
|
-
`--kpt-color-on-muted` (tonal), `--kpt-elevation-3
|
|
34
|
+
`--kpt-color-on-muted` (tonal), `--kpt-elevation-3` (`--kpt-elevation-4` on hover); shape
|
|
35
|
+
`--kpt-radius-lg` (md), `--kpt-radius-md` (mini), `--kpt-radius-full` (extended).
|
|
35
36
|
|
|
36
37
|
## Accessibility
|
|
37
38
|
A native `<button>`; in the round variant give it `aria-label`, in `extended` the text does the job.
|
|
@@ -16,6 +16,11 @@ export interface KptFileUploadProps extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
16
16
|
* Wgrywanie plików: strefa reagująca na kliknięcie i na upuszczenie, plus lista wybranych plików.
|
|
17
17
|
* Wartością jest tablica `File`.
|
|
18
18
|
*
|
|
19
|
+
* Strefa to `role="button"` z jednym przystankiem Tab: `Enter` i spacja otwierają okno wyboru,
|
|
20
|
+
* tak jak kliknięcie — natywne pole pliku jest ukryte, więc bez tego klawiatura nie miałaby
|
|
21
|
+
* jak wybrać pliku. `disabled` wyjmuje strefę z kolejności Tab. Stan błędu: `invalid`
|
|
22
|
+
* i `touched` (pominięty `touched` = błąd widać od razu, jak w `KptInput`).
|
|
23
|
+
*
|
|
19
24
|
* @example
|
|
20
25
|
* <KptFileUpload value={files} onValueChange={setFiles} accept="image/*" multiple />
|
|
21
26
|
*/
|
|
@@ -10,10 +10,19 @@ function formatSize(file) {
|
|
|
10
10
|
const kb = file.size / 1024;
|
|
11
11
|
return kb < 1024 ? `${kb.toFixed(0)} KB` : `${(kb / 1024).toFixed(1)} MB`;
|
|
12
12
|
}
|
|
13
|
+
/** Ikona pozycji listy: obrazek dla plików graficznych, spinacz dla pozostałych. */
|
|
14
|
+
function fileIcon(file) {
|
|
15
|
+
return file.type.startsWith('image/') ? 'photo' : 'paperclip';
|
|
16
|
+
}
|
|
13
17
|
/**
|
|
14
18
|
* Wgrywanie plików: strefa reagująca na kliknięcie i na upuszczenie, plus lista wybranych plików.
|
|
15
19
|
* Wartością jest tablica `File`.
|
|
16
20
|
*
|
|
21
|
+
* Strefa to `role="button"` z jednym przystankiem Tab: `Enter` i spacja otwierają okno wyboru,
|
|
22
|
+
* tak jak kliknięcie — natywne pole pliku jest ukryte, więc bez tego klawiatura nie miałaby
|
|
23
|
+
* jak wybrać pliku. `disabled` wyjmuje strefę z kolejności Tab. Stan błędu: `invalid`
|
|
24
|
+
* i `touched` (pominięty `touched` = błąd widać od razu, jak w `KptInput`).
|
|
25
|
+
*
|
|
17
26
|
* @example
|
|
18
27
|
* <KptFileUpload value={files} onValueChange={setFiles} accept="image/*" multiple />
|
|
19
28
|
*/
|
|
@@ -22,6 +31,18 @@ export function KptFileUpload({ value, defaultValue = EMPTY, onValueChange, acce
|
|
|
22
31
|
const inputRef = useRef(null);
|
|
23
32
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
24
33
|
const [dragover, setDragover] = useState(false);
|
|
34
|
+
const showError = invalid && (touched ?? true);
|
|
35
|
+
const openPicker = () => {
|
|
36
|
+
if (!disabled)
|
|
37
|
+
inputRef.current?.click();
|
|
38
|
+
};
|
|
39
|
+
/** `Enter` / spacja na strefie — spacja bez `preventDefault` przewinęłaby stronę. */
|
|
40
|
+
const onZoneKeyDown = (event) => {
|
|
41
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
42
|
+
return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
openPicker();
|
|
45
|
+
};
|
|
25
46
|
const addFiles = (list) => {
|
|
26
47
|
if (!list?.length)
|
|
27
48
|
return;
|
|
@@ -34,7 +55,7 @@ export function KptFileUpload({ value, defaultValue = EMPTY, onValueChange, acce
|
|
|
34
55
|
// Czyszczenie pola pozwala wybrać ten sam plik drugi raz — inaczej `change` by nie poleciał.
|
|
35
56
|
event.target.value = '';
|
|
36
57
|
};
|
|
37
|
-
return (_jsxs("div", { className: cn('kpt-file-upload-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-file-upload__zone', dragover && 'is-dragover', disabled && 'is-disabled'),
|
|
58
|
+
return (_jsxs("div", { className: cn('kpt-file-upload-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-file-upload__zone', dragover && 'is-dragover', disabled && 'is-disabled'), role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, "aria-invalid": showError || undefined, "data-invalid": showError ? '' : undefined, onClick: openPicker, onKeyDown: onZoneKeyDown, onDragOver: (event) => {
|
|
38
59
|
event.preventDefault();
|
|
39
60
|
if (!disabled)
|
|
40
61
|
setDragover(true);
|
|
@@ -43,8 +64,8 @@ export function KptFileUpload({ value, defaultValue = EMPTY, onValueChange, acce
|
|
|
43
64
|
setDragover(false);
|
|
44
65
|
if (!disabled)
|
|
45
66
|
addFiles(event.dataTransfer?.files);
|
|
46
|
-
}, children: [_jsx("input", { ref: inputRef, type: "file", hidden: true, accept: accept, multiple: multiple, onChange: onSelect }), _jsx(KptIcon, { name: "upload", className: "kpt-file-upload__icon" }), _jsx("div", { className: "kpt-file-upload__text", dangerouslySetInnerHTML: { __html: t('fileUpload.hint') } }), accept ? _jsx("div", { className: "kpt-file-upload__hint", children: accept }) : null] }), current.length ? (_jsx("ul", { className: "kpt-file-upload__list", children: current.map((file) => (_jsxs("li", { className: "kpt-file-upload__file", children: [_jsx(KptIcon, { name:
|
|
67
|
+
}, children: [_jsx("input", { ref: inputRef, type: "file", hidden: true, accept: accept, multiple: multiple, onChange: onSelect }), _jsx(KptIcon, { name: "upload", className: "kpt-file-upload__icon" }), _jsx("div", { className: "kpt-file-upload__text", dangerouslySetInnerHTML: { __html: t('fileUpload.hint') } }), accept ? _jsx("div", { className: "kpt-file-upload__hint", children: accept }) : null] }), current.length ? (_jsx("ul", { className: "kpt-file-upload__list", children: current.map((file, index) => (_jsxs("li", { className: "kpt-file-upload__file", children: [_jsx(KptIcon, { name: fileIcon(file) }), _jsx("span", { className: "kpt-file-upload__name", children: file.name }), _jsx("span", { className: "kpt-file-upload__size", children: formatSize(file) }), _jsx("button", { type: "button", className: "kpt-file-upload__remove", "aria-label": t('fileUpload.remove'), onClick: (event) => {
|
|
47
68
|
event.stopPropagation();
|
|
48
69
|
setCurrent(current.filter((item) => item !== file));
|
|
49
|
-
}, children: _jsx(KptIcon, { name: "trash" }) })] }, file.name))) })) : null] }));
|
|
70
|
+
}, children: _jsx(KptIcon, { name: "trash" }) })] }, `${index}-${file.name}`))) })) : null] }));
|
|
50
71
|
}
|
|
@@ -6,7 +6,7 @@ Import: `import { KptFileUpload } from '@konce-pt/react';`
|
|
|
6
6
|
|
|
7
7
|
## DOM
|
|
8
8
|
<div class="kpt-file-upload-host">
|
|
9
|
-
<div class="kpt-file-upload__zone is-dragover">
|
|
9
|
+
<div class="kpt-file-upload__zone is-dragover" role="button" tabindex="0">
|
|
10
10
|
<input type="file" hidden>
|
|
11
11
|
<span class="kpt-icon-host kpt-file-upload__icon">…</span>
|
|
12
12
|
<div class="kpt-file-upload__text"><strong>Click</strong> or drag files here</div>
|
|
@@ -40,9 +40,20 @@ comes from the dictionary (ours or the app's), never from user input.
|
|
|
40
40
|
The zone hint and the remove label come from the `fileUpload` namespace; `locale` and `dictionary`
|
|
41
41
|
override them per instance.
|
|
42
42
|
|
|
43
|
+
## Keyboard and states
|
|
44
|
+
- The zone is `role="button"` with a single Tab stop: `Enter` and `Space` open the file picker,
|
|
45
|
+
the same as a click. The native `<input type="file">` is hidden, so without this a keyboard user
|
|
46
|
+
could not pick a file at all.
|
|
47
|
+
- `disabled` takes the zone out of the Tab order (`tabindex="-1"`, `aria-disabled`) and blocks
|
|
48
|
+
both picking and dropping.
|
|
49
|
+
- The error state (`aria-invalid`, a `--kpt-color-danger` border via `[data-invalid]`) shows once
|
|
50
|
+
the field is invalid and touched (an omitted `touched` shows it right away, as in `KptInput`).
|
|
51
|
+
- List items get an icon by type: `photo` for `image/*`, `paperclip` for anything else.
|
|
52
|
+
|
|
43
53
|
## Tokens
|
|
44
|
-
The zone `--kpt-
|
|
54
|
+
The zone `--kpt-color-border-strong` (dashed), hover / dragover `--kpt-color-primary`, error `--kpt-color-danger`,
|
|
55
|
+
focus ring `--kpt-color-focus-ring`.
|
|
45
56
|
|
|
46
57
|
## Accessibility
|
|
47
|
-
The remove button is labelled from the dictionary
|
|
48
|
-
|
|
58
|
+
The remove button is labelled from the dictionary. The zone itself is the keyboard path to the
|
|
59
|
+
system picker — see "Keyboard and states" above.
|