@konce-pt/react 0.9.1 → 0.11.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 (200) 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/overlay.d.ts +7 -2
  96. package/dist/internal/overlay.js +4 -2
  97. package/dist/internal/roving.d.ts +9 -0
  98. package/dist/internal/roving.js +43 -0
  99. package/dist/internal/tween.d.ts +17 -0
  100. package/dist/internal/tween.js +40 -0
  101. package/dist/internal/use-modal-focus.d.ts +10 -0
  102. package/dist/internal/use-modal-focus.js +57 -0
  103. package/dist/knob/knob.d.ts +9 -1
  104. package/dist/knob/knob.js +41 -5
  105. package/dist/knob/llms.txt +15 -0
  106. package/dist/listbox/listbox.d.ts +13 -1
  107. package/dist/listbox/listbox.js +89 -6
  108. package/dist/listbox/llms.txt +21 -5
  109. package/dist/megamenu/llms.txt +28 -10
  110. package/dist/megamenu/megamenu.d.ts +10 -1
  111. package/dist/megamenu/megamenu.js +116 -5
  112. package/dist/menu/llms.txt +12 -1
  113. package/dist/menu/menu-keys.d.ts +15 -0
  114. package/dist/menu/menu-keys.js +53 -0
  115. package/dist/menu/menu-rows.js +1 -1
  116. package/dist/menu/menu.js +52 -4
  117. package/dist/menubar/llms.txt +24 -7
  118. package/dist/menubar/menubar.d.ts +8 -1
  119. package/dist/menubar/menubar.js +92 -6
  120. package/dist/meter-group/llms.txt +10 -3
  121. package/dist/meter-group/meter-group.d.ts +9 -2
  122. package/dist/meter-group/meter-group.js +8 -2
  123. package/dist/order-list/llms.txt +11 -3
  124. package/dist/order-list/order-list.d.ts +5 -1
  125. package/dist/order-list/order-list.js +46 -8
  126. package/dist/panel/llms.txt +9 -6
  127. package/dist/panel/panel.js +3 -1
  128. package/dist/password/llms.txt +8 -0
  129. package/dist/password/password.d.ts +6 -1
  130. package/dist/password/password.js +6 -5
  131. package/dist/pick-list/llms.txt +12 -3
  132. package/dist/pick-list/pick-list.d.ts +5 -1
  133. package/dist/pick-list/pick-list.js +52 -5
  134. package/dist/popover/llms.txt +9 -2
  135. package/dist/popover/popover.d.ts +9 -3
  136. package/dist/popover/popover.js +41 -6
  137. package/dist/public-api.d.ts +6 -6
  138. package/dist/public-api.js +1 -1
  139. package/dist/radio-group/llms.txt +11 -0
  140. package/dist/radio-group/radio-group.d.ts +9 -1
  141. package/dist/radio-group/radio-group.js +5 -2
  142. package/dist/rating/llms.txt +18 -5
  143. package/dist/rating/rating.d.ts +21 -3
  144. package/dist/rating/rating.js +87 -12
  145. package/dist/rich-text/llms.txt +20 -0
  146. package/dist/rich-text/rich-text.d.ts +7 -1
  147. package/dist/rich-text/rich-text.js +155 -28
  148. package/dist/scroll-top/llms.txt +5 -1
  149. package/dist/scroll-top/scroll-top.js +3 -1
  150. package/dist/select/llms.txt +21 -7
  151. package/dist/select/select.d.ts +12 -1
  152. package/dist/select/select.js +110 -38
  153. package/dist/sidenav/llms.txt +6 -0
  154. package/dist/skeleton/llms.txt +3 -2
  155. package/dist/skeleton/skeleton.d.ts +1 -1
  156. package/dist/skeleton/skeleton.js +2 -2
  157. package/dist/slider/llms.txt +14 -1
  158. package/dist/slider/slider.d.ts +11 -2
  159. package/dist/slider/slider.js +15 -2
  160. package/dist/speed-dial/llms.txt +6 -2
  161. package/dist/speed-dial/speed-dial.d.ts +1 -1
  162. package/dist/speed-dial/speed-dial.js +14 -3
  163. package/dist/spinner/llms.txt +12 -8
  164. package/dist/spinner/spinner.d.ts +5 -1
  165. package/dist/spinner/spinner.js +12 -2
  166. package/dist/split-button/llms.txt +11 -3
  167. package/dist/split-button/split-button.d.ts +3 -1
  168. package/dist/split-button/split-button.js +43 -3
  169. package/dist/splitter/llms.txt +14 -4
  170. package/dist/splitter/splitter.d.ts +9 -2
  171. package/dist/splitter/splitter.js +15 -2
  172. package/dist/stepper/llms.txt +8 -4
  173. package/dist/stepper/stepper.d.ts +3 -2
  174. package/dist/stepper/stepper.js +7 -2
  175. package/dist/switch/llms.txt +10 -0
  176. package/dist/switch/switch.d.ts +1 -1
  177. package/dist/switch/switch.js +4 -2
  178. package/dist/switch-group/llms.txt +11 -0
  179. package/dist/switch-group/switch-group.d.ts +5 -1
  180. package/dist/switch-group/switch-group.js +7 -2
  181. package/dist/tabs/llms.txt +10 -4
  182. package/dist/tabs/tabs.d.ts +4 -0
  183. package/dist/tabs/tabs.js +21 -2
  184. package/dist/textarea/llms.txt +10 -0
  185. package/dist/textarea/textarea.d.ts +3 -1
  186. package/dist/textarea/textarea.js +13 -8
  187. package/dist/toast/llms.txt +21 -7
  188. package/dist/toast/toast-container.d.ts +6 -1
  189. package/dist/toast/toast-container.js +52 -1
  190. package/dist/toast/toast-store.d.ts +12 -0
  191. package/dist/toast/toast-store.js +46 -7
  192. package/dist/tooltip/llms.txt +10 -3
  193. package/dist/tooltip/tooltip.d.ts +5 -0
  194. package/dist/tooltip/tooltip.js +80 -7
  195. package/dist/tree/llms.txt +20 -8
  196. package/dist/tree/tree-keys.d.ts +27 -0
  197. package/dist/tree/tree-keys.js +39 -0
  198. package/dist/tree/tree.d.ts +12 -5
  199. package/dist/tree/tree.js +75 -29
  200. package/package.json +12 -12
