@konce-pt/react 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/roving.d.ts +9 -0
  96. package/dist/internal/roving.js +43 -0
  97. package/dist/internal/use-modal-focus.d.ts +10 -0
  98. package/dist/internal/use-modal-focus.js +57 -0
  99. package/dist/knob/knob.d.ts +5 -1
  100. package/dist/knob/knob.js +4 -2
  101. package/dist/knob/llms.txt +8 -0
  102. package/dist/listbox/listbox.d.ts +13 -1
  103. package/dist/listbox/listbox.js +89 -6
  104. package/dist/listbox/llms.txt +21 -5
  105. package/dist/megamenu/llms.txt +28 -10
  106. package/dist/megamenu/megamenu.d.ts +10 -1
  107. package/dist/megamenu/megamenu.js +116 -5
  108. package/dist/menu/llms.txt +12 -1
  109. package/dist/menu/menu-keys.d.ts +15 -0
  110. package/dist/menu/menu-keys.js +53 -0
  111. package/dist/menu/menu-rows.js +1 -1
  112. package/dist/menu/menu.js +52 -4
  113. package/dist/menubar/llms.txt +24 -7
  114. package/dist/menubar/menubar.d.ts +8 -1
  115. package/dist/menubar/menubar.js +92 -6
  116. package/dist/meter-group/llms.txt +10 -3
  117. package/dist/meter-group/meter-group.d.ts +9 -2
  118. package/dist/meter-group/meter-group.js +8 -2
  119. package/dist/order-list/llms.txt +11 -3
  120. package/dist/order-list/order-list.d.ts +5 -1
  121. package/dist/order-list/order-list.js +46 -8
  122. package/dist/panel/llms.txt +9 -6
  123. package/dist/panel/panel.js +3 -1
  124. package/dist/password/llms.txt +8 -0
  125. package/dist/password/password.d.ts +6 -1
  126. package/dist/password/password.js +6 -5
  127. package/dist/pick-list/llms.txt +12 -3
  128. package/dist/pick-list/pick-list.d.ts +5 -1
  129. package/dist/pick-list/pick-list.js +52 -5
  130. package/dist/popover/llms.txt +9 -2
  131. package/dist/popover/popover.d.ts +9 -3
  132. package/dist/popover/popover.js +41 -6
  133. package/dist/public-api.d.ts +6 -6
  134. package/dist/public-api.js +1 -1
  135. package/dist/radio-group/llms.txt +11 -0
  136. package/dist/radio-group/radio-group.d.ts +9 -1
  137. package/dist/radio-group/radio-group.js +5 -2
  138. package/dist/rating/llms.txt +18 -5
  139. package/dist/rating/rating.d.ts +21 -3
  140. package/dist/rating/rating.js +87 -12
  141. package/dist/rich-text/llms.txt +20 -0
  142. package/dist/rich-text/rich-text.d.ts +7 -1
  143. package/dist/rich-text/rich-text.js +155 -28
  144. package/dist/scroll-top/llms.txt +5 -1
  145. package/dist/scroll-top/scroll-top.js +3 -1
  146. package/dist/select/llms.txt +21 -7
  147. package/dist/select/select.d.ts +12 -1
  148. package/dist/select/select.js +110 -38
  149. package/dist/sidenav/llms.txt +6 -0
  150. package/dist/skeleton/llms.txt +3 -2
  151. package/dist/skeleton/skeleton.d.ts +1 -1
  152. package/dist/skeleton/skeleton.js +2 -2
  153. package/dist/slider/llms.txt +14 -1
  154. package/dist/slider/slider.d.ts +11 -2
  155. package/dist/slider/slider.js +15 -2
  156. package/dist/speed-dial/llms.txt +6 -2
  157. package/dist/speed-dial/speed-dial.d.ts +1 -1
  158. package/dist/speed-dial/speed-dial.js +14 -3
  159. package/dist/spinner/llms.txt +12 -8
  160. package/dist/spinner/spinner.d.ts +5 -1
  161. package/dist/spinner/spinner.js +12 -2
  162. package/dist/split-button/llms.txt +11 -3
  163. package/dist/split-button/split-button.d.ts +3 -1
  164. package/dist/split-button/split-button.js +43 -3
  165. package/dist/splitter/llms.txt +14 -4
  166. package/dist/splitter/splitter.d.ts +9 -2
  167. package/dist/splitter/splitter.js +15 -2
  168. package/dist/stepper/llms.txt +8 -4
  169. package/dist/stepper/stepper.d.ts +3 -2
  170. package/dist/stepper/stepper.js +7 -2
  171. package/dist/switch/llms.txt +10 -0
  172. package/dist/switch/switch.d.ts +1 -1
  173. package/dist/switch/switch.js +4 -2
  174. package/dist/switch-group/llms.txt +11 -0
  175. package/dist/switch-group/switch-group.d.ts +5 -1
  176. package/dist/switch-group/switch-group.js +7 -2
  177. package/dist/tabs/llms.txt +10 -4
  178. package/dist/tabs/tabs.d.ts +4 -0
  179. package/dist/tabs/tabs.js +21 -2
  180. package/dist/textarea/llms.txt +10 -0
  181. package/dist/textarea/textarea.d.ts +3 -1
  182. package/dist/textarea/textarea.js +13 -8
  183. package/dist/toast/llms.txt +21 -7
  184. package/dist/toast/toast-container.d.ts +6 -1
  185. package/dist/toast/toast-container.js +52 -1
  186. package/dist/toast/toast-store.d.ts +12 -0
  187. package/dist/toast/toast-store.js +46 -7
  188. package/dist/tooltip/llms.txt +10 -3
  189. package/dist/tooltip/tooltip.d.ts +5 -0
  190. package/dist/tooltip/tooltip.js +80 -7
  191. package/dist/tree/llms.txt +20 -8
  192. package/dist/tree/tree-keys.d.ts +27 -0
  193. package/dist/tree/tree-keys.js +39 -0
  194. package/dist/tree/tree.d.ts +12 -5
  195. package/dist/tree/tree.js +75 -29
  196. package/package.json +12 -12
