@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
@@ -1,28 +1,139 @@
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
  * Mega-menu — pozioma belka z szerokim, wielokolumnowym panelem.
8
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.
9
13
  *
10
14
  * Panel kotwiczy się do CAŁEJ belki, nie do pojedynczego przycisku: jest szeroki, więc ma
11
15
  * zaczynać się przy krawędzi menu, a nie przy przypadkowej pozycji, w którą kliknięto.
12
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
+ *
13
23
  * @example
14
24
  * <KptMegamenu items={items} onSelect={onSelect} />
15
25
  */
16
- export function KptMegamenu({ items = [], onSelect, className, ...rest }) {
17
- const navRef = useRef(null);
26
+ export function KptMegamenu({ items = [], onSelect, ariaLabel, className, ...rest }) {
27
+ const barRef = useRef(null);
28
+ const panelRef = useRef(null);
29
+ const buttonsRef = useRef([]);
18
30
  const [openIndex, setOpenIndex] = useState(null);
19
- const closeAll = () => setOpenIndex(null);
31
+ // Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
32
+ const [focusIndex, setFocusIndex] = useState(0);
33
+ // Który odnośnik skupić po otwarciu — strzałka w górę otwiera „od końca”.
34
+ const focusEdge = useRef('first');
35
+ const panelId = useKptId(undefined, 'kpt-megamenu');
36
+ const disabled = items.map((item) => !!item.disabled);
37
+ // Pozycja z `url` (bez kolumn, niewyłączona) to `<a href>` — nawigację robi przeglądarka,
38
+ // więc działa nowa karta i podgląd adresu. `onSelect` i tak jest wołane.
39
+ const isLink = (item) => !!item.url && !item.items?.length && !item.disabled;
40
+ const tabStop = rovingStop(disabled, focusIndex);
20
41
  const columns = openIndex === null ? [] : (items[openIndex]?.items ?? []);
42
+ // Po otwarciu i po każdej zmianie pozycji fokus na skrajny odnośnik. Klatkę później:
43
+ // `KptOverlay` trzyma panel w `visibility: hidden`, dopóki go nie zmierzy.
44
+ useEffect(() => {
45
+ if (openIndex === null)
46
+ return;
47
+ const frame = requestAnimationFrame(() => {
48
+ if (panelRef.current)
49
+ focusMenuEdge(panelRef.current, focusEdge.current);
50
+ });
51
+ return () => cancelAnimationFrame(frame);
52
+ }, [openIndex]);
53
+ const closeAll = () => setOpenIndex(null);
54
+ const focusTop = (index) => {
55
+ setFocusIndex(index);
56
+ buttonsRef.current[index]?.focus();
57
+ };
58
+ /** Skupia pozycję górną i pokazuje jej panel, jeśli ma kolumny (inaczej zamyka otwarty). */
59
+ const openAt = (index, edge) => {
60
+ focusEdge.current = edge;
61
+ const hasMenu = !!items[index]?.items?.length;
62
+ if (hasMenu && openIndex === index && panelRef.current) {
63
+ focusMenuEdge(panelRef.current, edge);
64
+ return;
65
+ }
66
+ setOpenIndex(hasMenu ? index : null);
67
+ focusTop(index);
68
+ };
69
+ const toggle = (index, item) => {
70
+ if (item.disabled)
71
+ return;
72
+ if (item.items?.length) {
73
+ focusEdge.current = 'first';
74
+ setOpenIndex((current) => (current === index ? null : index));
75
+ }
76
+ else {
77
+ onSelect?.(item);
78
+ closeAll();
79
+ }
80
+ };
21
81
  const pick = (link) => {
22
82
  if (link.disabled)
23
83
  return;
84
+ const top = openIndex;
24
85
  onSelect?.(link);
25
86
  closeAll();
87
+ if (top !== null)
88
+ focusTop(top);
89
+ };
90
+ const onTopKeyDown = (event, index) => {
91
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
92
+ if (!items[index]?.items?.length)
93
+ return;
94
+ event.preventDefault();
95
+ openAt(index, event.key === 'ArrowUp' ? 'last' : 'first');
96
+ return;
97
+ }
98
+ const next = nextRovingIndex(disabled, index, event.key);
99
+ if (next === null)
100
+ return;
101
+ event.preventDefault();
102
+ // Przy otwartym panelu przejście w bok pokazuje panel sąsiada — jak w menu aplikacji.
103
+ if (openIndex === null)
104
+ focusTop(next);
105
+ else
106
+ openAt(next, 'first');
107
+ };
108
+ const onPanelKeyDown = (event) => {
109
+ if (openIndex === null)
110
+ return;
111
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
112
+ const next = nextRovingIndex(disabled, openIndex, event.key);
113
+ if (next === null)
114
+ return;
115
+ event.preventDefault();
116
+ openAt(next, 'first');
117
+ return;
118
+ }
119
+ if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close') {
120
+ const top = openIndex;
121
+ closeAll();
122
+ focusTop(top);
123
+ }
26
124
  };
