@konce-pt/react 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/roving.d.ts +9 -0
  96. package/dist/internal/roving.js +43 -0
  97. package/dist/internal/use-modal-focus.d.ts +10 -0
  98. package/dist/internal/use-modal-focus.js +57 -0
  99. package/dist/knob/knob.d.ts +5 -1
  100. package/dist/knob/knob.js +4 -2
  101. package/dist/knob/llms.txt +8 -0
  102. package/dist/listbox/listbox.d.ts +13 -1
  103. package/dist/listbox/listbox.js +89 -6
  104. package/dist/listbox/llms.txt +21 -5
  105. package/dist/megamenu/llms.txt +28 -10
  106. package/dist/megamenu/megamenu.d.ts +10 -1
  107. package/dist/megamenu/megamenu.js +116 -5
  108. package/dist/menu/llms.txt +12 -1
  109. package/dist/menu/menu-keys.d.ts +15 -0
  110. package/dist/menu/menu-keys.js +53 -0
  111. package/dist/menu/menu-rows.js +1 -1
  112. package/dist/menu/menu.js +52 -4
  113. package/dist/menubar/llms.txt +24 -7
  114. package/dist/menubar/menubar.d.ts +8 -1
  115. package/dist/menubar/menubar.js +92 -6
  116. package/dist/meter-group/llms.txt +10 -3
  117. package/dist/meter-group/meter-group.d.ts +9 -2
  118. package/dist/meter-group/meter-group.js +8 -2
  119. package/dist/order-list/llms.txt +11 -3
  120. package/dist/order-list/order-list.d.ts +5 -1
  121. package/dist/order-list/order-list.js +46 -8
  122. package/dist/panel/llms.txt +9 -6
  123. package/dist/panel/panel.js +3 -1
  124. package/dist/password/llms.txt +8 -0
  125. package/dist/password/password.d.ts +6 -1
  126. package/dist/password/password.js +6 -5
  127. package/dist/pick-list/llms.txt +12 -3
  128. package/dist/pick-list/pick-list.d.ts +5 -1
  129. package/dist/pick-list/pick-list.js +52 -5
  130. package/dist/popover/llms.txt +9 -2
  131. package/dist/popover/popover.d.ts +9 -3
  132. package/dist/popover/popover.js +41 -6
  133. package/dist/public-api.d.ts +6 -6
  134. package/dist/public-api.js +1 -1
  135. package/dist/radio-group/llms.txt +11 -0
  136. package/dist/radio-group/radio-group.d.ts +9 -1
  137. package/dist/radio-group/radio-group.js +5 -2
  138. package/dist/rating/llms.txt +18 -5
  139. package/dist/rating/rating.d.ts +21 -3
  140. package/dist/rating/rating.js +87 -12
  141. package/dist/rich-text/llms.txt +20 -0
  142. package/dist/rich-text/rich-text.d.ts +7 -1
  143. package/dist/rich-text/rich-text.js +155 -28
  144. package/dist/scroll-top/llms.txt +5 -1
  145. package/dist/scroll-top/scroll-top.js +3 -1
  146. package/dist/select/llms.txt +21 -7
  147. package/dist/select/select.d.ts +12 -1
  148. package/dist/select/select.js +110 -38
  149. package/dist/sidenav/llms.txt +6 -0
  150. package/dist/skeleton/llms.txt +3 -2
  151. package/dist/skeleton/skeleton.d.ts +1 -1
  152. package/dist/skeleton/skeleton.js +2 -2
  153. package/dist/slider/llms.txt +14 -1
  154. package/dist/slider/slider.d.ts +11 -2
  155. package/dist/slider/slider.js +15 -2
  156. package/dist/speed-dial/llms.txt +6 -2
  157. package/dist/speed-dial/speed-dial.d.ts +1 -1
  158. package/dist/speed-dial/speed-dial.js +14 -3
  159. package/dist/spinner/llms.txt +12 -8
  160. package/dist/spinner/spinner.d.ts +5 -1
  161. package/dist/spinner/spinner.js +12 -2
  162. package/dist/split-button/llms.txt +11 -3
  163. package/dist/split-button/split-button.d.ts +3 -1
  164. package/dist/split-button/split-button.js +43 -3
  165. package/dist/splitter/llms.txt +14 -4
  166. package/dist/splitter/splitter.d.ts +9 -2
  167. package/dist/splitter/splitter.js +15 -2
  168. package/dist/stepper/llms.txt +8 -4
  169. package/dist/stepper/stepper.d.ts +3 -2
  170. package/dist/stepper/stepper.js +7 -2
  171. package/dist/switch/llms.txt +10 -0
  172. package/dist/switch/switch.d.ts +1 -1
  173. package/dist/switch/switch.js +4 -2
  174. package/dist/switch-group/llms.txt +11 -0
  175. package/dist/switch-group/switch-group.d.ts +5 -1
  176. package/dist/switch-group/switch-group.js +7 -2
  177. package/dist/tabs/llms.txt +10 -4
  178. package/dist/tabs/tabs.d.ts +4 -0
  179. package/dist/tabs/tabs.js +21 -2
  180. package/dist/textarea/llms.txt +10 -0
  181. package/dist/textarea/textarea.d.ts +3 -1
  182. package/dist/textarea/textarea.js +13 -8
  183. package/dist/toast/llms.txt +21 -7
  184. package/dist/toast/toast-container.d.ts +6 -1
  185. package/dist/toast/toast-container.js +52 -1
  186. package/dist/toast/toast-store.d.ts +12 -0
  187. package/dist/toast/toast-store.js +46 -7
  188. package/dist/tooltip/llms.txt +10 -3
  189. package/dist/tooltip/tooltip.d.ts +5 -0
  190. package/dist/tooltip/tooltip.js +80 -7
  191. package/dist/tree/llms.txt +20 -8
  192. package/dist/tree/tree-keys.d.ts +27 -0
  193. package/dist/tree/tree-keys.js +39 -0
  194. package/dist/tree/tree.d.ts +12 -5
  195. package/dist/tree/tree.js +75 -29
  196. package/package.json +12 -12
@@ -2,8 +2,10 @@ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  import type { KptMenuItem } from '../menu/menu-item.ts';
4
4
  import type { KptButtonVariant } from '../button/button.tsx';