@@ -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';
@@ -71,7 +71,7 @@ export { KptOverlay } from "./internal/overlay.js";
71
71
  export { KptButtonGroup } from "./button-group/button-group.js";
72
72
  export { KptSelect } from "./select/select.js";
73
73
  export { KptFormField } from "./form-field/form-field.js";
74
- export { useKptFormFieldContext, useKptFormFieldFilled } from "./form-field/form-field-context.js";
74
+ export { useKptFieldA11y, useKptFormFieldContext, useKptFormFieldFilled } from "./form-field/form-field-context.js";
75
75
  export { KptCheckbox } from "./checkbox/checkbox.js";
76
76
  export { KptAutocomplete } from "./autocomplete/autocomplete.js";
77
77
  export { KptListbox } from "./listbox/listbox.js";
@@ -31,6 +31,17 @@ interfere.
31
31
  <KptRadioGroup options={plans} value={plan} onValueChange={setPlan} />
32
32
  <KptRadioGroup name="plan" options={plans} defaultValue="pro" />
33
33
 
34
+ ## Screen reader and keyboard
35
+ `orientation`: 'vertical' (default) | 'horizontal' — options in a column or in a wrapping row; also
36
+ sets `aria-orientation`.
37
+
38
+ ## Label and description
39
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
40
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
41
+ `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
42
+ is added to `aria-describedby`. New: `id`, `ariaLabel` and `required` (→ `aria-required`) on the
43
+ `radiogroup`.
44
+
34
45
  ## Tokens
35
46
  `--kpt-color-primary` (the checked dot), `--kpt-form-field-border`,
36
47
  `--kpt-form-field-border-error`.
@@ -7,6 +7,14 @@ export interface KptRadioGroupProps<T> extends Omit<ComponentPropsWithRef<'div'>
7
7
  onValueChange?: (value: T) => void;
8
8
  /** Nazwa grupy w formularzu; pominięta = generowana. */
9
9
  name?: string;
10
+ /** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
11
+ id?: string;
12
+ /** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
13
+ ariaLabel?: string | null;
14
+ /** Oznacza grupę jako wymaganą atrybutem `aria-required`. */
15
+ required?: boolean;
16
+ /** Układ opcji: pod sobą (domyślnie) albo w rzędzie, z zawijaniem. */
17
+ orientation?: 'vertical' | 'horizontal';
10
18
  disabled?: boolean;
11
19
  invalid?: boolean;
12
20
  touched?: boolean;
@@ -21,4 +29,4 @@ export interface KptRadioGroupProps<T> extends Omit<ComponentPropsWithRef<'div'>
21
29
  * @example
22
30
  * <KptRadioGroup options={plans} value={plan} onValueChange={setPlan} />
23
31
  */
24
- export declare function KptRadioGroup<T>({ options, value, defaultValue, onValueChange, name, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptRadioGroupProps<T>>): import("react").JSX.Element;
32
+ export declare function KptRadioGroup<T>({ options, value, defaultValue, onValueChange, name, id, ariaLabel, required, orientation, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptRadioGroupProps<T>>): import("react").JSX.Element;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../internal/cn.js";
3
3
  import { useControllableState } from "../internal/use-controllable-state.js";
4
4
  import { useKptId } from "../internal/use-id.js";
5
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
5
6
  /**
6
7
  * Grupa przycisków opcji (radio). Opcje przez `options`.
7
8
  *
@@ -11,7 +12,9 @@ import { useKptId } from "../internal/use-id.js";
11
12
  * @example
12
13
  * <KptRadioGroup options={plans} value={plan} onValueChange={setPlan} />
13
14
  */
14
- export function KptRadioGroup({ options = [], value, defaultValue, onValueChange, name, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
15
+ export function KptRadioGroup({ options = [], value, defaultValue, onValueChange, name, id, ariaLabel = null, required = false, orientation = 'vertical', disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
16
+ /** Grupa nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
17
+ const field = useKptFieldA11y(id, 'kpt-radio-group');
15
18
  const groupName = useKptId(name, 'kpt-radio');
16
19
  const [current, setCurrent] = useControllableState({
17
20
  value,
@@ -19,5 +22,5 @@ export function KptRadioGroup({ options = [], value, defaultValue, onValueChange
19
22
  onChange: onValueChange,
20
23
  });
21
24
  const showInvalid = invalid && (touched ?? true);
22
- return (_jsx("div", { className: cn('kpt-radio-group-host', className), ...rest, children: _jsx("div", { className: "kpt-radio-group", role: "radiogroup", "aria-invalid": showInvalid || undefined, children: options.map((option) => (_jsxs("label", { className: cn('kpt-radio', (disabled || option.disabled) && 'kpt-radio--disabled'), children: [_jsx("input", { type: "radio", className: "kpt-radio__input", name: groupName, checked: option.value === current, disabled: disabled || option.disabled, onChange: () => setCurrent(option.value), onBlur: () => onTouch?.() }), _jsx("span", { className: "kpt-radio__dot", "aria-hidden": "true" }), _jsx("span", { className: "kpt-radio__label", children: option.label })] }, String(option.value)))) }) }));
25
+ return (_jsx("div", { className: cn('kpt-radio-group-host', className), ...rest, children: _jsx("div", { className: cn('kpt-radio-group', orientation === 'horizontal' && 'kpt-radio-group--horizontal'), role: "radiogroup", "aria-orientation": orientation, "aria-invalid": showInvalid || undefined, id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, children: options.map((option) => (_jsxs("label", { className: cn('kpt-radio', (disabled || option.disabled) && 'kpt-radio--disabled'), children: [_jsx("input", { type: "radio", className: "kpt-radio__input", name: groupName, checked: option.value === current, disabled: disabled || option.disabled, onChange: () => setCurrent(option.value), onBlur: () => onTouch?.() }), _jsx("span", { className: "kpt-radio__dot", "aria-hidden": "true" }), _jsx("span", { className: "kpt-radio__label", children: option.label })] }, String(option.value)))) }) }));
23
26
  }
@@ -5,8 +5,8 @@ Import: `import { KptRating } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-rating-host">
8
- <div class="kpt-rating" role="slider" aria-valuenow="3" aria-valuemax="5">
9
- <button class="kpt-rating__star is-filled" aria-label="1 of 5">…</button>
8
+ <div class="kpt-rating" role="radiogroup" aria-label="…">
9
+ <button class="kpt-rating__star is-filled" role="radio" aria-checked="false" tabindex="-1" aria-label="1 of 5">…</button>
10
10
  …count buttons…
