@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
@@ -0,0 +1,57 @@
1
+ import { useEffect } from 'react';
2
+ /** Elementy z kolejki Tab — na nich zawija się pułapka. */
3
+ const TABBABLE = 'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
4
+ function tabbables(panel) {
5
+ return [...panel.querySelectorAll(TABBABLE)].filter((el) => !el.closest('[hidden], [inert]'));
6
+ }
7
+ /**
8
+ * Fokus panelu modalnego (`KptDrawer`, `KptBottomSheet`): przy otwarciu zapamiętuje element
9
+ * z fokusem, przenosi fokus do panelu (pierwszy element z kolejki Tab albo sam panel) i zawija
10
+ * `Tab` / `Shift+Tab` w jego obrębie; przy zamknięciu oddaje fokus zapamiętanemu elementowi.
11
+ * Bez tego `aria-modal="true"` obiecywał czytnikowi okno, z którego Tab po cichu wychodził.
12
+ *
13
+ * Port Angulara robi to samo na pułapce CDK (`shared/modal-focus.ts`) — zmiana zachowania w obu.
14
+ */
15
+ export function useModalFocus(open, panelRef) {
16
+ useEffect(() => {
17
+ const panel = panelRef.current;
18
+ if (!open || !panel)
19
+ return;
20
+ const active = document.activeElement;
21
+ const returnTo = active instanceof HTMLElement && !panel.contains(active) ? active : null;
22
+ // Klatkę później: panel dostaje `is-open` (i `visibility: visible`) w tym samym commicie,
23
+ // a przeglądarka nie skupi elementu, który w chwili wywołania jest jeszcze niewidoczny.
24
+ const frame = requestAnimationFrame(() => {
25
+ const first = tabbables(panel)[0];
26
+ (first ?? panel).focus();
27
+ });
28
+ const onKeyDown = (event) => {
29
+ if (event.key !== 'Tab')
30
+ return;
31
+ const items = tabbables(panel);
32
+ if (!items.length) {
33
+ event.preventDefault();
34
+ panel.focus();
35
+ return;
36
+ }
37
+ const first = items[0];
38
+ const last = items[items.length - 1];
39
+ const current = document.activeElement;
40
+ if (event.shiftKey && (current === first || !panel.contains(current))) {
41
+ event.preventDefault();
42
+ last.focus();
43
+ }
44
+ else if (!event.shiftKey && (current === last || !panel.contains(current))) {
45
+ event.preventDefault();
46
+ first.focus();
47
+ }
48
+ };
49
+ document.addEventListener('keydown', onKeyDown);
50
+ return () => {
51
+ cancelAnimationFrame(frame);
52
+ document.removeEventListener('keydown', onKeyDown);
53
+ if (returnTo?.isConnected)
54
+ returnTo.focus();
55
+ };
56
+ }, [open, panelRef]);
57
+ }
@@ -13,6 +13,10 @@ export interface KptKnobProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
13
13
  /** Dopisek za wartością w środku (np. '%'). */
14
14
  suffix?: string;
15
15
  showValue?: boolean;
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;
16
20
  disabled?: boolean;
17
21
  invalid?: boolean;
18
22
  touched?: boolean;
@@ -21,7 +25,11 @@ export interface KptKnobProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
21
25
  /**
22
26
  * Pokrętło — wybór wartości po łuku, przeciąganiem albo strzałkami.
23
27
  *
28
+ * Skok wartości (strzałka, `Home`/`End`, klik w łuk, zmiana z zewnątrz) łuk i licznik pokonują
29
+ * płynnie; przy przeciąganiu idą za kursorem bez opóźnienia. `prefers-reduced-motion` wyłącza
30
+ * animację, a `aria-valuenow` zawsze niesie wartość docelową, nie pośrednią.
31
+ *
24
32
  * @example
25
33
  * <KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
26
34
  */