5
+ /** Warianty, które mają styl — podzbiór `KptButtonVariant`. */
6
+ export type KptSplitButtonVariant = Extract<KptButtonVariant, 'filled' | 'tonal' | 'outline' | 'text'>;
5
7
  export interface KptSplitButtonProps<T> extends Omit<ComponentPropsWithRef<'span'>, 'onSelect'>, KptI18nOverrides {
6
- variant?: KptButtonVariant;
8
+ variant?: KptSplitButtonVariant;
7
9
  items?: readonly KptMenuItem<T>[];
8
10
  disabled?: boolean;
9
11
  /** Kliknięcie akcji głównej. */
@@ -1,11 +1,13 @@
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";
6
7
  import { useKptMessages } from "../i18n/i18n-context.js";
7
8
  import { KptIcon } from "../icon/icon.js";
8
9
  import { MenuRows } from "../menu/menu-rows.js";
10
+ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
9
11
  /**
10
12
  * Przycisk dzielony — akcja główna plus rozwijane menu dodatkowych akcji.
11
13
  *
@@ -18,17 +20,55 @@ import { MenuRows } from "../menu/menu-rows.js";
18
20
  export function KptSplitButton({ variant = 'filled', items = [], disabled = false, onPrimaryClick, onSelect, open, defaultOpen = false, onOpenChange, locale, dictionary, className, children, ...rest }) {
19
21
  const { t } = useKptMessages({ locale, dictionary });
20
22
  const toggleRef = useRef(null);
23
+ const panelRef = useRef(null);
24
+ /** Którą pozycję skupić po otwarciu — strzałka w górę na przycisku otwiera „od końca”. */
25
+ const focusEdge = useRef('first');
26
+ const panelId = useKptId(undefined, 'kpt-split-button-menu');
21
27
  const [isOpen, setOpen] = useControllableState({
22
28
  value: open,
23
29
  defaultValue: defaultOpen,
24
30
  onChange: onOpenChange,
25
31
  });
26
32
  const close = useCallback(() => setOpen(false), [setOpen]);
33
+ // Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
34
+ // panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
35
+ useEffect(() => {
36
+ if (!isOpen)
37
+ return;
38
+ const frame = requestAnimationFrame(() => {
39
+ if (panelRef.current)
40
+ focusMenuEdge(panelRef.current, focusEdge.current);
41
+ });
42
+ return () => cancelAnimationFrame(frame);
43
+ }, [isOpen]);
44
+ /** Fokus wraca na strzałkę — inaczej po zniknięciu panelu spadłby na `<body>`. */
45
+ const closeAndRestoreFocus = () => {
46
+ close();
47
+ toggleRef.current?.focus();
48
+ };
49
+ const onToggleKeyDown = (event) => {
50
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
51
+ return;
52
+ event.preventDefault();
53
+ focusEdge.current = event.key === 'ArrowUp' ? 'last' : 'first';
54
+ if (isOpen && panelRef.current)
55
+ focusMenuEdge(panelRef.current, focusEdge.current);
56
+ else
57
+ setOpen(true);
58
+ };
59
+ const onPanelKeyDown = (event) => {
60
+ if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close') {
61
+ closeAndRestoreFocus();
62
+ }
63
+ };
27
64
  const pick = (item) => {
28
65
  if (item.disabled)
29
66
  return;
30
67
  onSelect?.(item);
31
- close();
68
+ closeAndRestoreFocus();
32
69
  };
33
- return (_jsxs("span", { className: cn('kpt-split-button-host', className), ...rest, children: [_jsxs("div", { className: "kpt-split-button", "data-variant": variant, children: [_jsx("button", { className: "kpt-split-button__main", type: "button", disabled: disabled, onClick: onPrimaryClick, children: children }), _jsx("button", { ref: toggleRef, className: "kpt-split-button__toggle", type: "button", disabled: disabled, onClick: () => setOpen(!isOpen), "aria-label": t('splitButton.more'), children: _jsx(KptIcon, { name: "chevron-down" }) })] }), _jsx(KptOverlay, { open: isOpen, anchor: toggleRef.current, onClose: close, align: "end", children: _jsx("div", { className: "kpt-split-button__panel", role: "menu", children: _jsx(MenuRows, { items: items, block: "kpt-split-button", onPick: pick }) }) })] }));
70
+ return (_jsxs("span", { className: cn('kpt-split-button-host', className), ...rest, children: [_jsxs("div", { className: "kpt-split-button", "data-variant": variant, children: [_jsx("button", { className: "kpt-split-button__main", type: "button", disabled: disabled, onClick: onPrimaryClick, children: children }), _jsx("button", { ref: toggleRef, className: "kpt-split-button__toggle", type: "button", disabled: disabled, onClick: () => {
71
+ focusEdge.current = 'first';
72
+ setOpen(!isOpen);
73
+ }, onKeyDown: onToggleKeyDown, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, "aria-label": t('splitButton.more'), children: _jsx(KptIcon, { name: "chevron-down" }) })] }), _jsx(KptOverlay, { open: isOpen, anchor: toggleRef.current, onClose: close, align: "end", children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-split-button__panel", role: "menu", onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-split-button", onPick: pick }) }) })] }));
34
74
  }
@@ -6,8 +6,10 @@ Import: `import { KptSplitter } from '@konce-pt/react';`
6
6
  ## DOM
7
7
  <div class="kpt-splitter-host">
8
8
  <div class="kpt-splitter" data-orientation="horizontal">
9
- <div class="kpt-splitter__pane" style="flex-basis:40%">…</div>
10
- <div class="kpt-splitter__gutter" role="separator" tabindex="0" aria-valuenow="40"></div>
9
+ <div class="kpt-splitter__pane" id="kpt-splitter-start-r1" style="flex-basis:40%">…</div>
10
+ <div class="kpt-splitter__gutter" role="separator" tabindex="0" aria-orientation="vertical"
11
+ aria-valuenow="40" aria-valuemin="10" aria-valuemax="90"
12
+ aria-controls="kpt-splitter-start-r1" aria-label="Resize panes"></div>
11
13
  <div class="kpt-splitter__pane" style="flex-basis:60%">…</div>
12
14
  </div>
13
15
  </div>
@@ -16,6 +18,10 @@ Import: `import { KptSplitter } from '@konce-pt/react';`
16
18
  - `orientation`: `'horizontal'` (default) | `'vertical'`
17
19
  - `startSize` / `defaultStartSize` / `onStartSizeChange` — the first pane's size in percent
18
20
  - `minSize`: number (percent, default 10) — neither pane goes below it
