@konce-pt/react 0.11.2 → 0.12.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 (203) hide show
  1. package/README.md +8 -2
  2. package/dist/accordion/accordion.js +11 -10
  3. package/dist/alert/alert.js +1 -0
  4. package/dist/alert/llms.txt +1 -0
  5. package/dist/app-shell/app-shell.js +12 -5
  6. package/dist/app-shell/llms.txt +5 -4
  7. package/dist/auth/auth.js +17 -8
  8. package/dist/auth/llms.txt +3 -2
  9. package/dist/autocomplete/autocomplete.d.ts +1 -1
  10. package/dist/autocomplete/autocomplete.js +12 -4
  11. package/dist/autocomplete/llms.txt +3 -0
  12. package/dist/avatar/avatar.js +1 -0
  13. package/dist/avatar-group/avatar-group.js +1 -0
  14. package/dist/backdrop/backdrop.d.ts +0 -19
  15. package/dist/backdrop/backdrop.js +18 -13
  16. package/dist/bottom-sheet/bottom-sheet.d.ts +6 -1
  17. package/dist/bottom-sheet/bottom-sheet.js +3 -2
  18. package/dist/bottom-sheet/llms.txt +3 -0
  19. package/dist/breadcrumb/breadcrumb.d.ts +7 -1
  20. package/dist/breadcrumb/breadcrumb.js +10 -2
  21. package/dist/breadcrumb/llms.txt +8 -0
  22. package/dist/button/button.js +2 -1
  23. package/dist/button-group/button-group.d.ts +14 -1
  24. package/dist/button-group/button-group.js +30 -4
  25. package/dist/button-group/llms.txt +9 -6
  26. package/dist/card/card.js +4 -3
  27. package/dist/carousel/carousel.js +4 -1
  28. package/dist/charts/chart-legend.js +1 -0
  29. package/dist/charts/chart.d.ts +3 -2
  30. package/dist/charts/chart.js +12 -4
  31. package/dist/charts/llms.txt +6 -3
  32. package/dist/chat/chat.js +39 -9
  33. package/dist/chat/llms.txt +9 -0
  34. package/dist/chat-composer/chat-composer.d.ts +5 -2
  35. package/dist/chat-composer/chat-composer.js +10 -1
  36. package/dist/chat-dock/chat-dock.js +1 -0
  37. package/dist/chat-message/chat-message.js +2 -1
  38. package/dist/chat-typing/chat-typing.js +1 -0
  39. package/dist/checkbox/checkbox.d.ts +1 -1
  40. package/dist/checkbox/checkbox.js +9 -2
  41. package/dist/checkbox/llms.txt +3 -0
  42. package/dist/chip/chip.js +1 -0
  43. package/dist/chips-input/chips-input.d.ts +3 -1
  44. package/dist/chips-input/chips-input.js +14 -5
  45. package/dist/chips-input/llms.txt +3 -0
  46. package/dist/clock/clock.d.ts +9 -4
  47. package/dist/clock/clock.js +35 -5
  48. package/dist/clock/llms.txt +6 -2
  49. package/dist/color-picker/color-picker.d.ts +5 -1
  50. package/dist/color-picker/color-picker.js +36 -17
  51. package/dist/color-picker/llms.txt +3 -0
  52. package/dist/confirm/confirm-container.js +1 -0
  53. package/dist/confirm/confirm-store.js +6 -0
  54. package/dist/confirm/llms.txt +2 -1
  55. package/dist/context-menu/context-menu.js +35 -12
  56. package/dist/context-menu/llms.txt +3 -0
  57. package/dist/data-table/data-table.d.ts +12 -1
  58. package/dist/data-table/data-table.js +113 -51
  59. package/dist/data-table/export-csv.d.ts +2 -2
  60. package/dist/data-table/export-csv.js +7 -9
  61. package/dist/data-table/llms.txt +19 -4
  62. package/dist/data-view/data-view.js +1 -0
  63. package/dist/date-range/date-range.d.ts +16 -4
  64. package/dist/date-range/date-range.js +69 -46
  65. package/dist/date-range/llms.txt +17 -3
  66. package/dist/datepicker/datepicker.d.ts +17 -3
  67. package/dist/datepicker/datepicker.js +86 -54
  68. package/dist/datepicker/llms.txt +17 -3
  69. package/dist/dialog/dialog.js +29 -8
  70. package/dist/dialog/llms.txt +3 -0
  71. package/dist/divider/divider.d.ts +7 -1
  72. package/dist/divider/divider.js +3 -2
  73. package/dist/divider/llms.txt +1 -0
  74. package/dist/drawer/drawer.d.ts +6 -1
  75. package/dist/drawer/drawer.js +3 -2
  76. package/dist/drawer/llms.txt +3 -0
  77. package/dist/fieldset/fieldset.js +4 -1
  78. package/dist/file-upload/file-upload.d.ts +7 -1
  79. package/dist/file-upload/file-upload.js +18 -7
  80. package/dist/file-upload/llms.txt +6 -3
  81. package/dist/form-field/form-field.js +1 -0
  82. package/dist/galleria/galleria.js +4 -1
  83. package/dist/i18n/i18n-context.d.ts +3 -3
  84. package/dist/i18n/i18n-context.js +25 -3
  85. package/dist/icon/icon.js +4 -1
  86. package/dist/image/image.js +4 -4
  87. package/dist/input/input.js +1 -0
  88. package/dist/input-mask/input-mask.d.ts +7 -2
  89. package/dist/input-mask/input-mask.js +15 -41
  90. package/dist/input-mask/llms.txt +3 -0
  91. package/dist/input-number/input-number.d.ts +3 -1
  92. package/dist/input-number/input-number.js +15 -11
  93. package/dist/input-number/llms.txt +5 -2
  94. package/dist/input-otp/input-otp.d.ts +16 -3
  95. package/dist/input-otp/input-otp.js +43 -19
  96. package/dist/input-otp/llms.txt +11 -0
  97. package/dist/internal/mask.d.ts +14 -0
  98. package/dist/internal/mask.js +62 -0
  99. package/dist/internal/number-group.d.ts +22 -0
  100. package/dist/internal/number-group.js +53 -0
  101. package/dist/internal/option-key.d.ts +6 -0
  102. package/dist/internal/option-key.js +8 -0
  103. package/dist/internal/overlay.js +37 -7
  104. package/dist/internal/password-strength.d.ts +6 -0
  105. package/dist/internal/password-strength.js +74 -0
  106. package/dist/internal/scroll-lock.d.ts +6 -0
  107. package/dist/internal/scroll-lock.js +35 -0
  108. package/dist/internal/slot-children.d.ts +13 -0
  109. package/dist/internal/slot-children.js +31 -0
  110. package/dist/internal/step.d.ts +12 -0
  111. package/dist/internal/step.js +31 -0
  112. package/dist/internal/tween.js +2 -3
  113. package/dist/internal/use-controllable-state.d.ts +5 -0
  114. package/dist/internal/use-controllable-state.js +25 -4
  115. package/dist/internal/use-modal-focus.d.ts +1 -0
  116. package/dist/internal/use-modal-focus.js +12 -1
  117. package/dist/knob/knob.d.ts +5 -2
  118. package/dist/knob/knob.js +15 -14
  119. package/dist/knob/llms.txt +4 -2
  120. package/dist/listbox/listbox.d.ts +3 -1
  121. package/dist/listbox/listbox.js +30 -10
  122. package/dist/listbox/llms.txt +3 -0
  123. package/dist/map/llms.txt +5 -1
  124. package/dist/map/map-create-dialog.js +30 -22
  125. package/dist/map/map-hours-field.js +1 -0
  126. package/dist/map/map-image-field.js +2 -1
  127. package/dist/map/map-legend.js +1 -0
  128. package/dist/map/map-poi-card.js +2 -1
  129. package/dist/map/map-search.js +54 -30
  130. package/dist/map/map.d.ts +6 -1
  131. package/dist/map/map.js +62 -34
  132. package/dist/megamenu/llms.txt +11 -0
  133. package/dist/megamenu/megamenu.d.ts +7 -1
  134. package/dist/megamenu/megamenu.js +15 -6
  135. package/dist/menu/menu-rows.js +1 -0
  136. package/dist/menu/menu.js +10 -3
  137. package/dist/menubar/llms.txt +11 -0
  138. package/dist/menubar/menubar.d.ts +7 -1
  139. package/dist/menubar/menubar.js +15 -3
  140. package/dist/meter-group/meter-group.js +1 -0
  141. package/dist/order-list/llms.txt +3 -0
  142. package/dist/order-list/order-list.js +21 -6
  143. package/dist/paginator/paginator.js +7 -4
  144. package/dist/panel/panel.js +5 -3
  145. package/dist/password/llms.txt +6 -2
  146. package/dist/password/password.js +4 -14
  147. package/dist/peek/llms.txt +1 -1
  148. package/dist/peek/peek.js +37 -15
  149. package/dist/pick-list/llms.txt +3 -0
  150. package/dist/pick-list/pick-list.js +19 -3
  151. package/dist/popover/llms.txt +3 -0
  152. package/dist/popover/popover.d.ts +3 -1
  153. package/dist/popover/popover.js +10 -3
  154. package/dist/progress/progress.js +1 -0
  155. package/dist/public-api.d.ts +1 -1
  156. package/dist/radio-group/llms.txt +3 -0
  157. package/dist/radio-group/radio-group.d.ts +3 -1
  158. package/dist/radio-group/radio-group.js +13 -2
  159. package/dist/rating/rating.js +1 -0
  160. package/dist/rich-text/llms.txt +14 -0
  161. package/dist/rich-text/rich-text.d.ts +3 -9
  162. package/dist/rich-text/rich-text.js +241 -96
  163. package/dist/rich-text/sanitize.d.ts +9 -0
  164. package/dist/rich-text/sanitize.js +77 -29
  165. package/dist/roadmap/llms.txt +9 -3
  166. package/dist/roadmap/roadmap.js +88 -11
  167. package/dist/scroll-top/scroll-top.js +1 -0
  168. package/dist/select/llms.txt +3 -0
  169. package/dist/select/select.d.ts +3 -1
  170. package/dist/select/select.js +46 -32
  171. package/dist/shared/types.d.ts +21 -0
  172. package/dist/shared/types.js +9 -1
  173. package/dist/sidenav/sidenav.d.ts +1 -2
  174. package/dist/sidenav/sidenav.js +4 -3
  175. package/dist/slider/llms.txt +3 -0
  176. package/dist/slider/slider.d.ts +3 -1
  177. package/dist/slider/slider.js +15 -4
  178. package/dist/speed-dial/speed-dial.js +2 -1
  179. package/dist/spinner/spinner.js +1 -0
  180. package/dist/split-button/split-button.js +10 -3
  181. package/dist/splitter/splitter.js +23 -8
  182. package/dist/stepper/llms.txt +3 -0
  183. package/dist/stepper/stepper.js +9 -5
  184. package/dist/switch/llms.txt +3 -0
  185. package/dist/switch/switch.d.ts +1 -1
  186. package/dist/switch/switch.js +9 -2
  187. package/dist/switch-group/llms.txt +3 -0
  188. package/dist/switch-group/switch-group.d.ts +3 -1
  189. package/dist/switch-group/switch-group.js +5 -3
  190. package/dist/tabs/llms.txt +3 -0
  191. package/dist/tabs/tabs.js +30 -14
  192. package/dist/textarea/textarea.js +1 -0
  193. package/dist/toast/llms.txt +3 -0
  194. package/dist/toast/toast-container.js +1 -0
  195. package/dist/toast/toast-store.d.ts +5 -1
  196. package/dist/toast/toast-store.js +1 -1
  197. package/dist/tooltip/llms.txt +5 -3
  198. package/dist/tooltip/tooltip.d.ts +3 -2
  199. package/dist/tooltip/tooltip.js +90 -31
  200. package/dist/tree/llms.txt +1 -0
  201. package/dist/tree/tree.d.ts +6 -1
  202. package/dist/tree/tree.js +14 -7
  203. package/package.json +15 -14
