@konce-pt/react 0.9.0 → 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
@@ -1,17 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useControllableState } from "../internal/use-controllable-state.js";
4
- import { useKptFormFieldContext } from "../form-field/form-field-context.js";
5
- const DEFAULT_PRESETS = [
6
- '#ef4444',
7
- '#f59e0b',
8
- '#10b981',
9
- '#3b82f6',
10
- '#8b5cf6',
11
- '#ec4899',
12
- '#111827',
13
- '#ffffff',
14
- ];
5
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
6
+ import { useKptMessages } from "../i18n/i18n-context.js";
7
+ const DEFAULT_PRESETS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#111827', '#ffffff'];
8
+ /** Nazwy domyślnej palety (klucze `colorPicker.*`). */
9
+ const DEFAULT_NAMES = {
10
+ '#ef4444': 'red',
11
+ '#f59e0b': 'amber',
12
+ '#10b981': 'green',
13
+ '#3b82f6': 'blue',
14
+ '#8b5cf6': 'violet',
15
+ '#ec4899': 'pink',
16
+ '#111827': 'black',
17
+ '#ffffff': 'white',
18
+ };
19
+ const HEX = /^#[0-9a-f]{6}$/i;
15
20
  /**
16
21
  * Wybór koloru: próbka, pole hex i presety. Wartością jest hex.
17
22
  *
@@ -21,8 +26,37 @@ const DEFAULT_PRESETS = [
21
26
  * @example
22
27
  * <KptColorPicker value={color} onValueChange={setColor} />
23
28
  */
