@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
package/dist/menu/menu.js CHANGED
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useRef } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { KptOverlay } from "../internal/overlay.js";
5
5
  import { useControllableState } from "../internal/use-controllable-state.js";
6
+ import { useKptId } from "../internal/use-id.js";
7
+ import { focusMenuEdge, handleMenuKeydown } from "./menu-keys.js";
6
8
  import { MenuRows } from "./menu-rows.js";
7
9
  /**
8
10
  * Menu rozwijane (dropdown). Pozycje przez `items`, wyzwalacz przez prop `trigger`.
@@ -15,6 +17,10 @@ import { MenuRows } from "./menu-rows.js";
15
17
  */
16
18
  export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = false, onOpenChange, className, ...rest }) {
17
19
  const triggerRef = useRef(null);
20
+ const panelRef = useRef(null);
21
+ /** Którą pozycję skupić po otwarciu — strzałka w górę na wyzwalaczu otwiera „od końca”. */
22
+ const focusEdge = useRef('first');
23
+ const panelId = useKptId(undefined, 'kpt-menu');
18
24
  const [isOpen, setOpen] = useControllableState({
19
25
  value: open,
20
26
  defaultValue: defaultOpen,
@@ -26,17 +32,59 @@ export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = fal
26
32
  // zagnieżdżoną kontrolkę i drugi tab stop. `cloneElement` też odpada — `trigger` bywa
27
33
  // komponentem, który nie przekazuje nieznanych propsów dalej na DOM.
28
34
  useEffect(() => {
29
- const control = triggerRef.current?.querySelector('button, a, [role="button"]');
35
+ const control = findControl();
30
36
  if (!control)
31
37
  return;
32
38
  control.setAttribute('aria-haspopup', 'menu');
33
39
  control.setAttribute('aria-expanded', String(isOpen));
34
- }, [isOpen, trigger]);
40
+ if (isOpen)
41
+ control.setAttribute('aria-controls', panelId);
42
+ else
43
+ control.removeAttribute('aria-controls');
44
+ }, [isOpen, trigger, panelId]);
45
+ // Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
46
+ // panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
47
+ useEffect(() => {
48
+ if (!isOpen)
49
+ return;
50
+ const frame = requestAnimationFrame(() => {
51
+ if (panelRef.current)
52
+ focusMenuEdge(panelRef.current, focusEdge.current);
53
+ });
54
+ return () => cancelAnimationFrame(frame);
55
+ }, [isOpen]);
56
+ /** Prawdziwa kontrolka w slocie — tam idą ARIA i powrót fokusu. */
57
+ function findControl() {
58
+ return triggerRef.current?.querySelector('button, a, [role="button"]') ?? null;
59
+ }
60
+ /** Fokus wraca na kontrolkę — inaczej po zniknięciu panelu spadłby na `<body>`. */
61
+ const closeAndRestoreFocus = () => {
62
+ close();
63
+ findControl()?.focus();
64
+ };
65
+ const onTriggerKeyDown = (event) => {
66
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
67
+ return;
68
+ event.preventDefault();
69
+ focusEdge.current = event.key === 'ArrowUp' ? 'last' : 'first';
70
+ if (isOpen && panelRef.current)
71
+ focusMenuEdge(panelRef.current, focusEdge.current);
72
+ else
73
+ setOpen(true);
74
+ };
75
+ const onPanelKeyDown = (event) => {
76
+ if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close') {
77
+ closeAndRestoreFocus();
78
+ }
79
+ };
35
80
  const pick = (item) => {
36
81
  if (item.disabled)
37
82
  return;
38
83
  onSelect?.(item);
39
- close();
84
+ closeAndRestoreFocus();
40
85
  };
