@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,30 +1,105 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
5
  import { useKptMessages } from "../i18n/i18n-context.js";
6
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
6
7
  import { KptIcon } from "../icon/icon.js";
8
+ const fillOf = (star, shown) => shown >= star ? 'full' : shown >= star - 0.5 ? 'half' : 'empty';
7
9
  /**
8
10
  * Ocena gwiazdkami. Kliknięcie zapalonej gwiazdki zeruje ocenę, więc wystawioną można cofnąć
9
11
  * bez osobnego przycisku „wyczyść".
10
12
  *
13
+ * Do edycji to grupa radiowa (`role="radiogroup"`, gwiazdki jako `role="radio"`): jeden przystanek
14
+ * Tab, strzałki zmieniają ocenę o krok (w RTL odwrócone w poziomie), `Home`/`End` ustawiają
15
+ * skrajne, `Delete`/`Backspace` zeruje. Z `allowHalf` każda gwiazdka ma dwie połówki — dwa osobne
16
+ * `radio` („3,5 z 5”), a krok strzałek to 0,5. Tylko do odczytu (`readOnly`) to obrazek z nazwą
17
+ * „4,3 z 5” — bez przycisków, które czytnik ogłaszałby jako wyłączone; gwiazdki pokazują wartość
18
+ * zaokrągloną do połówki.
19
+ *
11
20
  * @example
12
- * <KptRating value={score} onValueChange={setScore} count={5} />
13
- * <KptRating value={4} readOnly />
21
+ * <KptRating value={score} onValueChange={setScore} count={5} ariaLabel="Ocena" />
22
+ * <KptRating value={score} onValueChange={setScore} allowHalf ariaLabel="Ocena" />
23
+ * <KptRating value={4.3} readOnly />
14
24
  */