@@ -11,6 +11,9 @@ import { KptIcon } from "../icon/icon.js";
11
11
  import { sanitizeHtml, sanitizeUrl } from "./sanitize.js";
12
12
  import { canIndentItem, canOutdentItem, selectedItems, shiftItems } from "./lists.js";
13
13
  import { CELL_SELECTED, deleteColumn, deleteRow, insertColumn, insertRow, mergeCells, regionBetween, splitCell, tableHtml, toggleHeaderColumn, toggleHeaderRow, } from "./tables.js";
14
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
15
+ import { nextRovingIndex } from "../internal/roving.js";
16
+ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
14
17
  const ICON = {
15
18
  copy: 'copy',
16
19
  paste: 'clipboard',
@@ -49,9 +52,22 @@ const COMMAND = {
49
52
  alignJustify: 'justifyFull',
50
53
  clear: 'removeFormat',
51
54
  };
55
+ /** Paleta kolorów z nazwami (`richText.swatches.*`) — sam kod szesnastkowy nic czytnikowi nie mówi. */
52
56
  const SWATCHES = [
53
- '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#ffffff',
54
- '#e11d48', '#ea580c', '#ca8a04', '#16a34a', '#0891b2', '#2563eb', '#7c3aed',
57
+ { color: '#000000', name: 'black' },
58
+ { color: '#434343', name: 'darkGray' },
59
+ { color: '#666666', name: 'gray' },
60
+ { color: '#999999', name: 'midGray' },
61
+ { color: '#b7b7b7', name: 'silver' },
62
+ { color: '#cccccc', name: 'lightGray' },
63
+ { color: '#ffffff', name: 'white' },
64
+ { color: '#e11d48', name: 'red' },
65
+ { color: '#ea580c', name: 'orange' },
66
+ { color: '#ca8a04', name: 'yellow' },
67
+ { color: '#16a34a', name: 'green' },
68
+ { color: '#0891b2', name: 'cyan' },
69
+ { color: '#2563eb', name: 'blue' },
70
+ { color: '#7c3aed', name: 'violet' },
55
71
  ];
56
72
  const DEFAULT_TOOLBAR = [
57
73
  ['copy', 'paste', 'pasteText'],
@@ -86,7 +102,9 @@ function legacyState(cmd) {
86
102
  * <KptRichText value={html} onValueChange={setHtml} />
87
103
  * <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
88
104
  */
89
- export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, disabled = false, invalid = false, touched, onTouch, mentions = [], mentionLimit = KPT_MENTION_LIMIT, onMention, locale, dictionary, className, ...rest }) {
105
+ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, mentions = [], mentionLimit = KPT_MENTION_LIMIT, onMention, locale, dictionary, className, ...rest }) {
106
+ /** `contenteditable` nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
107
+ const field = useKptFieldA11y(id, 'kpt-rich-text');
90
108
  const { t } = useKptMessages({ locale, dictionary });
91
109
  const container = useKptOverlayContainer();
92
110
  const editorRef = useRef(null);
@@ -98,7 +116,15 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
98
116
  const [foreAnchor, setForeAnchor] = useState(null);
99
117
  const [backAnchor, setBackAnchor] = useState(null);
100
118
  const [tableAnchor, setTableAnchor] = useState(null);
119
+ const [linkAnchor, setLinkAnchor] = useState(null);
120
+ /** Okienko adresu linku (zamiast `window.prompt()`) i wpisywany adres. */
121
+ const [linkOpen, setLinkOpen] = useState(false);
122
+ const [linkDraft, setLinkDraft] = useState('');
123
+ /** Menu tabeli otwarte z klawiatury — po zamontowaniu fokus idzie na pierwszą pozycję. */
124
+ const menuFromKeyboard = useRef(false);
101
125
  const [styleOpen, setStyleOpen] = useState(false);
126
+ /** Narzędzie, na którym ostatnio stał fokus paska — przystanek Tab przy powrocie. */
127
+ const [lastTool, setLastTool] = useState(null);
102
128
  const [colorOpen, setColorOpen] = useState(false);
103
129
  const [colorKind, setColorKind] = useState('foreColor');
104
130
  const [tableOpen, setTableOpen] = useState(false);
@@ -408,15 +434,9 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
408
434
  }