27
- return (_jsxs("div", { className: cn('kpt-megamenu-host', className), ...rest, children: [_jsx("nav", { className: "kpt-megamenu", role: "menubar", ref: navRef, children: items.map((item, index) => (_jsxs("button", { type: "button", className: cn('kpt-megamenu__top', openIndex === index && 'is-open'), role: "menuitem", onClick: () => setOpenIndex((current) => (current === index ? null : index)), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), _jsx(KptIcon, { name: "chevron-down", className: "kpt-megamenu__caret" })] }, index))) }), _jsx(KptOverlay, { open: openIndex !== null, anchor: navRef.current, onClose: closeAll, children: _jsx("div", { className: "kpt-megamenu__panel", role: "menu", children: columns.map((column, columnIndex) => (_jsxs("div", { className: "kpt-megamenu__column", children: [column.label ? _jsx("div", { className: "kpt-megamenu__col-title", children: column.label }) : null, (column.items ?? []).map((link, linkIndex) => (_jsxs("button", { type: "button", className: "kpt-megamenu__link", disabled: link.disabled, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)))] }, columnIndex))) }) })] }));
125
+ return (_jsxs("div", { className: cn('kpt-megamenu-host', className), ...rest, children: [_jsx("div", { className: "kpt-megamenu", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, ref: barRef, children: items.map((item, index) => {
126
+ const hasMenu = !!item.items?.length;
127
+ if (isLink(item)) {
128
+ return (_jsxs("a", { ref: (el) => {
129
+ buttonsRef.current[index] = el;
130
+ }, className: "kpt-megamenu__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, 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 })] }, index));
131
+ }
132
+ return (_jsxs("button", { ref: (el) => {
133
+ buttonsRef.current[index] = el;
134
+ }, type: "button", className: cn('kpt-megamenu__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 : 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-megamenu__caret" }) : null] }, index));
135
+ }) }), _jsx(KptOverlay, { open: openIndex !== null, anchor: barRef.current, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-megamenu__panel", role: "menu", id: panelId, "aria-label": openIndex === null ? undefined : items[openIndex]?.label, onKeyDown: onPanelKeyDown, children: columns.map((column, columnIndex) => {
136
+ const titleId = `${panelId}-col-${columnIndex}`;
137
+ return (_jsxs("div", { className: "kpt-megamenu__column", role: "group", "aria-labelledby": column.label ? titleId : undefined, children: [column.label ? (_jsx("div", { className: "kpt-megamenu__col-title", role: "presentation", id: titleId, children: column.label })) : null, (column.items ?? []).map((link, linkIndex) => isLink(link) ? (_jsxs("a", { className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, href: link.url, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)) : (_jsxs("button", { type: "button", className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, disabled: link.disabled, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)))] }, columnIndex));
138
+ }) }) })] }));
28
139
  }
@@ -45,12 +45,23 @@ Angular port, which stands on CDK. Position, backdrop and Escape behave the same
45
45
  `--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-danger`
46
46
  (the `danger` item), `--kpt-z-overlay`.
47
47
 
48
+ ## Keyboard
49
+ - `↓` / `↑` on the trigger open the menu with focus on the first / last item; a click, `Enter`
50
+ or `Space` opens it on the first.
51
+ - Inside: `↓` / `↑` move between items and wrap around, `Home` / `End` jump to the ends;
52
+ disabled items, separators and headers are skipped.
53
+ - `Escape` and `Tab` close the menu, and so does picking an item — focus returns to the trigger.
54
+ - Items carry `tabindex="-1"` (roving focus), so an open menu is one Tab stop, not one per item.
55
+
56
+ The logic lives in `menu-keys.ts`, shared with `KptSplitButton`; the other port has a twin file.
57
+
48
58
  ## Accessibility
49
59
  The panel is `role="menu"`, items are native buttons with `role="menuitem"`, separators carry
50
60
  `role="separator"` and header rows `role="presentation"`, so a screen reader skips them instead of
51
61
  announcing them as actions.
52
62
 
53
- The trigger itself gets `aria-haspopup="menu"` and a live `aria-expanded`. They are placed on the
63
+ The trigger itself gets `aria-haspopup="menu"`, a live `aria-expanded` and, while open,
64
+ `aria-controls` pointing at the panel. They are placed on the
54
65
  interactive control INSIDE the slot (the projected `<button>`/`<a>`), never on the
55
66
  `.kpt-menu__trigger` wrapper — giving that wrapper a role would create a nested control and a second
56
67
  tab stop, and state attributes on a roleless element violate `aria-allowed-attr`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Indeks pozycji, na którą ma przejść fokus. `count` to liczba **aktywnych** pozycji (wyłączone,
3
+ * separatory i nagłówki są już odfiltrowane), `current` to indeks obecnie skupionej albo `-1`.
4
+ * Strzałki zawijają się na końcach. Zwraca `null` dla klawisza, który nie nawiguje.
5
+ */
6
+ export declare function nextMenuIndex(count: number, current: number, key: string): number | null;
7
+ /** Aktywne pozycje panelu w kolejności DOM. */
8
+ export declare function menuItems(panel: HTMLElement): HTMLElement[];
9
+ /** Skupia pierwszą albo ostatnią aktywną pozycję — po otwarciu menu z klawiatury lub myszy. */
10
+ export declare function focusMenuEdge(panel: HTMLElement, edge: 'first' | 'last'): void;
11
+ /**
12
+ * Obsługa klawisza w otwartym panelu. Zwraca `'close'`, gdy menu ma się zamknąć z powrotem
13
+ * fokusu na przycisk (Escape, Tab), `true`, gdy klawisz przesunął fokus, `false` — inny klawisz.
14
+ */
15
+ export declare function handleMenuKeydown(panel: HTMLElement, event: KeyboardEvent): 'close' | boolean;
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Klawiatura panelu menu (wzorzec WAI-ARIA „menu button”), wspólna dla `kpt-menu`
3
+ * i `kpt-split-button` — oba mają ten sam panel z pozycjami `role="menuitem"`.
4
+ * Port Angulara ma bliźniaczy plik `lib/menu/menu-keys.ts` — zmiana zachowania w obu.
5
+ */
6
+ /** Klawisze przesuwające fokus po pozycjach. */
7
+ const NAV_KEYS = new Set(['ArrowDown', 'ArrowUp', 'Home', 'End']);
8
+ /**
9
+ * Indeks pozycji, na którą ma przejść fokus. `count` to liczba **aktywnych** pozycji (wyłączone,
10
+ * separatory i nagłówki są już odfiltrowane), `current` to indeks obecnie skupionej albo `-1`.
11
+ * Strzałki zawijają się na końcach. Zwraca `null` dla klawisza, który nie nawiguje.
12
+ */
13
+ export function nextMenuIndex(count, current, key) {
14
+ if (!NAV_KEYS.has(key) || count === 0)
15
+ return null;
16
+ switch (key) {
17
+ case 'Home':
18
+ return 0;
19
+ case 'End':
20
+ return count - 1;
21
+ case 'ArrowDown':
22
+ return current < 0 ? 0 : (current + 1) % count;
23
+ default:
24
+ return current < 0 ? count - 1 : (current - 1 + count) % count;
25
+ }
26
+ }
27
+ /** Aktywne pozycje panelu w kolejności DOM. */
28
+ export function menuItems(panel) {
29
+ return [...panel.querySelectorAll('[role="menuitem"]:not(:disabled)')];
30
+ }
31
+ /** Skupia pierwszą albo ostatnią aktywną pozycję — po otwarciu menu z klawiatury lub myszy. */
32
+ export function focusMenuEdge(panel, edge) {
33
+ const items = menuItems(panel);
34
+ (edge === 'first' ? items[0] : items[items.length - 1])?.focus();
35
+ }
36
+ /**
37
+ * Obsługa klawisza w otwartym panelu. Zwraca `'close'`, gdy menu ma się zamknąć z powrotem
38
+ * fokusu na przycisk (Escape, Tab), `true`, gdy klawisz przesunął fokus, `false` — inny klawisz.
39
+ */
40
+ export function handleMenuKeydown(panel, event) {
41
+ if (event.key === 'Escape' || event.key === 'Tab') {
42
+ event.preventDefault();
43
+ return 'close';
44
+ }
45
+ const items = menuItems(panel);
46
+ const current = items.indexOf(document.activeElement);
47
+ const next = nextMenuIndex(items.length, current, event.key);
48
+ if (next === null)
49
+ return false;
50
+ event.preventDefault();
51
+ items[next].focus();
52
+ return true;
53
+ }
@@ -10,6 +10,6 @@ export function MenuRows({ items, block, onPick }) {
10
10
  if (item.header) {
11
11
  return (_jsxs("div", { className: `${block}__header`, role: "presentation", children: [item.avatar ? (_jsx(KptAvatar, { src: item.avatar.src ?? null, name: item.avatar.name ?? '', icon: item.avatar.icon ?? null, size: "sm" })) : item.icon ? (_jsx(KptIcon, { name: item.icon })) : null, _jsxs("span", { className: `${block}__text`, children: [item.label ? _jsx("span", { className: `${block}__label`, children: item.label }) : null, item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] })] }, index));
12
12
  }
13
- return (_jsxs("button", { type: "button", className: `${block}__item${item.danger ? ` ${block}__item--danger` : ''}`, role: "menuitem", disabled: item.disabled, onClick: () => onPick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsxs("span", { className: `${block}__text`, children: [_jsx("span", { className: `${block}__label`, children: item.label }), item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] }), item.badge !== undefined && item.badge !== null ? (_jsx(KptBadge, { className: `${block}__badge`, value: item.badge, variant: item.badgeVariant ?? 'primary' })) : null] }, index));
13
+ return (_jsxs("button", { type: "button", className: `${block}__item${item.danger ? ` ${block}__item--danger` : ''}`, role: "menuitem", tabIndex: -1, disabled: item.disabled, onClick: () => onPick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsxs("span", { className: `${block}__text`, children: [_jsx("span", { className: `${block}__label`, children: item.label }), item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] }), item.badge !== undefined && item.badge !== null ? (_jsx(KptBadge, { className: `${block}__badge`, value: item.badge, variant: item.badgeVariant ?? 'primary' })) : null] }, index));
14
14
  });
15
15
  }
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;