24
- export function KptColorPicker({ value, defaultValue = '#3b82f6', onValueChange, presets = DEFAULT_PRESETS, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
25
- const field = useKptFormFieldContext();
29
+ export function KptColorPicker({ value, defaultValue = '#3b82f6', onValueChange, presets = DEFAULT_PRESETS, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
30
+ const { t } = useKptMessages({ locale, dictionary });
31
+ const field = useKptFieldA11y(id, 'kpt-color-picker');
32
+ /** Tekst w polu hex, dopóki nie jest poprawnym kodem — wartość zmienia się dopiero na `#rrggbb`. */
33
+ const [hexDraft, setHexDraft] = useState(null);
34
+ const hexInvalid = hexDraft !== null && !HEX.test(hexDraft);
35
+ const presetList = presets.map((preset) => {
36
+ const item = typeof preset === 'string' ? { value: preset, label: '' } : preset;
37
+ const named = DEFAULT_NAMES[item.value.toLowerCase()];
38
+ return { value: item.value, label: item.label || (named ? t(`colorPicker.${named}`) : item.value) };
39
+ });
40
+ const isActive = (color) => color.toLowerCase() === current.toLowerCase();
26
41
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
27
- return (_jsx("div", { className: cn('kpt-color-picker-host', className), ...rest, children: _jsxs("div", { className: "kpt-color-picker", children: [_jsx("label", { className: "kpt-color-picker__swatch", style: { backgroundColor: current }, children: _jsx("input", { type: "color", value: current, disabled: disabled, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }) }), _jsx("input", { type: "text", className: "kpt-color-picker__hex", id: field?.controlId, value: current, disabled: disabled, maxLength: 7, "aria-invalid": (invalid && (touched ?? true)) || undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("div", { className: "kpt-color-picker__presets", children: presets.map((color) => (_jsx("button", { type: "button", className: cn('kpt-color-picker__preset', color === current && 'is-active'), style: { backgroundColor: color }, disabled: disabled, onClick: () => setCurrent(color), "aria-label": color }, color))) })] }) }));
42
+ return (_jsx("div", { className: cn('kpt-color-picker-host', className), ...rest, children: _jsxs("div", { className: "kpt-color-picker", children: [_jsx("label", { className: "kpt-color-picker__swatch", style: { backgroundColor: current }, children: _jsx("input", { type: "color", value: current, disabled: disabled, "aria-label": t('colorPicker.picker'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }) }), _jsx("input", { type: "text", className: "kpt-color-picker__hex", id: field.id, "aria-label": field.labelledBy ? undefined : (ariaLabel ?? t('colorPicker.hex')), "aria-describedby": field.describedBy, value: hexDraft ?? current, disabled: disabled, maxLength: 7, "aria-invalid": (invalid && (touched ?? true)) || hexInvalid || undefined, onChange: (event) => {
43
+ let text = event.target.value.trim();
44
+ if (text && !text.startsWith('#'))
45
+ text = `#${text}`;
46
+ if (HEX.test(text)) {
47
+ setHexDraft(null);
48
+ setCurrent(text.toLowerCase());
49
+ }
50
+ else {
51
+ setHexDraft(text);
52
+ }
53
+ }, onBlur: () => {
54
+ // Wyjście z pola z niepełnym kodem zostawia go widocznym i oznaczonym `aria-invalid`.
55
+ if (hexDraft === '')
56
+ setHexDraft(null);
57
+ onTouch?.();
58
+ } }), _jsx("div", { className: "kpt-color-picker__presets", children: presetList.map((preset) => (_jsx("button", { type: "button", className: cn('kpt-color-picker__preset', isActive(preset.value) && 'is-active'), style: { backgroundColor: preset.value }, disabled: disabled, "aria-pressed": isActive(preset.value), onClick: () => {
59
+ setHexDraft(null);
60
+ setCurrent(preset.value);
61
+ }, "aria-label": preset.label }, preset.value))) })] }) }));
28
62
  }
@@ -31,6 +31,20 @@ same value as text, and the presets write it directly.
31
31
  <KptColorPicker value={color} onValueChange={setColor} />
32
32
  <KptColorPicker value={color} onValueChange={setColor} presets={['#ef4444', '#3b82f6']} />
33
33
 
34
+ ## Screen reader and keyboard
35
+ `presets` accepts codes or `{ value, label }` (the `KptColorPreset` type): the label names the
36
+ swatch, because a hex code says little to a screen reader; the default palette is named from i18n
37
+ (`colorPicker.red`…). The active swatch has `aria-pressed`. The hex field changes the value only on
38
+ a full `#rrggbb` code (a missing `#` is added); an incomplete code stays visible with
39
+ `aria-invalid`.
40
+
41
+ ## Label and description
42
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
43
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
44
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `ariaLabel`.
45
+ `id` and the label go to the hex field; the native colour well is named "Pick a colour"
46
+ (`colorPicker.picker`), and the hex field falls back to "Hex code" (`colorPicker.hex`).
47
+
34
48
  ## Tokens
35
49
  The border `--kpt-form-field-border`, the field `--kpt-form-field-{bg,text,radius}`, the active
36
50
  preset `--kpt-color-focus-ring`.
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithRef } from 'react';
1
+ import { type ComponentPropsWithRef } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  export interface KptConfirmContainerProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
4
4
  }
@@ -6,6 +6,10 @@ export interface KptConfirmContainerProps extends ComponentPropsWithRef<'div'>,
6
6
  * Kontener potwierdzeń — renderuje żądanie z `kptConfirm()` na `KptDialog`. Umieść raz
7
7
  * w aplikacji, obok `<KptToastContainer />`.
8
8
  *
9
+ * Okno to `role="alertdialog"` z treścią pytania w `aria-describedby`, więc czytnik odczytuje
10
+ * je od razu po otwarciu. Fokus startuje na przycisku odmowy — bezpieczniejszej odpowiedzi,
11
+ * której przypadkowy `Enter` niczego nie usunie.
12
+ *
9
13
  * @example
10
14
  * <KptConfirmContainer />
11
15
  * // gdziekolwiek: if (await kptConfirm({ message: 'Usunąć?' })) { … }
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
2
3
  import { cn } from "../internal/cn.js";
4
+ import { useKptId } from "../internal/use-id.js";
3
5
  import { useKptMessages } from "../i18n/i18n-context.js";
4
6
  import { KptButton } from "../button/button.js";
5
7
  import { KptDialog } from "../dialog/dialog.js";
@@ -9,6 +11,10 @@ import { acceptKptConfirm, rejectKptConfirm, useKptConfirmRequest } from "./conf
9
11
  * Kontener potwierdzeń — renderuje żądanie z `kptConfirm()` na `KptDialog`. Umieść raz
10
12
  * w aplikacji, obok `<KptToastContainer />`.
11
13
  *
14
+ * Okno to `role="alertdialog"` z treścią pytania w `aria-describedby`, więc czytnik odczytuje
15
+ * je od razu po otwarciu. Fokus startuje na przycisku odmowy — bezpieczniejszej odpowiedzi,
16
+ * której przypadkowy `Enter` niczego nie usunie.
17
+ *
12
18
  * @example
13
19
  * <KptConfirmContainer />
14
20
  * // gdziekolwiek: if (await kptConfirm({ message: 'Usunąć?' })) { … }
@@ -16,11 +22,19 @@ import { acceptKptConfirm, rejectKptConfirm, useKptConfirmRequest } from "./conf
16
22
  export function KptConfirmContainer({ locale, dictionary, className, ...rest }) {
17
23
  const { t } = useKptMessages({ locale, dictionary });
18
24
  const request = useKptConfirmRequest();
25
+ const messageId = useKptId(undefined, 'kpt-confirm-message');
26
+ const rejectRef = useRef(null);
27
+ // `showModal()` skupia pierwszy element okna, czyli „×” w nagłówku. Efekt dziecka (`KptDialog`)
28
+ // biegnie przed efektem rodzica, więc tu okno jest już otwarte i fokus można przenieść.
29
+ useEffect(() => {
30
+ if (request)
31
+ rejectRef.current?.focus();
32
+ }, [request]);
19
33
  return (_jsx("div", { className: cn('kpt-confirm-container-host', className), ...rest, children: _jsxs(KptDialog, { open: request !== null,
20
34
  // Escape i × zamykają okno bez odpowiedzi — traktujemy to jak odmowę, żeby czekający
21
35
  // `await` zawsze coś dostał.
22
36
  onOpenChange: (open) => {
23
37
  if (!open)
24
38
  rejectKptConfirm();
25
- }, header: request?.header ?? t('confirm.header'), locale: locale, dictionary: dictionary, children: [_jsxs("div", { className: "kpt-confirm__body", children: [request?.icon ? (_jsx(KptIcon, { className: "kpt-confirm__icon", name: request.icon, "data-variant": request.variant ?? 'filled' })) : null, _jsx("p", { className: "kpt-confirm__message", children: request?.message })] }), _jsxs(KptDialog.Footer, { children: [_jsx(KptButton, { variant: "text", onClick: rejectKptConfirm, children: request?.rejectLabel ?? t('confirm.reject') }), _jsx(KptButton, { variant: request?.variant ?? 'filled', onClick: acceptKptConfirm, children: request?.acceptLabel ?? t('confirm.accept') })] })] }) }));
39
+ }, header: request?.header ?? t('confirm.header'), role: "alertdialog", "aria-describedby": messageId, locale: locale, dictionary: dictionary, children: [_jsxs("div", { className: "kpt-confirm__body", children: [request?.icon ? (_jsx(KptIcon, { className: "kpt-confirm__icon", name: request.icon, "data-variant": request.variant ?? 'filled' })) : null, _jsx("p", { className: "kpt-confirm__message", id: messageId, children: request?.message })] }), _jsxs(KptDialog.Footer, { children: [_jsx(KptButton, { ref: rejectRef, variant: "text", onClick: rejectKptConfirm, children: request?.rejectLabel ?? t('confirm.reject') }), _jsx(KptButton, { variant: request?.variant ?? 'filled', onClick: acceptKptConfirm, children: request?.acceptLabel ?? t('confirm.accept') })] })] }) }));
26
40
  }
