@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,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;
@@ -4,7 +4,7 @@ import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
5
  import { useKptId } from "../internal/use-id.js";
6
6
  import { useKptMessages } from "../i18n/i18n-context.js";
7
- import { useKptFormFieldContext } from "../form-field/form-field-context.js";
7
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
8
8
  import { KptIcon } from "../icon/icon.js";
9
9
  /** Cztery niezależne kryteria; każde spełnione podnosi wynik o jeden. */
10
10
  function scoreOf(password) {
@@ -25,13 +25,14 @@ function scoreOf(password) {
25
25
  * @example
26
26
  * <KptPassword value={password} onValueChange={setPassword} feedback autoComplete="new-password" />
27
27
  */
28
- export function KptPassword({ value, defaultValue = '', onValueChange, placeholder = '', id, autoComplete = 'current-password', feedback = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
28
+ export function KptPassword({ value, defaultValue = '', onValueChange, placeholder = '', id, autoComplete = 'current-password', feedback = false, ariaLabel = null, required = false, readOnly = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
29
29
  const { t, messages } = useKptMessages({ locale, dictionary });
30
- const field = useKptFormFieldContext();
31
- const inputId = useKptId(id ?? field?.controlId, 'kpt-password');
30
+ const field = useKptFieldA11y(id, 'kpt-password');
31
+ const inputId = field.id;
32
+ const meterId = useKptId(undefined, 'kpt-password-strength');
32
33
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
33
34
  const [visible, setVisible] = useState(false);
34
35
  const showInvalid = invalid && (touched ?? true);
35
36
  const score = scoreOf(current);
36
- return (_jsxs("div", { className: cn('kpt-password-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-password', disabled && 'kpt-password--disabled', showInvalid && 'kpt-password--invalid'), children: [_jsx("input", { className: "kpt-password__input", id: inputId, type: visible ? 'text' : 'password', value: current, disabled: disabled, placeholder: placeholder, autoComplete: autoComplete ?? undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("button", { type: "button", className: "kpt-password__toggle", disabled: disabled, onClick: () => setVisible(!visible), "aria-label": visible ? t('password.hide') : t('password.show'), children: _jsx(KptIcon, { name: visible ? 'eye-off' : 'eye' }) })] }), feedback && current ? (_jsxs("div", { className: "kpt-password__meter", "data-score": score, children: [_jsx("div", { className: "kpt-password__bars", children: [0, 1, 2, 3].map((index) => (_jsx("span", { className: cn('kpt-password__bar', index < score && 'is-on') }, index))) }), _jsx("span", { className: "kpt-password__hint", children: messages.password.strength[score] })] })) : null] }));
37
+ return (_jsxs("div", { className: cn('kpt-password-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-password', disabled && 'kpt-password--disabled', showInvalid && 'kpt-password--invalid'), children: [_jsx("input", { className: "kpt-password__input", id: inputId, type: visible ? 'text' : 'password', value: current, disabled: disabled, readOnly: readOnly, "aria-invalid": showInvalid || undefined, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-describedby": joinIds(field.describedBy, feedback && current && meterId), placeholder: placeholder, autoComplete: autoComplete ?? undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("button", { type: "button", className: "kpt-password__toggle", disabled: disabled, onClick: () => setVisible(!visible), "aria-label": visible ? t('password.hide') : t('password.show'), children: _jsx(KptIcon, { name: visible ? 'eye-off' : 'eye' }) })] }), feedback && current ? (_jsxs("div", { className: "kpt-password__meter", id: meterId, "data-score": score, children: [_jsx("div", { className: "kpt-password__bars", "aria-hidden": "true", children: [0, 1, 2, 3].map((index) => (_jsx("span", { className: cn('kpt-password__bar', index < score && 'is-on') }, index))) }), _jsx("span", { className: "kpt-password__hint", children: messages.password.strength[score] })] })) : null] }));
37
38
  }
@@ -60,7 +60,16 @@ double-click carry no visible label, so they add no dictionary keys.
60
60
  ## Tokens
61
61
  The selection `--kpt-color-primary`, the headers `--kpt-color-surface-variant`.
62
62
 
63
+ ## Keyboard
64
+ Each list is a `listbox` with `aria-multiselectable` and a single Tab stop (roving tabindex):
65
+ - `↓` / `↑` move between items (wrapping), `Home` / `End` jump to the ends;
66
+ - `Space` toggles the focused item's selection (`aria-selected`), the same as a click;
67
+ - `Enter` moves the focused item to the other list, the same as a double-click — focus stays at
68
+ that position in the list, on the next item.
69
+ The move buttons act on the same selection, so the whole control works without a mouse.
70
+
63
71
  ## Accessibility
64
- Every move button is labelled from the dictionary, so "move all right" is distinguishable from
65
- "move right" without seeing the double chevron. Native HTML5 drag and drop has no keyboard
66
- equivalent, so those buttons stay the full keyboard path — `dragdrop` never replaces them.
72
+ Each list is named after its header (`aria-label`). The four move buttons are labelled from the
73
+ dictionary, so "move all right" is distinguishable from "move right" without seeing the double
74
+ chevron. Native HTML5 drag and drop has no keyboard equivalent — `Enter` and the buttons cover
75
+ everything it does, and `dragdrop` adds to them, it never replaces them.
@@ -22,6 +22,10 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
22
22
  * Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
23
23
  * konkretną pozycję drugiego panelu.
24
24
  *
25
+ * Każda lista to `listbox` z wielokrotnym wyborem i jednym przystankiem Tab (roving tabindex):
26
+ * strzałki / `Home` / `End` przechodzą po pozycjach, spacja zaznacza, `Enter` przenosi pozycję
27
+ * na drugą stronę (jak dwuklik).
28
+ *
25
29
  * @example
26
30
  * <KptPickList
27
31
  * source={available} onSourceChange={setAvailable}
@@ -30,4 +34,4 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
30
34
  * renderItem={(item) => item.name}
31
35
  * />
32
36
  */
33
- export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
37
+ export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, dragdrop, renderItem, locale, dictionary, className, ref, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
@@ -1,9 +1,11 @@
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
  * PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
@@ -11,6 +13,10 @@ const EMPTY = [];
11
13
  * Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
12
14
  * konkretną pozycję drugiego panelu.
13
15
  *
16
+ * Każda lista to `listbox` z wielokrotnym wyborem i jednym przystankiem Tab (roving tabindex):
17
+ * strzałki / `Home` / `End` przechodzą po pozycjach, spacja zaznacza, `Enter` przenosi pozycję
18
+ * na drugą stronę (jak dwuklik).
19
+ *
14
20
  * @example
15
21
  * <KptPickList
16
22
  * source={available} onSourceChange={setAvailable}
@@ -19,7 +25,7 @@ const EMPTY = [];
19
25
  * renderItem={(item) => item.name}
20
26
  * />
21
27
  */
22
- export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
28
+ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ref, ...rest }) {
23
29
  const { t } = useKptMessages({ locale, dictionary });
24
30
  const [left, setLeft] = useControllableState({
25
31
  value: source,
@@ -41,11 +47,33 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
41
47
  const [over, setOver] = useState(null);
42
48
  /** Panel, nad którego pustym obszarem jest kursor — upuszczenie tam dopina na koniec. */
43
49
  const [listOver, setListOver] = useState(null);
50
+ /** Pozycja, która ostatnio miała fokus w każdej liście — ona jest przystankiem Tab. */
51
+ const [focusLeft, setFocusLeft] = useState(0);
52
+ const [focusRight, setFocusRight] = useState(0);
53
+ /** Pozycja, na którą po renderze trzeba przenieść fokus DOM (klawiatura). */
54
+ const [focusRequest, setFocusRequest] = useState(null);
55
+ const rootRef = useRef(null);
44
56
  const listOf = (side) => (side === 'source' ? left : right);
45
57
  const setListOf = (side) => (side === 'source' ? setLeft : setRight);
46
58
  const selectionOf = (side) => (side === 'source' ? selLeft : selRight);
47
59
  const setSelectionOf = (side) => side === 'source' ? setSelLeft : setSelRight;
48
60
  const other = (side) => (side === 'source' ? 'target' : 'source');
61
+ const focusOf = (side) => (side === 'source' ? focusLeft : focusRight);
62
+ const setFocusOf = (side) => (side === 'source' ? setFocusLeft : setFocusRight);
63
+ /** Przystanek Tab listy — ostatnio skupiona pozycja, przycięta do długości listy. */
64
+ const tabIndexOf = (side) => Math.min(focusOf(side), listOf(side).length - 1);
65
+ useEffect(() => {
66
+ if (!focusRequest)
67
+ return;
68
+ const list = rootRef.current?.querySelectorAll('.kpt-pick-list__items')[focusRequest.side === 'source' ? 0 : 1];
69
+ const options = list?.querySelectorAll('[role="option"]') ?? [];
70
+ options[Math.min(focusRequest.index, options.length - 1)]?.focus();
71
+ setFocusRequest(null);
72
+ }, [focusRequest]);
73
+ const focusItem = (side, index) => {
74
+ setFocusOf(side)(index);
75
+ setFocusRequest({ side, index });
76
+ };
49
77
  const toggle = (side, item) => {
50
78
  const next = new Set(selectionOf(side));
51
79
  if (next.has(item))
@@ -91,6 +119,25 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
91
119
  const to = other(from);
92
120
  transfer(from, index, to, listOf(to).length);
93
121
  };
122
+ const onItemKeyDown = (side, index, item, event) => {
123
+ if (event.key === ' ') {
124
+ event.preventDefault();
125
+ toggle(side, item);
126
+ return;
127
+ }
128
+ if (event.key === 'Enter') {
129
+ event.preventDefault();
130
+ moveOne(side, item);
131
+ // Pozycja zniknęła z listy — fokus zostaje w tym samym miejscu listy, na następnej.
132
+ focusItem(side, index);
133
+ return;
134
+ }
135
+ const next = nextMenuIndex(listOf(side).length, index, event.key);
136
+ if (next === null)
137
+ return;
138
+ event.preventDefault();
139
+ focusItem(side, next);
140
+ };
94
141
  const clearDrag = () => {
95
142
  setDrag(null);
96
143
  setOver(null);
@@ -162,10 +209,10 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
162
209
  const items = listOf(side);
163
210
  const selection = selectionOf(side);
164
211
  const header = side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
165
- return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
212
+ return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), role: "listbox", "aria-multiselectable": "true", "aria-label": header, onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
166
213
  const dragging = drag?.side === side && drag.index === index;
167
- return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
214
+ return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), role: "option", "aria-selected": selection.has(item), tabIndex: index === tabIndexOf(side) ? 0 : -1, onFocus: () => setFocusOf(side)(index), onKeyDown: (event) => onItemKeyDown(side, index, item, event), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
168
215
  }) })] }));
169
216
  };
170
- return (_jsx("div", { className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
217
+ return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
171
218
  }
@@ -14,6 +14,7 @@ Import: `import { KptPopover } from '@konce-pt/react';`
14
14
  - `trigger`: ReactNode — the element that opens the bubble, rendered in place
15
15
  - `children`: ReactNode — the panel content
16
16
  - `open` / `defaultOpen` / `onOpenChange` — controlled or uncontrolled
17
+ - `placement`: 'bottom' (default) | 'top' — the preferred side; it flips when there is no room
17
18
  - `className`, `ref` and every other `<span>` prop go to the host
18
19
 
19
20
  ## The panel is a portal
@@ -38,5 +39,11 @@ backdrop takes that click, exactly as the CDK backdrop does.
38
39
  `--kpt-z-overlay`.
39
40
 
40
41
  ## Accessibility
41
- The panel is `role="dialog"` and Escape closes it. Focus is not trapped — the popover is meant for
42
- supporting content, so for a real modal reach for a dialog instead.
42
+ The panel is `role="dialog"` with an `id` the trigger points to.
43
+
44
+ ## Focus and keyboard
45
+ The control inside the trigger slot gets `aria-haspopup="dialog"`, a live `aria-expanded` and, while
46
+ open, `aria-controls` — on the real `<button>`, not on the wrapper. Opening moves focus to the first
47
+ interactive element of the bubble; `Escape` or a click outside closes it, and focus returns to the
48
+ trigger when it was inside the bubble. Focus is not trapped — the popover is supporting content, not
49
+ a modal.
@@ -7,17 +7,23 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
7
7
  open?: boolean;
8
8
  defaultOpen?: boolean;
9
9
  onOpenChange?: (open: boolean) => void;
10
+ /** Preferowana strona dymka; gdy brakuje miejsca, przechodzi na drugą. */
11
+ placement?: 'bottom' | 'top';
10
12
  }
11
13
  /**
12
14
  * Popover — dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
13
15
  *
14
16
  * Port Angulara stoi na CDK Overlay; tutaj panel renderuje się w portalu do `document.body`,
15
- * z tym samym zachowaniem: pozycja pod wyzwalaczem z odbiciem w górę przy braku miejsca,
16
- * zamykanie kliknięciem poza panelem i klawiszem Escape.
17
+ * z tym samym zachowaniem: pozycja pod wyzwalaczem (albo nad nim przy `placement="top"`)
18
+ * z odbiciem przy braku miejsca, zamykanie kliknięciem poza panelem i klawiszem Escape.
19
+ *
20
+ * Dostępność (jak w Angularze): kontrolka w wyzwalaczu dostaje `aria-haspopup="dialog"`,
21
+ * `aria-expanded` i `aria-controls`; po otwarciu fokus przechodzi do pierwszego interaktywnego
22
+ * elementu dymka, a po zamknięciu wraca na wyzwalacz, jeśli był w dymku.
17
23
  *
18
24
  * @example
19
25
  * <KptPopover trigger={<KptButton variant="outline">Szczegóły</KptButton>}>
20
26
  * <div>Bogata treść popovera…</div>
21
27
  * </KptPopover>
22
28
  */
23
- export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
29
+ export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
@@ -1,27 +1,62 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useRef } from 'react';
2
+ 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
+ /** Elementy, które same przyjmują fokus — pierwszy z nich dostaje fokus po otwarciu dymka. */
8
+ const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select, textarea, [tabindex]:not([tabindex="-1"])';
6
9
  /**
7
10
  * Popover — dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
8
11
  *
9
12
  * Port Angulara stoi na CDK Overlay; tutaj panel renderuje się w portalu do `document.body`,
10
- * z tym samym zachowaniem: pozycja pod wyzwalaczem z odbiciem w górę przy braku miejsca,
11
- * zamykanie kliknięciem poza panelem i klawiszem Escape.
13
+ * z tym samym zachowaniem: pozycja pod wyzwalaczem (albo nad nim przy `placement="top"`)
14
+ * z odbiciem przy braku miejsca, zamykanie kliknięciem poza panelem i klawiszem Escape.
15
+ *
16
+ * Dostępność (jak w Angularze): kontrolka w wyzwalaczu dostaje `aria-haspopup="dialog"`,
17
+ * `aria-expanded` i `aria-controls`; po otwarciu fokus przechodzi do pierwszego interaktywnego
18
+ * elementu dymka, a po zamknięciu wraca na wyzwalacz, jeśli był w dymku.
12
19
  *
13
20
  * @example
14
21
  * <KptPopover trigger={<KptButton variant="outline">Szczegóły</KptButton>}>
15
22
  * <div>Bogata treść popovera…</div>
16
23
  * </KptPopover>
17
24
  */
18
- export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, className, ...rest }) {
25
+ export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', className, ...rest }) {
19
26
  const triggerRef = useRef(null);
27
+ const panelRef = useRef(null);
28
+ const panelId = useKptId(undefined, 'kpt-popover');
20
29
  const [isOpen, setOpen] = useControllableState({
21
30
  value: open,
22
31
  defaultValue: defaultOpen,
23
32
  onChange: onOpenChange,
24
33
  });
25
- const close = useCallback(() => setOpen(false), [setOpen]);
26
- return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: triggerRef, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, offset: 6, children: _jsx("div", { className: "kpt-popover__panel", role: "dialog", children: children }) })] }));
34
+ const findControl = () => triggerRef.current?.querySelector('button, a, [role="button"]') ?? null;
35
+ // ARIA na prawdziwej kontrolce w slocie, nie na opakowaniu `<span>` (patrz `KptMenu`).
36
+ useEffect(() => {
37
+ const control = findControl();
38
+ if (!control)
39
+ return;
40
+ control.setAttribute('aria-haspopup', 'dialog');
41
+ control.setAttribute('aria-expanded', String(isOpen));
42
+ if (isOpen)
43
+ control.setAttribute('aria-controls', panelId);
44
+ else
45
+ control.removeAttribute('aria-controls');
46
+ }, [isOpen, trigger, panelId]);
47
+ // Fokus do dymka klatkę później — `KptOverlay` trzyma panel niewidoczny, dopóki go nie zmierzy.
48
+ useEffect(() => {
49
+ if (!isOpen)
50
+ return;
51
+ const frame = requestAnimationFrame(() => panelRef.current?.querySelector(FOCUSABLE)?.focus());
52
+ return () => cancelAnimationFrame(frame);
53
+ }, [isOpen]);
54
+ const close = useCallback(() => {
55
+ const active = document.activeElement;
56
+ // Fokus wraca na wyzwalacz tylko wtedy, gdy był w dymku (albo przepadł na `<body>`).
57
+ if (!active || active === document.body || panelRef.current?.contains(active))
58
+ findControl()?.focus();
59
+ setOpen(false);
60
+ }, [setOpen]);
61
+ return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: triggerRef, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", children: children }) })] }));
27
62
  }