@@ -1,6 +1,7 @@
1
+ 'use client';
1
2
  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, initialFocusDate, initialPage, moveCellIndex, moveFocusDate, pad, pageBy, parseDate, parseDateTime, parseTime, resolveStartView, selectedYearMonth, toIso, } from '@konce-pt/datetime';
3
+ import { useEffect, useEffectEvent, useRef, useState, } from 'react';
4
+ import { KPT_CALENDAR_LEVELS, buildDayGrid, buildDecadeCells, buildMonthCells, buildYearCells, canPageBack, canPageForward, centuryStart, commitLevelFor, datePlaceholder, dayPeriodLabels, decadeStart, displayDate, displayMonthYear, formatTime, initialFocusDate, kptFirstDayOfWeek, orderWeekdays, initialPage, moveCellIndex, moveFocusDate, pad, pageBy, parseDate, parseDateTime, parseTime, resolveStartView, selectedYearMonth, toIso, } from '@konce-pt/datetime';
4
5
  import { monthNames, weekdayNames } from '@konce-pt/i18n';
5
6
  import { cn } from "../internal/cn.js";
6
7
  import { useControllableState } from "../internal/use-controllable-state.js";
@@ -23,11 +24,15 @@ import { focusCalendarActive, trapTab } from "../internal/calendar-focus.js";
23
24
  * w panelu, `Escape` zamyka go, a wybór i zamknięcie oddają fokus polu. `Delete`/`Backspace` na polu