11
11
  </div>
12
12
  </div>
@@ -14,9 +14,11 @@ Import: `import { KptRating } from '@konce-pt/react';`
14
14
  ## Props
15
15
  - `value` / `defaultValue` / `onValueChange` — the score as a number, `0` means unrated
16
16
  - `count`: number (default 5)
17
+ - `allowHalf`: boolean — half stars, the value goes in steps of 0.5
17
18
  - `readOnly`: boolean — display only, no clicking and no hover preview
18
19
  - `disabled`: boolean
19
- - `onTouch()` — fired together with a pick
20
+ - `onTouch()` — when focus leaves the group
21
+ - `id`, `ariaLabel`, `required`, `invalid`, `touched`
20
22
  - `className`, `ref` and every other `<div>` prop go to the host
21
23
 
22
24
  ## Clicking a lit star clears the score
@@ -35,9 +37,20 @@ per instance with the `locale` / `dictionary` props.
35
37
  <KptRating value={score} onValueChange={setScore} count={5} />
36
38
  <KptRating value={4} readOnly />
37
39
 
40
+ ## Half stars
41
+ `allowHalf` splits every star into two halves, each its own `role="radio"` ("3.5 of 5" — the number
42
+ follows the page language, PL "3,5 z 5"), so the group keeps the radio pattern: clicking the left half
43
+ gives x.5, and the arrows step by 0.5. Read-only stars show the value rounded to a half (4.3 → four
44
+ and a half stars) while the name carries the exact score ("4.3 of 5"), so an average can be shown
45
+ as it is.
46
+
38
47
  ## Tokens