@@ -90,7 +90,7 @@ export type { KptConfirmOptions, KptConfirmRequest } from './confirm/confirm-sto
90
90
  export { KptToastContainer } from './toast/toast-container.tsx';
91
91
  export type { KptToastContainerProps } from './toast/toast-container.tsx';
92
92
  export { kptToast, useKptToasts } from './toast/toast-store.ts';
93
- export type { KptToast, KptToastOptions } from './toast/toast-store.ts';
93
+ export type { KptToast, KptToastAction, KptToastOptions } from './toast/toast-store.ts';
94
94
  export { KptMenu } from './menu/menu.tsx';
95
95
  export { KptMenubar } from './menubar/menubar.tsx';
96
96
  export type { KptMenubarProps } from './menubar/menubar.tsx';
@@ -123,8 +123,8 @@ export type { KptSelectProps } from './select/select.tsx';
123
123
  export type { KptAlertVariant, KptOption } from './shared/types.ts';
124
124
  export { KptFormField } from './form-field/form-field.tsx';
125
125
  export type { KptFormFieldLabelType, KptFormFieldProps } from './form-field/form-field.tsx';
126
- export { useKptFormFieldContext, useKptFormFieldFilled } from './form-field/form-field-context.ts';
127
- export type { KptFormFieldContextValue } from './form-field/form-field-context.ts';
126
+ export { useKptFieldA11y, useKptFormFieldContext, useKptFormFieldFilled } from './form-field/form-field-context.ts';
127
+ export type { KptFieldA11y, KptFormFieldContextValue } from './form-field/form-field-context.ts';
128
128
  export { KptCheckbox } from './checkbox/checkbox.tsx';