24
25
  * czyści wartość.
25
26
  *
27
+ * Pole to `role="combobox"` z `aria-haspopup="dialog"`, więc niesie `aria-required`,
28
+ * `aria-readonly` i `aria-invalid`. Tydzień zaczyna się wg aktywnego języka, chyba że podano
29
+ * `firstDayOfWeek`; przewinięcie strony strzałką ‹ › jest ogłaszane.
30
+ *
26
31
  * @example
27
32
  * <KptDatepicker value={date} onValueChange={setDate} startView="decade" minDate="1920-01-01" />
28
33
  * <KptDatepicker selectionMode="datetime" hourFormat={12} value={at} onValueChange={setAt} />
29
34
  */
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 }) {
35
+ export function KptDatepicker({ value, defaultValue = '', onValueChange, selectionMode = 'date', startView = null, minDate = null, maxDate = null, placeholder = '', monthDisplay = 'long', displayFormat = 'pl', firstDayOfWeek = null, hourFormat = 24, minuteStep = 1, open, onOpenChange, ariaLabel = null, id, disabled = false, readOnly = false, required = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
31
36
  const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
32
37
  /** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
33
38
  const field = useKptFieldA11y(id, 'kpt-datepicker');
@@ -35,8 +40,13 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
35
40
  const [isOpen, setOpen] = useControllableState({ value: open, defaultValue: false, onChange: onOpenChange });
36
41
  const [anchor, setAnchor] = useState(null);
37
42
  const triggerRef = useRef(null);
38
- const today = useRef(new Date()).current;
43
+ /** „Dziś” liczone przy każdym otwarciu — panel otwarty po północy nie pokazuje wczorajszego dnia. */
44
+ const [today, setToday] = useState(() => new Date());
39
45
  const todayIso = toIso(today);
46
+ const firstDay = firstDayOfWeek ?? kptFirstDayOfWeek(activeLocale);
47
+ const periods = dayPeriodLabels(activeLocale);
48
+ /** Ogłoszenie strony kalendarza po przewinięciu strzałką ‹ ›. */
49
+ const [pageAnnouncement, setPageAnnouncement] = useState('');
40
50
  const showInvalid = invalid && (touched ?? true);
41
51
  const [page, setPage] = useState(() => initialPage(current, selectionMode, today));
42
52
  const [panelView, setPanelView] = useState(() => resolveStartView(selectionMode, startView));
@@ -49,17 +59,30 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
49
59
  const [focusCell, setFocusCell] = useState(0);
50
60
  const panelId = useKptId(undefined, 'kpt-datepicker-panel');
51
61
  const panelRef = useRef(null);
52
- /** Prośba o przeniesienie fokusu po najbliższym renderze (otwarcie, ruch klawiszem, zmiana poziomu). */
53
- const wantFocus = useRef(false);
62
+ /**
63
+ * Prośba o przeniesienie fokusu po najbliższym renderze (otwarcie, ruch klawiszem, zmiana
64
+ * poziomu) — licznik w stanie, nie ref zapisywany w renderze.
65
+ */
66
+ const [focusRequest, setFocusRequest] = useState(0);
67
+ const requestFocus = () => setFocusRequest((n) => n + 1);
54
68
  const months = monthNames(activeLocale);
55
- const weekdays = weekdayNames(activeLocale);
69
+ const weekdays = orderWeekdays(weekdayNames(activeLocale), firstDay);
56
70
  const commitLevel = commitLevelFor(selectionMode);
57
- // Panel można otworzyć także z zewnątrz (`open`), nie tylko klikiem w trigger — widok
58
- // i bufor czasu ustawiamy więc przy każdym otwarciu, a nie w obsłudze kliknięcia.
59
- useEffect(() => {
60
- if (!isOpen)
61
- return;
62
- setPage(initialPage(current, selectionMode, today));
71
+ // Panel można otworzyć także z zewnątrz (`open`), nie tylko klikiem w trigger — widok i bufor
72
+ // czasu ustawiamy więc przy każdym przejściu w stan otwarty. Wzorzec „poprzednia wartość
73
+ // w stanie” z react.dev: stan liczy się w tym samym renderze, bez efektu i bez klatki ze starym
74
+ // widokiem. Późniejsze zmiany wartości w otwartym panelu go nie resetują.
75
+ const [wasOpen, setWasOpen] = useState(isOpen);
76
+ if (wasOpen !== isOpen) {
77
+ setWasOpen(isOpen);
78
+ if (isOpen)
79
+ prepareOpen();
80
+ }
81
+ function prepareOpen() {
82
+ const now = new Date();
83
+ setToday(now);
84
+ setPageAnnouncement('');
85
+ setPage(initialPage(current, selectionMode, now));
63
86
  setPanelView(resolveStartView(selectionMode, startView));
64
87
  setDtView(selectionMode === 'time' ? 'clock' : 'calendar');
65
88
  if (selectionMode === 'time') {
@@ -73,18 +96,15 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
73
96
  }
74
97
  const startLevel = resolveStartView(selectionMode, startView);
75
98
  const selected = selectionMode === 'datetime' ? (parseDateTime(current)?.date ?? '') : current;
76
- const focus = initialFocusDate(selected, todayIso, minDate, maxDate);
99
+ const focus = initialFocusDate(selected, toIso(now), minDate, maxDate);
77
100
  setFocusIso(focus);
78
101
  if (startLevel === 'day' && selectionMode !== 'time') {
79
102
  const date = parseDate(focus);
80
103
  setPage({ y: date.getFullYear(), m: date.getMonth() });
81
104
  }
82
105
  setFocusCell(-1); // -1 = domyślna komórka nowej siatki, wyliczana przy renderze
83
- wantFocus.current = true;
84
- // Zależność wyłącznie od `isOpen`: to otwarcie ma ustawić stan początkowy, a późniejsze
85
- // zmiany wartości w otwartym panelu nie mogą go zresetować pod palcami użytkownika.
86
- // eslint-disable-next-line react-hooks/exhaustive-deps
87
- }, [isOpen]);
106
+ requestFocus();
107
+ }
88
108
  const showClock = selectionMode === 'time' || (selectionMode === 'datetime' && dtView === 'clock');
89
109
  const hasFooter = selectionMode === 'time' || selectionMode === 'datetime';
90
110
  const hasNav = selectionMode !== 'month';
@@ -100,48 +120,42 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
100
120
  triggerRef.current?.focus();
101
121
  };
102
122
  // Fokus po renderze. Panel w nakładce jest niewidoczny do pierwszego pomiaru — stąd rAF.
123
+ // `focusIso` czytamy przez zdarzenie efektu: liczy się prośba, nie każda zmiana dnia.
124
+ const focusActive = useEffectEvent(() => focusCalendarActive(panelRef.current, focusIso));
103
125
  useEffect(() => {
104
- if (!isOpen || !wantFocus.current)
126
+ if (!isOpen || focusRequest === 0)
105
127
  return;
106
- const frame = requestAnimationFrame(() => {
107
- wantFocus.current = false;
108
- focusCalendarActive(panelRef.current, focusIso);
109
- });
128
+ const frame = requestAnimationFrame(() => focusActive());
110
129
  return () => cancelAnimationFrame(frame);
111
- });
130
+ }, [focusRequest, isOpen]);
112
131
  // ── Prezentacja w triggerze ──
