@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.
Files changed (196) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/roving.d.ts +9 -0
  96. package/dist/internal/roving.js +43 -0
  97. package/dist/internal/use-modal-focus.d.ts +10 -0
  98. package/dist/internal/use-modal-focus.js +57 -0
  99. package/dist/knob/knob.d.ts +5 -1
  100. package/dist/knob/knob.js +4 -2
  101. package/dist/knob/llms.txt +8 -0
  102. package/dist/listbox/listbox.d.ts +13 -1
  103. package/dist/listbox/listbox.js +89 -6
  104. package/dist/listbox/llms.txt +21 -5
  105. package/dist/megamenu/llms.txt +28 -10
  106. package/dist/megamenu/megamenu.d.ts +10 -1
  107. package/dist/megamenu/megamenu.js +116 -5
  108. package/dist/menu/llms.txt +12 -1
  109. package/dist/menu/menu-keys.d.ts +15 -0
  110. package/dist/menu/menu-keys.js +53 -0
  111. package/dist/menu/menu-rows.js +1 -1
  112. package/dist/menu/menu.js +52 -4
  113. package/dist/menubar/llms.txt +24 -7
  114. package/dist/menubar/menubar.d.ts +8 -1
  115. package/dist/menubar/menubar.js +92 -6
  116. package/dist/meter-group/llms.txt +10 -3
  117. package/dist/meter-group/meter-group.d.ts +9 -2
  118. package/dist/meter-group/meter-group.js +8 -2
  119. package/dist/order-list/llms.txt +11 -3
  120. package/dist/order-list/order-list.d.ts +5 -1
  121. package/dist/order-list/order-list.js +46 -8
  122. package/dist/panel/llms.txt +9 -6
  123. package/dist/panel/panel.js +3 -1
  124. package/dist/password/llms.txt +8 -0
  125. package/dist/password/password.d.ts +6 -1
  126. package/dist/password/password.js +6 -5
  127. package/dist/pick-list/llms.txt +12 -3
  128. package/dist/pick-list/pick-list.d.ts +5 -1
  129. package/dist/pick-list/pick-list.js +52 -5
  130. package/dist/popover/llms.txt +9 -2
  131. package/dist/popover/popover.d.ts +9 -3
  132. package/dist/popover/popover.js +41 -6
  133. package/dist/public-api.d.ts +6 -6
  134. package/dist/public-api.js +1 -1
  135. package/dist/radio-group/llms.txt +11 -0
  136. package/dist/radio-group/radio-group.d.ts +9 -1
  137. package/dist/radio-group/radio-group.js +5 -2
  138. package/dist/rating/llms.txt +18 -5
  139. package/dist/rating/rating.d.ts +21 -3
  140. package/dist/rating/rating.js +87 -12
  141. package/dist/rich-text/llms.txt +20 -0
  142. package/dist/rich-text/rich-text.d.ts +7 -1
  143. package/dist/rich-text/rich-text.js +155 -28
  144. package/dist/scroll-top/llms.txt +5 -1
  145. package/dist/scroll-top/scroll-top.js +3 -1
  146. package/dist/select/llms.txt +21 -7
  147. package/dist/select/select.d.ts +12 -1
  148. package/dist/select/select.js +110 -38
  149. package/dist/sidenav/llms.txt +6 -0
  150. package/dist/skeleton/llms.txt +3 -2
  151. package/dist/skeleton/skeleton.d.ts +1 -1
  152. package/dist/skeleton/skeleton.js +2 -2
  153. package/dist/slider/llms.txt +14 -1
  154. package/dist/slider/slider.d.ts +11 -2
  155. package/dist/slider/slider.js +15 -2
  156. package/dist/speed-dial/llms.txt +6 -2
  157. package/dist/speed-dial/speed-dial.d.ts +1 -1
  158. package/dist/speed-dial/speed-dial.js +14 -3
  159. package/dist/spinner/llms.txt +12 -8
  160. package/dist/spinner/spinner.d.ts +5 -1
  161. package/dist/spinner/spinner.js +12 -2
  162. package/dist/split-button/llms.txt +11 -3
  163. package/dist/split-button/split-button.d.ts +3 -1
  164. package/dist/split-button/split-button.js +43 -3
  165. package/dist/splitter/llms.txt +14 -4
  166. package/dist/splitter/splitter.d.ts +9 -2
  167. package/dist/splitter/splitter.js +15 -2
  168. package/dist/stepper/llms.txt +8 -4
  169. package/dist/stepper/stepper.d.ts +3 -2
  170. package/dist/stepper/stepper.js +7 -2
  171. package/dist/switch/llms.txt +10 -0
  172. package/dist/switch/switch.d.ts +1 -1
  173. package/dist/switch/switch.js +4 -2
  174. package/dist/switch-group/llms.txt +11 -0
  175. package/dist/switch-group/switch-group.d.ts +5 -1
  176. package/dist/switch-group/switch-group.js +7 -2
  177. package/dist/tabs/llms.txt +10 -4
  178. package/dist/tabs/tabs.d.ts +4 -0
  179. package/dist/tabs/tabs.js +21 -2
  180. package/dist/textarea/llms.txt +10 -0
  181. package/dist/textarea/textarea.d.ts +3 -1
  182. package/dist/textarea/textarea.js +13 -8
  183. package/dist/toast/llms.txt +21 -7
  184. package/dist/toast/toast-container.d.ts +6 -1
  185. package/dist/toast/toast-container.js +52 -1
  186. package/dist/toast/toast-store.d.ts +12 -0
  187. package/dist/toast/toast-store.js +46 -7
  188. package/dist/tooltip/llms.txt +10 -3
  189. package/dist/tooltip/tooltip.d.ts +5 -0
  190. package/dist/tooltip/tooltip.js +80 -7
  191. package/dist/tree/llms.txt +20 -8
  192. package/dist/tree/tree-keys.d.ts +27 -0
  193. package/dist/tree/tree-keys.js +39 -0
  194. package/dist/tree/tree.d.ts +12 -5
  195. package/dist/tree/tree.js +75 -29
  196. 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 { useKptFormFieldContext } from "../form-field/form-field-context.js";
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
- const field = useKptFormFieldContext();
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
- close();
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
- close();
274
+ closeAndFocus();
173
275
  return;