@@ -40,5 +40,7 @@ Inherited from `KptDialog`; the icon follows the variant (`--kpt-color-danger` /
40
40
  `--kpt-color-primary`).
41
41
 
42
42
  ## Accessibility
43
- Everything comes from `KptDialog`, that is from the native `<dialog>`: the focus trap, Escape and
44
- the backdrop are the platform's, not ours.
43
+ Built on `KptDialog`, so the focus trap, Escape and the backdrop are the native `<dialog>`'s.
44
+ The window is `role="alertdialog"` named by its header, with the message tied to it through
45
+ `aria-describedby`, so a screen reader reads the question as soon as it opens. Focus starts on the
46
+ reject button — the safe answer, so a stray `Enter` deletes nothing. Escape and × reject.
@@ -9,6 +9,11 @@ export interface KptContextMenuProps<T> extends Omit<ComponentPropsWithRef<'div'
9
9
  /**
10
10
  * Menu kontekstowe (PPM) na obszarze dzieci. Panel pozycjonowany do kursora.
11
11
  *
12
+ * Klawiatura jak w `KptMenu` (wspólne `menu-keys.ts`): po otwarciu fokus na pierwszej pozycji,
13
+ * strzałki / `Home` / `End` chodzą po pozycjach, `Escape` i `Tab` zamykają, a fokus wraca tam,
14
+ * gdzie był. `Shift+F10` i klawisz Menu na skupionym elemencie obszaru też otwierają menu
15
+ * (panel staje wtedy przy tym elemencie). Panel jest dociskany do krawędzi okna.
16
+ *
12
17
  * W odróżnieniu od `KptMenu` panel nie idzie przez `KptOverlay` — nie ma tu wyzwalacza,
13
18
  * względem którego dałoby się go przypiąć; pozycję wyznaczają współrzędne kliknięcia,
14
19
  * a wygląd i warstwę niesie wspólny SCSS (`.kpt-context-menu__panel`).
@@ -1,10 +1,18 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { MenuRows } from "../menu/menu-rows.js";
5
+ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
6
+ /** Odstęp panelu od krawędzi okna przy dociskaniu. */
7
+ const EDGE = 8;
5
8
  /**
6
9
  * Menu kontekstowe (PPM) na obszarze dzieci. Panel pozycjonowany do kursora.
7
10
  *
11
+ * Klawiatura jak w `KptMenu` (wspólne `menu-keys.ts`): po otwarciu fokus na pierwszej pozycji,
12
+ * strzałki / `Home` / `End` chodzą po pozycjach, `Escape` i `Tab` zamykają, a fokus wraca tam,
13
+ * gdzie był. `Shift+F10` i klawisz Menu na skupionym elemencie obszaru też otwierają menu
14
+ * (panel staje wtedy przy tym elemencie). Panel jest dociskany do krawędzi okna.
15
+ *
8
16
  * W odróżnieniu od `KptMenu` panel nie idzie przez `KptOverlay` — nie ma tu wyzwalacza,
9
17
  * względem którego dałoby się go przypiąć; pozycję wyznaczają współrzędne kliknięcia,
10
18
  * a wygląd i warstwę niesie wspólny SCSS (`.kpt-context-menu__panel`).
@@ -17,12 +25,48 @@ import { MenuRows } from "../menu/menu-rows.js";
17
25
  export function KptContextMenu({ items = [], children, onSelect, className, ...rest }) {
18
26
  const [open, setOpen] = useState(false);
19
27
  const [point, setPoint] = useState({ x: 0, y: 0 });
28
+ const panelRef = useRef(null);
29
+ /** Element, który miał fokus przed otwarciem — tam fokus wraca po zamknięciu. */
30
+ const returnTo = useRef(null);
20
31
  const onContextMenu = (event) => {
21
32
  event.preventDefault();
22
- setPoint({ x: event.clientX, y: event.clientY });
33
+ const active = document.activeElement;
34
+ returnTo.current = active instanceof HTMLElement ? active : null;
35
+ // Z klawiatury (`Shift+F10`, klawisz Menu) zdarzenie nie niesie pozycji myszy.
36
+ if (event.clientX === 0 && event.clientY === 0 && event.target instanceof HTMLElement) {
37
+ const r = event.target.getBoundingClientRect();
38
+ setPoint({ x: r.left, y: r.bottom });
39
+ }
40
+ else {
41
+ setPoint({ x: event.clientX, y: event.clientY });
42
+ }
23
43
  setOpen(true);
24
44
  };