113
132
  const effectivePlaceholder = (() => {
114
133
  if (placeholder)
115
134
  return placeholder;
116
- const iso = displayFormat === 'iso';
135
+ const tokens = { year: t('datepicker.yearToken'), month: t('datepicker.monthToken'), day: t('datepicker.dayToken') };
117
136
  const time = hourFormat === 12 ? '--:-- --' : '--:--';
118
137
  switch (selectionMode) {
119
138
  case 'monthYear':
120
- return iso ? 'RRRR-MM' : 'MM.RRRR';
139
+ return datePlaceholder(displayFormat, tokens, 'monthYear', activeLocale);
121
140
  case 'year':
122
- return 'RRRR';
141
+ return tokens.year;
123
142
  case 'month':
124
- return monthDisplay === 'number' ? 'MM' : t('datepicker.month');
143
+ return monthDisplay === 'number' ? tokens.month : t('datepicker.month');
125
144
  case 'time':
126
145
  return time;
127
146
  case 'datetime':
128
- return `${iso ? 'RRRR-MM-DD' : 'DD.MM.RRRR'} ${time}`;
147
+ return `${datePlaceholder(displayFormat, tokens, 'date', activeLocale)} ${time}`;
129
148
  default:
130
- return iso ? 'RRRR-MM-DD' : 'DD.MM.RRRR';
149
+ return datePlaceholder(displayFormat, tokens, 'date', activeLocale);
131
150
  }
132
151
  })();
133
152
  /** Wartość niepasująca do formatu trybu daje pusty tekst — trigger nie pokazuje śmieci. */
134
153
  const display = (() => {
135
154
  if (!current)
136
155
  return '';
137
- const iso = displayFormat === 'iso';
138
156
  switch (selectionMode) {
139
- case 'monthYear': {
140
- const match = /^(\d{4})-(\d{2})$/.exec(current);
141
- if (!match)
142
- return '';
143
- return iso ? current : `${match[2]}.${match[1]}`;
144
- }
157
+ case 'monthYear':
158
+ return displayMonthYear(current, displayFormat, activeLocale);
145
159
  case 'year':
146
160
  return /^\d{4}$/.test(current) ? current : '';
147
161
  case 'month': {
@@ -152,22 +166,22 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
152
166
  }
153
167
  case 'time': {
154
168
  const parsed = parseTime(current);
155
- return parsed ? formatTime(parsed.h, parsed.m, hourFormat) : '';
169
+ return parsed ? formatTime(parsed.h, parsed.m, hourFormat, periods) : '';
156
170
  }
157
171
  case 'datetime': {
158
172
  const parsed = parseDateTime(current);
159
173
  if (!parsed)
160
174
  return '';
161
- const sep = iso ? 'T' : ' ';
162
- return `${formatDate(parsed.date, displayFormat)}${sep}${formatTime(parsed.h, parsed.m, hourFormat)}`;
175
+ const sep = displayFormat === 'iso' ? 'T' : ' ';
176
+ return `${displayDate(parsed.date, displayFormat, activeLocale)}${sep}${formatTime(parsed.h, parsed.m, hourFormat, periods)}`;
163
177
  }
164
178
  default:
165
- return formatDate(current, displayFormat);
179
+ return displayDate(current, displayFormat, activeLocale);
166
180
  }
167
181
  })();
168
182
  // ── Siatki ──