409
435
  focusEditor();
410
436
  switch (item) {
411
- case 'link': {
412
- const url = window.prompt(t('richText.linkPrompt'));
413
- if (url) {
414
- const safe = sanitizeUrl(url);
415
- if (safe)
416
- legacyExec('createLink', safe);
417
- }
418
- break;
419
- }
437
+ case 'link':
438
+ openLink();
439
+ return;
420
440
  case 'copy':
421
441
  void copySelection();
422
442
  return;
@@ -482,6 +502,10 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
482
502
  // co innego niż w samym edytorze.
483
503
  if (mentionOpen && onMentionKeyDown(event))
484
504
  return;
505
+ if ((event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) && openMenuFromKeyboard()) {
506
+ event.preventDefault();
507
+ return;
508
+ }
485
509
  if (event.key !== 'Tab' || event.ctrlKey || event.altKey || event.metaKey)
486
510
  return;
487
511
  if (shiftItems(editorRef.current, event.shiftKey ? 'outdent' : 'indent', maxListLevel)) {
@@ -556,9 +580,67 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
556
580
  pushValue();
557
581
  };
558
582
  const afterTableOp = () => {
583
+ const fromMenu = !!document.activeElement?.closest('.kpt-rt-menu');
559
584
  setMenuOpen(false);
585
+ // Pozycja wybrana z klawiatury znika razem z menu — fokus wraca do edytora, nie na <body>.
586
+ if (fromMenu)
587
+ focusEditor();
560
588
  pushValue();
561
589
  };
590
+ /** Zamknięcie menu z powrotem fokusu (i kursora) do komórki, z której je otwarto. */
591
+ const closeMenuToEditor = () => {
592
+ setMenuOpen(false);
593
+ focusEditor();
594
+ restoreSelection();
595
+ };
596
+ /** Klawiatura menu kontekstowego: strzałki, `Home`/`End`; `Escape`/`Tab` zamykają i wracają do tekstu. */
597
+ const onMenuKeyDown = (event) => {
598
+ if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close')
599
+ closeMenuToEditor();
600
+ };
601
+ /** Menu tabeli z klawiatury: klawisz menu albo `Shift+F10` w komórce — jak natywne menu kontekstowe. */
602
+ const openMenuFromKeyboard = () => {
603
+ const node = window.getSelection()?.anchorNode ?? null;
604
+ const el = node instanceof Element ? node : (node?.parentElement ?? null);
605
+ const cell = el?.closest('td, th');
606
+ if (!cell || !editorRef.current?.contains(cell))
607
+ return false;
608
+ const rect = cell.getBoundingClientRect();
609
+ saveSelection();
610
+ ctxCell.current = cell;
611
+ canSplitCtx.current = (cell.colSpan || 1) > 1 || (cell.rowSpan || 1) > 1;
612
+ setMenuPoint({ x: rect.left, y: rect.bottom });
613
+ menuFromKeyboard.current = true;
614
+ setMenuOpen(true);
615
+ return true;
616
+ };
617
+ /** Okienko adresu zamiast `window.prompt()` — nazwane pole, `Enter` wstawia, `Escape` wraca do tekstu. */
618
+ const openLink = () => {
619
+ if (disabled)
620
+ return;
621
+ saveSelection();
622
+ const node = savedRange.current?.commonAncestorContainer ?? null;
623
+ const el = node instanceof Element ? node : (node?.parentElement ?? null);
624
+ setLinkDraft(el?.closest('a')?.getAttribute('href') ?? '');
625
+ setLinkOpen(true);
626
+ };
627
+ const applyLink = () => {
628
+ const safe = sanitizeUrl(linkDraft.trim());
629
+ setLinkOpen(false);
630
+ focusEditor();
631
+ restoreSelection();
632
+ if (safe) {
633
+ legacyExec('createLink', safe);
634
+ pushValue();
635
+ }
636
+ };
637
+ const closeLink = (refocus) => {
638
+ setLinkOpen(false);
639
+ if (refocus) {
640
+ focusEditor();
641
+ restoreSelection();
642
+ }
643
+ };
562
644
  const onContextMenu = (event) => {
563
645
  if (disabled)
564
646
  return;
@@ -566,6 +648,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
566
648
  if (!cell || !editorRef.current?.contains(cell))
567
649
  return; // poza tabelą zostaje natywne menu
568
650
  event.preventDefault();
651
+ saveSelection();
569
652
  ctxCell.current = cell;
570
653
  canSplitCtx.current = (cell.colSpan || 1) > 1 || (cell.rowSpan || 1) > 1;
571
654
  setMenuPoint({ x: event.clientX, y: event.clientY });
@@ -655,7 +738,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
655
738
  return canOutdent;
656
739
  return true;
657
740
  };
658
- const palette = (onPick, customLabel) => (_jsxs("div", { className: "kpt-rich-text__palette", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((color) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: color }, "aria-label": color, onClick: () => onPick(color) }, color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: customLabel }), _jsx("input", { type: "color", onInput: (event) => onPick(event.target.value) })] })] }));
741
+ const palette = (onPick, customLabel) => (_jsxs("div", { className: "kpt-rich-text__palette", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: swatch.color }, "aria-label": t(`richText.swatches.${swatch.name}`), onClick: () => onPick(swatch.color) }, swatch.color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: customLabel }), _jsx("input", { type: "color", onInput: (event) => onPick(event.target.value) })] })] }));
659
742
  /**
660
743
  * Panel otwarty przy dolnej albo prawej krawędzi okna wychodziłby poza ekran — CDK robił to
661
744
  * w Angularze sam, tutaj przesuwamy go po zamontowaniu, gdy już znać jego rozmiar.
@@ -683,11 +766,35 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
683
766
  onClose();
684
767
  } }), _jsx("div", { ref: clampToViewport, style: { position: 'fixed', left: menuPoint.x, top: menuPoint.y, ...layer }, children: content })] }), container);
685
768
  };
686
- const toolButton = (item) => (_jsx("button", { type: "button", className: cn('kpt-rich-text__tool', isActive(item) && 'is-active'), "aria-label": t(`richText.${item}`), "aria-pressed": pressed(item), title: t(`richText.${item}`), disabled: disabled || !enabled(item), onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
687
- const menuItem = (icon, label, onClick, extra) => (_jsxs("button", { type: "button", className: cn('kpt-rt-menu__item', extra), onClick: onClick, children: [_jsx(KptIcon, { name: icon }), " ", label] }));
688
- return (_jsxs("div", { className: cn('kpt-rich-text-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-rich-text', showInvalid && 'kpt-rich-text--invalid', disabled && 'kpt-rich-text--disabled'), children: [_jsx("div", { className: "kpt-rich-text__toolbar", role: "toolbar", children: sections.map((section, index) => (_jsxs("span", { style: { display: 'contents' }, children: [index > 0 ? _jsx("span", { className: "kpt-rich-text__divider", "aria-hidden": "true" }) : null, section.map((item) => {
769
+ /**
770
+ * Przystanek Tab paska: ostatnio użyte narzędzie, a gdy jest wyłączone albo go nie ma —
771
+ * pierwsze dostępne. Dzięki temu `Tab` z paska idzie od razu do edytora, a nie przez kilkanaście
772
+ * przycisków.
773
+ */
774
+ const usableTools = sections.flat().filter((item) => !disabled && enabled(item));
775
+ const toolStop = lastTool && usableTools.includes(lastTool) ? lastTool : (usableTools[0] ?? null);
776
+ const rovingProps = (item) => ({ 'data-tool': item, tabIndex: item === toolStop ? 0 : -1 });
777
+ /** Strzałki po pasku (w RTL odwrócone), `Home`/`End` — skrajne; wyłączone narzędzia są pomijane. */
778
+ const onToolbarKeyDown = (event) => {
779
+ const bar = event.currentTarget;
780
+ const buttons = [...bar.querySelectorAll('[data-tool]')];
781
+ const current = buttons.indexOf(event.target);
782
+ if (current < 0)
783
+ return;
784
+ const rtl = getComputedStyle(bar).direction === 'rtl';
785
+ const key = rtl && event.key === 'ArrowLeft' ? 'ArrowRight' : rtl && event.key === 'ArrowRight' ? 'ArrowLeft' : event.key;
786
+ const next = nextRovingIndex(buttons.map((button) => button.disabled), current, key);
787
+ if (next === null)
788
+ return;
789
+ event.preventDefault();
790
+ setLastTool(buttons[next].dataset['tool']);
791
+ buttons[next].focus();
792
+ };
793
+ const toolButton = (item) => (_jsx("button", { ...rovingProps(item), ref: item === 'link' ? setLinkAnchor : undefined, "aria-haspopup": item === 'link' ? 'dialog' : undefined, "aria-expanded": item === 'link' ? linkOpen : undefined, type: "button", className: cn('kpt-rich-text__tool', isActive(item) && 'is-active'), "aria-label": t(`richText.${item}`), "aria-pressed": pressed(item), title: t(`richText.${item}`), disabled: disabled || !enabled(item), onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
794
+ const menuItem = (icon, label, onClick, extra) => (_jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, className: cn('kpt-rt-menu__item', extra), "aria-disabled": extra === 'is-disabled' || undefined, onClick: onClick, children: [_jsx(KptIcon, { name: icon }), " ", label] }));
795
+ return (_jsxs("div", { className: cn('kpt-rich-text-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-rich-text', showInvalid && 'kpt-rich-text--invalid', disabled && 'kpt-rich-text--disabled'), children: [_jsx("div", { className: "kpt-rich-text__toolbar", role: "toolbar", "aria-label": t('richText.toolbar'), "aria-controls": field.id, onKeyDown: onToolbarKeyDown, children: sections.map((section, index) => (_jsxs("span", { style: { display: 'contents' }, children: [index > 0 ? _jsx("span", { className: "kpt-rich-text__divider", "aria-hidden": "true" }) : null, section.map((item) => {
689
796
  if (item === 'heading') {
690
- return (_jsxs("button", { ref: setStyleAnchor, type: "button", className: cn('kpt-rich-text__style', styleOpen && 'is-open'), disabled: disabled, "aria-label": t('richText.heading'), "aria-expanded": styleOpen, title: t('richText.heading'), onMouseDown: (event) => event.preventDefault(), onClick: () => {
797
+ return (_jsxs("button", { ref: setStyleAnchor, ...rovingProps(item), type: "button", className: cn('kpt-rich-text__style', styleOpen && 'is-open'), disabled: disabled, "aria-label": t('richText.heading'), "aria-expanded": styleOpen, title: t('richText.heading'), onMouseDown: (event) => event.preventDefault(), onClick: () => {
691
798
  if (disabled)
692
799
  return;
693
800
  saveSelection();
@@ -695,7 +802,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
695
802
  }, children: [_jsx("span", { children: styleLabel }), _jsx(KptIcon, { name: "chevron-down", className: "kpt-rich-text__style-caret" })] }, item));
696
803
  }
697
804
  if (item === 'foreColor' || item === 'backColor') {
698
- return (_jsx("button", { ref: item === 'foreColor' ? setForeAnchor : setBackAnchor, type: "button", className: "kpt-rich-text__tool kpt-rich-text__tool--color", "aria-label": t(`richText.${item}`), title: t(`richText.${item}`), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
805
+ return (_jsx("button", { ref: item === 'foreColor' ? setForeAnchor : setBackAnchor, ...rovingProps(item), type: "button", className: "kpt-rich-text__tool kpt-rich-text__tool--color", "aria-label": t(`richText.${item}`), title: t(`richText.${item}`), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
699
806
  if (disabled)
700
807
  return;
701
808
  saveSelection();
@@ -704,10 +811,10 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
704
811
  }, children: _jsx(KptIcon, { name: ICON[item] }) }, item));
705
812
  }
706
813
  if (item === 'table') {
707
- return (_jsx("button", { ref: setTableAnchor, type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
814
+ return (_jsx("button", { ref: setTableAnchor, ...rovingProps(item), type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
708
815
  }
709
816
  return toolButton(item);
710
- })] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", onInput: () => {
817
+ })] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", id: field.id, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onInput: () => {
711
818
  pushValue();
712
819
  syncMention();
713
820
  }, onKeyDown: onKeyDown, onPaste: onPaste, onContextMenu: onContextMenu, onMouseDown: onEditorMouseDown, onMouseMove: onEditorMouseMove, onBlur: () => onTouch?.() }))] }), mentionOpen && container
@@ -722,7 +829,22 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
722
829
  event.preventDefault();
723
830
  insertMention(item);
724
831
  }, onMouseEnter: () => setMentionActive(index), children: [_jsx("span", { className: "kpt-rt-mentions__label", children: item.label }), item.kind ? _jsx("span", { className: "kpt-rt-mentions__kind", children: item.kind }) : null] }, item.id))) }), container)
725
- : null, _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
832
+ : null, _jsx(KptOverlay, { open: linkOpen, anchor: linkAnchor, onClose: () => closeLink(false), offset: 4, children: _jsxs("form", { className: "kpt-rt-link", role: "dialog",
833
+ // Bez walidacji przeglądarki: „example.com” jest poprawnym wpisem — `sanitizeUrl` dopisze https://.
834
+ noValidate: true, "aria-label": t('richText.link'), onSubmit: (event) => {
835
+ event.preventDefault();
836
+ applyLink();
837
+ }, onKeyDown: (event) => {
838
+ if (event.key !== 'Escape')
839
+ return;
840
+ event.preventDefault();
841
+ event.stopPropagation();
842
+ closeLink(true);
843
+ }, children: [_jsx("label", { className: "kpt-rt-link__label", htmlFor: `${field.id}-link`, children: t('richText.linkPrompt') }), _jsx("input", { ref: (el) => {
844
+ // Panel jest niewidoczny do pierwszego pomiaru — fokus po klatce.
845
+ if (el)
846
+ requestAnimationFrame(() => el.focus());
847
+ }, className: "kpt-rt-link__input", id: `${field.id}-link`, type: "url", inputMode: "url", placeholder: "https://", value: linkDraft, onChange: (event) => setLinkDraft(event.target.value) }), _jsx("button", { type: "submit", className: "kpt-rt-link__apply", children: t('richText.linkApply') })] }) }), _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
726
848
  setStyleOpen(false);
727
849
  focusEditor();
728
850
  restoreSelection();
@@ -733,7 +855,12 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
733
855
  setTableOpen(false);
734
856
  setGrid({ r: 0, c: 0 });
735
857
  }, offset: 4, children: _jsxs("div", { className: "kpt-rt-grid-pop", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rt-grid", onMouseLeave: () => setGrid({ r: 0, c: 0 }), children: GRID_ROWS.map((r) => (_jsx("div", { className: "kpt-rt-grid__row", children: GRID_COLS.map((c) => (_jsx("span", { className: cn('kpt-rt-grid__cell', r <= grid.r && c <= grid.c && 'is-on'), onMouseEnter: () => setGrid({ r, c }), onClick: () => insertTableAt(r, c) }, c))) }, r))) }), _jsxs("div", { className: "kpt-rt-grid__label", children: [grid.r, " \u00D7 ", grid.c] })] }) }), menuOpen
736
- ? pointPanel(_jsxs("div", { className: "kpt-rt-menu", onContextMenu: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.column') }), menuItem('plus', t('richText.menu.insertLeft'), () => {
858
+ ? pointPanel(_jsxs("div", { ref: (el) => {
859
+ if (el && menuFromKeyboard.current) {
860
+ menuFromKeyboard.current = false;
861
+ requestAnimationFrame(() => focusMenuEdge(el, 'first'));
862
+ }
863
+ }, className: "kpt-rt-menu", role: "menu", "aria-label": t('richText.table'), onContextMenu: (event) => event.preventDefault(), onKeyDown: onMenuKeyDown, children: [_jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.column') }), menuItem('plus', t('richText.menu.insertLeft'), () => {
737
864
  if (ctxCell.current)
738
865
  insertColumn(ctxCell.current, 'left');
739
866
  afterTableOp();
@@ -750,7 +877,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
750
877
  if (ctxCell.current)
751
878
  deleteColumn(ctxCell.current);
752
879
  afterTableOp();
753
- }), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.row') }), menuItem('plus', t('richText.menu.insertAbove'), () => {
880
+ }), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), _jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.row') }), menuItem('plus', t('richText.menu.insertAbove'), () => {
754
881
  if (ctxCell.current)
755
882
  insertRow(ctxCell.current, 'above');
756
883
  afterTableOp();
@@ -767,7 +894,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
767
894
  if (ctxCell.current)
768
895
  deleteRow(ctxCell.current);
769
896
  afterTableOp();
770
- }), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.cell') }), menuItem('palette', t('richText.menu.cellBackground'), () => {
897
+ }), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), _jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.cell') }), menuItem('palette', t('richText.menu.cellBackground'), () => {
771
898
  setMenuOpen(false);
772
899
  setColorKind('cellBg');
773
900
  setColorOpen(true);
@@ -783,7 +910,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
783
910
  for (const cell of activeCells())
784
911
  cell.style.textAlign = 'right';
785
912
  afterTableOp();
786
- }), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.mergeCells'), () => {
913
+ }), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('table', t('richText.menu.mergeCells'), () => {
787
914
  mergeCells([...selCells.current]);
788
915
  clearCellSelection();
789
916
  afterTableOp();
@@ -791,15 +918,15 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
791
918
  if (ctxCell.current)
792
919
  splitCell(ctxCell.current);
793
920
  afterTableOp();
794
- }, canSplitCtx.current ? undefined : 'is-disabled'), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.border'), () => {
921
+ }, canSplitCtx.current ? undefined : 'is-disabled'), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('table', t('richText.menu.border'), () => {
795
922
  setMenuOpen(false);
796
923
  setBorderOpen(true);
797
- }), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('trash', t('richText.menu.deleteTable'), () => {
924
+ }), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('trash', t('richText.menu.deleteTable'), () => {
798
925
  ctxCell.current?.closest('table')?.remove();
799
926
  afterTableOp();
800
927
  }, 'kpt-rt-menu__item--danger')] }), () => setMenuOpen(false))