39
48
  Lit stars `--kpt-color-warning`, empty ones `--kpt-color-border-strong`.
40
49
 
41
50
  ## Accessibility
42
- `role="slider"` with `aria-valuenow` / `aria-valuemax`; every star is a real `<button>`, so the
43
- row is reachable with Tab and each value can be picked from the keyboard.
51
+ `role="radiogroup"` named by `ariaLabel` or the `KptFormField` label; each star is
52
+ `role="radio"` with `aria-checked`, labelled from `rating.star` (`{value}`, `{max}`) — EN `1 of 5`,
53
+ PL `1 z 5`. One Tab stop: `→`/`↑` raise the score by one, `←`/`↓` lower it (mirrored in RTL),
54
+ `Home`/`End` set the extremes, `Delete`/`Backspace` clear it; clicking the chosen star also clears it.
55
+ `readOnly` renders `role="img"` named "4 of 5" with no buttons, so a screen reader does not announce
56
+ a row of disabled controls. `touch` fires when focus leaves the group.
@@ -6,17 +6,35 @@ export interface KptRatingProps extends Omit<ComponentPropsWithRef<'div'>, 'onCh
6
6
  onValueChange?: (value: number) => void;
7
7
  /** Liczba gwiazdek. */
8
8
  count?: number;
9
+ /** Połówki gwiazdek: wartość co 0,5, każda połówka to osobne `radio`. */
10
+ allowHalf?: boolean;
9
11
  /** Ocena tylko do odczytu — bez klikania i bez podglądu pod kursorem. */
10
12
  readOnly?: boolean;
13
+ /** Id grupy (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
14
+ id?: string;
15
+ /** Nazwa dostępna grupy, gdy nie opisuje jej etykieta (`KptFormField`). */
16
+ ariaLabel?: string | null;
17
+ /** Oznacza ocenę jako wymaganą atrybutem `aria-required`. */
18
+ required?: boolean;
11
19
  disabled?: boolean;
20
+ invalid?: boolean;
21
+ touched?: boolean;
12
22
  onTouch?: () => void;
13
23
  }
14
24
  /**
15
25
  * Ocena gwiazdkami. Kliknięcie zapalonej gwiazdki zeruje ocenę, więc wystawioną można cofnąć
16
26
  * bez osobnego przycisku „wyczyść".
17
27
  *
28
+ * Do edycji to grupa radiowa (`role="radiogroup"`, gwiazdki jako `role="radio"`): jeden przystanek
29
+ * Tab, strzałki zmieniają ocenę o krok (w RTL odwrócone w poziomie), `Home`/`End` ustawiają
30
+ * skrajne, `Delete`/`Backspace` zeruje. Z `allowHalf` każda gwiazdka ma dwie połówki — dwa osobne
31
+ * `radio` („3,5 z 5”), a krok strzałek to 0,5. Tylko do odczytu (`readOnly`) to obrazek z nazwą
32
+ * „4,3 z 5” — bez przycisków, które czytnik ogłaszałby jako wyłączone; gwiazdki pokazują wartość
33
+ * zaokrągloną do połówki.
34
+ *
18
35
  * @example
19
- * <KptRating value={score} onValueChange={setScore} count={5} />
20
- * <KptRating value={4} readOnly />
36
+ * <KptRating value={score} onValueChange={setScore} count={5} ariaLabel="Ocena" />
37
+ * <KptRating value={score} onValueChange={setScore} allowHalf ariaLabel="Ocena" />
38
+ * <KptRating value={4.3} readOnly />
21
39
  */
22
- export declare function KptRating({ value, defaultValue, onValueChange, count, readOnly, disabled, onTouch, locale, dictionary, className, ...rest }: Readonly<KptRatingProps>): import("react").JSX.Element;
40
+ export declare function KptRating({ value, defaultValue, onValueChange, count, allowHalf, readOnly, id, ariaLabel, required, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptRatingProps>): import("react").JSX.Element;