169
183
  const selectedDateIso = selectionMode === 'datetime' ? pendingDate : current;
170
- const weeks = buildDayGrid({ page, selectedIso: selectedDateIso, todayIso, minDate, maxDate });
184
+ const weeks = buildDayGrid({ page, selectedIso: selectedDateIso, todayIso, minDate, maxDate, firstDay });
171
185
  const selectedYM = selectedYearMonth(selectionMode === 'datetime' ? pendingDate : current, selectionMode);
172
186
  const levelCells = panelView === 'year'
173
187
  ? buildYearCells({ year: page.y, selected: selectedYM, today, minDate, maxDate })
@@ -199,7 +213,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
199
213
  };
200
214
  const isRtl = () => !!panelRef.current && getComputedStyle(panelRef.current).direction === 'rtl';
201
215
  const onDayKeyDown = (event) => {
202
- const next = moveFocusDate(focusIso, event.key, { shift: event.shiftKey, rtl: isRtl(), minDate, maxDate });
216
+ const next = moveFocusDate(focusIso, event.key, { shift: event.shiftKey, rtl: isRtl(), minDate, maxDate, firstDay });
203
217
  if (next === null)
204
218
  return;
205
219
  event.preventDefault();
@@ -207,7 +221,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
207
221
  const date = parseDate(next);
208
222
  if (date.getFullYear() !== page.y || date.getMonth() !== page.m)
209
223
  setPage({ y: date.getFullYear(), m: date.getMonth() });
210
- wantFocus.current = true;
224
+ requestFocus();
211
225
  };
212
226
  const onCellKeyDown = (event) => {
213
227
  const next = moveCellIndex(levelCells.map((c) => c.disabled), activeCell, event.key, 3, isRtl());
@@ -215,23 +229,24 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
215
229
  return;
216
230
  event.preventDefault();
217
231
  setFocusCell(next);
218
- wantFocus.current = true;
232
+ requestFocus();
219
233
  };
220
234
  /** Tytuł, który kliknięto, znika razem z poziomem — fokus idzie do nowej siatki. */
221
235
  const goTo = (level) => {
222
236
  setPanelView(level);
223
237
  setFocusCell(-1);
224
- wantFocus.current = true;
238
+ requestFocus();
225
239
  };
226
240
  /** Strzałki ‹ ›: fokus zostaje na strzałce, ale przystanek siatki dni idzie za stroną. */
227
241
  const turnPage = (dir) => {
228
242
  const next = pageBy(page, panelView, dir);
229
243
  setPage(next);
244
+ setPageAnnouncement(pageTitle(next));
230
245
  if (panelView === 'day') {
231
- const moved = moveFocusDate(focusIso, dir === 1 ? 'PageDown' : 'PageUp', { minDate, maxDate });
246
+ const moved = moveFocusDate(focusIso, dir === 1 ? 'PageDown' : 'PageUp', { minDate, maxDate, firstDay });
232
247
  // Dzień przycięty do granicy może leżeć poza nową stroną — wtedy pierwszy dozwolony dzień miesiąca.
233
248
  const inPage = moved?.startsWith(`${next.y}-${pad(next.m + 1)}`);
234
- const fallback = buildDayGrid({ page: next, selectedIso: selectedDateIso, todayIso, minDate, maxDate })
249
+ const fallback = buildDayGrid({ page: next, selectedIso: selectedDateIso, todayIso, minDate, maxDate, firstDay })
235
250
  .flat()
236
251
  .find((c) => c.inMonth && !c.disabled)?.iso;
237
252
  const target = inPage ? moved : fallback;
@@ -242,6 +257,19 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
242
257
  setFocusCell(-1);
243
258
  }
244
259
  };