174
276
  }
175
- setPage(({ m }) => ({ y: cell.y, m: panelView === 'month' ? cell.m : m }));
176
- setPanelView(KPT_CALENDAR_LEVELS[KPT_CALENDAR_LEVELS.indexOf(panelView) - 1]);
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
- close();
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?.controlId, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? undefined, onClick: () => !disabled && setOpen(!isOpen), onKeyDown: (event) => {
199
- if (event.key === 'Enter' && !disabled)
200
- setOpen(!isOpen);
201
- }, onBlur: () => 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", 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", { className: cn('kpt-calendar', showClock && 'kpt-calendar--clock'), 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: () => setPage(pageBy(page, panelView, -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: () => setPanelView('month'), "aria-label": t('datepicker.chooseMonth'), children: months[page.m] }), _jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => setPanelView('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: () => setPanelView('year'), "aria-label": t('datepicker.chooseYear'), children: page.y }))) : null, panelView === 'year' ? (_jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => setPanelView('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: () => setPage(pageBy(page, panelView, 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", 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'), 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'), children: levelCells.map((cell) => (_jsx("button", { type: "button", className: cn('kpt-calendar__cell', cell.selected && 'kpt-calendar__cell--selected', cell.current && 'kpt-calendar__cell--current'), 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] }) })] }));
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
  }
@@ -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`, `onTouch()`
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 `tabindex="0"` and Enter; every cell is a real `<button>` with
74
- `aria-pressed`; `ariaLabel` names the field when no `<label for>` describes it.
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.
@@ -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ąć.
@@ -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
@@ -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.
@@ -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
  }
@@ -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">`, so the browser does
33
- not trap focus and does not make the rest of the page inert. Escape works while the focus is inside
34
- the panel. For a hard modal reach for `KptDialog`.
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"` with `aria-modal="true"` and `tabindex="-1"`, so the panel can take focus. The close
47
- button gets its label from `common.close` in the dictionary.
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.
@@ -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;
@@ -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
  }
@@ -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
- A plain container with no role of its own — the title is a `<p>`, not a heading, so it does not
34
- break the heading outline of the page. Where the empty state IS the heading of its section, render
35
- your own `<h2>` in the children instead.
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`, `--kpt-radius-full`.
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'), onClick: () => !disabled && inputRef.current?.click(), onDragOver: (event) => {
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: "photo" }), _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) => {
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-form-field-border` (dashed), the dragover state `--kpt-color-primary`.
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; the file input stays a real `<input type="file">`,
48
- so the system picker and its keyboard path are untouched.
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.