27
- export declare function KptKnob({ value, defaultValue, onValueChange, min, max, step, size, stroke, suffix, showValue, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptKnobProps>): import("react").JSX.Element;
35
+ export declare function KptKnob({ value, defaultValue, onValueChange, min, max, step, size, stroke, suffix, showValue, id, ariaLabel, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptKnobProps>): import("react").JSX.Element;
package/dist/knob/knob.js CHANGED
@@ -1,24 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
+ import { kptStepDecimals, kptTween } from "../internal/tween.js";
4
5
  import { useControllableState } from "../internal/use-controllable-state.js";
6
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
5
7
  const TAU = Math.PI * 2;
6
8
  /** Przerwa u dołu — kąt „otwarcia" łuku, żeby początek i koniec były rozróżnialne. */
7
9
  const GAP = Math.PI / 4;
8
10
  const START_ANGLE = Math.PI / 2 + GAP;
9
11
  const SWEEP = TAU - GAP * 2;
12
+ /** Czas dojścia łuku i licznika po skoku wartości (klawisz, klik, zmiana z zewnątrz). */
13
+ const TWEEN_MS = 220;
10
14
  /**
11
15
  * Pokrętło — wybór wartości po łuku, przeciąganiem albo strzałkami.
12
16
  *
17
+ * Skok wartości (strzałka, `Home`/`End`, klik w łuk, zmiana z zewnątrz) łuk i licznik pokonują
18
+ * płynnie; przy przeciąganiu idą za kursorem bez opóźnienia. `prefers-reduced-motion` wyłącza
19
+ * animację, a `aria-valuenow` zawsze niesie wartość docelową, nie pośrednią.
20
+ *
13
21
  * @example
14
22
  * <KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
15
23
  */
16
- export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max = 100, step = 1, size = 96, stroke = 10, suffix = '', showValue = true, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
24
+ export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max = 100, step = 1, size = 96, stroke = 10, suffix = '', showValue = true, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
25
+ const field = useKptFieldA11y(id, 'kpt-knob');
17
26
  const svgRef = useRef(null);
18
27
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
19
28
  const showInvalid = invalid && (touched ?? true);
20
29
  const range = max - min;
21
- const ratio = range <= 0 ? 0 : (current - min) / range;
30
+ /** Wartość na rysunku — w trakcie animacji pośrednia, poza nią równa `current`. */
31
+ const [shown, setShown] = useState(current);
32
+ const shownRef = useRef(current);
33
+ /** Przeciąganie trwa — wtedy rysunek idzie za kursorem bez animacji. */
34
+ const dragging = useRef(false);
35
+ useEffect(() => {
36
+ const paint = (v) => {
37
+ shownRef.current = v;
38
+ setShown(v);
39
+ };
40
+ if (dragging.current) {
41
+ paint(current);
42
+ return;
43
+ }
44
+ return kptTween(shownRef.current, current, TWEEN_MS, paint);
45
+ }, [current]);
46
+ const ratio = range <= 0 ? 0 : Math.min(1, Math.max(0, (shown - min) / range));
22
47
  /** Wycinek łuku jako ścieżka SVG; `to <= from` daje samo `M`, czyli nic widocznego. */
23
48
  const arc = (from, to) => {
24
49
  const r = (size - stroke) / 2;
@@ -52,17 +77,28 @@ export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max =
52
77
  const onPointerDown = (event) => {
53
78
  if (disabled)
54
79
  return;
80
+ // Bez tego przeciąganie zaznacza tekst: licznik w środku i — przez SVG — całe koło, jak po
81
+ // `Ctrl+A`. Zablokowanie domyślnej akcji zabiera też fokus, więc oddajemy go ręcznie.
82
+ event.preventDefault();
83
+ event.currentTarget.focus();
84
+ // Klik w łuk to skok — animowany. Dopiero ruch po nim jest przeciąganiem.
55
85
  setFromPoint(event.clientX, event.clientY);
56
86
  // Nasłuch wisi na oknie, a nie na pokrętle: przeciąganie ma nie gubić wartości,
57
87
  // gdy kursor wyjedzie poza koło — a wyjeżdża praktycznie zawsze.
58
- const move = (e) => setFromPoint(e.clientX, e.clientY);
88
+ const move = (e) => {
89
+ dragging.current = true;
90
+ setFromPoint(e.clientX, e.clientY);
91
+ };
59
92
  const up = () => {
60
93
  window.removeEventListener('pointermove', move);
61
94
  window.removeEventListener('pointerup', up);
95
+ window.removeEventListener('pointercancel', up);
96
+ dragging.current = false;
62
97
  onTouch?.();
63
98
  };
64
99
  window.addEventListener('pointermove', move);
65
100
  window.addEventListener('pointerup', up);
101
+ window.addEventListener('pointercancel', up);
66
102
  };
67
103
  const onKeyDown = (event) => {
68
104
  if (disabled)
@@ -82,5 +118,5 @@ export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max =
82
118
  setCurrent(Math.min(max, Math.max(min, next)));
83
119
  onTouch?.();
84
120
  };
85
- return (_jsx("div", { className: cn('kpt-knob-host', className), ...rest, children: _jsx("div", { className: cn('kpt-knob', showInvalid && 'kpt-knob--invalid'), role: "slider", tabIndex: disabled ? -1 : 0, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-disabled": disabled || undefined, onPointerDown: onPointerDown, onKeyDown: onKeyDown, children: _jsxs("svg", { ref: svgRef, viewBox: `0 0 ${size} ${size}`, style: { width: size, height: size }, children: [_jsx("path", { className: "kpt-knob__range", d: arc(0, 1), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), _jsx("path", { className: "kpt-knob__value", d: arc(0, ratio), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), showValue ? (_jsxs("text", { className: "kpt-knob__text", x: size / 2, y: size / 2, textAnchor: "middle", dominantBaseline: "central", children: [current, suffix] })) : null] }) }) }));
121
+ return (_jsx("div", { className: cn('kpt-knob-host', className), ...rest, children: _jsx("div", { className: cn('kpt-knob', showInvalid && 'kpt-knob--invalid'), role: "slider", id: field.id, tabIndex: disabled ? -1 : 0, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": suffix ? `${current}${suffix}` : undefined, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onPointerDown: onPointerDown, onKeyDown: onKeyDown, children: _jsxs("svg", { ref: svgRef, viewBox: `0 0 ${size} ${size}`, style: { width: size, height: size }, children: [_jsx("path", { className: "kpt-knob__range", d: arc(0, 1), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), _jsx("path", { className: "kpt-knob__value", d: arc(0, ratio), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), showValue ? (_jsxs("text", { className: "kpt-knob__text", x: size / 2, y: size / 2, textAnchor: "middle", dominantBaseline: "central", children: [shown.toFixed(kptStepDecimals(step)), suffix] })) : null] }) }) }));
86
122
  }
@@ -36,10 +36,25 @@ on `pointerup`, which is also where `onTouch` fires — one touch per drag, not
36
36
  <KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
37
37
  <KptKnob value={level} onValueChange={setLevel} size={64} stroke={6} showValue={false} />
38
38
 
39
+ ## Label and description
40
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
41
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
42
+ `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
43
+ is added to `aria-describedby`. New: `id`, `ariaLabel`; `aria-valuetext` includes `suffix` ("40%"),
44
+ and a disabled knob leaves the Tab order. Use `ariaLabel`, not `aria-label` on the host — that one
45
+ lands on the wrapper.
46
+
39
47
  ## Tokens
40
48
  The background arc `--kpt-color-border`, the value `--kpt-color-primary`, the text
41
49
  `--kpt-color-on-surface`.
42
50
 
51
+ ## Motion
52
+ A jump in value — an arrow key, Home/End, a click on the arc, a change from outside — moves the arc
53
+ and counts the number up or down over ~220 ms (ease-out). Dragging follows the pointer with no
54
+ delay, so it never feels laggy. `prefers-reduced-motion: reduce` turns the animation off.
55
+ `aria-valuenow` always carries the target value, never an in-between frame. Dragging does not
56
+ select the number or the arc.
57
+
43
58
  ## Accessibility
44
59
  `role="slider"` with `aria-valuemin` / `aria-valuemax` / `aria-valuenow`; arrows step by `step`,
45
60
  Home and End jump to the bounds. `disabled` sets `aria-disabled` and drops the knob out of the
@@ -9,6 +9,12 @@ export interface KptListboxProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
9
9
  multiple?: boolean;
10
10
  /** Pole filtrowania nad listą. */
11
11
  filter?: boolean;
12
+ /** Id listy (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
13
+ id?: string;
14
+ /** Nazwa dostępna listy, gdy nie opisuje jej etykieta (`KptFormField`). */
15
+ ariaLabel?: string | null;
16
+ /** Oznacza listę jako wymaganą atrybutem `aria-required`. */
17
+ required?: boolean;
12
18
  disabled?: boolean;
13
19
  invalid?: boolean;
14
20
  touched?: boolean;
@@ -21,8 +27,14 @@ export interface KptListboxProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
21
27
  * W trybie pojedynczym ponowny klik w zaznaczoną opcję odznacza ją — lista bywa filtrem,
22
28
  * a filtr trzeba dać się wyczyścić bez sięgania po osobny przycisk.
23
29
  *
30
+ * Klawiatura według wzorca WAI-ARIA „listbox”: lista to jeden przystanek Tab, a aktywną opcję
31
+ * ogłasza `aria-activedescendant`. Strzałki, `Home`/`End`, `PageUp`/`PageDown` przesuwają aktywną
32
+ * opcję (wyłączone są pomijane), pisanie liter skacze do kolejnej pasującej (bez względu na
33
+ * diakrytyki), `Spacja`/`Enter` wybiera, a w trybie wielokrotnym `Ctrl`/`⌘`+`A` zaznacza wszystkie
34
+ * widoczne. Pole filtra stoi przed listą; `↓` przenosi z niego fokus do listy.
35
+ *
24
36
  * @example
25
37
  * <KptListbox options={roles} value={role} onValueChange={setRole} filter />
26
38
  * <KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
27
39
  */
28
- export declare function KptListbox<T>({ options, value, defaultValue, onValueChange, multiple, filter, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptListboxProps<T>>): import("react").JSX.Element;
40
+ export declare function KptListbox<T>({ options, value, defaultValue, onValueChange, multiple, filter, id, ariaLabel, required, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptListboxProps<T>>): import("react").JSX.Element;
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { foldMentionText } from '@konce-pt/mention';
3
4
  import { cn } from "../internal/cn.js";
4
5
  import { useControllableState } from "../internal/use-controllable-state.js";
6
+ import { KptTypeahead, OPTION_NAV_KEYS, isTypeaheadKey, nextOptionIndex, typeaheadIndex } from "../internal/option-nav.js";
5
7
  import { useKptMessages } from "../i18n/i18n-context.js";
8
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
6
9
  import { KptIcon } from "../icon/icon.js";
7
10
  /**
8
11
  * Lista wyboru widoczna w całości, bez rozwijania. Pojedynczy albo wielokrotny wybór,
@@ -11,23 +14,44 @@ import { KptIcon } from "../icon/icon.js";
11
14
  * W trybie pojedynczym ponowny klik w zaznaczoną opcję odznacza ją — lista bywa filtrem,
12
15
  * a filtr trzeba dać się wyczyścić bez sięgania po osobny przycisk.
13
16
  *
17
+ * Klawiatura według wzorca WAI-ARIA „listbox”: lista to jeden przystanek Tab, a aktywną opcję
18
+ * ogłasza `aria-activedescendant`. Strzałki, `Home`/`End`, `PageUp`/`PageDown` przesuwają aktywną
19
+ * opcję (wyłączone są pomijane), pisanie liter skacze do kolejnej pasującej (bez względu na
20
+ * diakrytyki), `Spacja`/`Enter` wybiera, a w trybie wielokrotnym `Ctrl`/`⌘`+`A` zaznacza wszystkie
21
+ * widoczne. Pole filtra stoi przed listą; `↓` przenosi z niego fokus do listy.
22
+ *
14
23
  * @example
15
24
  * <KptListbox options={roles} value={role} onValueChange={setRole} filter />
16
25
  * <KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
17
26
  */
18
- export function KptListbox({ options = [], value, defaultValue = null, onValueChange, multiple = false, filter = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
27
+ export function KptListbox({ options = [], value, defaultValue = null, onValueChange, multiple = false, filter = false, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
19
28
  const { t } = useKptMessages({ locale, dictionary });
29
+ /** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
30
+ const field = useKptFieldA11y(id, 'kpt-listbox');
20
31
  const [current, setCurrent] = useControllableState({
21
32
  value,
22
33
  defaultValue,
23
34
  onChange: onValueChange,
24
35
  });
25
36
  const [query, setQuery] = useState('');
37
+ /** Aktywna opcja (indeks w `visible`); `-1` = żadna. */
38
+ const [activeIndex, setActiveIndex] = useState(-1);
39
+ const listRef = useRef(null);
40
+ const typeahead = useRef(new KptTypeahead()).current;
26
41
  const showInvalid = invalid && (touched ?? true);
42
+ /** Filtr bez względu na wielkość liter i diakrytyki („lodz” znajduje „Łódź”). */
27
43
  const visible = useMemo(() => {
28
- const q = query.trim().toLowerCase();
29
- return q ? options.filter((option) => option.label.toLowerCase().includes(q)) : options;
44
+ const q = foldMentionText(query.trim());
45
+ return q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
30
46
  }, [options, query]);
47
+ const disabledMap = visible.map((option) => disabled || !!option.disabled);
48
+ const optionId = (index) => `${field.id}-opt-${index}`;
49
+ const activeOptionId = activeIndex >= 0 && activeIndex < visible.length ? optionId(activeIndex) : undefined;
50
+ // Aktywna opcja zawsze w polu widzenia listy (przewijana `max-height`).
51
+ useEffect(() => {
52
+ if (activeOptionId)
53
+ document.getElementById(activeOptionId)?.scrollIntoView({ block: 'nearest' });
54
+ }, [activeOptionId]);
31
55
  const isSelected = (candidate) => multiple ? Array.isArray(current) && current.includes(candidate) : current === candidate;
32
56
  const toggle = (candidate) => {
33
57
  if (multiple) {
@@ -37,7 +61,66 @@ export function KptListbox({ options = [], value, defaultValue = null, onValueCh
37
61
  else {
38
62
  setCurrent(current === candidate ? null : candidate);
39
63
  }
40
- onTouch?.();
41
64
  };
42
- return (_jsx("div", { className: cn('kpt-listbox-host', className), ...rest, children: _jsxs("div", { className: cn('kpt-listbox', showInvalid && 'kpt-listbox--invalid'), role: "listbox", "aria-multiselectable": multiple, children: [filter ? (_jsxs("div", { className: "kpt-listbox__filter", children: [_jsx(KptIcon, { name: "search" }), _jsx("input", { type: "text", placeholder: t('listbox.search'), value: query, onChange: (event) => setQuery(event.target.value) })] })) : null, _jsx("div", { className: "kpt-listbox__items", children: visible.length ? (visible.map((option) => (_jsxs("button", { type: "button", role: "option", className: cn('kpt-listbox__item', isSelected(option.value) && 'is-selected'), "aria-selected": isSelected(option.value), disabled: disabled || option.disabled, onClick: () => toggle(option.value), children: [_jsx("span", { children: option.label }), isSelected(option.value) ? _jsx(KptIcon, { name: "check" }) : null] }, String(option.value))))) : (_jsx("div", { className: "kpt-listbox__empty", children: t('listbox.empty') })) })] }) }));
65
+ const pick = (index) => {
66
+ const option = visible[index];
67
+ if (!option || disabledMap[index])
68
+ return;
69
+ setActiveIndex(index);
70
+ toggle(option.value);
71
+ };
72
+ /** Wejście fokusem: aktywna zostaje wybrana opcja, a bez niej pierwsza dozwolona. */
73
+ const onFocus = () => {
74
+ if (activeIndex >= 0 && activeIndex < visible.length)
75
+ return;
76
+ const selected = visible.findIndex((option, index) => isSelected(option.value) && !disabledMap[index]);
77
+ setActiveIndex(selected >= 0 ? selected : (nextOptionIndex(disabledMap, -1, 'Home') ?? -1));
78
+ };
79
+ const onQuery = (text) => {
80
+ setQuery(text);
81
+ const q = foldMentionText(text.trim());
82
+ const next = q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
83
+ setActiveIndex(nextOptionIndex(next.map((option) => disabled || !!option.disabled), -1, 'Home') ?? -1);
84
+ };
85
+ const onKeyDown = (event) => {
86
+ if (disabled)
87
+ return;
88
+ if (OPTION_NAV_KEYS.has(event.key)) {
89
+ event.preventDefault();
90
+ const next = nextOptionIndex(disabledMap, activeIndex, event.key);
91
+ if (next !== null)
92
+ setActiveIndex(next);
93
+ return;
94
+ }
95
+ if (event.key === ' ' || event.key === 'Enter') {
96
+ if (activeIndex < 0)
97
+ return;
98
+ event.preventDefault();
99
+ pick(activeIndex);
100
+ return;
101
+ }
102
+ if (multiple && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
103
+ event.preventDefault();
104
+ const all = visible.filter((_, index) => !disabledMap[index]).map((option) => option.value);
105
+ const list = Array.isArray(current) ? current : [];
106
+ // Drugi raz `Ctrl+A` przy wszystkich zaznaczonych — odznacza widoczne.
107
+ const allOn = all.every((item) => list.includes(item));
108
+ setCurrent(allOn ? list.filter((item) => !all.includes(item)) : [...list, ...all.filter((item) => !list.includes(item))]);
109
+ return;
110
+ }
111
+ if (isTypeaheadKey(event.nativeEvent)) {
112
+ const hit = typeaheadIndex(visible.map((option) => option.label), disabledMap, activeIndex, typeahead.push(event.key));
113
+ if (hit !== null)
114
+ setActiveIndex(hit);
115
+ }
116
+ };
117
+ return (_jsx("div", { className: cn('kpt-listbox-host', className), ...rest, children: _jsxs("div", { className: cn('kpt-listbox', showInvalid && 'kpt-listbox--invalid', disabled && 'kpt-listbox--disabled'), children: [filter ? (_jsxs("div", { className: "kpt-listbox__filter", children: [_jsx(KptIcon, { name: "search" }), _jsx("input", { type: "text", placeholder: t('listbox.search'), "aria-label": t('listbox.search'), "aria-controls": field.id, disabled: disabled, value: query, onChange: (event) => onQuery(event.target.value), onKeyDown: (event) => {
118
+ if (event.key !== 'ArrowDown')
119
+ return;
120
+ event.preventDefault();
121
+ listRef.current?.focus();
122
+ } })] })) : null, _jsx("div", { ref: listRef, className: "kpt-listbox__items", role: "listbox", id: field.id, tabIndex: disabled ? -1 : 0, "aria-multiselectable": multiple || undefined, "aria-activedescendant": activeOptionId, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: () => onTouch?.(), children: visible.length ? (visible.map((option, index) => (_jsxs("div", { role: "option", id: optionId(index), className: cn('kpt-listbox__item', isSelected(option.value) && 'is-selected', index === activeIndex && 'is-active'), "aria-selected": isSelected(option.value), "aria-disabled": disabledMap[index] || undefined, onMouseDown: (event) => {
123
+ event.preventDefault();
124
+ listRef.current?.focus();
125
+ }, onClick: () => pick(index), children: [_jsx("span", { children: option.label }), isSelected(option.value) ? _jsx(KptIcon, { name: "check" }) : null] }, String(option.value))))) : (_jsx("div", { className: "kpt-listbox__empty", role: "status", children: t('listbox.empty') })) })] }) }));
43
126
  }
@@ -5,10 +5,10 @@ Import: `import { KptListbox } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-listbox-host">
8
- <div class="kpt-listbox" role="listbox" aria-multiselectable="false">
8
+ <div class="kpt-listbox">
9
9
  <div class="kpt-listbox__filter">…icon + input…</div>
10
- <div class="kpt-listbox__items">
11
- <button class="kpt-listbox__item is-selected" role="option" aria-selected="true">Admin</button>
10
+ <div class="kpt-listbox__items" role="listbox" tabindex="0" aria-activedescendant="…-opt-0">
11
+ <div class="kpt-listbox__item is-selected is-active" role="option" id="…-opt-0" aria-selected="true">Admin</div>
12
12
  </div>
13
13
  </div>
14
14
  </div>
@@ -17,6 +17,7 @@ Import: `import { KptListbox } from '@konce-pt/react';`
17
17
  - `options`: `KptOption<T>[]`
18
18
  - `value` / `defaultValue` / `onValueChange` — `T` (single) or `T[]` (multiple)
19
19
  - `multiple`, `filter`, `disabled`, `invalid`, `touched`, `onTouch()`
20
+ - `id`, `ariaLabel`, `required`
20
21
  - `locale`, `dictionary` — i18n overrides for this component only
21
22
 
22
23
  ## Clicking the selection clears it
@@ -30,10 +31,25 @@ filter has to be clearable without a separate button.
30
31
  <KptListbox options={roles} value={role} onValueChange={setRole} filter />
31
32
  <KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
32
33
 
34
+ ## Keyboard
35
+ The WAI-ARIA listbox pattern: the list is one Tab stop and the active option is announced through
36
+ `aria-activedescendant`. `↑`/`↓`, `Home`/`End` and `PageUp`/`PageDown` move the active option,
37
+ skipping disabled ones (no wrapping); typing letters jumps to the next match, diacritics ignored;
38
+ `Space`/`Enter` picks; in `multiple` mode `Ctrl`/`⌘`+`A` selects every visible option (again —
39
+ clears them). The filter field sits before the list (outside the `listbox`) and `↓` moves focus
40
+ from it into the list; the filter itself ignores diacritics ("lodz" finds "Łódź").
41
+
42
+ ## Label and description
43
+ `id` goes to the list (`role="listbox"`), never the host. A `div` with a role is not named by
44
+ `label[for]`, so inside `KptFormField` the list gets `aria-labelledby` pointing at the wrapper's
45
+ label (unless `ariaLabel` is set) and the hint or error in `aria-describedby`. New: `id`,
46
+ `ariaLabel`, `required` (→ `aria-required`); `aria-invalid` after touch; `touch` fires on blur of the list.
47
+
33
48
  ## Tokens
34
49
  `--kpt-color-surface`, `--kpt-color-border`, `--kpt-color-primary-subtle` (the selected row),
35
50
  `--kpt-form-field-border-error`.
36
51
 
37
52
  ## Accessibility
38
- `role="listbox"` with `aria-multiselectable`; every option is a native button with
39
- `role="option"` and `aria-selected`, so the keyboard walks the list without extra handling.
53
+ The list is `role="listbox"` with `aria-multiselectable`, `aria-activedescendant` and a name from
54
+ `ariaLabel` or `KptFormField`; options are `role="option"` with `aria-selected`/`aria-disabled`.
55
+ The keyboard is described above.
@@ -5,21 +5,23 @@ Import: `import { KptMegamenu } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-megamenu-host">
8
- <nav class="kpt-megamenu" role="menubar">
9
- <button class="kpt-megamenu__top is-open" role="menuitem">Products …caret…</button>
10
- </nav>
8
+ <div class="kpt-megamenu" role="menubar" aria-orientation="horizontal" aria-label="Main menu">
9
+ <button class="kpt-megamenu__top is-open" role="menuitem" tabindex="0"
10
+ aria-haspopup="menu" aria-expanded="true" aria-controls="kpt-megamenu-r1">Products …caret…</button>
11
+ </div>
11
12
  </div>
12
13
  <!-- while open, in a portal: -->
13
- <div class="kpt-megamenu__panel" role="menu">
14
- <div class="kpt-megamenu__column">
15
- <div class="kpt-megamenu__col-title">Components</div>
16
- <button class="kpt-megamenu__link">Forms</button>
14
+ <div class="kpt-megamenu__panel" role="menu" id="kpt-megamenu-r1" aria-label="Products">
15
+ <div class="kpt-megamenu__column" role="group" aria-labelledby="kpt-megamenu-r1-col-0">
16
+ <div class="kpt-megamenu__col-title" role="presentation" id="kpt-megamenu-r1-col-0">Components</div>
17
+ <button class="kpt-megamenu__link" role="menuitem" tabindex="-1">Forms</button>
17
18
  </div>
18
19
  </div>
19
20
 
20
21
  ## Props
21
22
  - `items`: `KptMenuItem<T>[]` — three levels: top item → `items` (columns) → column `items` (links)
22
- - `onSelect(link)` — a picked link from a column
23
+ - `onSelect(link)` — a picked link from a column, or a top item without columns
24
+ - `ariaLabel`: string — the bar's name for screen readers, e.g. "Main menu"
23
25
  - `className`, `ref` and every other `<div>` prop go to the host
24
26
 
25
27
  ## The panel is anchored to the whole bar
@@ -39,10 +41,26 @@ without closing the panel, so browsing across sections costs one click each.
39
41
  ],
40
42
  }];
41
43
 
44
+ ## Links (`url`)
45
+ An entry with `url` and no children (and not disabled) renders as a real `<a href>` with the same
46
+ look, so the browser handles navigation — opening in a new tab, the status-bar preview. `onSelect`
47
+ still fires before the browser follows the link.
48
+
42
49
  ## Tokens
43
50
  `--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`,
44
51
  `--kpt-color-on-surface-muted` (the column titles).
45
52
 
46
53
  ## Accessibility
47
- The bar is `role="menubar"` with `role="menuitem"` buttons; the panel is `role="menu"`. Column
48
- titles are plain text, not headings, so they do not disturb the page outline.
54
+ The bar is `role="menubar"` (`aria-orientation="horizontal"`, named by `ariaLabel`) with
55
+ `role="menuitem"` buttons; those with columns carry `aria-haspopup="menu"`, `aria-expanded` and
56
+ `aria-controls`. The panel is `role="menu"` named after the open item, each column is
57
+ `role="group"` named by its title, and links are `role="menuitem"`. Column titles are not headings,
58
+ so they do not disturb the page outline. A top item without columns fires `onSelect` instead of
59
+ opening an empty panel.
60
+
61
+ ## Keyboard
62
+ The same as `KptMenubar`: one `Tab` stop for the bar, `ArrowLeft`/`ArrowRight` and `Home`/`End`
63
+ along it, `ArrowDown` (or `Enter`/`Space`) opens the panel on the first link, `ArrowUp` on the last.
64
+ In the panel `ArrowDown`/`ArrowUp`/`Home`/`End` walk through all links column by column,
65
+ `ArrowLeft`/`ArrowRight` show the neighbouring item's panel, `Escape` or `Tab` close it and return
66
+ focus to the top item.
@@ -3,15 +3,24 @@ import type { KptMenuItem } from '../menu/menu-item.ts';
3
3
  export interface KptMegamenuProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
4
4
  items?: readonly KptMenuItem<T>[];
5
5
  onSelect?: (item: KptMenuItem<T>) => void;
6
+ /** Nazwa belki dla czytników ekranu, np. „Menu główne”. */
7
+ ariaLabel?: string;
6
8
  }
7
9
  /**
8
10
  * Mega-menu — pozioma belka z szerokim, wielokolumnowym panelem.
9
11
  * Struktura: pozycja górna → `items` (kolumny) → `items` kolumny (odnośniki).
12
+ * Pozycja górna bez kolumn działa jak akcja — wywołuje `onSelect` zamiast otwierać pusty panel.
10
13
  *
11
14
  * Panel kotwiczy się do CAŁEJ belki, nie do pojedynczego przycisku: jest szeroki, więc ma
12
15
  * zaczynać się przy krawędzi menu, a nie przy przypadkowej pozycji, w którą kliknięto.
13
16
  *
17
+ * Klawiatura jak w `KptMenubar`: belka to jeden przystanek Tab, strzałki w lewo/prawo
18
+ * i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera panel
19
+ * z fokusem na pierwszym odnośniku. W panelu strzałki w górę/dół przechodzą po wszystkich
20
+ * odnośnikach kolumna po kolumnie, w lewo/prawo — do panelu sąsiedniej pozycji, a `Escape`
21
+ * zamyka panel i oddaje fokus belce.
22
+ *
14
23
  * @example
15
24
  * <KptMegamenu items={items} onSelect={onSelect} />
16
25
  */
17
- export declare function KptMegamenu<T>({ items, onSelect, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;
26
+ export declare function KptMegamenu<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;