15
- export function KptRating({ value, defaultValue = 0, onValueChange, count = 5, readOnly = false, disabled = false, onTouch, locale, dictionary, className, ...rest }) {
16
- const { t } = useKptMessages({ locale, dictionary });
25
+ export function KptRating({ value, defaultValue = 0, onValueChange, count = 5, allowHalf = false, readOnly = false, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
26
+ const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
27
+ const field = useKptFieldA11y(id, 'kpt-rating');
17
28
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
18
29
  const [hover, setHover] = useState(0);
19
- const locked = readOnly || disabled;
30
+ const groupRef = useRef(null);
31
+ /** Prośba o fokus na przystanek Tab po najbliższym renderze (zmiana oceny klawiszem). */
32
+ const wantFocus = useRef(false);
33
+ const showInvalid = invalid && (touched ?? true);
34
+ const stars = Array.from({ length: count }, (_, index) => index + 1);
35
+ const step = allowHalf ? 0.5 : 1;
20
36
  // Podgląd pod kursorem wygrywa z oceną, ale tylko dopóki kursor jest nad gwiazdkami —
21
37
  // po `mouseleave` wraca 0 i zapalona zostaje sama wartość.
22
38
  const shown = hover || current;
23
- const pick = (star) => {
24
- if (locked)
39
+ /** Przystanek Tab: wybrana wartość, a bez oceny — pierwsza możliwa. */
40
+ const tabStop = current >= step && current <= count ? current : step;
41
+ /** Liczba w zapisie języka strony („3,5” po polsku). */
42
+ const numberFormat = new Intl.NumberFormat(activeLocale, { maximumFractionDigits: 2 });
43
+ const starLabel = (score) => t('rating.star', { value: numberFormat.format(score), max: count });
44
+ useEffect(() => {
45
+ if (!wantFocus.current)
46
+ return;
47
+ wantFocus.current = false;
48
+ groupRef.current?.querySelector(`[data-value="${tabStop}"]`)?.focus();
49
+ });
50
+ const starVisual = (star, fill, children, hidden = false) => (_jsxs("span", { className: "kpt-rating__star", "data-fill": fill, "aria-hidden": hidden || undefined, children: [_jsx(KptIcon, { name: "star" }), _jsx("span", { className: "kpt-rating__fill", "aria-hidden": "true", children: _jsx(KptIcon, { name: "star" }) }), children] }, star));
51
+ if (readOnly) {
52
+ const score = starLabel(current);
53
+ // Gwiazdki pokazują wartość zaokrągloną do połówki (4,3 → 4,5), nazwa — dokładną.
54
+ const display = Math.round(current * 2) / 2;
55
+ return (_jsx("div", { className: cn('kpt-rating-host', className), ...rest, children: _jsx("div", { className: "kpt-rating kpt-rating--readonly", role: "img", id: field.id, "aria-label": ariaLabel ? `${ariaLabel}: ${score}` : score, "aria-describedby": field.describedBy, children: stars.map((star) => starVisual(star, fillOf(star, display), null, true)) }) }));
56
+ }
57
+ const pick = (score) => {
58
+ if (disabled)
59
+ return;
60
+ setCurrent(current === score ? 0 : score);
61
+ };
62
+ const onKeyDown = (event) => {
63
+ if (disabled)
64
+ return;
65
+ const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
66
+ const forward = rtl ? 'ArrowLeft' : 'ArrowRight';
67
+ const back = rtl ? 'ArrowRight' : 'ArrowLeft';
68
+ let next = null;
69
+ if (event.key === forward || event.key === 'ArrowUp')
70
+ next = Math.min(count, Math.max(step, current + step));
71
+ else if (event.key === back || event.key === 'ArrowDown')
72
+ next = Math.max(step, current - step);
73
+ else if (event.key === 'Home')
74
+ next = step;
75
+ else if (event.key === 'End')
76
+ next = count;
77
+ else if (event.key === 'Delete' || event.key === 'Backspace')
78
+ next = 0;
79
+ else if (event.key === ' ' || event.key === 'Enter') {
80
+ // Spacja na gwiazdce wybiera ją (jak radio); ponowna nie zeruje — od tego jest Delete.
81
+ const score = Number(event.target.dataset['value']);
82
+ if (score)
83
+ next = score;
84
+ }
85
+ if (next === null)
25
86
  return;
26
- setCurrent(current === star ? 0 : star);
27
- onTouch?.();
87
+ event.preventDefault();
88
+ setCurrent(next);
89
+ wantFocus.current = true;
90
+ };
91
+ /** „Dotknięcie” dopiero przy wyjściu fokusu z całej grupy, nie przy przejściu między gwiazdkami. */
92
+ const onBlur = (event) => {
93
+ const next = event.relatedTarget;
94
+ if (!next || !event.currentTarget.contains(next))
95
+ onTouch?.();
28
96
  };
29
- return (_jsx("div", { className: cn('kpt-rating-host', className), ...rest, children: _jsx("div", { className: "kpt-rating", role: "slider", "aria-valuenow": current, "aria-valuemax": count, children: Array.from({ length: count }, (_, index) => index + 1).map((star) => (_jsx("button", { type: "button", className: cn('kpt-rating__star', star <= shown && 'is-filled'), disabled: locked, onClick: () => pick(star), onMouseEnter: () => setHover(star), onMouseLeave: () => setHover(0), "aria-label": t('rating.star', { value: star, max: count }), children: _jsx(KptIcon, { name: "star" }) }, star))) }) }));
97
+ /** Pola wyboru jednej gwiazdki: całość albo — z `allowHalf` — dwie połówki. */
98
+ const optionsOf = (star) => allowHalf
99
+ ? [
100
+ { score: star - 0.5, part: 'start' },
101
+ { score: star, part: 'end' },
102
+ ]
103
+ : [{ score: star, part: 'full' }];
104
+ return (_jsx("div", { className: cn('kpt-rating-host', className), ...rest, children: _jsx("div", { ref: groupRef, className: cn('kpt-rating', disabled && 'kpt-rating--disabled'), role: "radiogroup", id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, "aria-invalid": showInvalid || undefined, "aria-disabled": disabled || undefined, onKeyDown: onKeyDown, onBlur: onBlur, onMouseLeave: () => setHover(0), children: stars.map((star) => starVisual(star, fillOf(star, shown), optionsOf(star).map((option) => (_jsx("button", { type: "button", role: "radio", className: cn('kpt-rating__hit', `kpt-rating__hit--${option.part}`), "aria-checked": option.score === current, tabIndex: !disabled && option.score === tabStop ? 0 : -1, "aria-disabled": disabled || undefined, "data-value": option.score, "aria-label": starLabel(option.score), onClick: () => pick(option.score), onMouseEnter: () => !disabled && setHover(option.score) }, option.part))))) }) }));
30
105
  }
@@ -45,6 +45,11 @@ only when the operation actually changed something — otherwise Tab stays nativ
45
45
  never becomes a keyboard trap (WCAG 2.1.2). In a table cell Tab moves to the next cell; a list
46
46
  inside the cell takes precedence.
47
47
 
48
+ ## The toolbar is one Tab stop
49
+ The toolbar is a named `role="toolbar"` ("Formatting", `richText.toolbar`) with a roving tabindex:
50
+ `←`/`→` (mirrored in RTL) and `Home`/`End` move between tools, skipping disabled ones; the stop
51
+ remembers the last tool, so Tab from the toolbar goes straight to the editor.
52
+
48
53
  ## Tables
49
54
  Insert from an 8×8 grid popover. Dragging across cells makes a rectangular selection that grows
50
55
  until it fully covers merged cells on its edges. Right-click opens the menu: insert and delete rows
@@ -69,3 +74,18 @@ Every label comes from the `richText` namespace, including `richText.menu.*` and
69
74
  Formatting goes through `document.execCommand`. The API is formally deprecated, but it is the only
70
75
  route into the browser's native undo stack (Ctrl+Z); replacing it would mean writing our own
71
76
  history, that is, a different editor.
77
+
78
+ ## Screen reader and keyboard
79
+ `link` opens a small dialog instead of the browser's `prompt()`: a labelled URL field (prefilled
80
+ with the current link), `Enter` inserts, `Escape` returns to the text with the selection restored.
81
+ The table context menu is a `role="menu"` of `menuitem`s: it also opens from the keyboard (the menu
82
+ key or `Shift+F10` in a cell), the arrows and `Home`/`End` move, `Escape`/`Tab` close it, and focus
83
+ goes back to the cell. Colour swatches are named ("Dark grey", `richText.swatches.*`) instead of
84
+ their hex codes. The table size grid is still mouse-only.
85
+
86
+ ## Label and description
87
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
88
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
89
+ `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
90
+ is added to `aria-describedby`. New: `id`, `ariaLabel` and `required` (→ `aria-required`) on the
91
+ `role="textbox"` area.
@@ -13,6 +13,12 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
13
13
  placeholder?: string;
14
14
  /** Maksymalna głębokość zagnieżdżenia list (Tab, „Dodaj wcięcie"). */
15
15
  maxListLevel?: number;
16
+ /** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
17
+ id?: string;
18
+ /** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
19
+ ariaLabel?: string | null;
20
+ /** Oznacza pole jako wymagane atrybutem `aria-required`. */
21
+ required?: boolean;
16
22
  disabled?: boolean;
17
23
  invalid?: boolean;
18
24
  touched?: boolean;
@@ -35,4 +41,4 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
35
41
  * <KptRichText value={html} onValueChange={setHtml} />
36
42
  * <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
37
43
  */
38
- export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
44
+ export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, id, ariaLabel, required, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
@@ -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.