129
129
  export type { KptCheckboxProps } from './checkbox/checkbox.tsx';
130
130
  export { KptAutocomplete } from './autocomplete/autocomplete.tsx';
@@ -156,7 +156,7 @@ export type { KptDateRangeMode, KptDateRangeProps, KptDateRangeValue } from './d
156
156
  export { KptClock } from './clock/clock.tsx';
157
157
  export type { KptClockProps } from './clock/clock.tsx';
158
158
  export { KptColorPicker } from './color-picker/color-picker.tsx';
159
- export type { KptColorPickerProps } from './color-picker/color-picker.tsx';
159
+ export type { KptColorPickerProps, KptColorPreset } from './color-picker/color-picker.tsx';
160
160
  export { KptFileUpload } from './file-upload/file-upload.tsx';
161
161
  export type { KptFileUploadProps } from './file-upload/file-upload.tsx';
162
162
  export { KptSwitchGroup } from './switch-group/switch-group.tsx';
@@ -175,10 +175,10 @@ export type { KptIconButtonProps } from './icon-button/icon-button.tsx';
175
175
  export { KptFab } from './fab/fab.tsx';
176
176
  export type { KptFabProps } from './fab/fab.tsx';
177
177
  export { KptSplitButton } from './split-button/split-button.tsx';
178
- export type { KptSplitButtonProps } from './split-button/split-button.tsx';
178
+ export type { KptSplitButtonProps, KptSplitButtonVariant } from './split-button/split-button.tsx';
179
179
  export type { KptButtonProps, KptButtonSize, KptButtonVariant } from './button/button.tsx';
180
180
  export { KptCard, KptCardActions, KptCardFooter, KptCardHeader, KptCardLead, KptCardMedia, KptCardMeta, KptCardSection, KptCardSubtitle, KptCardTags, KptCardTitle, } from './card/card.tsx';
181
- export type { KptCardProps, KptCardVariant } from './card/card.tsx';
181
+ export type { KptCardHeadingLevel, KptCardProps, KptCardVariant } from './card/card.tsx';
182
182
  export { KptI18nProvider, useKptI18n, useKptMessages } from './i18n/i18n-context.tsx';
183
183
  export type { KptI18nContextValue, KptI18nOverrides, KptI18nProviderProps } from './i18n/i18n-context.tsx';
184
184
  export { KPT_MESSAGES_EN, KPT_MESSAGES_PL } from '@konce-pt/i18n';