260
+ /** Tytuł strony do ogłoszenia — miesiąc z rokiem, rok albo zakres lat. */
261
+ const pageTitle = (target) => {
262
+ switch (panelView) {
263
+ case 'day':
264
+ return `${months[target.m]} ${target.y}`;
265
+ case 'month':
266
+ return String(target.y);
267
+ case 'year':
268
+ return `${decadeStart(target.y)} – ${decadeStart(target.y) + 9}`;
269
+ default:
270
+ return `${centuryStart(target.y)} – ${centuryStart(target.y) + 119}`;
271
+ }
272
+ };
245
273
  const navLabel = (dir) => {
246
274
  switch (panelView) {
247
275
  case 'month':
@@ -286,7 +314,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
286
314
  else {
287
315
  setFocusCell(-1);
288
316
  }
289
- wantFocus.current = true;
317
+ requestFocus();
290
318
  };
291
319
  const commit = () => {
292
320
  const time = timeStr || '00:00';
@@ -301,15 +329,19 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
301
329
  };
302
330
  const clear = (event) => {
303
331
  event.stopPropagation();
332
+ if (readOnly)
333
+ return;
304
334
  setCurrent('');
305
335
  setPendingDate('');
336
+ // Wyczyszczenie to interakcja z polem — błąd „wymagane” ma się pokazać od razu.
337
+ onTouch?.();
306
338
  };
307
339
  const timeParts = parseTime(timeStr);
308
340
  return (_jsxs("div", { className: cn('kpt-datepicker-host', className), ...rest, children: [_jsxs("div", { ref: (el) => {
309
341
  triggerRef.current = el;
310
342
  setAnchor(el);
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)
343
+ }, className: cn('kpt-datepicker', isOpen && 'kpt-datepicker--open', disabled && 'kpt-datepicker--disabled', readOnly && 'kpt-datepicker--readonly', showInvalid && 'kpt-datepicker--invalid'), role: "combobox", tabIndex: 0, "aria-haspopup": "dialog", "aria-keyshortcuts": "Delete Backspace", "aria-required": required || undefined, "aria-readonly": readOnly || undefined, "aria-invalid": showInvalid || undefined, "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 && !readOnly && setOpen(!isOpen), onKeyDown: (event) => {
344
+ if (disabled || readOnly)
313
345
  return;
314
346
  if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
315
347
  event.preventDefault();
@@ -322,7 +354,7 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
322
354
  }
323
355
  },
324
356
  // 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) => {
357
+ 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 && !readOnly ? (_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
358
  if (event.key === 'Escape') {
327
359
  event.preventDefault();
328
360
  event.stopPropagation();
@@ -331,5 +363,5 @@ export function KptDatepicker({ value, defaultValue = '', onValueChange, selecti
331
363
  else if (event.key === 'Tab') {
332
364
  trapTab(event.currentTarget, event.nativeEvent);
333
365
  }
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] }) })] }));
366
+ }, 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: [_jsx("span", { className: "kpt-calendar__live", "aria-live": "polite", children: pageAnnouncement }), _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": `${months[page.m]}, ${t('datepicker.chooseMonth')}`, children: months[page.m] }), _jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('year'), "aria-label": `${page.y}, ${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": `${page.y}, ${t('datepicker.chooseYear')}`, children: page.y }))) : null, panelView === 'year' ? (_jsx("button", { type: "button", className: "kpt-calendar__title-btn", onClick: () => goTo('decade'), "aria-label": `${decadeStart(page.y)} – ${decadeStart(page.y) + 9}, ${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, periods) })] })) : null] })), hasFooter ? (_jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })) : null] }) })] }));
335
367
  }
@@ -5,7 +5,7 @@ Import: `import { KptDatepicker } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-datepicker-host">
8
- <div class="kpt-datepicker" role="button" tabindex="0">
8
+ <div class="kpt-datepicker" role="combobox" tabindex="0" aria-haspopup="dialog">
9
9
  <span class="kpt-datepicker__value">06.09.2026</span>
10
10
  <span class="kpt-datepicker__spacer"></span>
11
11
  <button class="kpt-datepicker__clear">…</button>
@@ -22,7 +22,7 @@ Import: `import { KptDatepicker } from '@konce-pt/react';`
22
22
  means the level where the mode commits. It is never lowered below that level, so
23
23
  `startView="day"` in 'year' mode is raised to 'year'.
24
24
  - `minDate`, `maxDate`: 'YYYY-MM-DD' strings
25
- - `monthDisplay`: `'long'` (default) | `'number'`; `displayFormat`: `'pl'` (default) | `'iso'`
25
+ - `monthDisplay`: `'long'` (default) | `'number'`; `displayFormat`: `'pl'` (default) | `'iso'` | `'locale'`
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
@@ -81,6 +81,20 @@ Weekday and month names come from `Intl` for the active locale, not from the dic
81
81
  navigation labels and "Done" come from the `datepicker` and `common` namespaces; `locale` and
82
82
  `dictionary` override them per instance.
83
83
 
84
+ ## Week start, display format and field state
85
+ - The week starts by the active language (`Intl.Locale#getWeekInfo`, with a fallback list of regions):
86
+ en / en-US on Sunday, pl, de, en-GB on Monday. `firstDayOfWeek` (1 = Monday … 7 = Sunday) overrides it;
87
+ the headers, the grid and `Home`/`End` follow it.
88
+ - `displayFormat`: `'pl'` (DD.MM.YYYY, default), `'iso'` (YYYY-MM-DD) or `'locale'` — order and separators
89
+ from `Intl` (en-US: MM/DD/YYYY). The placeholder follows the format and its letters follow the language
90
+ (EN `YYYY`, PL `RRRR`, the `datepicker.yearToken`/`monthToken`/`dayToken` keys). AM/PM come from `Intl`.
91
+ - The field is `role="combobox"` with `aria-haspopup="dialog"` (the APG select-only combobox), so it carries
92
+ `aria-required` (`required`), `aria-readonly` (`readOnly` — the value shows, the panel does not open,
93
+ there is no clear button) and `aria-invalid`.
94
+ - Turning the page with ‹ › is announced (`aria-live="polite"`); the title buttons are named
95
+ "October, Choose month" — the visible text first (WCAG 2.5.3). Clearing the value counts as a touch.
96
+ - "Today" is computed on every open, so a panel opened after midnight marks the right day.
97
+
84
98
  ## Label and description
85
99
  `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
86
100
  generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
@@ -92,7 +106,7 @@ is added to `aria-describedby`. New: `id`; the trigger is the element that carri
92
106
  `--kpt-color-surface-raised`, the selection `--kpt-color-primary`.
93
107
 
94
108
  ## Accessibility
95
- The trigger is `role="button"` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`;
109
+ The trigger is `role="combobox"` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`;
96
110
  the panel is a named `role="dialog"`; every cell is a real `<button>` with `aria-pressed`, days carry
97
111
  the full date and `aria-current="date"`; `ariaLabel` names the field when no `<label for>` describes
98
112
  it. The keyboard is described above.
@@ -1,6 +1,9 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, isValidElement, useEffect, useRef, useState, } from 'react';
3
+ import { isValidElement, useEffect, useRef, useState, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
5
+ import { slotChildren } from "../internal/slot-children.js";
6
+ import { kptLockScroll } from "../internal/scroll-lock.js";
4
7
  import { composeRefs } from "../internal/compose-refs.js";
5
8
  import { useKptId } from "../internal/use-id.js";
6
9
  import { KptOverlayContainerContext } from "../internal/overlay-container.js";
@@ -31,19 +34,37 @@ export function KptDialog({ open, onOpenChange, header = '', size = 'md', dismis
31
34
  const [dialogEl, setDialogEl] = useState(null);
32
35
  useEffect(() => {
33
36
  const el = dialogRef.current;
34
- if (!el)
37
+ if (!el || !open)
35
38
  return;
36
- if (open && !el.open)
39
+ if (!el.open)
37
40
  el.showModal();
38
- else if (!open && el.open)
39
- el.close();
41
+ // `showModal()` blokuje interakcję z tłem, ale nie jego przewijanie kółkiem.
42
+ const releaseScroll = kptLockScroll();
43
+ return () => {
44
+ releaseScroll();
45
+ if (el.open)
46
+ el.close();
47
+ };
40
48
  }, [open]);
49
+ /**
50
+ * `close` przychodzi z każdej drogi. Przy `dismissible=false` zamknąć wolno tylko z kodu:
51
+ * Chromium z CloseWatcherem pozwala anulować `cancel` jedynie po aktywacji użytkownika, więc drugi
52
+ * Escape z rzędu zamyka okno mimo `preventDefault` — wtedy otwieramy je z powrotem.
53
+ */
54
+ const onClose = () => {
55
+ const el = dialogRef.current;
56
+ if (!dismissible && open && el && !el.open) {
57
+ el.showModal();
58
+ return;
59
+ }
60
+ onOpenChange(false);
61
+ };
41
62
  const footers = [];
42
63
  const body = [];
43
- Children.forEach(children, (child) => {
64
+ for (const child of slotChildren(children)) {
44
65
  const isFooter = isValidElement(child) && child.type?.kptDialogSlot === 'footer';
45
66
  (isFooter ? footers : body).push(child);
46
- });
67
+ }
47
68
  const onCancel = (event) => {
48
69
  // Esc idzie przez `cancel`; blokujemy go, gdy okna nie wolno zamknąć.
49
70
  if (!dismissible) {
@@ -52,7 +73,7 @@ export function KptDialog({ open, onOpenChange, header = '', size = 'md', dismis
52
73
  }
53
74
  onOpenChange(false);
54
75
  };
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] }) }));
76
+ 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, closedby: dismissible ? undefined : 'none', onClose: onClose, 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] }) }));
56
77
  }
57
78
  /**
58
79
  * Stopka okna. Nosi atrybut `kptdialogfooter`, w który celuje wspólny SCSS (nazwy atrybutów
@@ -51,3 +51,6 @@ page becomes inert, and Escape closes it — all from the browser. With `dismiss
51
51
  `cancel` event is prevented, so Escape stops working; give the user another way out.
52
52
  With `header` the dialog is named by its title (`aria-labelledby`); without one pass `aria-label`.
53
53
  `role="alertdialog"` and `aria-describedby` go straight through as `<dialog>` props.
54
+
55
+ ## Scrolling and non-dismissible windows
56
+ An open window locks page scrolling underneath (a shared counter with drawers and bottom sheets, so nested windows do not release each other). With `dismissible` off the window gets `closedby="none"`, and if a browser still closes it on a gesture (Chromium lets the second Escape in a row through `cancel`), it is reopened — only code can close it.
@@ -4,6 +4,11 @@ export interface KptDividerProps extends Omit<ComponentPropsWithRef<'div'>, 'chi
4
4
  orientation?: KptDividerOrientation;
5
5
  /** Wcięcie (odstęp od krawędzi kontenera). */
6
6
  inset?: boolean;
7
+ /**
8
+ * Linia czysto ozdobna (np. między kartami, które i tak są osobnymi regionami) — bez
9
+ * `role="separator"`, ukryta przed czytnikiem. Domyślnie separator jest ogłaszany.
10
+ */
11
+ decorative?: boolean;
7
12
  }
8
13
  /**
9
14
  * Separator (linia pozioma lub pionowa). Kolor z `--kpt-color-border`.
@@ -12,5 +17,6 @@ export interface KptDividerProps extends Omit<ComponentPropsWithRef<'div'>, 'chi
12
17
  * <KptDivider />
13
18
  * <KptDivider orientation="vertical" />
14
19
  * <KptDivider inset />
20
+ * <KptDivider decorative />
15
21
  */
16
- export declare function KptDivider({ orientation, inset, className, ...rest }: Readonly<KptDividerProps>): import("react").JSX.Element;
22
+ export declare function KptDivider({ orientation, inset, decorative, className, ...rest }: Readonly<KptDividerProps>): import("react").JSX.Element;
@@ -7,7 +7,8 @@ import { cn } from "../internal/cn.js";
7
7
  * <KptDivider />
8
8
  * <KptDivider orientation="vertical" />
9
9
  * <KptDivider inset />
10
+ * <KptDivider decorative />
10
11
  */
11
- export function KptDivider({ orientation = 'horizontal', inset = false, className, ...rest }) {
12
- return (_jsx("div", { className: cn('kpt-divider', inset && 'kpt-divider--inset', className), role: "separator", "aria-orientation": orientation, "data-orientation": orientation, ...rest }));
12
+ export function KptDivider({ orientation = 'horizontal', inset = false, decorative = false, className, ...rest }) {
13
+ return (_jsx("div", { className: cn('kpt-divider', inset && 'kpt-divider--inset', className), role: decorative ? 'none' : 'separator', "aria-orientation": decorative ? undefined : orientation, "aria-hidden": decorative || undefined, "data-orientation": orientation, ...rest }));
13
14
  }
@@ -9,6 +9,7 @@ Import: `import { KptDivider } from '@konce-pt/react';`
9
9
  ## Props
10
10
  - `orientation`: 'horizontal' | 'vertical' (default 'horizontal')
11
11
  - `inset`: boolean — an indent from the container edges
12
+ - `decorative`: boolean — a purely ornamental line: no `role="separator"`, hidden from screen readers
12
13
  - `className`, `ref` and every other `<div>` prop go to the divider
13
14
 
14
15
  ## Vertical needs a flex parent
@@ -6,6 +6,11 @@ export interface KptDrawerProps extends Omit<ComponentPropsWithRef<'div'>, 'onCh
6
6
  onOpenChange: (open: boolean) => void;
7
7
  position?: KptDrawerPosition;
8
8
  header?: string;
9
+ /**
10
+ * Nazwa panelu bez `header` — okno dialogowe bez nazwy czytnik ogłasza jako samo „okno
11
+ * dialogowe” (WCAG 4.1.2). Z nagłówkiem nazwą jest nagłówek (`aria-labelledby`).
12
+ */
13
+ ariaLabel?: string;
9
14
  /** Pokazuje przycisk × i pozwala zamknąć klawiszem Escape. */
10
15
  dismissible?: boolean;
11
16
  closeOnBackdrop?: boolean;
@@ -22,4 +27,4 @@ export interface KptDrawerProps extends Omit<ComponentPropsWithRef<'div'>, 'onCh
22
27
  * Treść panelu…
23
28
  * </KptDrawer>
24
29
  */
25
- export declare function KptDrawer({ open, onOpenChange, position, header, dismissible, closeOnBackdrop, locale, dictionary, className, children, ...rest }: Readonly<KptDrawerProps>): import("react").JSX.Element;
30
+ export declare function KptDrawer({ open, onOpenChange, position, header, ariaLabel, dismissible, closeOnBackdrop, locale, dictionary, className, children, ...rest }: Readonly<KptDrawerProps>): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useCallback, useRef } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -16,7 +17,7 @@ import { KptIcon } from "../icon/icon.js";
16
17
  * Treść panelu…
17
18
  * </KptDrawer>
18
19
  */
19
- export function KptDrawer({ open, onOpenChange, position = 'left', header = '', dismissible = true, closeOnBackdrop = true, locale, dictionary, className, children, ...rest }) {
20
+ export function KptDrawer({ open, onOpenChange, position = 'left', header = '', ariaLabel = '', dismissible = true, closeOnBackdrop = true, locale, dictionary, className, children, ...rest }) {
20
21
  const { t } = useKptMessages({ locale, dictionary });
21
22
  const close = useCallback(() => onOpenChange(false), [onOpenChange]);
22
23
  const panelRef = useRef(null);
@@ -26,5 +27,5 @@ export function KptDrawer({ open, onOpenChange, position = 'left', header = '',
26
27
  if (event.key === 'Escape' && dismissible)
27
28
  close();
28
29
  };
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 })] })] }));
30
+ 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, "aria-label": header ? undefined : ariaLabel || 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 })] })] }));
30
31
  }