41
- return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, children: _jsx("div", { className: "kpt-menu__panel", role: "menu", children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
86
+ return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () => {
87
+ focusEdge.current = 'first';
88
+ setOpen(!isOpen);
89
+ }, onKeyDown: onTriggerKeyDown, children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-menu__panel", role: "menu", onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
42
90
  }
@@ -5,18 +5,20 @@ Import: `import { KptMenubar } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-menubar-host">
8
- <nav class="kpt-menubar" role="menubar">
9
- <div class="kpt-menubar__group">
10
- <button class="kpt-menubar__top is-open" role="menuitem">File …caret…</button>
8
+ <div class="kpt-menubar" role="menubar" aria-orientation="horizontal" aria-label="Editor menu">
9
+ <div class="kpt-menubar__group" role="none">
10
+ <button class="kpt-menubar__top is-open" role="menuitem" tabindex="0"
11
+ aria-haspopup="menu" aria-expanded="true" aria-controls="kpt-menubar-r1-menu-0">File …caret…</button>
11
12
  </div>
12
- </nav>
13
+ </div>
13
14
  </div>
14
15
  <!-- while open, in a portal: -->
15
- <div class="kpt-menubar__dropdown" role="menu">…items and separators…</div>
16
+ <div class="kpt-menubar__dropdown" role="menu" id="kpt-menubar-r1-menu-0" aria-label="File">…items and separators…</div>
16
17
 
17
18
  ## Props
18
19
  - `items`: `KptMenuItem<T>[]` — a top item's `items` become its dropdown
19
20
  - `onSelect(item)` — an action item, or a pick from a dropdown
21
+ - `ariaLabel`: string — the bar's name for screen readers, e.g. "Editor menu"
20
22
  - `className`, `ref` and every other `<div>` prop go to the host
21
23
 
22
24
  ## The dropdown is a portal
@@ -36,10 +38,25 @@ bar can mix menus and plain actions with no extra flag.
36
38
  { label: 'Help' },
37
39
  ];
38
40
 
41
+ ## Links (`url`)
42
+ An entry with `url` and no children (and not disabled) renders as a real `<a href>` with the same
43
+ look, so the browser handles navigation — opening in a new tab, the status-bar preview. `onSelect`
44
+ still fires before the browser follows the link.
45
+
39
46
  ## Tokens
40
47
  `--kpt-color-surface`, `--kpt-color-muted` (the open top item), `--kpt-color-surface-raised` and
41
48
  `--kpt-elevation-3` for the dropdown, `--kpt-color-danger`.
42
49
 
43
50
  ## Accessibility
44
- The bar is `role="menubar"`, top items and dropdown rows are `role="menuitem"`, separators carry
45
- `role="separator"`.
51
+ The bar is `role="menubar"` (`aria-orientation="horizontal"`, named by `ariaLabel`), top items and
52
+ dropdown rows are `role="menuitem"`, separators carry `role="separator"`. Top items with a dropdown
53
+ carry `aria-haspopup="menu"`, `aria-expanded` and, while open, `aria-controls`. The bar is a plain
54
+ `<div>`, not `<nav>` — wrap it in your own `<nav aria-label="…">` when it is the site navigation.
55
+
56
+ ## Keyboard
57
+ The bar is a single `Tab` stop (roving `tabindex`), following the WAI-ARIA menubar pattern.
58
+ - On a top item: `ArrowLeft`/`ArrowRight` move along the bar (wrapping, skipping disabled items),
59
+ `Home`/`End` jump to the ends; `ArrowDown`, `Enter` or `Space` open the dropdown with focus on
60
+ its first row, `ArrowUp` on its last one.
61
+ - In a dropdown: `ArrowDown`/`ArrowUp`/`Home`/`End` move between rows, `ArrowLeft`/`ArrowRight`
62
+ open the neighbouring dropdown, `Escape` and `Tab` close it and return focus to the top item.
@@ -3,6 +3,8 @@ import type { KptMenuItem } from '../menu/menu-item.ts';
3
3
  export interface KptMenubarProps<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 edytora”. */
7
+ ariaLabel?: string;
6
8
  }
7
9
  /**
8
10
  * Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
@@ -10,7 +12,12 @@ export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
10
12
  * Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
11
13
  * korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
12
14
  *
15
+ * Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
16
+ * i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
17
+ * pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
18
+ * w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
19
+ *
13
20
  * @example
14
21
  * <KptMenubar items={items} onSelect={onSelect} />
15
22
  */
16
- export declare function KptMenubar<T>({ items, onSelect, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
23
+ export declare function KptMenubar<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
@@ -1,25 +1,72 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { KptOverlay } from "../internal/overlay.js";
5
+ import { nextRovingIndex, rovingStop } from "../internal/roving.js";
6
+ import { useKptId } from "../internal/use-id.js";
5
7
  import { KptIcon } from "../icon/icon.js";
8
+ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
6
9
  /**
7
10
  * Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
8
11
  *
9
12
  * Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
10
13
  * korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
11
14
  *
15
+ * Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
16
+ * i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
17
+ * pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
18
+ * w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
19
+ *
12
20
  * @example
13
21
  * <KptMenubar items={items} onSelect={onSelect} />
14
22
  */
15
- export function KptMenubar({ items = [], onSelect, className, ...rest }) {
23
+ export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest }) {
16
24
  const [openIndex, setOpenIndex] = useState(null);
25
+ // Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
26
+ const [focusIndex, setFocusIndex] = useState(0);
17
27
  const buttonsRef = useRef([]);
28
+ const panelRef = useRef(null);
29
+ // Którą pozycję pod-menu skupić po otwarciu — strzałka w górę otwiera „od końca”.
30
+ const focusEdge = useRef('first');
31
+ const baseId = useKptId(undefined, 'kpt-menubar');
32
+ const panelId = (index) => `${baseId}-menu-${index}`;
33
+ const disabled = items.map((item) => !!item.disabled);
34
+ // Pozycja z `url` (bez pod-menu, niewyłączona) to `<a href>` — nawigację robi przeglądarka,
35
+ // więc działa nowa karta i podgląd adresu. `onSelect` i tak jest wołane.
36
+ const isLink = (item) => !!item.url && !item.items?.length && !item.disabled;
37
+ const tabStop = rovingStop(disabled, focusIndex);
38
+ // Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
39
+ // panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
40
+ useEffect(() => {
41
+ if (openIndex === null)
42
+ return;
43
+ const frame = requestAnimationFrame(() => {
44
+ if (panelRef.current)
45
+ focusMenuEdge(panelRef.current, focusEdge.current);
46
+ });
47
+ return () => cancelAnimationFrame(frame);
48
+ }, [openIndex]);
18
49
  const closeAll = () => setOpenIndex(null);
50
+ const focusTop = (index) => {
51
+ setFocusIndex(index);
52
+ buttonsRef.current[index]?.focus();
53
+ };
54
+ /** Skupia pozycję górną i otwiera jej pod-menu, jeśli je ma (inaczej zamyka otwarte). */
55
+ const openAt = (index, edge) => {
56
+ focusEdge.current = edge;
57
+ const hasMenu = !!items[index]?.items?.length;
58
+ if (hasMenu && openIndex === index && panelRef.current) {
59
+ focusMenuEdge(panelRef.current, edge);
60
+ return;
61
+ }
62
+ setOpenIndex(hasMenu ? index : null);
63
+ focusTop(index);
64
+ };
19
65
  const toggle = (index, item) => {
20
66
  if (item.disabled)
21
67
  return;
22
68
  if (item.items?.length) {
69
+ focusEdge.current = 'first';
23
70
  setOpenIndex((current) => (current === index ? null : index));
24
71
  }
25
72
  else {
@@ -27,13 +74,52 @@ export function KptMenubar({ items = [], onSelect, className, ...rest }) {
27
74
  closeAll();
28
75
  }
29
76
  };
30
- const pick = (item) => {
77
+ const pick = (item, index) => {
31
78
  if (item.disabled)
32
79
  return;
33
80
  onSelect?.(item);
34
81
  closeAll();
82
+ // Fokus wraca na pozycję górną — inaczej po zniknięciu pod-menu spadłby na `<body>`.
83
+ focusTop(index);
84
+ };
85
+ const onTopKeyDown = (event, index) => {
86
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
87
+ if (!items[index]?.items?.length)
88
+ return;
89
+ event.preventDefault();
90
+ openAt(index, event.key === 'ArrowUp' ? 'last' : 'first');
91
+ return;
92
+ }
93
+ const next = nextRovingIndex(disabled, index, event.key);
94
+ if (next === null)
95
+ return;
96
+ event.preventDefault();
97
+ // Przy otwartym pod-menu przejście w bok otwiera pod-menu sąsiada — jak w menu aplikacji.
98
+ if (openIndex === null)
99
+ focusTop(next);
100
+ else
101
+ openAt(next, 'first');
102
+ };
103
+ const onPanelKeyDown = (event, index) => {
104
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
105
+ const next = nextRovingIndex(disabled, index, event.key);
106
+ if (next === null)
107
+ return;
108
+ event.preventDefault();
109
+ openAt(next, 'first');
110
+ return;
111
+ }
112
+ if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close') {
113
+ closeAll();
114
+ focusTop(index);
115
+ }
35
116
  };
36
- return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("nav", { className: "kpt-menubar", role: "menubar", children: items.map((item, index) => (_jsxs("div", { className: "kpt-menubar__group", children: [_jsxs("button", { ref: (el) => {
37
- buttonsRef.current[index] = el;
38
- }, type: "button", className: cn('kpt-menubar__top', openIndex === index && 'is-open'), role: "menuitem", disabled: item.disabled, onClick: () => toggle(index, item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), item.items?.length ? _jsx(KptIcon, { name: "chevron-down", className: "kpt-menubar__caret" }) : null] }), _jsx(KptOverlay, { open: openIndex === index && !!item.items?.length, anchor: buttonsRef.current[index] ?? null, onClose: closeAll, children: _jsx("div", { className: "kpt-menubar__dropdown", role: "menu", children: (item.items ?? []).map((sub, subIndex) => sub.separator ? (_jsx("div", { className: "kpt-menubar__separator", role: "separator" }, subIndex)) : (_jsxs("button", { type: "button", className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", disabled: sub.disabled, onClick: () => pick(sub), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex))) }) })] }, index))) }) }));
117
+ return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("div", { className: "kpt-menubar", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, children: items.map((item, index) => {
118
+ const hasMenu = !!item.items?.length;
119
+ return (_jsxs("div", { className: "kpt-menubar__group", role: "none", children: [isLink(item) ? (_jsxs("a", { ref: (el) => {
120
+ buttonsRef.current[index] = el;
121
+ }, className: "kpt-menubar__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: () => pick(item, index), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label })] })) : (_jsxs("button", { ref: (el) => {
122
+ buttonsRef.current[index] = el;
123
+ }, type: "button", className: cn('kpt-menubar__top', openIndex === index && 'is-open'), role: "menuitem", disabled: item.disabled, tabIndex: index === tabStop ? 0 : -1, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? openIndex === index : undefined, "aria-controls": openIndex === index ? panelId(index) : undefined, onClick: () => toggle(index, item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), hasMenu ? _jsx(KptIcon, { name: "chevron-down", className: "kpt-menubar__caret" }) : null] })), _jsx(KptOverlay, { open: openIndex === index && hasMenu, anchor: buttonsRef.current[index] ?? null, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-menubar__dropdown", role: "menu", id: panelId(index), "aria-label": item.label, onKeyDown: (event) => onPanelKeyDown(event, index), children: (item.items ?? []).map((sub, subIndex) => sub.separator ? (_jsx("div", { className: "kpt-menubar__separator", role: "separator" }, subIndex)) : isLink(sub) ? (_jsxs("a", { className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, href: sub.url, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex)) : (_jsxs("button", { type: "button", className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, disabled: sub.disabled, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex))) }) })] }, index));
124
+ }) }) }));
39
125
  }
@@ -6,7 +6,8 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
6
6
  ## DOM
7
7
  <div class="kpt-meter-group-host">
8
8
  <div class="kpt-meter-group">
9
- <div class="kpt-meter-group__track" role="meter" aria-valuenow="100">
9
+ <div class="kpt-meter-group__track" role="meter" aria-label="Usage" aria-valuemin="0"
10
+ aria-valuenow="100" aria-valuemax="100" aria-valuetext="Documents 45, …">
10
11
  <span class="kpt-meter-group__segment" style="width:45%" title="Documents"></span>…
11
12
  </div>
12
13
  <ul class="kpt-meter-group__legend">
@@ -22,6 +23,9 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
22
23
  - `values`: `KptMeterItem[]` — `{ label, value, color? }`
23
24
  - `max`: number — the reference total; without it the values' sum is 100%
24
25
  - `showLegend`: boolean (default true)
26
+ - `label`: string — the meter's name; empty = `meterGroup.label`
27
+ - `legendValue`: 'percent' | 'value' (default 'percent') — what the legend shows next to each label
28
+ - `unit`: string — appended to the values in the legend and in `aria-valuetext` (e.g. 'GB')
25
29
  - `className`, `ref` and every other `<div>` prop go to the host
26
30
 
27
31
  ## Percentages come from the sum, not from 100
@@ -43,5 +47,8 @@ The palette `--kpt-color-{primary,info,success,warning,danger}`, the track
43
47
  `--kpt-color-surface-sunken`.
44
48
 
45
49
  ## Accessibility
46
- The track is `role="meter"` with `aria-valuenow`; every segment carries a native `title`, and the
47
- legend repeats the same values as text.
50
+ The track is `role="meter"` named by `label` (or `meterGroup.label`), with `aria-valuemin="0"`,
51
+ `aria-valuenow` set to the sum and `aria-valuemax` set to the real total — `max`, or the sum without
52
+ it. `aria-valuetext` spells out the split (“Photos 42 GB, Videos 28 GB, …”), so the segments reach
53
+ the screen reader even with the legend hidden. Keep the legend visible anyway: it is the text
54
+ alternative to the colours.
@@ -1,15 +1,22 @@
1
1
  import type { ComponentPropsWithRef } from 'react';
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
2
3
  export interface KptMeterItem {
3
4
  label: string;
4
5
  value: number;
5
6
  /** Własny kolor segmentu; bez niego wchodzi kolejny z palety. */
6
7
  color?: string;
7
8
  }
8
- export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'> {
9
+ export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
9
10
  values?: readonly KptMeterItem[];
10
11
  /** Suma odniesienia; domyślnie suma wartości, czyli segmenty wypełniają całość. */
11
12
  max?: number;
12
13
  showLegend?: boolean;
14
+ /** Nazwa paska dla czytników ekranu, np. „Miejsce na dysku”; domyślnie `meterGroup.label`. */
15
+ label?: string;
16
+ /** Co pokazuje legenda przy etykiecie: udział procentowy albo samą wartość (z `unit`). */
17
+ legendValue?: 'percent' | 'value';
18
+ /** Jednostka dopisywana do wartości — w legendzie (`legendValue="value"`) i w `aria-valuetext`. */
19
+ unit?: string;
13
20
  }
14
21
  /**
15
22
  * Wielosegmentowy miernik z opcjonalną legendą.
@@ -17,4 +24,4 @@ export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'> {
17
24
  * @example
18
25
  * <KptMeterGroup values={[{ label: 'Użyte', value: 70 }, { label: 'Wolne', value: 30 }]} />
19
26
  */
20
- export declare function KptMeterGroup({ values, max, showLegend, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
27
+ export declare function KptMeterGroup({ values, max, showLegend, label, legendValue, unit, locale, dictionary, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../internal/cn.js";
3
+ import { useKptMessages } from "../i18n/i18n-context.js";
3
4
  const PALETTE = [
4
5
  'var(--kpt-color-primary)',
5
6
  'var(--kpt-color-info)',
@@ -13,12 +14,17 @@ const PALETTE = [
13
14
  * @example
14
15
  * <KptMeterGroup values={[{ label: 'Użyte', value: 70 }, { label: 'Wolne', value: 30 }]} />
15
16
  */
16
- export function KptMeterGroup({ values = [], max, showLegend = true, className, ...rest }) {
17
+ export function KptMeterGroup({ values = [], max, showLegend = true, label = '', legendValue = 'percent', unit = '', locale, dictionary, className, ...rest }) {
18
+ const { t } = useKptMessages({ locale, dictionary });
17
19
  const sum = values.reduce((acc, item) => acc + item.value, 0);
18
20
  // Bez `max` odniesieniem jest suma wartości; `|| 1` chroni przed dzieleniem przez zero,
19
21
  // gdy wszystkie segmenty są zerowe.
20
22
  const total = max ?? (sum || 1);
21
23
  const percent = (item) => (item.value / total) * 100;
22
24
  const color = (item, index) => item.color || PALETTE[index % PALETTE.length];
23
- return (_jsx("div", { className: cn('kpt-meter-group-host', className), ...rest, children: _jsxs("div", { className: "kpt-meter-group", children: [_jsx("div", { className: "kpt-meter-group__track", role: "meter", "aria-valuenow": sum, "aria-valuemax": max, children: values.map((item, index) => (_jsx("span", { className: "kpt-meter-group__segment", style: { width: `${percent(item)}%`, backgroundColor: color(item, index) }, title: item.label }, index))) }), showLegend ? (_jsx("ul", { className: "kpt-meter-group__legend", children: values.map((item, index) => (_jsxs("li", { className: "kpt-meter-group__legend-item", children: [_jsx("span", { className: "kpt-meter-group__dot", style: { backgroundColor: color(item, index) } }), _jsx("span", { children: item.label }), _jsxs("span", { className: "kpt-meter-group__val", children: [Math.round(percent(item)), "%"] })] }, index))) })) : null] }) }));
25
+ const withUnit = (value) => (unit ? `${value} ${unit}` : String(value));
26
+ const legendText = (item) => legendValue === 'value' ? withUnit(item.value) : `${Math.round(percent(item))}%`;
27
+ // Podział dla czytnika („Zdjęcia 42 GB, Filmy 28 GB…”) — sam `aria-valuenow` mówi tylko sumę.
28
+ const valueText = values.map((item) => `${item.label} ${withUnit(item.value)}`).join(', ');
29
+ return (_jsx("div", { className: cn('kpt-meter-group-host', className), ...rest, children: _jsxs("div", { className: "kpt-meter-group", children: [_jsx("div", { className: "kpt-meter-group__track", role: "meter", "aria-valuemin": 0, "aria-valuenow": sum, "aria-valuemax": total, "aria-valuetext": valueText, "aria-label": label || t('meterGroup.label'), children: values.map((item, index) => (_jsx("span", { className: "kpt-meter-group__segment", style: { width: `${percent(item)}%`, backgroundColor: color(item, index) }, title: item.label }, index))) }), showLegend ? (_jsx("ul", { className: "kpt-meter-group__legend", children: values.map((item, index) => (_jsxs("li", { className: "kpt-meter-group__legend-item", children: [_jsx("span", { className: "kpt-meter-group__dot", style: { backgroundColor: color(item, index) } }), _jsx("span", { children: item.label }), _jsx("span", { className: "kpt-meter-group__val", children: legendText(item) })] }, index))) })) : null] }) }));
24
30
  }
@@ -46,7 +46,15 @@ no dictionary keys.
46
46
  ## Tokens
47
47
  The selection `--kpt-color-primary`, the buttons `--kpt-color-surface-raised`.
48
48
 
49
+ ## Keyboard
50
+ The list is a `listbox` with a single Tab stop (roving tabindex), and the selection follows focus:
51
+ - `↓` / `↑` select the next / previous item (wrapping), `Home` / `End` the first / last;
52
+ - `Alt`+`↑` / `Alt`+`↓` move the selected item one place up / down — focus travels with it, so
53
+ repeated presses keep moving the same entry.
54
+ The up / down buttons stay and act on the same selection.
55
+
49
56
  ## Accessibility
50
- Both buttons are labelled from the dictionary and go disabled while nothing is selected, so the
51
- control never looks broken. Native HTML5 drag and drop has no keyboard equivalent, so they stay the
52
- full keyboard path — `dragdrop` never replaces them.
57
+ The list is named after `header` (`aria-label`) and the selected item carries `aria-selected`.
58
+ Both buttons are labelled from the dictionary and go disabled while nothing is selected. Native
59
+ HTML5 drag and drop has no keyboard equivalent — `Alt`+arrows and the buttons cover it, and
60
+ `dragdrop` adds to them, it never replaces them.
@@ -14,7 +14,11 @@ export interface KptOrderListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
14
14
  * OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
15
15
  * w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
16
16
  *
17
+ * Lista to `listbox` z jednym przystankiem Tab (roving tabindex), w którym zaznaczenie idzie
18
+ * za fokusem: strzałki / `Home` / `End` wybierają pozycję, `Alt`+↑/↓ przestawia ją o jedno
19
+ * miejsce (fokus jedzie razem z nią).
20
+ *
17
21
  * @example
18
22
  * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
19
23
  */
20
- export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
24
+ export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, dragdrop, renderItem, locale, dictionary, className, ref, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
@@ -1,18 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
+ import { composeRefs } from "../internal/compose-refs.js";
4
5
  import { useControllableState } from "../internal/use-controllable-state.js";
5
6
  import { useKptMessages } from "../i18n/i18n-context.js";
6
7
  import { KptIcon } from "../icon/icon.js";
8
+ import { nextMenuIndex } from "../menu/menu-keys.js";
7
9
  const EMPTY = [];
8
10
  /**
9
11
  * OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
10
12
  * w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
11
13
  *
14
+ * Lista to `listbox` z jednym przystankiem Tab (roving tabindex), w którym zaznaczenie idzie
15
+ * za fokusem: strzałki / `Home` / `End` wybierają pozycję, `Alt`+↑/↓ przestawia ją o jedno
16
+ * miejsce (fokus jedzie razem z nią).
17
+ *
12
18
  * @example
13
19
  * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
14
20
  */
15
- export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
21
+ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ref, ...rest }) {
16
22
  const { t } = useKptMessages({ locale, dictionary });
17
23
  const [list, setList] = useControllableState({
18
24
  value: items,
@@ -26,18 +32,50 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
26
32
  const [overIndex, setOverIndex] = useState(null);
27
33
  /** Kursor nad pustym obszarem listy — upuszczenie tam dopina na koniec. */
28
34
  const [listOver, setListOver] = useState(false);
29
- const move = (dir) => {
30
- if (selected === null)
35
+ /** Pozycja, na którą po renderze trzeba przenieść fokus DOM (klawiatura). */
36
+ const [focusRequest, setFocusRequest] = useState(null);
37
+ const rootRef = useRef(null);
38
+ useEffect(() => {
39
+ if (focusRequest === null)
31
40
  return;
32
- const target = selected + dir;
41
+ rootRef.current?.querySelectorAll('[role="option"]')[focusRequest]?.focus();
42
+ setFocusRequest(null);
43
+ }, [focusRequest]);
44
+ /**
45
+ * Przesuwa pozycję spod `index` o jedno miejsce i zwraca jej nowy indeks. Jawny indeks zamiast
46
+ * `selected`: klawiatura zaznacza i przesuwa w jednym zdarzeniu, a stan z domknięcia byłby stary.
47
+ */
48
+ const moveFrom = (index, dir) => {
49
+ const target = index + dir;
33
50
  if (target < 0 || target >= list.length)
34
- return;
51
+ return index;
35
52
  const next = [...list];
36
- [next[selected], next[target]] = [next[target], next[selected]];
53
+ [next[index], next[target]] = [next[target], next[index]];
37
54
  setList(next);
38
55
  // Zaznaczenie wędruje za elementem, nie zostaje na pozycji — inaczej kolejne kliknięcie
39
56
  // przesuwałoby sąsiada zamiast tego samego wpisu.
40
57
  setSelected(target);
58
+ return target;
59
+ };
60
+ const move = (dir) => {
61
+ if (selected !== null)
62
+ moveFrom(selected, dir);
63
+ };
64
+ /** Przystanek Tab: zaznaczona pozycja albo pierwsza. */
65
+ const tabStop = selected !== null && selected < list.length ? selected : 0;
66
+ // Strzałki wybierają pozycję (zaznaczenie idzie za fokusem), `Alt`+↑/↓ ją przestawia.
67
+ const onItemKeyDown = (index, event) => {
68
+ if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
69
+ event.preventDefault();
70
+ setFocusRequest(moveFrom(index, event.key === 'ArrowUp' ? -1 : 1));
71
+ return;
72
+ }
73
+ const next = nextMenuIndex(list.length, index, event.key);
74
+ if (next === null)
75
+ return;
76
+ event.preventDefault();
77
+ setSelected(next);
78
+ setFocusRequest(next);
41
79
  };
42
80
  const clearDrag = () => {
43
81
  setDragIndex(null);
@@ -98,5 +136,5 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
98
136
  setList(next);
99
137
  setSelected(next.length - 1);
100
138
  };
101
- return (_jsx("div", { className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
139
+ return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), role: "listbox", "aria-label": header || undefined, onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), role: "option", "aria-selected": selected === index, tabIndex: index === tabStop ? 0 : -1, onFocus: () => setSelected(index), onKeyDown: (event) => onItemKeyDown(index, event), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
102
140
  }
@@ -9,9 +9,10 @@ Import: `import { KptPanel } from '@konce-pt/react';`
9
9
  <div class="kpt-panel__header">
10
10
  <span class="kpt-panel__title">Settings</span>
11
11
  <span class="kpt-panel__actions">…actions…</span>
12
- <button class="kpt-panel__toggle" aria-expanded="true" aria-label="Collapse / expand">…</button>
12
+ <button class="kpt-panel__toggle" aria-expanded="true" aria-controls="kpt-panel-body-r1"
13
+ aria-label="Collapse / expand Settings">…</button>
13
14
  </div>
14
- <div class="kpt-panel__collapse is-open">
15
+ <div class="kpt-panel__collapse is-open" id="kpt-panel-body-r1">
15
16
  <div class="kpt-panel__collapse-inner">
16
17
  <div class="kpt-panel__body">…children…</div>
17
18
  </div>
@@ -36,7 +37,7 @@ nothing inside can take focus or be read out. That is also why a collapsed panel
36
37
  whatever lives in it — a half-filled form inside survives a collapse.
37
38
 
38
39
  ## i18n
39
- The toggle's label comes from `panel.toggle` in the dictionary.
40
+ The toggle's label comes from `panel.toggleNamed` (with `{header}`) or `panel.toggle` in the dictionary.
40
41
 
41
42
  ## Examples
42
43
  <KptPanel header="Settings" toggleable>Panel content…</KptPanel>
@@ -50,6 +51,8 @@ The toggle's label comes from `panel.toggle` in the dictionary.
50
51
  `--kpt-color-surface`, `--kpt-color-border`, `--kpt-radius-lg`, `--kpt-motion-easing-standard`.
51
52
 
52
53
  ## Accessibility
53
- The toggle is a native button with `aria-expanded` and a label from the dictionary. The header
54
- title is a `<span>`, not a heading — give the panel your own heading when it needs one in the
55
- document outline.
54
+ The toggle is a native button with `aria-expanded` and `aria-controls` pointing at the body. Its
55
+ name says which panel it collapses — `panel.toggleNamed` (“Collapse / expand Settings”) when there
56
+ is a `header`, `panel.toggle` otherwise — so several panels on one page are told apart. A collapsed
57
+ body is `inert` and `aria-hidden`. The header title is not a heading — add your own when the panel
58
+ should appear in the document outline.
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, isValidElement } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptId } from "../internal/use-id.js";
5
6
  import { useKptMessages } from "../i18n/i18n-context.js";
6
7
  import { KptIcon } from "../icon/icon.js";
7
8
  /**
@@ -15,6 +16,7 @@ import { KptIcon } from "../icon/icon.js";
15
16
  */
16
17
  export function KptPanel({ header = '', toggleable = false, collapsed, defaultCollapsed = false, onCollapsedChange, animated = true, animationDuration = 500, locale, dictionary, className, style, children, ...rest }) {
17
18
  const { t } = useKptMessages({ locale, dictionary });
19
+ const bodyId = useKptId(undefined, 'kpt-panel-body');
18
20
  const [isCollapsed, setCollapsed] = useControllableState({
19
21
  value: collapsed,
20
22
  defaultValue: defaultCollapsed,
@@ -26,7 +28,7 @@ export function KptPanel({ header = '', toggleable = false, collapsed, defaultCo
26
28
  const isActions = isValidElement(child) && child.type?.kptPanelSlot === 'actions';
27
29
  (isActions ? actions : body).push(child);
28
30
  });
29
- return (_jsx("div", { className: cn('kpt-panel-host', className), style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsxs("div", { className: "kpt-panel", children: [_jsxs("div", { className: "kpt-panel__header", children: [_jsx("span", { className: "kpt-panel__title", children: header }), _jsx("span", { className: "kpt-panel__actions", children: actions }), toggleable ? (_jsx("button", { type: "button", className: "kpt-panel__toggle", onClick: () => setCollapsed(!isCollapsed), "aria-expanded": !isCollapsed, "aria-label": t('panel.toggle'), children: _jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }) })) : null] }), _jsx("div", { className: cn('kpt-panel__collapse', !isCollapsed && 'is-open'), inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-panel__collapse-inner", children: _jsx("div", { className: "kpt-panel__body", children: body }) }) })] }) }));
31
+ return (_jsx("div", { className: cn('kpt-panel-host', className), style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsxs("div", { className: "kpt-panel", children: [_jsxs("div", { className: "kpt-panel__header", children: [_jsx("span", { className: "kpt-panel__title", children: header }), _jsx("span", { className: "kpt-panel__actions", children: actions }), toggleable ? (_jsx("button", { type: "button", className: "kpt-panel__toggle", onClick: () => setCollapsed(!isCollapsed), "aria-expanded": !isCollapsed, "aria-controls": bodyId, "aria-label": header ? t('panel.toggleNamed', { header }) : t('panel.toggle'), children: _jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }) })) : null] }), _jsx("div", { className: cn('kpt-panel__collapse', !isCollapsed && 'is-open'), id: bodyId, inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-panel__collapse-inner", children: _jsx("div", { className: "kpt-panel__body", children: body }) }) })] }) }));
30
32
  }
31
33
  /** Akcje w nagłówku panelu, obok tytułu. */
32
34
  export function KptPanelActions({ className, ...rest }) {
@@ -36,6 +36,14 @@ the dictionary — indexed by the score, so a translation controls the wording w
36
36
  <KptPassword value={pass} onValueChange={setPass} feedback />
37
37
  <KptPassword value={pass} onValueChange={setPass} autoComplete="new-password" feedback />
38
38
 
39
+ ## Label and description
40
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
41
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
42
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `ariaLabel`. The
43
+ host no longer carries the `id` (it used to, so `label[for]` pointed at `kpt-password` and the same
44
+ id existed twice). New: `ariaLabel`, `required`, `readonly`/`readOnly`, `aria-invalid` after touch;
45
+ with `feedback` the strength label is added to `aria-describedby`.
46
+
39
47
  ## Tokens
40
48
  `--kpt-form-field-*` for the field, `--kpt-color-danger` / `-warning` / `-success` for the meter
41
49
  bars by score.
@@ -14,6 +14,11 @@ export interface KptPasswordProps extends Omit<ComponentPropsWithRef<'div'>, 'on
14
14
  autoComplete?: string | null;
15
15
  /** Miernik siły pod polem. */
16
16
  feedback?: boolean;
17
+ /** Nazwa dostępna, gdy pola nie opisuje etykieta (`<label for>` albo `KptFormField`). */
18
+ ariaLabel?: string | null;
19
+ /** Oznacza pole jako wymagane atrybutem `aria-required`. */
20
+ required?: boolean;
21
+ readOnly?: boolean;
17
22
  disabled?: boolean;
18
23
  invalid?: boolean;
19
24
  touched?: boolean;
@@ -25,4 +30,4 @@ export interface KptPasswordProps extends Omit<ComponentPropsWithRef<'div'>, 'on
25
30
  * @example
26
31
  * <KptPassword value={password} onValueChange={setPassword} feedback autoComplete="new-password" />
27
32
  */
28
- export declare function KptPassword({ value, defaultValue, onValueChange, placeholder, id, autoComplete, feedback, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptPasswordProps>): import("react").JSX.Element;
33
+ export declare function KptPassword({ value, defaultValue, onValueChange, placeholder, id, autoComplete, feedback, ariaLabel, required, readOnly, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptPasswordProps>): import("react").JSX.Element;