21
+ - `ariaLabel`: string — the handle's name; empty = `splitter.resize` from the dictionary
22
+ - `ariaLabel`: string — the handle's name; empty = `splitter.resize` from the dictionary
23
+ - `ariaLabel`: string — the handle's name; empty = `splitter.resize` from the dictionary
24
+ - `ariaLabel`: string — the handle's name; empty = `splitter.resize` from the dictionary
19
25
  - `className`, `ref` and every other `<div>` prop go to the host
20
26
 
21
27
  ## Panes
@@ -40,5 +46,9 @@ The handle `--kpt-color-border` → `--kpt-color-primary` on hover and focus; th
40
46
  `--kpt-color-border` with `--kpt-radius-md`.
41
47
 
42
48
  ## Accessibility
43
- The handle is `role="separator"` with `aria-orientation` and `aria-valuenow`, reachable with Tab;
44
- the arrow keys resize by 2% a press, clamped by `minSize`.
49
+ The handle is a focusable `role="separator"` (the WAI-ARIA window splitter): `aria-valuenow`,
50
+ `aria-valuemin`/`aria-valuemax` (from `minSize`), `aria-controls` pointing at the first pane, and a
51
+ name from `ariaLabel` or `splitter.resize` in the dictionary. Arrow keys resize by 2%, `Home`/`End`
52
+ jump to the minimum/maximum. `aria-orientation` describes the handle's line, not the layout —
53
+ side-by-side panes have a vertical handle. The host takes `height: 100%`, so a height set on the
54
+ parent reaches the panes.
@@ -1,4 +1,5 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
2
3
  export type KptSplitterOrientation = 'horizontal' | 'vertical';
3
4
  export interface KptSplitterPaneProps {
4
5
  children?: ReactNode;
@@ -12,7 +13,7 @@ export declare function KptSplitterEnd(_props: Readonly<KptSplitterPaneProps>):
12
13
  export declare namespace KptSplitterEnd {
13
14
  var kptSplitterSlot: "end";
14
15
  }
15
- export interface KptSplitterProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'> {
16
+ export interface KptSplitterProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'>, KptI18nOverrides {
16
17
  orientation?: KptSplitterOrientation;
17
18
  /** Rozmiar pierwszego panelu w procentach (0–100). */
18
19
  startSize?: number;
@@ -20,19 +21,25 @@ export interface KptSplitterProps extends Omit<ComponentPropsWithRef<'div'>, 'on
20
21
  onStartSizeChange?: (size: number) => void;
21
22
  /** Minimalny rozmiar panelu w procentach. */
22
23
  minSize?: number;
24
+ /** Nazwa uchwytu dla czytników ekranu; domyślnie `splitter.resize` ze słownika. */
25
+ ariaLabel?: string;
23
26
  children?: ReactNode;
24
27
  }
25
28
  /**
26
29
  * Dwa panele z przeciąganym uchwytem. `horizontal` stawia je obok siebie, `vertical` jeden pod
27
30
  * drugim.
28
31
  *
32
+ * Uchwyt to `role="separator"` z wartością (wzorzec WAI-ARIA „window splitter”): strzałki
33
+ * przesuwają go o 2%, `Home`/`End` dosuwają do minimum i maksimum. `aria-orientation` opisuje
34
+ * linię uchwytu, nie układ — przy panelach obok siebie linia jest pionowa.
35
+ *
29
36
  * @example
30
37
  * <KptSplitter startSize={size} onStartSizeChange={setSize}>
31
38
  * <KptSplitter.Start>Lewy</KptSplitter.Start>
32
39
  * <KptSplitter.End>Prawy</KptSplitter.End>
33
40
  * </KptSplitter>
34
41
  */
35
- export declare function KptSplitter({ orientation, startSize, defaultStartSize, onStartSizeChange, minSize, className, children, ...rest }: Readonly<KptSplitterProps>): import("react").JSX.Element;
42
+ export declare function KptSplitter({ orientation, startSize, defaultStartSize, onStartSizeChange, minSize, ariaLabel, locale, dictionary, className, children, ...rest }: Readonly<KptSplitterProps>): import("react").JSX.Element;
36
43
  export declare namespace KptSplitter {
37
44
  var Start: typeof KptSplitterStart;
38
45
  var End: typeof KptSplitterEnd;
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, isValidElement, useRef, } 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";
6
+ import { useKptMessages } from "../i18n/i18n-context.js";
5
7
  /** Panel splittera. Umieszczaj jako `<KptSplitter.Start>` i `<KptSplitter.End>`. */