@@ -50,3 +50,6 @@ is named by its `header` through `aria-labelledby`. The shared helper is `intern
50
50
  ## Accessibility
51
51
  `role="dialog"`, `aria-modal="true"` and `aria-labelledby` pointing at the title; the close button gets
52
52
  its label from `common.close`. Focus handling — see "Focus and keyboard" above.
53
+
54
+ ## Name and scrolling
55
+ Without `header` give the panel `ariaLabel` — a `role="dialog"` without a name is announced as a bare "dialog". An open drawer locks page scrolling underneath.
@@ -1,6 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptId } from "../internal/use-id.js";
4
6
  import { KptIcon } from "../icon/icon.js";
5
7
  /**
6
8
  * Grupa pól z legendą (`<fieldset>` + `<legend>`), opcjonalnie zwijana.
@@ -12,6 +14,7 @@ import { KptIcon } from "../icon/icon.js";
12
14
  * <KptFieldset legend="Dane osobowe" toggleable>…</KptFieldset>
13
15
  */
14
16
  export function KptFieldset({ legend = '', toggleable = false, collapsed, defaultCollapsed = false, onCollapsedChange, animated = true, animationDuration = 500, className, style, children, ...rest }) {
17
+ const bodyId = useKptId(undefined, 'kpt-fieldset-body');
15
18
  const [isCollapsed, setCollapsed] = useControllableState({
16
19
  value: collapsed,
17
20
  defaultValue: defaultCollapsed,
@@ -20,5 +23,5 @@ export function KptFieldset({ legend = '', toggleable = false, collapsed, defaul
20
23
  return (_jsx("div", { className: cn('kpt-fieldset-host', className), style: {
21
24
  ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`,