801
928
  : null, borderOpen
802
- ? pointPanel(_jsxs("div", { className: "kpt-rt-border", onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "kpt-rt-border__seg", children: [_jsx("button", { type: "button", className: cn(borderTarget === 'table' && 'is-active'), onClick: () => setBorderTarget('table'), children: t('richText.border.table') }), _jsx("button", { type: "button", className: cn(borderTarget === 'cell' && 'is-active'), onClick: () => setBorderTarget('cell'), children: t('richText.border.cell') })] }), _jsxs("div", { className: "kpt-rt-border__widths", children: [_jsx("button", { type: "button", onClick: () => setBorderWidth('none'), children: t('richText.border.none') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thin'), children: t('richText.border.thin') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('medium'), children: t('richText.border.medium') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thick'), children: t('richText.border.thick') })] }), _jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((color) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: color }, "aria-label": color, onClick: () => setBorderColor(color) }, color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: t('richText.border.customColor') }), _jsx("input", { type: "color", onInput: (event) => setBorderColor(event.target.value) })] })] }), () => setBorderOpen(false))
929
+ ? pointPanel(_jsxs("div", { className: "kpt-rt-border", onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "kpt-rt-border__seg", children: [_jsx("button", { type: "button", className: cn(borderTarget === 'table' && 'is-active'), onClick: () => setBorderTarget('table'), children: t('richText.border.table') }), _jsx("button", { type: "button", className: cn(borderTarget === 'cell' && 'is-active'), onClick: () => setBorderTarget('cell'), children: t('richText.border.cell') })] }), _jsxs("div", { className: "kpt-rt-border__widths", children: [_jsx("button", { type: "button", onClick: () => setBorderWidth('none'), children: t('richText.border.none') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thin'), children: t('richText.border.thin') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('medium'), children: t('richText.border.medium') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thick'), children: t('richText.border.thick') })] }), _jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: swatch.color }, "aria-label": t(`richText.swatches.${swatch.name}`), onClick: () => setBorderColor(swatch.color) }, swatch.color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: t('richText.border.customColor') }), _jsx("input", { type: "color", onInput: (event) => setBorderColor(event.target.value) })] })] }), () => setBorderOpen(false))
803
930
  : null, colorOpen && colorKind === 'cellBg'