6
8
  export function KptSplitterStart(_props) {
7
9
  // Panel jest nośnikiem treści: `KptSplitter` czyta jego dzieci i sam opakowuje je w `__pane`,
@@ -17,14 +19,20 @@ KptSplitterEnd.kptSplitterSlot = 'end';
17
19
  * Dwa panele z przeciąganym uchwytem. `horizontal` stawia je obok siebie, `vertical` jeden pod
18
20
  * drugim.
19
21
  *
22
+ * Uchwyt to `role="separator"` z wartością (wzorzec WAI-ARIA „window splitter”): strzałki
23
+ * przesuwają go o 2%, `Home`/`End` dosuwają do minimum i maksimum. `aria-orientation` opisuje
24
+ * linię uchwytu, nie układ — przy panelach obok siebie linia jest pionowa.
25
+ *
20
26
  * @example
21
27
  * <KptSplitter startSize={size} onStartSizeChange={setSize}>
22
28
  * <KptSplitter.Start>Lewy</KptSplitter.Start>
23
29
  * <KptSplitter.End>Prawy</KptSplitter.End>
24
30
  * </KptSplitter>
25
31
  */
26
- export function KptSplitter({ orientation = 'horizontal', startSize, defaultStartSize = 50, onStartSizeChange, minSize = 10, className, children, ...rest }) {
32
+ export function KptSplitter({ orientation = 'horizontal', startSize, defaultStartSize = 50, onStartSizeChange, minSize = 10, ariaLabel, locale, dictionary, className, children, ...rest }) {
27
33
  const containerRef = useRef(null);
34
+ const { t } = useKptMessages({ locale, dictionary });
35
+ const startId = useKptId(undefined, 'kpt-splitter-start');
28
36
  const [size, setSize] = useControllableState({
29
37
  value: startSize,
30
38
  defaultValue: defaultStartSize,
@@ -64,6 +72,11 @@ export function KptSplitter({ orientation = 'horizontal', startSize, defaultStar
64
72
  window.addEventListener('pointerup', up);
65
73
  };
66
74
  const onKeyDown = (event) => {
75
+ if (event.key === 'Home' || event.key === 'End') {
76
+ event.preventDefault();
77
+ setSize(clamp(event.key === 'Home' ? 0 : 100));
78
+ return;
79
+ }
67
80
  const dir = event.key === 'ArrowLeft' || event.key === 'ArrowUp'
68
81
  ? -2
69
82
  : event.key === 'ArrowRight' || event.key === 'ArrowDown'
@@ -74,7 +87,7 @@ export function KptSplitter({ orientation = 'horizontal', startSize, defaultStar
74
87
  event.preventDefault();
75
88
  setSize(clamp(size + dir));
76
89
  };
77
- return (_jsx("div", { className: cn('kpt-splitter-host', className), ...rest, children: _jsxs("div", { ref: containerRef, className: "kpt-splitter", "data-orientation": orientation, children: [_jsx("div", { className: "kpt-splitter__pane", style: { flexBasis: `${size}%` }, children: start }), _jsx("div", { className: "kpt-splitter__gutter", role: "separator", tabIndex: 0, "aria-orientation": orientation, "aria-valuenow": size, onPointerDown: onPointerDown, onKeyDown: onKeyDown }), _jsx("div", { className: "kpt-splitter__pane", style: { flexBasis: `${100 - size}%` }, children: end })] }) }));
90
+ return (_jsx("div", { className: cn('kpt-splitter-host', className), ...rest, children: _jsxs("div", { ref: containerRef, className: "kpt-splitter", "data-orientation": orientation, children: [_jsx("div", { className: "kpt-splitter__pane", id: startId, style: { flexBasis: `${size}%` }, children: start }), _jsx("div", { className: "kpt-splitter__gutter", role: "separator", tabIndex: 0, "aria-orientation": orientation === 'horizontal' ? 'vertical' : 'horizontal', "aria-valuenow": size, "aria-valuemin": minSize, "aria-valuemax": 100 - minSize, "aria-controls": startId, "aria-label": ariaLabel || t('splitter.resize'), onPointerDown: onPointerDown, onKeyDown: onKeyDown }), _jsx("div", { className: "kpt-splitter__pane", style: { flexBasis: `${100 - size}%` }, children: end })] }) }));
78
91
  }
79
92
  KptSplitter.Start = KptSplitterStart;
80
93
  KptSplitter.End = KptSplitterEnd;
@@ -7,8 +7,8 @@ Import: `import { KptStepper, useKptStepper } from '@konce-pt/react';`
7
7
  <div class="kpt-stepper-host" style="--kpt-anim-duration:500ms; --kpt-anim-dir:1">
8
8
  <ol class="kpt-stepper__header">
9
9
  <li class="kpt-stepper__step is-done">
10
- <button class="kpt-stepper__marker">✓</button>
11
- <span class="kpt-stepper__label">Account</span>
10
+ <button class="kpt-stepper__marker" aria-label="Step 1: Account, completed">✓</button>
11
+ <span class="kpt-stepper__label" aria-hidden="true">Account</span>
12
12
  <span class="kpt-stepper__connector"></span>
13
13
  </li>
14
14
  …
@@ -60,5 +60,9 @@ The active and completed step: `--kpt-color-primary` (marker, connector); the re
60
60
  `--kpt-color-border-strong` and `--kpt-color-on-surface-muted`.
61
61
 
62
62
  ## Accessibility
63
- The header is an `<ol>` of real buttons — every step is reachable with the keyboard; the active
64
- marker carries `aria-current="step"`.
63
+ The header is an `<ol>` of real buttons, so every step is reachable with the keyboard and the list
64
+ announces how many there are. Each marker is named from the dictionary — `stepper.step`
65
+ (“Step 2: Delivery”) or `stepper.stepDone` (“…, completed”) — because the visible marker is only a
66
+ number or a check; the visible label is `aria-hidden` so it is not read twice. The active marker
67
+ carries `aria-current="step"`. After `next()` focus stays where it was — move it into the new step
68
+ yourself when the step is long.
@@ -1,4 +1,5 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
2
3
  export interface KptStepperContextValue {
3
4
  activeIndex: number;
4
5
  /** Liczba kroków — pozwala treści kroku ukryć „Dalej" na ostatnim. */
@@ -27,7 +28,7 @@ export declare function KptStep(_props: Readonly<KptStepProps>): ReactNode;
27
28
  export declare namespace KptStep {
28
29
  var kptStepperSlot: "step";
29
30
  }
30
- export interface KptStepperProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'> {
31
+ export interface KptStepperProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'>, KptI18nOverrides {
31
32
  activeIndex?: number;
32
33
  defaultActiveIndex?: number;
33
34
  onActiveIndexChange?: (index: number) => void;
@@ -45,7 +46,7 @@ export interface KptStepperProps extends Omit<ComponentPropsWithRef<'div'>, 'onC
45
46
  * <KptStepper.Step label="Profil">…</KptStepper.Step>
46
47
  * </KptStepper>
47
48
  */
48
- export declare function KptStepper({ activeIndex, defaultActiveIndex, onActiveIndexChange, animated, animationDuration, className, style, children, ...rest }: Readonly<KptStepperProps>): import("react").JSX.Element;
49
+ export declare function KptStepper({ activeIndex, defaultActiveIndex, onActiveIndexChange, animated, animationDuration, locale, dictionary, className, style, children, ...rest }: Readonly<KptStepperProps>): import("react").JSX.Element;
49
50
  export declare namespace KptStepper {
50
51
  var Step: typeof KptStep;
51
52
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, createContext, isValidElement, useContext, useState, } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptMessages } from "../i18n/i18n-context.js";
5
6
  import { KptIcon } from "../icon/icon.js";
6
7
  const StepperContext = createContext(null);
7
8
  /**
@@ -36,7 +37,8 @@ KptStep.kptStepperSlot = 'step';
36
37
  * <KptStepper.Step label="Profil">…</KptStepper.Step>
37
38
  * </KptStepper>
38
39
  */
39
- export function KptStepper({ activeIndex, defaultActiveIndex = 0, onActiveIndexChange, animated = true, animationDuration = 500, className, style, children, ...rest }) {
40
+ export function KptStepper({ activeIndex, defaultActiveIndex = 0, onActiveIndexChange, animated = true, animationDuration = 500, locale, dictionary, className, style, children, ...rest }) {
41
+ const { t } = useKptMessages({ locale, dictionary });
40
42
  const [active, setActive] = useControllableState({
41
43
  value: activeIndex,
42
44
  defaultValue: defaultActiveIndex,
@@ -66,6 +68,9 @@ export function KptStepper({ activeIndex, defaultActiveIndex = 0, onActiveIndexC
66
68
  ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`,
67
69
  ['--kpt-anim-dir']: direction,
68
70
  ...style,
69
- }, ...rest, children: [_jsx("ol", { className: "kpt-stepper__header", children: steps.map((step, index) => (_jsxs("li", { className: cn('kpt-stepper__step', index === active && 'is-active', index < active && 'is-done'), children: [_jsx("button", { type: "button", className: "kpt-stepper__marker", "aria-current": index === active ? 'step' : undefined, onClick: () => goTo(index), children: index < active ? _jsx(KptIcon, { name: "check" }) : index + 1 }), _jsx("span", { className: "kpt-stepper__label", children: step.label }), index < steps.length - 1 ? _jsx("span", { className: "kpt-stepper__connector" }) : null] }, index))) }), _jsx(StepperContext.Provider, { value: context, children: _jsx("div", { className: "kpt-stepper__body", children: steps[active] ? (_jsx("div", { className: "kpt-stepper__panel", children: steps[active].children }, active)) : null }) })] }));
71
+ }, ...rest, children: [_jsx("ol", { className: "kpt-stepper__header", children: steps.map((step, index) => (_jsxs("li", { className: cn('kpt-stepper__step', index === active && 'is-active', index < active && 'is-done'), children: [_jsx("button", { type: "button", className: "kpt-stepper__marker", "aria-current": index === active ? 'step' : undefined, "aria-label": t(index < active ? 'stepper.stepDone' : 'stepper.step', {
72
+ index: index + 1,
73
+ label: step.label,
74
+ }), onClick: () => goTo(index), children: index < active ? _jsx(KptIcon, { name: "check" }) : index + 1 }), _jsx("span", { className: "kpt-stepper__label", "aria-hidden": "true", children: step.label }), index < steps.length - 1 ? _jsx("span", { className: "kpt-stepper__connector" }) : null] }, index))) }), _jsx(StepperContext.Provider, { value: context, children: _jsx("div", { className: "kpt-stepper__body", children: steps[active] ? (_jsx("div", { className: "kpt-stepper__panel", children: steps[active].children }, active)) : null }) })] }));
70
75
  }
71
76
  KptStepper.Step = KptStep;
@@ -33,6 +33,16 @@ show one.
33
33
  <KptSwitch checked={on} onCheckedChange={setOn} onLabel="On" offLabel="Off" />
34
34
  <KptSwitch checked={on} onCheckedChange={setOn} label="Dark mode" labelPosition="start" />
35
35
 
36
+ ## Screen reader and keyboard
37
+ The `onLabel`/`offLabel` text is `aria-hidden`: `role="switch"` already announces the state, and
38
+ text inside the `<label>` would change the switch's name on every toggle.
39
+
40
+ ## Label and description
41
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
42
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
43
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
44
+ Inside `kpt-form-field` the switch gets both names: the wrapper label and its own `label`.
45
+
36
46
  ## Tokens
37
47
  `--kpt-color-primary` (the on track), `--kpt-color-muted` (the off track), `--kpt-color-surface`
38
48
  (the thumb), `--kpt-motion-duration-fast`.
@@ -36,4 +36,4 @@ export interface KptSwitchProps extends Omit<ComponentPropsWithRef<'input'>, 'ty
36
36
  * <KptSwitch name="notifications" label="Powiadomienia" />
37
37
  * <KptSwitch checked={on} onCheckedChange={setOn} onLabel="Włączony" offLabel="Wyłączony" />
38
38
  */
39
- export declare function KptSwitch({ label, labelPosition, onLabel, offLabel, required, invalid, touched, onCheckedChange, onTouch, hostProps, className, disabled, checked, onChange, onBlur, children, 'aria-invalid': ariaInvalid, ...rest }: Readonly<KptSwitchProps>): import("react").JSX.Element;
39
+ export declare function KptSwitch({ label, labelPosition, onLabel, offLabel, required, invalid, touched, onCheckedChange, onTouch, hostProps, className, disabled, checked, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, ...rest }: Readonly<KptSwitchProps>): import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
4
5
  /**
5
6
  * Przełącznik (switch/toggle). Pod spodem natywny `<input type="checkbox" role="switch">`,
6
7
  * więc klawiatura i czytniki ekranu działają bez dodatkowej obsługi.
@@ -14,7 +15,8 @@ import { cn } from "../internal/cn.js";
14
15
  * <KptSwitch name="notifications" label="Powiadomienia" />
15
16
  * <KptSwitch checked={on} onCheckedChange={setOn} onLabel="Włączony" offLabel="Wyłączony" />
16
17
  */
17
- export function KptSwitch({ label = '', labelPosition = 'end', onLabel = '', offLabel = '', required = false, invalid = false, touched, onCheckedChange, onTouch, hostProps, className, disabled = false, checked, onChange, onBlur, children, 'aria-invalid': ariaInvalid, ...rest }) {
18
+ export function KptSwitch({ label = '', labelPosition = 'end', onLabel = '', offLabel = '', required = false, invalid = false, touched, onCheckedChange, onTouch, hostProps, className, disabled = false, checked, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, ...rest }) {
19
+ const field = useKptFieldA11y(id, 'kpt-switch');
18
20
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
19
21
  const showInvalid = invalid && (touched ?? true);
20
22
  const hasStateLabels = !!(onLabel || offLabel);
@@ -26,5 +28,5 @@ export function KptSwitch({ label = '', labelPosition = 'end', onLabel = '', off
26
28
  onBlur?.(event);
27
29
  onTouch?.();
28
30
  }, [onBlur, onTouch]);
29
- return (_jsx("span", { className: cn('kpt-switch-host', hostClassName), ...hostRest, children: _jsxs("label", { className: cn('kpt-switch', disabled && 'kpt-switch--disabled', className), children: [label && labelPosition === 'start' ? (_jsx("span", { className: "kpt-switch__label kpt-switch__label--start", children: label })) : null, _jsx("input", { type: "checkbox", role: "switch", className: "kpt-switch__input", checked: checked, disabled: disabled, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }), _jsx("span", { className: "kpt-switch__track", "aria-hidden": "true", children: _jsx("span", { className: "kpt-switch__thumb" }) }), label && labelPosition === 'end' ? _jsx("span", { className: "kpt-switch__label", children: label }) : null, hasStateLabels ? (_jsx("span", { className: cn('kpt-switch__state', checked && 'kpt-switch__state--on'), children: checked ? onLabel : offLabel })) : null, children] }) }));
31
+ return (_jsx("span", { className: cn('kpt-switch-host', hostClassName), ...hostRest, children: _jsxs("label", { className: cn('kpt-switch', disabled && 'kpt-switch--disabled', className), children: [label && labelPosition === 'start' ? (_jsx("span", { className: "kpt-switch__label kpt-switch__label--start", children: label })) : null, _jsx("input", { type: "checkbox", role: "switch", className: "kpt-switch__input", id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), checked: checked, disabled: disabled, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }), _jsx("span", { className: "kpt-switch__track", "aria-hidden": "true", children: _jsx("span", { className: "kpt-switch__thumb" }) }), label && labelPosition === 'end' ? _jsx("span", { className: "kpt-switch__label", children: label }) : null, hasStateLabels ? (_jsx("span", { className: cn('kpt-switch__state', checked && 'kpt-switch__state--on'), "aria-hidden": "true", children: checked ? onLabel : offLabel })) : null, children] }) }));
30
32
  }
@@ -34,6 +34,17 @@ re-applies the controlled `checked` and the switch snaps back on.
34
34
  <KptSwitchGroup multiple value={channels} onValueChange={setChannels} options={options} />
35
35
  <KptSwitchGroup orientation="horizontal" value={mode} onValueChange={setMode} options={options} />
36
36
 
37
+ ## Screen reader and keyboard
38
+ `aria-invalid` is not supported on `role="group"`, so the error state is carried by each switch. In
39
+ single mode the switches are mutually exclusive and nothing announces that turning one on turned
40
+ another off — for a single choice prefer `KptRadioGroup`.
41
+
42
+ ## Label and description
43
+ `id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
44
+ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
45
+ `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
46
+ is added to `aria-describedby`. New: `id` and `ariaLabel` on the `group`.
47
+
37
48
  ## Tokens
38
49
  Layout only: `--kpt-space-2` (vertical gap) and `--kpt-space-4` (horizontal). The look comes from
39
50
  `KptSwitch` and its tokens.
@@ -11,6 +11,10 @@ export interface KptSwitchGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
11
11
  orientation?: 'vertical' | 'horizontal';
12
12
  /** Czy wyłączenie aktywnej opcji zeruje wartość. Dotyczy tylko trybu pojedynczego. */
13
13
  allowDeselect?: boolean;
14
+ /** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
15
+ id?: string;
16
+ /** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
17
+ ariaLabel?: string | null;
14
18
  disabled?: boolean;
15
19
  invalid?: boolean;
16
20
  touched?: boolean;
@@ -26,4 +30,4 @@ export interface KptSwitchGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
26
30
  * <KptSwitchGroup options={plans} value={plan} onValueChange={setPlan} />
27
31
  * <KptSwitchGroup multiple options={tags} value={selected} onValueChange={setSelected} />
28
32
  */
29
- export declare function KptSwitchGroup<T>({ options, value, defaultValue, onValueChange, multiple, orientation, allowDeselect, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptSwitchGroupProps<T>>): import("react").JSX.Element;
33
+ export declare function KptSwitchGroup<T>({ options, value, defaultValue, onValueChange, multiple, orientation, allowDeselect, id, ariaLabel, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptSwitchGroupProps<T>>): import("react").JSX.Element;
@@ -2,6 +2,7 @@ import { jsx as _jsx } 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 { KptSwitch } from "../switch/switch.js";
5
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
5
6
  /**
6
7
  * Grupa przełączników. Domyślnie wzajemnie wykluczających się — włączenie jednego wyłącza
7
8
  * pozostałe; z `multiple` wartością jest tablica zaznaczonych opcji.
@@ -12,7 +13,9 @@ import { KptSwitch } from "../switch/switch.js";
12
13
  * <KptSwitchGroup options={plans} value={plan} onValueChange={setPlan} />
13
14
  * <KptSwitchGroup multiple options={tags} value={selected} onValueChange={setSelected} />
14
15
  */
15
- export function KptSwitchGroup({ options = [], value, defaultValue = null, onValueChange, multiple = false, orientation = 'vertical', allowDeselect = true, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
16
+ export function KptSwitchGroup({ options = [], value, defaultValue = null, onValueChange, multiple = false, orientation = 'vertical', allowDeselect = true, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
17
+ /** Grupa nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
18
+ const field = useKptFieldA11y(id, 'kpt-switch-group');
16
19
  const [current, setCurrent] = useControllableState({
17
20
  value,
18
21
  defaultValue,
@@ -43,5 +46,7 @@ export function KptSwitchGroup({ options = [], value, defaultValue = null, onVal
43
46
  // Bez `allowDeselect` nie robimy nic: `checked` jest wyliczane z wartości, a ta się nie
44
47
  // zmieniła, więc przełącznik sam wraca do włączonego przy najbliższym renderze.
45
48
  };
46
- return (_jsx("div", { className: cn('kpt-switch-group-host', className), ...rest, children: _jsx("div", { className: cn('kpt-switch-group', orientation === 'horizontal' && 'kpt-switch-group--horizontal'), role: "group", "aria-invalid": showInvalid || undefined, children: options.map((option) => (_jsx(KptSwitch, { hostProps: { className: 'kpt-switch-group__item' }, checked: isChecked(option.value), label: option.label, disabled: disabled || option.disabled, onCheckedChange: (checked) => toggle(option, checked), onTouch: onTouch }, String(option.value)))) }) }));
49
+ return (_jsx("div", { className: cn('kpt-switch-group-host', className), ...rest, children: _jsx("div", { className: cn('kpt-switch-group', orientation === 'horizontal' && 'kpt-switch-group--horizontal'), role: "group", id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, children: options.map((option) => (_jsx(KptSwitch, { hostProps: { className: 'kpt-switch-group__item' }, checked: isChecked(option.value), label: option.label, disabled: disabled || option.disabled,
50
+ // `aria-invalid` nie działa na `role="group"` — stan błędu niesie każdy przełącznik.
51
+ invalid: showInvalid, onCheckedChange: (checked) => toggle(option, checked), onTouch: onTouch }, String(option.value)))) }) }));
47
52
  }
@@ -6,11 +6,14 @@ Import: `import { KptTabs } from '@konce-pt/react';`
6
6
  ## DOM
7
7
  <div class="kpt-tabs-host" style="--kpt-anim-duration: 500ms; --kpt-anim-dir: 1">
8
8
  <div class="kpt-tabs__header" role="tablist">
9
- <button class="kpt-tabs__tab is-active" role="tab" aria-selected="true">General</button>
10
- <button class="kpt-tabs__tab" role="tab" aria-selected="false">Advanced</button>
9
+ <button class="kpt-tabs__tab is-active" role="tab" id="kpt-tabs-r1-tab-0"
10
+ aria-selected="true" aria-controls="kpt-tabs-r1-panel" tabindex="0">General</button>
11
+ <button class="kpt-tabs__tab" role="tab" id="kpt-tabs-r1-tab-1"
12
+ aria-selected="false" tabindex="-1">Advanced</button>
11
13
  <span class="kpt-tabs__ink" style="width: 95px; transform: translateX(85px)"></span>
12
14
  </div>
13
- <div class="kpt-tabs__body" role="tabpanel">
15
+ <div class="kpt-tabs__body" role="tabpanel" tabindex="0" id="kpt-tabs-r1-panel"
16
+ aria-labelledby="kpt-tabs-r1-tab-0">
14
17
  <div class="kpt-tabs__panel">…active tab content…</div>
15
18
  </div>
16
19
  </div>
@@ -45,4 +48,7 @@ schedule another render and the component would loop.
45
48
 
46
49
  ## Accessibility
47
50
  The header is `role="tablist"` with `role="tab"` buttons carrying `aria-selected`; the body is
48
- `role="tabpanel"`. A disabled tab is a disabled button, so the keyboard skips it.
51
+ `role="tabpanel"`, named by the active tab (`aria-labelledby`), which points back at it with
52
+ `aria-controls`. The tab row is a single `Tab` stop (roving `tabindex`): `ArrowLeft`/`ArrowRight`
53
+ move between tabs, wrapping at the ends, `Home`/`End` jump to the first/last one, and disabled tabs
54
+ are skipped. Moving activates the tab straight away; the next `Tab` goes to the panel.
@@ -23,6 +23,10 @@ export interface KptTabsProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
23
23
  /**
24
24
  * Zakładki — przełączanie widoków, treść renderowana leniwie.
25
25
  *
26
+ * Klawiatura według wzorca WAI-ARIA „tabs”: rząd zakładek to jeden przystanek Tab, strzałki
27
+ * w lewo/prawo oraz `Home`/`End` przechodzą po zakładkach (z pominięciem wyłączonych) i od razu je
28
+ * aktywują, a kolejny `Tab` prowadzi do panelu.
29
+ *
26
30
  * Podkreślenie aktywnej zakładki (`__ink`) jest mierzone z DOM przycisku, nie liczone z indeksu:
27
31
  * zakładki mają różne szerokości, a etykiety zmieniają się z językiem.
28
32
  *
package/dist/tabs/tabs.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, isValidElement, useCallback, useLayoutEffect, useRef, useState, } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
+ import { nextRovingIndex, rovingStop } from "../internal/roving.js";
4
5
  import { useControllableState } from "../internal/use-controllable-state.js";
6
+ import { useKptId } from "../internal/use-id.js";
5
7
  /**
6
8
  * Pojedyncza zakładka. Umieszczaj w `<KptTabs>`. Treść renderuje się leniwie — dopiero gdy
7
9
  * zakładka jest aktywna, tak samo jak szablon `kpt-tab` w porcie Angulara.
@@ -15,6 +17,10 @@ KptTab.kptTabsSlot = 'tab';
15
17
  /**
16
18
  * Zakładki — przełączanie widoków, treść renderowana leniwie.
17
19
  *
20
+ * Klawiatura według wzorca WAI-ARIA „tabs”: rząd zakładek to jeden przystanek Tab, strzałki
21
+ * w lewo/prawo oraz `Home`/`End` przechodzą po zakładkach (z pominięciem wyłączonych) i od razu je
22
+ * aktywują, a kolejny `Tab` prowadzi do panelu.
23
+ *
18
24
  * Podkreślenie aktywnej zakładki (`__ink`) jest mierzone z DOM przycisku, nie liczone z indeksu:
19
25
  * zakładki mają różne szerokości, a etykiety zmieniają się z językiem.
20
26
  *
@@ -33,6 +39,7 @@ export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChan
33
39
  const [direction, setDirection] = useState(1);
34
40
  const [ink, setInk] = useState({ x: 0, w: 0 });
35
41
  const buttonsRef = useRef([]);
42
+ const baseId = useKptId(undefined, 'kpt-tabs');
36
43
  const tabs = [];
37
44
  const contents = [];
38
45
  Children.forEach(children, (child) => {
@@ -57,18 +64,30 @@ export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChan
57
64
  window.addEventListener('resize', measureInk);
58
65
  return () => window.removeEventListener('resize', measureInk);
59
66
  }, [measureInk]);
67
+ const disabled = tabs.map((tab) => !!tab.disabled);
68
+ // Jedyna zakładka w kolejności Tab: aktywna, a gdy ta jest wyłączona — pierwsza dostępna.
69
+ const tabStop = rovingStop(disabled, active);
60
70
  const select = (index) => {
61
71
  if (tabs[index]?.disabled)
62
72
  return;
63
73
  setDirection(index >= active ? 1 : -1);
64
74
  setActive(index);
65
75
  };
76
+ const onKeyDown = (event) => {
77
+ const current = buttonsRef.current.indexOf(document.activeElement);
78
+ const next = nextRovingIndex(disabled, current < 0 ? tabStop : current, event.key);
79
+ if (next === null)
80
+ return;
81
+ event.preventDefault();
82
+ select(next);
83
+ buttonsRef.current[next]?.focus();
84
+ };
66
85
  return (_jsxs("div", { className: cn('kpt-tabs-host', className), style: {
67
86
  ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`,
68
87
  ['--kpt-anim-dir']: direction,
69
88
  ...style,
70
- }, ...rest, children: [_jsxs("div", { className: "kpt-tabs__header", role: "tablist", children: [tabs.map((tab, index) => (_jsx("button", { ref: (el) => {
89
+ }, ...rest, children: [_jsxs("div", { className: "kpt-tabs__header", role: "tablist", onKeyDown: onKeyDown, children: [tabs.map((tab, index) => (_jsx("button", { ref: (el) => {
71
90
  buttonsRef.current[index] = el;
72
- }, type: "button", className: cn('kpt-tabs__tab', index === active && 'is-active'), role: "tab", disabled: tab.disabled, "aria-selected": index === active, onClick: () => select(index), children: tab.label }, index))), _jsx("span", { className: "kpt-tabs__ink", style: { width: ink.w, transform: `translateX(${ink.x}px)` } })] }), _jsx("div", { className: "kpt-tabs__body", role: "tabpanel", children: contents[active] !== undefined ? (_jsx("div", { className: "kpt-tabs__panel", children: contents[active] }, active)) : null })] }));
91
+ }, type: "button", className: cn('kpt-tabs__tab', index === active && 'is-active'), role: "tab", id: `${baseId}-tab-${index}`, disabled: tab.disabled, "aria-selected": index === active, "aria-controls": index === active ? `${baseId}-panel` : undefined, tabIndex: index === tabStop ? 0 : -1, onClick: () => select(index), children: tab.label }, index))), _jsx("span", { className: "kpt-tabs__ink", style: { width: ink.w, transform: `translateX(${ink.x}px)` } })] }), _jsx("div", { className: "kpt-tabs__body", role: "tabpanel", tabIndex: 0, id: `${baseId}-panel`, "aria-labelledby": `${baseId}-tab-${active}`, children: contents[active] !== undefined ? (_jsx("div", { className: "kpt-tabs__panel", children: contents[active] }, active)) : null })] }));
73
92
  }
74
93
  KptTabs.Tab = KptTab;
@@ -31,6 +31,16 @@ height.
31
31
  <KptTextarea invalid={!!err} onTouch={markTouched} />
32
32
  </KptFormField>
33
33
 
34
+ ## Screen reader and keyboard
35
+ `counter`: boolean — a "12 / 280" counter under the field (requires the native `maxLength`), read
36
+ together with it through `aria-describedby`.
37
+
38
+ ## Label and description
39
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
40
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
41
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
42
+ New: `id`, `ariaLabel` and `required` (→ `aria-required`); `[formField]` binds `required`.
43
+
34
44
  ## Tokens
35
45
  `--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}` —
36
46
  the same set as the text field, so both controls stay in step.
@@ -5,6 +5,8 @@ export interface KptTextareaProps extends Omit<ComponentPropsWithRef<'textarea'>
5
5
  autoResize?: boolean;
6
6
  /** Oznacza pole jako wymagane atrybutem `aria-required`, a NIE natywnym `required`. */
7
7
  required?: boolean;
8
+ /** Licznik „12 / 280” pod polem (wymaga `maxLength`), czytany razem z polem przez `aria-describedby`. */
9
+ counter?: boolean;
8
10
  /** Pole jest nieprawidłowe. Widoczne dopiero, gdy `touched` też jest prawdziwe. */
9
11
  invalid?: boolean;
10
12
  /** Czy pole było dotknięte; pominięty = stan błędu pokazuje się od razu po `invalid`. */
@@ -25,4 +27,4 @@ export interface KptTextareaProps extends Omit<ComponentPropsWithRef<'textarea'>
25
27
  * @example
26
28
  * <KptTextarea name="notes" placeholder="Notatki…" autoResize />
27
29
  */
28
- export declare function KptTextarea({ autoResize, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, rows, onChange, onBlur, ref, 'aria-invalid': ariaInvalid, ...rest }: Readonly<KptTextareaProps>): import("react").JSX.Element;
30
+ export declare function KptTextarea({ autoResize, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, rows, onChange, onBlur, ref, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, counter, ...rest }: Readonly<KptTextareaProps>): import("react").JSX.Element;
@@ -1,9 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useLayoutEffect, useRef } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { composeRefs } from "../internal/compose-refs.js";
5
- import { useKptId } from "../internal/use-id.js";
6
- import { useKptFormFieldContext } from "../form-field/form-field-context.js";
5
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
7
6
  /**
8
7
  * Pole wieloliniowe (textarea), z opcjonalnym auto-resize.
9
8
  *
@@ -13,11 +12,16 @@ import { useKptFormFieldContext } from "../form-field/form-field-context.js";
13
12
  * @example
14
13
  * <KptTextarea name="notes" placeholder="Notatki…" autoResize />
15
14
  */
16
- export function KptTextarea({ autoResize = false, required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, rows = 3, onChange, onBlur, ref, 'aria-invalid': ariaInvalid, ...rest }) {
17
- const field = useKptFormFieldContext();
15
+ export function KptTextarea({ autoResize = false, required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, rows = 3, onChange, onBlur, ref, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, counter = false, ...rest }) {
18
16
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
19
- const textareaId = useKptId(id ?? field?.controlId, 'kpt-textarea');
17
+ const field = useKptFieldA11y(id, 'kpt-textarea');
18
+ const textareaId = field.id;
20
19
  const innerRef = useRef(null);
20
+ // Długość tekstu dla licznika: w trybie kontrolowanym z `value`, w niekontrolowanym — z wpisów.
21
+ const [typedLength, setTypedLength] = useState(() => String(rest.value ?? rest.defaultValue ?? '').length);
22
+ const length = rest.value !== undefined ? String(rest.value).length : typedLength;
23
+ const showCounter = counter && !!rest.maxLength;
24
+ const counterId = `${textareaId}-counter`;
21
25
  const showInvalid = invalid && (touched ?? true);
22
26
  const resize = useCallback(() => {
23
27
  const el = innerRef.current;
@@ -34,11 +38,12 @@ export function KptTextarea({ autoResize = false, required = false, invalid = fa
34
38
  const handleChange = useCallback((event) => {
35
39
  onChange?.(event);
36
40
  onValueChange?.(event.target.value);
41
+ setTypedLength(event.target.value.length);
37
42
  resize();
38
43
  }, [onChange, onValueChange, resize]);
39
44
  const handleBlur = useCallback((event) => {
40
45
  onBlur?.(event);
41
46
  onTouch?.();
42
47
  }, [onBlur, onTouch]);
43
- return (_jsx("span", { className: cn('kpt-textarea-host', hostClassName), ...hostRest, children: _jsx("textarea", { className: cn('kpt-textarea__field', className), id: textareaId, rows: rows, ref: composeRefs(innerRef, ref), "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }) }));
48
+ return (_jsxs("span", { className: cn('kpt-textarea-host', hostClassName), ...hostRest, children: [_jsx("textarea", { className: cn('kpt-textarea__field', className), id: textareaId, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy, showCounter && counterId), rows: rows, ref: composeRefs(innerRef, ref), "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }), showCounter ? (_jsxs("span", { className: "kpt-textarea__counter", id: counterId, children: [length, " / ", rest.maxLength] })) : null] }));
44
49
  }