22
25
  ...style,
23
- }, ...rest, children: _jsxs("fieldset", { className: "kpt-fieldset", children: [_jsx("legend", { className: "kpt-fieldset__legend", children: toggleable ? (_jsxs("button", { type: "button", className: "kpt-fieldset__toggle", "aria-expanded": !isCollapsed, onClick: () => setCollapsed(!isCollapsed), children: [_jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }), _jsx("span", { children: legend })] })) : (_jsx("span", { children: legend })) }), _jsx("div", { className: cn('kpt-fieldset__collapse', !isCollapsed && 'is-open'), inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-fieldset__collapse-inner", children: _jsx("div", { className: "kpt-fieldset__body", children: children }) }) })] }) }));
26
+ }, ...rest, children: _jsxs("fieldset", { className: "kpt-fieldset", children: [_jsx("legend", { className: "kpt-fieldset__legend", children: toggleable ? (_jsxs("button", { type: "button", className: "kpt-fieldset__toggle", "aria-expanded": !isCollapsed, "aria-controls": bodyId, onClick: () => setCollapsed(!isCollapsed), children: [_jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }), _jsx("span", { children: legend })] })) : (_jsx("span", { children: legend })) }), _jsx("div", { id: bodyId, className: cn('kpt-fieldset__collapse', !isCollapsed && 'is-open'), inert: isCollapsed, children: _jsx("div", { className: "kpt-fieldset__collapse-inner", children: _jsx("div", { className: "kpt-fieldset__body", children: children }) }) })] }) }));
24
27
  }
@@ -8,6 +8,12 @@ export interface KptFileUploadProps extends Omit<ComponentPropsWithRef<'div'>, '
8
8
  accept?: string;
9
9
  multiple?: boolean;
10
10
  disabled?: boolean;
11
+ /** Tylko do odczytu — listę plików widać, nie da się jej zmienić. */
12
+ readOnly?: boolean;
13
+ /** Id strefy; bez niego generowane, w `KptFormField` — z wrappera. */
14
+ id?: string;
15
+ /** Nazwa dostępna, gdy strefy nie opisuje etykieta (`KptFormField`). */
16
+ ariaLabel?: string | null;
11
17
  invalid?: boolean;
12
18
  touched?: boolean;
13
19
  onTouch?: () => void;
@@ -24,4 +30,4 @@ export interface KptFileUploadProps extends Omit<ComponentPropsWithRef<'div'>, '
24
30
  * @example
25
31
  * <KptFileUpload value={files} onValueChange={setFiles} accept="image/*" multiple />
26
32
  */
27
- export declare function KptFileUpload({ value, defaultValue, onValueChange, accept, multiple, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptFileUploadProps>): import("react").JSX.Element;
33
+ export declare function KptFileUpload({ value, defaultValue, onValueChange, accept, multiple, disabled, readOnly, id, ariaLabel, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptFileUploadProps>): import("react").JSX.Element;