804
931
  ? pointPanel(palette(applyColor, t('richText.paletteCustom')), () => setColorOpen(false))
805
932
  : null] }));
@@ -38,4 +38,8 @@ The label comes from `scrollTop.label`.
38
38
  `--kpt-color-primary` / `--kpt-color-on-primary`, `--kpt-elevation-3`.
39
39
 
40
40
  ## Accessibility
41
- A real `<button>` with a label from the dictionary; while hidden it is out of the tab order.
41
+ A real `<button>` labelled from `scrollTop.label`; while hidden it is `visibility: hidden`, so it is
42
+ out of the tab order and the accessibility tree. It sits in the bottom-end corner
43
+ (`inset-inline-end`), so it moves to the left in RTL. With `prefers-reduced-motion` the scroll is a
44
+ jump instead of smooth and the button appears without a transition. Focus stays on the button after
45
+ the jump — move it to your page heading if that matters.
@@ -29,7 +29,9 @@ export function KptScrollTop({ threshold = 300, target = null, locale, dictionar
29
29
  const scroller = target
30
30
  ? (document.querySelector(target) ?? window)
31
31
  : window;
32
- scroller.scrollTo({ top: 0, behavior: 'smooth' });
32
+ // Przy `prefers-reduced-motion` skok zamiast płynnego przewijania.
33
+ const behavior = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
34
+ scroller.scrollTo({ top: 0, behavior });
33
35
  };
34
36
  return (_jsx("div", { className: cn('kpt-scroll-top-host', className), ...rest, children: _jsx("button", { type: "button", className: cn('kpt-scroll-top', visible && 'is-visible'),
35
37
  // Ukryty przycisk zostaje w DOM (animacja pojawiania), więc z kolejności Tab wyjmuje go
@@ -18,7 +18,8 @@ A select on its own panel. Variants: single, with a search box (`filter`) and mu
18
18
  - `value` / `defaultValue` / `onValueChange` — a single `T`, or `T[]` in `multiple` mode
19
19
  - `open` / `defaultOpen` / `onOpenChange` — the panel state, so the list can be opened from outside
20
20
  - `placeholder`, `multiple`, `filter`, `disabled`, `invalid`, `touched`
21
- - `onTouch()` — fires when the panel closes or the trigger loses focus
21
+ - `onTouch()` — fires when the panel closes or focus leaves the closed trigger
22
+ - `aria-label` (through the rest props) names the trigger when no `<label for>` describes it
22
23
  - `locale`, `dictionary` — i18n overrides for this component only
23
24
  - `className`, `ref` and every other `<div>` prop go to the trigger
24
25
 
@@ -29,9 +30,14 @@ flips above when there is no room, a transparent backdrop that closes on click,
29
30
  follows scrolling and resizing, so the panel never drifts away from its trigger.
30
31
 
31
32
  ## Keyboard
32
- On the trigger: ArrowDown, Enter or Space open the list; Escape closes it. In the panel: ArrowUp and
33
- ArrowDown move the active option, Enter picks it, Escape closes. Hovering an option makes it active,
34
- so the mouse and the keyboard share one highlight.
33
+ The select-only combobox pattern: focus stays on the trigger and the active option is announced
34
+ through `aria-activedescendant` (with `filter`, focus moves to the search field). Closed: ArrowDown,
35
+ ArrowUp, Enter, Space, Home and End open the list, typing letters opens it on the first match, and
36
+ in `multiple` mode Backspace/Delete removes the last chip. Open: ArrowUp/ArrowDown move by one,
37
+ Home/End to the ends, PageUp/PageDown by ten, skipping disabled options without wrapping; Enter (and
38
+ Space on the trigger) picks — a single select closes and returns focus to the trigger; typing jumps
39
+ to the next match, diacritics ignored; Escape and Tab close. Hovering an option makes it active, so
40
+ the mouse and the keyboard share one highlight, and clicking it does not take focus from the trigger.
35
41
 
36
42
  ## Inside KptFormField
37
43
  There is no native field for the wrapper to read, so the select reports its filled state with
@@ -46,11 +52,19 @@ There is no native field for the wrapper to read, so the select reports its fill
46
52
  <KptSelect options={roles} value={role} onValueChange={setRole} filter />
47
53
  <KptSelect options={tags} value={tags} onValueChange={setTags} multiple filter />
48
54
 
55
+ ## Label and description
56
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
57
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
58
+ `aria-labelledby` pointing at the wrapper's label (unless `aria-label` is set), and the hint or
59
+ error is added to `aria-describedby`. New: `id` and `required` (→ `aria-required`); the list gets
60
+ the same name as the trigger.
61
+
49
62
  ## Tokens
50
63
  `--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}`,
51
64
  `--kpt-color-surface-raised` and `--kpt-elevation-3` for the panel, `--kpt-z-overlay` for the layer.
52
65
 
53
66
  ## Accessibility
54
- The trigger is `role="combobox"` with `aria-expanded` and `tabindex="0"`; the list is
55
- `role="listbox"` with `aria-multiselectable`, and each option is `role="option"` with
56
- `aria-selected`.
67
+ The trigger is `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`
68
+ and `aria-activedescendant`; the list is `role="listbox"` with `aria-multiselectable`, and each
69
+ option is `role="option"` with `aria-selected` and `aria-disabled`. Chip remove buttons sit outside
70
+ the Tab order and are named with the option label.
@@ -8,6 +8,10 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
8
8
  multiple?: boolean;
9
9
  /** Pole wyszukiwania nad listą. */
10
10
  filter?: boolean;
11
+ /** Id wyzwalacza; bez niego generowane, w `KptFormField` — z wrappera. */
12
+ id?: string;
13
+ /** Oznacza pole jako wymagane atrybutem `aria-required`. */
14
+ required?: boolean;
11
15
  disabled?: boolean;
12
16
  invalid?: boolean;
13
17
  touched?: boolean;
@@ -26,6 +30,13 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
26
30
  * Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
27
31
  * i wielokrotny (`multiple`, chipy).
28
32
  *
33
+ * Klawiatura według wzorca WAI-ARIA „select-only combobox”: fokus zostaje na polu, a aktywna opcja
34
+ * idzie przez `aria-activedescendant`. `↓`/`↑`/`Enter`/`Spacja` otwierają listę na wybranej opcji,
35
+ * strzałki, `Home`/`End`, `PageUp`/`PageDown` chodzą po opcjach (z pominięciem wyłączonych),
36
+ * wpisywane litery skaczą do pasującej opcji (bez polskich znaków), `Enter` wybiera, `Escape`
37
+ * zamyka. Z `filter` fokus przechodzi do pola wyszukiwania, które obsługuje te same klawisze.
38
+ * W trybie `multiple` `Backspace` na zamkniętym polu usuwa ostatni wybór.
39
+ *
29
40
  * Wewnątrz `KptFormField` zgłasza wypełnienie hookiem `useKptFormFieldFilled`, więc etykieta
30
41
  * Float Label unosi się nad wybraną wartością — natywnego pola, z którego wrapper mógłby to
31
42
  * odczytać, tu nie ma.
@@ -34,4 +45,4 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
34
45
  * <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz…" />
35
46
  * <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
36
47
  */
37
- export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
48
+ export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;