25
- const close = () => setOpen(false);
45
+ // Po otwarciu: dociśnięcie do okna i fokus na pierwszej pozycji.
46
+ useEffect(() => {
47
+ const panel = panelRef.current;
48
+ if (!open || !panel)
49
+ return;
50
+ const r = panel.getBoundingClientRect();
51
+ const maxX = window.innerWidth - r.width - EDGE;
52
+ const maxY = window.innerHeight - r.height - EDGE;
53
+ if (point.x > maxX || point.y > maxY) {
54
+ setPoint({ x: Math.max(EDGE, Math.min(point.x, maxX)), y: Math.max(EDGE, Math.min(point.y, maxY)) });
55
+ }
56
+ focusMenuEdge(panel, 'first');
57
+ // Celowo tylko `open`: dociśnięcie zmienia `point`, a ponowne uruchomienie przeniosłoby fokus.
58
+ }, [open]);
59
+ const close = () => {
60
+ setOpen(false);
61
+ const target = returnTo.current;
62
+ returnTo.current = null;
63
+ if (target?.isConnected)
64
+ target.focus();
65
+ };
66
+ const onPanelKeyDown = (event) => {
67
+ if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close')
68
+ close();
69
+ };
26
70
  const pick = (item) => {
27
71
  if (item.disabled)
28
72
  return;
@@ -32,5 +76,5 @@ export function KptContextMenu({ items = [], children, onSelect, className, ...r
32
76
  return (_jsxs("div", { className: cn('kpt-context-menu-host', className), ...rest, children: [_jsx("div", { className: "kpt-context-menu__area", onContextMenu: onContextMenu, children: children }), open ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-context-menu__backdrop", onClick: close, onContextMenu: (event) => {
33
77
  event.preventDefault();
34
78
  close();
35
- } }), _jsx("div", { className: "kpt-context-menu__panel", role: "menu", style: { left: point.x, top: point.y }, children: _jsx(MenuRows, { items: items, block: "kpt-context-menu", onPick: pick }) })] })) : null] }));
79
+ } }), _jsx("div", { ref: panelRef, className: "kpt-context-menu__panel", role: "menu", style: { left: point.x, top: point.y }, onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-context-menu", onPick: pick }) })] })) : null] }));
36
80
  }
@@ -23,7 +23,15 @@ the position comes from the click coordinates, and the look and the layer come f
23
23
 
24
24
  ## Closing
25
25
  A left click on the backdrop closes it; a right click on the backdrop closes it too and does not
26
- open the browser's own menu. Picking an item closes it and reports the choice.
26
+ open the browser's own menu. Picking an item closes it and reports the choice; `Escape` closes it too.
27
+
28
+ ## Keyboard
29
+ The same model as `KptMenu` (the shared `menu-keys.ts`): opening moves focus to the first item, arrows /
30
+ `Home` / `End` move between items (disabled ones and separators are skipped), `Escape` and `Tab` close
31
+ it and focus returns to where it was. `Shift+F10` and the context-menu key on a focused element inside
32
+ the area open it too — the browser then sends `contextmenu` without mouse coordinates, so the panel is
33
+ placed at that element. The panel is kept inside the viewport (8px from the edges). Touch screens have
34
+ no right click, so still offer the same actions through a visible button with `KptMenu`.
27
35
 
28
36
  ## Examples
29
37
  <KptContextMenu items={items} onSelect={onSelect}>
@@ -34,6 +42,5 @@ open the browser's own menu. Picking an item closes it and reports the choice.
34
42
  `--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-danger`.
35
43
 
36
44
  ## Accessibility
37
- The panel is `role="menu"` with `role="menuitem"` buttons. There is no keyboard path to open it —
38
- that is what the right mouse button and the platform's context key are for, so pair the same actions
39
- with a `KptMenu` when they must be reachable from the keyboard.
45
+ The panel is `role="menu"` with `role="menuitem"` buttons at `tabindex="-1"` (one Tab stop). Keyboard — see
46
+ "Keyboard" above.
@@ -13,6 +13,14 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
13
13
  minuteStep?: number;
14
14
  placeholder?: string;
15
15
  displayFormat?: 'pl' | 'iso';
16
+ /** Najwcześniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
17
+ minDate?: string | null;
18
+ /** Najpóźniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
19
+ maxDate?: string | null;
20
+ /** Nazwa dostępna pola, gdy nie opisuje go `<label for>`. */
21
+ ariaLabel?: string | null;
22
+ /** Id pola (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
23
+ id?: string;
16
24
  disabled?: boolean;
17
25
  invalid?: boolean;
18
26
  touched?: boolean;
@@ -23,8 +31,13 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
23
31
  * `visibleMonths` pokazuje kilka miesięcy obok siebie, a zakres podświetla się ciągłym pasmem
24
32
  * przez wszystkie panele.
25
33
  *
34
+ * Klawiatura jak w `KptDatepicker`: `Enter`/`Spacja`/`↓` otwierają panel z fokusem na początku
35
+ * zakresu (albo dziś), strzałki, `Home`/`End` i `PageUp`/`PageDown` (z `Shift` — rok) chodzą po
36
+ * dniach przez wszystkie panele, `Tab` krąży w panelu, `Escape` i wybór końca oddają fokus polu.
37
+ * Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór ogłasza region `polite`.
38
+ *
26
39
  * @example
27
40
  * <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
28
41
  * <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
29
42
  */
30
- export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
43
+ export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, minDate, maxDate, ariaLabel, id, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
@@ -1,31 +1,48 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
3
- import { buildRangeGrid, clampVisibleMonths, datePart, formatDate, formatTime, pad, parseTime, pickRangeDay, timePart, toIso, } from '@konce-pt/datetime';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { buildRangeGrid, clampVisibleMonths, datePart, formatDate, formatTime, initialFocusDate, moveFocusDate, pad, parseDate, parseTime, pickRangeDay, timePart, 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
  const EMPTY_RANGE = { start: '', end: '' };
14
16
  /**
15
17
  * Wybór zakresu dat (i godzin) z kalendarzem. Pierwszy klik ustawia początek, drugi koniec;
16
18
  * `visibleMonths` pokazuje kilka miesięcy obok siebie, a zakres podświetla się ciągłym pasmem
17
19
  * przez wszystkie panele.
18
20
  *
21
+ * Klawiatura jak w `KptDatepicker`: `Enter`/`Spacja`/`↓` otwierają panel z fokusem na początku
22
+ * zakresu (albo dziś), strzałki, `Home`/`End` i `PageUp`/`PageDown` (z `Shift` — rok) chodzą po
23
+ * dniach przez wszystkie panele, `Tab` krąży w panelu, `Escape` i wybór końca oddają fokus polu.
24
+ * Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór ogłasza region `polite`.
25
+ *
19
26
  * @example
20
27
  * <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
21
28
  * <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
22
29
  */
23
- export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange, mode = 'date', visibleMonths = 1, hourFormat = 24, minuteStep = 1, placeholder, displayFormat = 'pl', disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
30
+ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange, mode = 'date', visibleMonths = 1, hourFormat = 24, minuteStep = 1, placeholder, displayFormat = 'pl', minDate = null, maxDate = null, ariaLabel = null, id, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
24
31
  const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
25
- 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-date-range');
26
34
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
27
35
  const [isOpen, setOpen] = useState(false);
28
36
  const [anchor, setAnchor] = useState(null);
37
+ const triggerRef = useRef(null);
38
+ const panelRef = useRef(null);
39
+ const panelId = useKptId(undefined, 'kpt-date-range-panel');
40
+ /** Dzień z fokusem — jedyny przystanek Tab siatek dni (roving tabindex przez wszystkie panele). */
41
+ const [focusIso, setFocusIso] = useState('');
42
+ /** Komunikat regionu `polite` po wyborze początku i całego zakresu. */
43
+ const [announcement, setAnnouncement] = useState('');
44
+ /** Prośba o przeniesienie fokusu po najbliższym renderze (otwarcie, ruch klawiszem). */
45
+ const wantFocus = useRef(false);
29
46
  const today = useRef(new Date()).current;
30
47
  const todayIso = toIso(today);
31
48
  const showInvalid = invalid && (touched ?? true);
@@ -40,6 +57,22 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
40
57
  const weekdays = weekdayNames(activeLocale);
41
58
  const panelCount = mode === 'time' ? 1 : clampVisibleMonths(visibleMonths);
42
59
  const multi = panelCount > 1;
60
+ const dayFormat = new Intl.DateTimeFormat(activeLocale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
61
+ const dayName = (iso) => {
62
+ const date = parseDate(iso);
63
+ return date ? dayFormat.format(date) : iso;
64
+ };
65
+ /** Pełna nazwa dnia z rolą w zakresie — kolor pasma nie jest jedynym nośnikiem informacji. */
66
+ const dayLabel = (cell) => {
67
+ const name = dayName(cell.iso);
68
+ if (cell.isStart)
69
+ return t('dateRange.rangeStart', { date: name });
70
+ if (cell.isEnd)
71
+ return t('dateRange.rangeEnd', { date: name });
72
+ if (cell.inRange)
73
+ return t('dateRange.inRange', { date: name });
74
+ return name;
75
+ };
43
76
  const fmtTime = (hhmm) => {
44
77
  const parsed = parseTime(hhmm);
45
78
  return parsed ? formatTime(parsed.h, parsed.m, hourFormat) : '';
@@ -68,9 +101,11 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
68
101
  return;
69
102
  if (!isOpen) {
70
103
  const { start, end } = current;
71
- const base = datePart(start) || todayIso;
72
- const [y, m] = [+base.slice(0, 4), +base.slice(5, 7) - 1];
73
- setPage({ y, m });
104
+ const focus = initialFocusDate(datePart(start), todayIso, minDate, maxDate);
105
+ setFocusIso(focus);
106
+ setAnnouncement('');
107
+ setPage({ y: +focus.slice(0, 4), m: +focus.slice(5, 7) - 1 });
108
+ wantFocus.current = true;
74
109
  if (mode === 'datetime') {
75
110
  setStartDate(datePart(start));
76
111
  setEndDate(datePart(end));
@@ -91,6 +126,59 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
91
126
  setOpen(false);
92
127
  onTouch?.();
93
128
  };
129
+ /** Zamknięcie z powrotem fokusu na pole — po wyborze z klawiatury, `Escape`. */
130
+ const closeAndFocus = () => {
131
+ close();
132
+ triggerRef.current?.focus();
133
+ };
134
+ // Fokus po renderze. Panel w nakładce jest niewidoczny do pierwszego pomiaru — stąd rAF.
135
+ useEffect(() => {
136
+ if (!isOpen || !wantFocus.current)
137
+ return;
138
+ const frame = requestAnimationFrame(() => {
139
+ wantFocus.current = false;
140
+ focusCalendarActive(panelRef.current, focusIso);
141
+ });
142
+ return () => cancelAnimationFrame(frame);
143
+ });
144
+ /** Przesuwa okno paneli tak, żeby miesiąc dnia `iso` był widoczny — z najmniejszym skokiem. */
145
+ const revealMonth = (iso) => {
146
+ const target = +iso.slice(0, 4) * 12 + (+iso.slice(5, 7) - 1);
147
+ const first = page.y * 12 + page.m;
148
+ if (target < first)
149
+ setPage({ y: Math.floor(target / 12), m: target % 12 });
150
+ else if (target > first + panelCount - 1) {
151
+ const start = target - panelCount + 1;
152
+ setPage({ y: Math.floor(start / 12), m: start % 12 });
153
+ }
154
+ };
155
+ const onDayKeyDown = (event) => {
156
+ const rtl = !!panelRef.current && getComputedStyle(panelRef.current).direction === 'rtl';
157
+ const next = moveFocusDate(focusIso, event.key, { shift: event.shiftKey, rtl, minDate, maxDate });
158
+ if (next === null)
159
+ return;
160
+ event.preventDefault();
161
+ setFocusIso(next);
162
+ revealMonth(next);
163
+ wantFocus.current = true;
164
+ };
165
+ /** Strzałki ‹ ›: fokus zostaje na strzałce, ale przystanek siatki idzie za stroną. */
166
+ const turnPage = (dir) => {
167
+ const first = page.y * 12 + page.m + dir;
168
+ setPage({ y: Math.floor(first / 12), m: ((first % 12) + 12) % 12 });
169
+ const moved = moveFocusDate(focusIso, dir === 1 ? 'PageDown' : 'PageUp', { minDate, maxDate });
170
+ if (moved) {
171
+ const target = +moved.slice(0, 4) * 12 + (+moved.slice(5, 7) - 1);
172
+ // Nowe okno zaczyna się od `first` — dzień spoza niego nie dostałby przystanku Tab.
173
+ if (target >= first && target < first + panelCount)
174
+ setFocusIso(moved);
175
+ }
176
+ };
177
+ const announce = (start, end) => {
178
+ setAnnouncement(end
179
+ ? t('dateRange.picked', { start: dayName(start), end: dayName(end) })
180
+ : t('dateRange.pickEnd', { date: dayName(start) }));
181
+ };
94
182
  const panels = Array.from({ length: panelCount }, (_, i) => {
95
183
  const first = new Date(page.y, page.m + i, 1);
96
184
  const py = first.getFullYear();
@@ -104,21 +192,26 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
104
192
  end: mode === 'datetime' ? endDate : current.end,
105
193
  todayIso,
106
194
  hideOutside: multi,
195
+ minDate,
196
+ maxDate,
107
197
  }),
108
198
  };
109
199
  });
110
200
  const selectDay = (iso) => {
201
+ setFocusIso(iso);
111
202
  if (mode === 'datetime') {
112
203
  const next = pickRangeDay({ start: startDate, end: endDate }, iso);
113
204
  setStartDate(next.start);
114
205
  setEndDate(next.end);
206
+ announce(next.start, next.end);
115
207
  return; // czas do wyboru → zamknięcie przez „Gotowe"
116
208
  }
117
209
  const next = pickRangeDay(current, iso);
118
210
  setCurrent(next);
211
+ announce(next.start, next.end);
119
212
  // Panel zostaje otwarty do czasu domknięcia zakresu — inaczej drugi klik nie miałby gdzie paść.
120
213
  if (next.end)
121
- close();
214
+ closeAndFocus();
122
215
  };
123
216
  const commit = () => {
124
217
  if (mode === 'time') {
@@ -135,7 +228,7 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
135
228
  const end = `${endDate || startDate}T${endTime || '00:00'}`;
136
229
  setCurrent(start <= end ? { start, end } : { start: end, end: start });
137
230
  }
138
- close();
231
+ closeAndFocus();
139
232
  };
140
233
  const clear = (event) => {
141
234
  event.stopPropagation();
@@ -145,11 +238,34 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
145
238
  };
146
239
  const clockValue = timeEnd === 'start' ? startTime : endTime;
147
240
  const setClockValue = timeEnd === 'start' ? setStartTime : setEndTime;
148
- return (_jsxs("div", { className: cn('kpt-date-range-host', className), ...rest, children: [_jsxs("div", { ref: setAnchor, className: cn('kpt-date-range', isOpen && 'kpt-date-range--open', disabled && 'kpt-date-range--disabled', showInvalid && 'kpt-date-range--invalid'), role: "button", tabIndex: 0, id: field?.controlId, "aria-disabled": disabled || undefined, onClick: toggleOpen, onKeyDown: (event) => {
149
- if (event.key === 'Enter')
150
- toggleOpen();
151
- }, onBlur: () => onTouch?.(), children: [display ? (_jsx("span", { className: "kpt-date-range__value", children: display })) : (_jsx("span", { className: "kpt-date-range__placeholder", children: placeholder ?? t('dateRange.placeholder') })), _jsx("span", { className: "kpt-date-range__spacer" }), display ? (_jsx("button", { type: "button", className: "kpt-date-range__clear", onClick: clear, "aria-label": t('common.clear'), children: _jsx(KptIcon, { name: "x" }) })) : null, _jsx(KptIcon, { className: "kpt-date-range__icon", name: "calendar" })] }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 4, children: _jsx("div", { className: cn('kpt-calendar', mode === 'time' && 'kpt-calendar--clock', mode !== 'time' && 'kpt-calendar--range', multi && mode !== 'time' && 'kpt-calendar--multi'), children: mode === 'time' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.from') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.to') })] }), _jsx(KptClock
152
- // Klucz na edytowanym końcu wymusza remount przy przełączeniu Od/Koniec — tarcza ma
153
- // wtedy wrócić do etapu godzin, tak jak dwie osobne instancje w porcie Angulara.
154
- , { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-calendar__panels", children: panels.map((panel, index) => (_jsxs("div", { className: "kpt-calendar__panel", children: [_jsxs("div", { className: "kpt-calendar__header", children: [index === 0 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => setPage(({ y, m }) => (m === 0 ? { y: y - 1, m: 11 } : { y, m: m - 1 })), "aria-label": t('datepicker.prevMonth'), children: _jsx(KptIcon, { name: "chevron-left" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" })), _jsx("span", { className: "kpt-calendar__title", children: panel.label }), index === panels.length - 1 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => setPage(({ y, m }) => (m === 11 ? { y: y + 1, m: 0 } : { y, m: m + 1 })), "aria-label": t('datepicker.nextMonth'), children: _jsx(KptIcon, { name: "chevron-right" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" }))] }), _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: panel.weeks.flat().map((cell) => cell.blank ? (_jsx("span", { className: "kpt-calendar__day kpt-calendar__day--blank" }, cell.iso)) : (_jsx("button", { type: "button", className: cn('kpt-calendar__day', !cell.inMonth && 'kpt-calendar__day--muted', cell.today && 'kpt-calendar__day--today', cell.inRange && 'kpt-calendar__day--range', cell.rangeStart && 'kpt-calendar__day--range-start', cell.rangeEnd && 'kpt-calendar__day--range-end', (cell.isStart || cell.isEnd) && 'kpt-calendar__day--selected'), onClick: () => selectDay(cell.iso), children: cell.day }, cell.iso))) })] }, panel.key))) }), mode === 'datetime' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.fromTime') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.toTime') })] }), _jsx(KptClock, { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : null] })) }) })] }));
241
+ return (_jsxs("div", { className: cn('kpt-date-range-host', className), ...rest, children: [_jsxs("div", { ref: (el) => {
242
+ triggerRef.current = el;
243
+ setAnchor(el);
244
+ }, className: cn('kpt-date-range', isOpen && 'kpt-date-range--open', disabled && 'kpt-date-range--disabled', showInvalid && 'kpt-date-range--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-invalid": showInvalid || undefined, "aria-label": ariaLabel ?? undefined, onClick: toggleOpen, onKeyDown: (event) => {
245
+ if (disabled)
246
+ return;
247
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
248
+ event.preventDefault();
249
+ if (!isOpen)
250
+ toggleOpen();
251
+ }
252
+ else if ((event.key === 'Delete' || event.key === 'Backspace') && current.start) {
253
+ event.preventDefault();
254
+ clear(event);
255
+ }
256
+ },
257
+ // Przy otwartym panelu fokus idzie do kalendarza — „dotknięcie” wyśle dopiero zamknięcie.
258
+ onBlur: () => !isOpen && onTouch?.(), children: [display ? (_jsx("span", { className: "kpt-date-range__value", children: display })) : (_jsx("span", { className: "kpt-date-range__placeholder", children: placeholder ?? t('dateRange.placeholder') })), _jsx("span", { className: "kpt-date-range__spacer" }), display ? (_jsx("button", { type: "button", className: "kpt-date-range__clear", tabIndex: -1, onClick: clear, "aria-label": t('common.clear'), children: _jsx(KptIcon, { name: "x" }) })) : null, _jsx(KptIcon, { className: "kpt-date-range__icon", name: "calendar" })] }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 4, children: _jsxs("div", { ref: panelRef, id: panelId, role: "dialog", "aria-label": t('dateRange.dialog'), className: cn('kpt-calendar', mode === 'time' && 'kpt-calendar--clock', mode !== 'time' && 'kpt-calendar--range', multi && mode !== 'time' && 'kpt-calendar--multi'), onKeyDown: (event) => {
259
+ if (event.key === 'Escape') {
260
+ event.preventDefault();
261
+ event.stopPropagation();
262
+ closeAndFocus();
263
+ }
264
+ else if (event.key === 'Tab') {
265
+ trapTab(event.currentTarget, event.nativeEvent);
266
+ }
267
+ }, children: [_jsx("span", { className: "kpt-calendar__live", "aria-live": "polite", children: announcement }), mode === 'time' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.from') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.to') })] }), _jsx(KptClock
268
+ // Klucz na edytowanym końcu wymusza remount przy przełączeniu Od/Koniec — tarcza ma
269
+ // wtedy wrócić do etapu godzin, tak jak dwie osobne instancje w porcie Angulara.
270
+ , { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-calendar__panels", children: panels.map((panel, index) => (_jsxs("div", { className: "kpt-calendar__panel", children: [_jsxs("div", { className: "kpt-calendar__header", children: [index === 0 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => turnPage(-1), "aria-label": t('datepicker.prevMonth'), children: _jsx(KptIcon, { name: "chevron-left" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" })), _jsx("span", { className: "kpt-calendar__title", children: panel.label }), index === panels.length - 1 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => turnPage(1), "aria-label": t('datepicker.nextMonth'), children: _jsx(KptIcon, { name: "chevron-right" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" }))] }), _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: panel.weeks.flat().map((cell) => cell.blank ? (_jsx("span", { className: "kpt-calendar__day kpt-calendar__day--blank" }, cell.iso)) : (_jsx("button", { type: "button", className: cn('kpt-calendar__day', !cell.inMonth && 'kpt-calendar__day--muted', cell.today && 'kpt-calendar__day--today', cell.inRange && 'kpt-calendar__day--range', cell.rangeStart && 'kpt-calendar__day--range-start', cell.rangeEnd && 'kpt-calendar__day--range-end', (cell.isStart || cell.isEnd) && 'kpt-calendar__day--selected'), "data-iso": cell.iso, tabIndex: cell.iso === focusIso ? 0 : -1, "aria-label": dayLabel(cell), "aria-current": cell.today ? 'date' : undefined, "aria-pressed": cell.isStart || cell.isEnd, disabled: cell.disabled, onClick: () => selectDay(cell.iso), children: cell.day }, cell.iso))) })] }, panel.key))) }), mode === 'datetime' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.fromTime') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.toTime') })] }), _jsx(KptClock, { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : null] }))] }) })] }));
155
271
  }
@@ -20,7 +20,10 @@ Import: `import { KptDateRange, type KptDateRangeValue } from '@konce-pt/react';
20
20
  - `hourFormat`: 12 | 24 (default 24); `minuteStep`: number (default 1)
21
21
  - `placeholder` — without one it falls back to `dateRange.placeholder`
22
22
  - `displayFormat`: `'pl'` (default) | `'iso'`
23
- - `disabled`, `invalid`, `touched`, `onTouch()`
23
+ - `minDate`, `maxDate`: 'YYYY-MM-DD' bounds (inclusive); days outside are disabled
24
+ - `ariaLabel` — the accessible name when no `<label for>` describes the field
25
+ - `disabled`, `invalid`, `touched`
26
+ - `onTouch()` — fires when the panel closes or focus leaves the closed trigger
24
27
  - `className`, `ref` and every other `<div>` prop go to the host
25
28
 
26
29
  ## Two clicks, one band
@@ -31,6 +34,18 @@ The panel stays open until the range closes — the second click needs somewhere
31
34
  With several panels the adjacent-month days become blank slots: otherwise the same date would be
32
35
  clickable twice, and the band would paint in both places.
33
36
 
37
+ ## Keyboard
38
+ As in the date picker: the trigger has `aria-haspopup="dialog"` and `aria-expanded`; `Enter`, `Space`
39
+ and `↓` open the panel (a `role="dialog"` named "Choose a date range") with focus on the range start,
40
+ or today. The arrows, `Home`/`End` and `PageUp`/`PageDown` (with `Shift` — a year) walk the days across
41
+ every panel, shifting the set of months when focus leaves it. `Tab` cycles inside the panel, `Escape`
42
+ closes it, and picking the end (or "Done") returns focus to the trigger. `Delete`/`Backspace` on the
43
+ trigger clears the range.
44
+
45
+ Each day is named with the full date and its role in the range ("…, start of range", "…, end of range",
46
+ "…, in range") — the band colour is not the only carrier. The ends have `aria-pressed`, today has
47
+ `aria-current="date"`. After the first pick a polite live region says "Start: … Choose the end date.".
48
+
34
49
  ## The clock remounts when the end switches
35
50
  `key={timeEnd}` on `KptClock`, so the dial returns to the hours stage after the From/To toggle —
36
51
  in Angular that came for free from two separate template branches.
@@ -45,10 +60,18 @@ in Angular that came for free from two separate template branches.
45
60
  Day and month names come from `Intl`; "From/To", the navigation, "Done" and the placeholder come
46
61
  from the `dateRange`, `datepicker` and `common` namespaces.
47
62
 
63
+ ## Label and description
64
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
65
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
66
+ `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
67
+ is added to `aria-describedby`. New: `id`; the trigger is the element that carries it.
68
+
48
69
  ## Tokens
49
70
  The field `--kpt-form-field-*`; the calendar `--kpt-color-surface-raised`, the range
50
71
  `--kpt-color-primary`.
51
72
 
52
73
  ## Accessibility
53
- The trigger is `role="button"` with Enter support; the days are real buttons, and the time ends are
54
- switched with a two-button toggle rather than a custom control.
74
+ The trigger is `role="button"` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`;
75
+ the panel is a named `role="dialog"`; the days are real buttons named with the date and their range
76
+ role, and the time ends are switched with a two-button toggle rather than a custom control. The
77
+ keyboard is described above.