@konce-pt/react 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/overlay.d.ts +7 -2
  96. package/dist/internal/overlay.js +4 -2
  97. package/dist/internal/roving.d.ts +9 -0
  98. package/dist/internal/roving.js +43 -0
  99. package/dist/internal/tween.d.ts +17 -0
  100. package/dist/internal/tween.js +40 -0
  101. package/dist/internal/use-modal-focus.d.ts +10 -0
  102. package/dist/internal/use-modal-focus.js +57 -0
  103. package/dist/knob/knob.d.ts +9 -1
  104. package/dist/knob/knob.js +41 -5
  105. package/dist/knob/llms.txt +15 -0
  106. package/dist/listbox/listbox.d.ts +13 -1
  107. package/dist/listbox/listbox.js +89 -6
  108. package/dist/listbox/llms.txt +21 -5
  109. package/dist/megamenu/llms.txt +28 -10
  110. package/dist/megamenu/megamenu.d.ts +10 -1
  111. package/dist/megamenu/megamenu.js +116 -5
  112. package/dist/menu/llms.txt +12 -1
  113. package/dist/menu/menu-keys.d.ts +15 -0
  114. package/dist/menu/menu-keys.js +53 -0
  115. package/dist/menu/menu-rows.js +1 -1
  116. package/dist/menu/menu.js +52 -4
  117. package/dist/menubar/llms.txt +24 -7
  118. package/dist/menubar/menubar.d.ts +8 -1
  119. package/dist/menubar/menubar.js +92 -6
  120. package/dist/meter-group/llms.txt +10 -3
  121. package/dist/meter-group/meter-group.d.ts +9 -2
  122. package/dist/meter-group/meter-group.js +8 -2
  123. package/dist/order-list/llms.txt +11 -3
  124. package/dist/order-list/order-list.d.ts +5 -1
  125. package/dist/order-list/order-list.js +46 -8
  126. package/dist/panel/llms.txt +9 -6
  127. package/dist/panel/panel.js +3 -1
  128. package/dist/password/llms.txt +8 -0
  129. package/dist/password/password.d.ts +6 -1
  130. package/dist/password/password.js +6 -5
  131. package/dist/pick-list/llms.txt +12 -3
  132. package/dist/pick-list/pick-list.d.ts +5 -1
  133. package/dist/pick-list/pick-list.js +52 -5
  134. package/dist/popover/llms.txt +9 -2
  135. package/dist/popover/popover.d.ts +9 -3
  136. package/dist/popover/popover.js +41 -6
  137. package/dist/public-api.d.ts +6 -6
  138. package/dist/public-api.js +1 -1
  139. package/dist/radio-group/llms.txt +11 -0
  140. package/dist/radio-group/radio-group.d.ts +9 -1
  141. package/dist/radio-group/radio-group.js +5 -2
  142. package/dist/rating/llms.txt +18 -5
  143. package/dist/rating/rating.d.ts +21 -3
  144. package/dist/rating/rating.js +87 -12
  145. package/dist/rich-text/llms.txt +20 -0
  146. package/dist/rich-text/rich-text.d.ts +7 -1
  147. package/dist/rich-text/rich-text.js +155 -28
  148. package/dist/scroll-top/llms.txt +5 -1
  149. package/dist/scroll-top/scroll-top.js +3 -1
  150. package/dist/select/llms.txt +21 -7
  151. package/dist/select/select.d.ts +12 -1
  152. package/dist/select/select.js +110 -38
  153. package/dist/sidenav/llms.txt +6 -0
  154. package/dist/skeleton/llms.txt +3 -2
  155. package/dist/skeleton/skeleton.d.ts +1 -1
  156. package/dist/skeleton/skeleton.js +2 -2
  157. package/dist/slider/llms.txt +14 -1
  158. package/dist/slider/slider.d.ts +11 -2
  159. package/dist/slider/slider.js +15 -2
  160. package/dist/speed-dial/llms.txt +6 -2
  161. package/dist/speed-dial/speed-dial.d.ts +1 -1
  162. package/dist/speed-dial/speed-dial.js +14 -3
  163. package/dist/spinner/llms.txt +12 -8
  164. package/dist/spinner/spinner.d.ts +5 -1
  165. package/dist/spinner/spinner.js +12 -2
  166. package/dist/split-button/llms.txt +11 -3
  167. package/dist/split-button/split-button.d.ts +3 -1
  168. package/dist/split-button/split-button.js +43 -3
  169. package/dist/splitter/llms.txt +14 -4
  170. package/dist/splitter/splitter.d.ts +9 -2
  171. package/dist/splitter/splitter.js +15 -2
  172. package/dist/stepper/llms.txt +8 -4
  173. package/dist/stepper/stepper.d.ts +3 -2
  174. package/dist/stepper/stepper.js +7 -2
  175. package/dist/switch/llms.txt +10 -0
  176. package/dist/switch/switch.d.ts +1 -1
  177. package/dist/switch/switch.js +4 -2
  178. package/dist/switch-group/llms.txt +11 -0
  179. package/dist/switch-group/switch-group.d.ts +5 -1
  180. package/dist/switch-group/switch-group.js +7 -2
  181. package/dist/tabs/llms.txt +10 -4
  182. package/dist/tabs/tabs.d.ts +4 -0
  183. package/dist/tabs/tabs.js +21 -2
  184. package/dist/textarea/llms.txt +10 -0
  185. package/dist/textarea/textarea.d.ts +3 -1
  186. package/dist/textarea/textarea.js +13 -8
  187. package/dist/toast/llms.txt +21 -7
  188. package/dist/toast/toast-container.d.ts +6 -1
  189. package/dist/toast/toast-container.js +52 -1
  190. package/dist/toast/toast-store.d.ts +12 -0
  191. package/dist/toast/toast-store.js +46 -7
  192. package/dist/tooltip/llms.txt +10 -3
  193. package/dist/tooltip/tooltip.d.ts +5 -0
  194. package/dist/tooltip/tooltip.js +80 -7
  195. package/dist/tree/llms.txt +20 -8
  196. package/dist/tree/tree-keys.d.ts +27 -0
  197. package/dist/tree/tree-keys.js +39 -0
  198. package/dist/tree/tree.d.ts +12 -5
  199. package/dist/tree/tree.js +75 -29
  200. package/package.json +12 -12
@@ -5,23 +5,26 @@ Import: `import { kptToast, KptToastContainer } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-toast-container-host">
8
- <div class="kpt-toast-container" aria-live="polite" aria-atomic="false">
9
- <div class="kpt-toast" data-variant="success" role="status">
8
+ <div class="kpt-toast-container" role="region" aria-label="Notifications">
9
+ <div class="kpt-toast" data-variant="success">
10
10
  <span class="kpt-icon-host kpt-toast__icon">…</span>
11
11
  <div class="kpt-toast__content">
12
12
  <div class="kpt-toast__title">Saved</div>
13
13
  <div class="kpt-toast__message">Your changes are stored.</div>
14
14
  </div>
15
+ <button class="kpt-toast__action" type="button">Undo</button>
15
16
  <button class="kpt-toast__close" type="button" aria-label="Close">…</button>
16
17
  </div>
17
18
  </div>
19
+ <span class="kpt-toast-container__live" aria-live="polite" aria-atomic="true">Saved. Your changes are stored.</span>
20
+ <span class="kpt-toast-container__live" aria-live="assertive" aria-atomic="true"></span>
18
21
  </div>
19
22
 
20
23
  ## kptToast
21
24
  - `show(message, options?)` — returns the toast id
22
25
  - `info` / `success` / `warning` / `error` — the same, with the variant preset
23
- - `dismiss(id)`, `clear()`
24
- - options: `title`, `variant`, `duration` (ms, default 4000; `0` disables auto-dismiss)
26
+ - `dismiss(id)`, `clear()`, `pause()`, `resume()`
27
+ - options: `title`, `variant`, `duration` (ms, default 4000; `0` disables auto-dismiss), `action: { label, run }`
25
28
 
26
29
  ## A module singleton, not a provider
27
30
  The store lives in the module, which is the closest equivalent of Angular's
@@ -42,11 +45,22 @@ mounts are kept in the store and appear as soon as it does.
42
45
  const id = kptToast.info('Uploading…', { duration: 0 });
43
46
  kptToast.dismiss(id);
44
47
 
48
+ ## Actions and pausing
49
+ `action: { label, run }` adds one button to the toast (typically “Undo”); a click calls `run` and
50
+ dismisses the toast. Give such toasts a longer `duration`, and keep the action available elsewhere.
51
+ `pause()` / `resume()` stop and continue every timer; the container calls them on hover and focus,
52
+ and resumes by itself when the stack empties.
53
+
45
54
  ## Tokens
46
55
  The semantic colour pairs of each variant (`--kpt-color-info` / `-success` / `-warning` /
47
56
  `-danger` with their `-subtle` and `on-` counterparts), `--kpt-elevation-3`, `--kpt-z-overlay`.
48
57
 
49
58
  ## Accessibility
50
- The container is `aria-live="polite"` with `aria-atomic="false"`, and each toast is
51
- `role="status"`, so a screen reader announces new ones without interrupting the current task.
52
- The close button gets its label from `common.close`.
59
+ Announcements go through two permanent, visually hidden live regions next to the stack —
60
+ `aria-live="polite"` for ordinary toasts and `aria-live="assertive"` for `danger` — and not through
61
+ the toasts themselves: a live region has to exist before content appears in it, and nested regions
62
+ are read twice by some screen readers. Each region is cleared before it is written, so the same
63
+ text twice is announced twice, and a dismissed toast never re-announces an older one. While there
64
+ are toasts the stack is `role="region"` named `toast.region` (“Notifications”). Pointer or focus on
65
+ the stack pauses every timer (WCAG 2.2.1); the timers resume with the time that was left. The close
66
+ button is labelled from `common.close`.
@@ -1,10 +1,15 @@
1
- import type { ComponentPropsWithRef } from 'react';
1
+ import { type ComponentPropsWithRef } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  export interface KptToastContainerProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
4
4
  }
5
5
  /**
6
6
  * Kontener renderujący toasty z `kptToast`. Umieść raz w aplikacji, np. w komponencie głównym.
7
7
  *
8
+ * Ogłaszanie idzie przez dwa stałe, niewidoczne regiony na żywo — uprzejmy dla zwykłych toastów
9
+ * i natychmiastowy dla błędów — a nie przez same toasty: region musi istnieć, zanim pojawi się
10
+ * w nim treść, a zagnieżdżone regiony część czytników ogłasza dwa razy. Gdy wskaźnik albo fokus
11
+ * jest na toastach, liczniki stoją, więc komunikat nie znika w trakcie czytania.
12
+ *
8
13
  * @example
9
14
  * <KptToastContainer />
10
15
  * // gdziekolwiek: kptToast.success('Zapisano');
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptMessages } from "../i18n/i18n-context.js";
4
5
  import { KptIcon } from "../icon/icon.js";
@@ -12,6 +13,11 @@ const ICONS = {
12
13
  /**
13
14
  * Kontener renderujący toasty z `kptToast`. Umieść raz w aplikacji, np. w komponencie głównym.
14
15
  *
16
+ * Ogłaszanie idzie przez dwa stałe, niewidoczne regiony na żywo — uprzejmy dla zwykłych toastów
17
+ * i natychmiastowy dla błędów — a nie przez same toasty: region musi istnieć, zanim pojawi się
18
+ * w nim treść, a zagnieżdżone regiony część czytników ogłasza dwa razy. Gdy wskaźnik albo fokus
19
+ * jest na toastach, liczniki stoją, więc komunikat nie znika w trakcie czytania.
20
+ *
15
21
  * @example
16
22
  * <KptToastContainer />
17
23
  * // gdziekolwiek: kptToast.success('Zapisano');
@@ -19,5 +25,50 @@ const ICONS = {
19
25
  export function KptToastContainer({ locale, dictionary, className, ...rest }) {
20
26
  const { t } = useKptMessages({ locale, dictionary });
21
27
  const toasts = useKptToasts();
22
- return (_jsx("div", { className: cn('kpt-toast-container-host', className), ...rest, children: _jsx("div", { className: "kpt-toast-container", "aria-live": "polite", "aria-atomic": "false", children: toasts.map((toast) => (_jsxs("div", { className: "kpt-toast", "data-variant": toast.variant, role: "status", children: [_jsx(KptIcon, { className: "kpt-toast__icon", name: ICONS[toast.variant] }), _jsxs("div", { className: "kpt-toast__content", children: [toast.title ? _jsx("div", { className: "kpt-toast__title", children: toast.title }) : null, _jsx("div", { className: "kpt-toast__message", children: toast.message })] }), _jsx("button", { className: "kpt-toast__close", type: "button", onClick: () => kptToast.dismiss(toast.id), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })] }, toast.id))) }) }));
28
+ const [politeText, setPoliteText] = useState('');
29
+ const [alertText, setAlertText] = useState('');
30
+ // Ostatni ogłoszony toast — zamknięcie nowszego nie może ponownie ogłosić starszego.
31
+ const lastAnnounced = useRef(0);
32
+ const holds = useRef(new Set());
33
+ const hold = (reason, on) => {
34
+ if (on)
35
+ holds.current.add(reason);
36
+ else
37
+ holds.current.delete(reason);
38
+ if (holds.current.size)
39
+ kptToast.pause();
40
+ else
41
+ kptToast.resume();
42
+ };
43
+ useEffect(() => {
44
+ // Pusty kontener nie dostanie już `mouseleave`, więc zdejmujemy wstrzymanie sami —
45
+ // inaczej kolejne toasty czekałyby w nieskończoność.
46
+ if (!toasts.length && holds.current.size) {
47
+ holds.current.clear();
48
+ kptToast.resume();
49
+ }
50
+ const fresh = toasts.filter((toast) => toast.id > lastAnnounced.current);
51
+ if (!fresh.length)
52
+ return;
53
+ lastAnnounced.current = fresh[fresh.length - 1].id;
54
+ const frames = [];
55
+ for (const toast of fresh) {
56
+ const text = toast.title ? `${toast.title}. ${toast.message}` : toast.message;
57
+ const set = toast.variant === 'danger' ? setAlertText : setPoliteText;
58
+ // Czyścimy region przed wpisaniem — ten sam tekst drugi raz inaczej nie zostałby ogłoszony.
59
+ set('');
60
+ frames.push(requestAnimationFrame(() => set(text)));
61
+ }
62
+ return () => frames.forEach(cancelAnimationFrame);
63
+ }, [toasts]);
64
+ // Fokus przechodzący między przyciskami toastów nie wznawia liczników.
65
+ const onBlur = (event) => {
66
+ if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget))
67
+ return;
68
+ hold('focus', false);
69
+ };
70
+ return (_jsxs("div", { className: cn('kpt-toast-container-host', className), ...rest, children: [_jsx("div", { className: "kpt-toast-container", role: toasts.length ? 'region' : undefined, "aria-label": toasts.length ? t('toast.region') : undefined, onMouseEnter: () => hold('pointer', true), onMouseLeave: () => hold('pointer', false), onFocus: () => hold('focus', true), onBlur: onBlur, children: toasts.map((toast) => (_jsxs("div", { className: "kpt-toast", "data-variant": toast.variant, children: [_jsx(KptIcon, { className: "kpt-toast__icon", name: ICONS[toast.variant] }), _jsxs("div", { className: "kpt-toast__content", children: [toast.title ? _jsx("div", { className: "kpt-toast__title", children: toast.title }) : null, _jsx("div", { className: "kpt-toast__message", children: toast.message })] }), toast.action ? (_jsx("button", { className: "kpt-toast__action", type: "button", onClick: () => {
71
+ toast.action?.run();
72
+ kptToast.dismiss(toast.id);
73
+ }, children: toast.action.label })) : null, _jsx("button", { className: "kpt-toast__close", type: "button", onClick: () => kptToast.dismiss(toast.id), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })] }, toast.id))) }), _jsx("span", { className: "kpt-toast-container__live", "aria-live": "polite", "aria-atomic": "true", children: politeText }), _jsx("span", { className: "kpt-toast-container__live", "aria-live": "assertive", "aria-atomic": "true", children: alertText })] }));
23
74
  }
@@ -1,4 +1,9 @@
1
1
  import type { KptAlertVariant } from '../shared/types.ts';
2
+ /** Przycisk akcji w toaście, np. „Cofnij”. Kliknięcie uruchamia `run` i zamyka toast. */
3
+ export interface KptToastAction {
4
+ label: string;
5
+ run: () => void;
6
+ }
2
7
  export interface KptToast {
3
8
  id: number;
4
9
  message: string;
@@ -6,11 +11,16 @@ export interface KptToast {
6
11
  variant: KptAlertVariant;
7
12
  /** Czas życia w ms; 0 = bez auto-zamykania. */
8
13
  duration: number;
14
+ action?: KptToastAction;
9
15
  }
10
16
  export type KptToastOptions = Partial<Omit<KptToast, 'id' | 'message'>>;
11
17
  declare function show(message: string, options?: KptToastOptions): number;
12
18
  declare function dismiss(id: number): void;
13
19
  declare function clear(): void;
20
+ /** Wstrzymuje wszystkie liczniki; toasty dodane w tym czasie też czekają. */
21
+ declare function pause(): void;
22
+ /** Wznawia liczniki od pozostałego czasu. */
23
+ declare function resume(): void;
14
24
  /** Powiadomienia (toasty). Renderuje je `<KptToastContainer />`, umieszczony raz w aplikacji. */
15
25
  export declare const kptToast: {
16
26
  show: typeof show;
@@ -20,6 +30,8 @@ export declare const kptToast: {
20
30
  error: (message: string, options?: KptToastOptions) => number;
21
31
  dismiss: typeof dismiss;
22
32
  clear: typeof clear;
33
+ pause: typeof pause;
34
+ resume: typeof resume;
23
35
  };
24
36
  /** Bieżąca lista toastów. Używane przez kontener; przydaje się też do własnego renderowania. */
25
37
  export declare function useKptToasts(): KptToast[];
@@ -1,11 +1,8 @@
1
1
  import { useSyncExternalStore } from 'react';
2
- /*
3
- * Magazyn toastów jako moduł-singleton — odpowiednik serwisu `providedIn: 'root'` z Angulara.
4
- * Dzięki temu `kptToast.success(...)` woła się z dowolnego miejsca, także spoza drzewa Reacta
5
- * (obsługa błędu w warstwie danych, interceptor), bez owijania aplikacji providerem.
6
- */
7
2
  let toasts = [];
8
3
  let seq = 0;
4
+ let paused = false;
5
+ const timers = new Map();
9
6
  const listeners = new Set();
10
7
  function emit() {
11
8
  for (const listener of listeners)
@@ -16,18 +13,58 @@ function show(message, options = {}) {
16
13
  const toast = { id, message, variant: 'info', duration: 4000, ...options };
17
14
  toasts = [...toasts, toast];
18
15
  emit();
19
- if (toast.duration > 0)
20
- setTimeout(() => dismiss(id), toast.duration);
16
+ if (toast.duration > 0) {
17
+ timers.set(id, { handle: null, remaining: toast.duration, startedAt: 0 });
18
+ if (!paused)
19
+ start(id);
20
+ }
21
21
  return id;
22
22
  }
23
+ function start(id) {
24
+ const timer = timers.get(id);
25
+ if (!timer || timer.handle)
26
+ return;
27
+ timer.startedAt = Date.now();
28
+ timer.handle = setTimeout(() => dismiss(id), timer.remaining);
29
+ }
23
30
  function dismiss(id) {
31
+ const timer = timers.get(id);
32
+ if (timer?.handle)
33
+ clearTimeout(timer.handle);
34
+ timers.delete(id);
24
35
  toasts = toasts.filter((toast) => toast.id !== id);
25
36
  emit();
26
37
  }
27
38
  function clear() {
39
+ for (const timer of timers.values())
40
+ if (timer.handle)
41
+ clearTimeout(timer.handle);
42
+ timers.clear();
28
43
  toasts = [];
29
44
  emit();
30
45
  }
46
+ /** Wstrzymuje wszystkie liczniki; toasty dodane w tym czasie też czekają. */
47
+ function pause() {
48
+ if (paused)
49
+ return;
50
+ paused = true;
51
+ const now = Date.now();
52
+ for (const timer of timers.values()) {
53
+ if (!timer.handle)
54
+ continue;
55
+ clearTimeout(timer.handle);
56
+ timer.handle = null;
57
+ timer.remaining = Math.max(0, timer.remaining - (now - timer.startedAt));
58
+ }
59
+ }
60
+ /** Wznawia liczniki od pozostałego czasu. */
61
+ function resume() {
62
+ if (!paused)
63
+ return;
64
+ paused = false;
65
+ for (const id of timers.keys())
66
+ start(id);
67
+ }
31
68
  /** Powiadomienia (toasty). Renderuje je `<KptToastContainer />`, umieszczony raz w aplikacji. */
32
69
  export const kptToast = {
33
70
  show,
@@ -37,6 +74,8 @@ export const kptToast = {
37
74
  error: (message, options) => show(message, { ...options, variant: 'danger' }),
38
75
  dismiss,
39
76
  clear,
77
+ pause,
78
+ resume,
40
79
  };
41
80
  const EMPTY = [];
42
81
  function subscribe(listener) {
@@ -37,6 +37,13 @@ attach a stylesheet rule to reliably.
37
37
  `--kpt-font-size-xs`, `--kpt-elevation-2`.
38
38
 
39
39
  ## Accessibility
40
- The bubble is `role="tooltip"` and appears on focus as well as hover, so it is reachable from the
41
- keyboard. It is `pointer-events: none`, so it never steals a click. It does NOT replace an
42
- accessible name — for an icon-only button give the button its own `aria-label`.
40
+ - The bubble appears on hover and on keyboard focus. Focus is tracked with `focusin`/`focusout`,
41
+ so it also works when the focus lands on an inner element (`kpt-button`, `kpt-icon-button`).
42
+ - The text reaches screen readers through a persistent, hidden description linked with
43
+ `aria-describedby` to the element that actually takes focus (the bubble itself only exists while
44
+ shown). It is skipped when it would repeat the element's name (an icon button whose label equals
45
+ the tooltip) and with `onlyWhenTruncated`, where the text is already in the DOM; the visible
46
+ bubble is then `aria-hidden`.
47
+ - `Escape` closes the bubble, and the pointer can move onto it without it disappearing (a 120 ms
48
+ grace period) — WCAG 1.4.13.
49
+ - It does NOT replace an accessible name — an icon-only button still needs its own label.
@@ -19,6 +19,11 @@ export interface KptTooltipProps {
19
19
  * Port dyrektywy `[kptTooltip]`: nie renderuje własnego opakowania, tylko dokleja handlery
20
20
  * i ref do przekazanego elementu, więc drzewo DOM zostaje takie samo jak bez tooltipa.
21
21
  *
22
+ * Dostępność (jak w porcie Angulara): tekst trafia do czytników przez trwały, ukryty opis
23
+ * powiązany `aria-describedby` z elementem przyjmującym fokus — pomijany, gdy powtarzałby
24
+ * jego nazwę albo przy `onlyWhenTruncated`. `Escape` zamyka dymek, a wskaźnik może na niego
25
+ * przejść bez zniknięcia (WCAG 1.4.13).
26
+ *
22
27
  * @example
23
28
  * <KptTooltip text="Zapisz zmiany" position="bottom"><KptButton>Zapisz</KptButton></KptTooltip>
24
29
  * <KptTooltip text={name} onlyWhenTruncated><span className="ellipsis">{name}</span></KptTooltip>
@@ -1,6 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { cloneElement, isValidElement, useCallback, useRef } from 'react';
2
+ import { cloneElement, isValidElement, useCallback, useEffect, useId, useRef } from 'react';
3
3
  import { composeRefs } from "../internal/compose-refs.js";
4
+ /** Elementy, które same przyjmują fokus — na nie trafia `aria-describedby`. */
5
+ const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
6
+ /** Ile czasu ma wskaźnik na przejście z elementu na dymek, zanim ten zniknie (WCAG 1.4.13). */
7
+ const HIDE_DELAY_MS = 120;
4
8
  /**
5
9
  * Dymek podpowiedzi pokazywany na hover i focus. Lekki — element dodawany do `body`
6
10
  * i pozycjonowany względem dziecka.
@@ -8,6 +12,11 @@ import { composeRefs } from "../internal/compose-refs.js";
8
12
  * Port dyrektywy `[kptTooltip]`: nie renderuje własnego opakowania, tylko dokleja handlery
9
13
  * i ref do przekazanego elementu, więc drzewo DOM zostaje takie samo jak bez tooltipa.
10
14
  *
15
+ * Dostępność (jak w porcie Angulara): tekst trafia do czytników przez trwały, ukryty opis
16
+ * powiązany `aria-describedby` z elementem przyjmującym fokus — pomijany, gdy powtarzałby
17
+ * jego nazwę albo przy `onlyWhenTruncated`. `Escape` zamyka dymek, a wskaźnik może na niego
18
+ * przejść bez zniknięcia (WCAG 1.4.13).
19
+ *
11
20
  * @example
12
21
  * <KptTooltip text="Zapisz zmiany" position="bottom"><KptButton>Zapisz</KptButton></KptTooltip>
13
22
  * <KptTooltip text={name} onlyWhenTruncated><span className="ellipsis">{name}</span></KptTooltip>
@@ -15,6 +24,37 @@ import { composeRefs } from "../internal/compose-refs.js";
15
24
  export function KptTooltip({ text, position = 'top', onlyWhenTruncated = false, children }) {
16
25
  const hostRef = useRef(null);
17
26
  const tipRef = useRef(null);
27
+ const descRef = useRef(null);
28
+ const hideTimer = useRef(undefined);
29
+ const descId = `kpt-tooltip-desc-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
30
+ // Trwały opis dla czytników: dymek powstaje dopiero przy pokazaniu, więc nie da się go
31
+ // wskazać wcześniej. Opis żyje razem z komponentem i aktualizuje się ze zmianą tekstu.
32
+ useEffect(() => {
33
+ const host = hostRef.current;
34
+ if (!host || !text || onlyWhenTruncated)
35
+ return;
36
+ const target = host.matches(FOCUSABLE) ? host : (host.querySelector(FOCUSABLE) ?? host);
37
+ const name = (target.getAttribute('aria-label') ?? target.textContent ?? '').trim();
38
+ if (name === text.trim())
39
+ return;
40
+ const desc = document.createElement('span');
41
+ desc.id = descId;
42
+ desc.hidden = true;
43
+ desc.textContent = text;
44
+ document.body.appendChild(desc);
45
+ descRef.current = desc;
46
+ const ids = (target.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
47
+ target.setAttribute('aria-describedby', [...ids, descId].join(' '));
48
+ return () => {
49
+ const rest = (target.getAttribute('aria-describedby') ?? '').split(/\s+/).filter((id) => id && id !== descId);
50
+ if (rest.length)
51
+ target.setAttribute('aria-describedby', rest.join(' '));
52
+ else
53
+ target.removeAttribute('aria-describedby');
54
+ desc.remove();
55
+ descRef.current = null;
56
+ };
57
+ }, [text, onlyWhenTruncated, descId]);
18
58
  const reposition = useCallback(() => {
19
59
  const tip = tipRef.current;
20
60
  const host = hostRef.current;
@@ -45,13 +85,33 @@ export function KptTooltip({ text, position = 'top', onlyWhenTruncated = false,
45
85
  tip.style.top = `${Math.max(4, top)}px`;
46
86
  tip.style.left = `${Math.max(4, left)}px`;
47
87
  }, [position]);
88
+ const onKeyDown = useCallback((event) => {
89
+ if (event.key === 'Escape')
90
+ hideRef.current();
91
+ }, []);
92
+ const cancelHide = useCallback(() => {
93
+ if (hideTimer.current !== undefined)
94
+ clearTimeout(hideTimer.current);
95
+ hideTimer.current = undefined;
96
+ }, []);
48
97
  const hide = useCallback(() => {
98
+ cancelHide();
49
99
  window.removeEventListener('scroll', reposition, true);
50
100
  window.removeEventListener('resize', reposition);
101
+ document.removeEventListener('keydown', onKeyDown);
51
102
  tipRef.current?.remove();
52
103
  tipRef.current = null;
53
- }, [reposition]);
104
+ }, [reposition, onKeyDown, cancelHide]);
105
+ // `onKeyDown` potrzebuje aktualnego `hide`, a `hide` zdejmuje `onKeyDown` — ref przerywa krąg.
106
+ const hideRef = useRef(hide);
107
+ hideRef.current = hide;
108
+ const scheduleHide = useCallback(() => {
109
+ cancelHide();
110
+ hideTimer.current = setTimeout(() => hideRef.current(), HIDE_DELAY_MS);
111
+ }, [cancelHide]);
112
+ useEffect(() => () => hideRef.current(), []);
54
113
  const show = useCallback(() => {
114
+ cancelHide();
55
115
  if (tipRef.current || !text)
56
116
  return;
57
117
  // Sprawdzamy dopiero teraz, a nie przy renderze: dzięki temu wynik jest zawsze aktualny
@@ -61,12 +121,17 @@ export function KptTooltip({ text, position = 'top', onlyWhenTruncated = false,
61
121
  const tip = document.createElement('div');
62
122
  tip.className = 'kpt-tooltip';
63
123
  tip.setAttribute('role', 'tooltip');
124
+ // Treść czytnik dostaje z trwałego opisu — dymek by ją zdublował.
125
+ if (descRef.current)
126
+ tip.setAttribute('aria-hidden', 'true');
64
127
  tip.textContent = text;
65
128
  tip.style.cssText =
66
129
  'position:fixed;z-index:1400;max-width:16rem;padding:0.35rem 0.6rem;' +
67
130
  'border-radius:var(--kpt-radius-sm,0.25rem);font-size:var(--kpt-font-size-xs,0.75rem);' +
68
131
  'line-height:1.4;background:var(--kpt-color-on-surface);color:var(--kpt-color-surface);' +
69
- 'box-shadow:var(--kpt-elevation-2);pointer-events:none;opacity:0;transition:opacity 120ms;';
132
+ 'box-shadow:var(--kpt-elevation-2);opacity:0;transition:opacity 120ms;';
133
+ tip.addEventListener('mouseenter', cancelHide);
134
+ tip.addEventListener('mouseleave', scheduleHide);
70
135
  document.body.appendChild(tip);
71
136
  tipRef.current = tip;
72
137
  reposition();
@@ -74,11 +139,19 @@ export function KptTooltip({ text, position = 'top', onlyWhenTruncated = false,
74
139
  // `scroll` elementu się nie propaguje.
75
140
  window.addEventListener('scroll', reposition, true);
76
141
  window.addEventListener('resize', reposition);
142
+ document.addEventListener('keydown', onKeyDown);
77
143
  requestAnimationFrame(() => {
78
144
  if (tipRef.current)
79
145
  tipRef.current.style.opacity = '1';
80
146
  });
81
- }, [text, onlyWhenTruncated, reposition]);
147
+ }, [text, onlyWhenTruncated, reposition, onKeyDown, cancelHide, scheduleHide]);
148
+ /** Fokus przeszedł do innego elementu w obrębie hosta — dymek zostaje. */
149
+ const onBlur = (event) => {
150
+ const next = event.relatedTarget;
151
+ if (next && hostRef.current?.contains(next))
152
+ return;
153
+ hide();
154
+ };
82
155
  if (!isValidElement(children))
83
156
  return _jsx(_Fragment, { children: children });
84
157
  const child = children;
@@ -86,9 +159,9 @@ export function KptTooltip({ text, position = 'top', onlyWhenTruncated = false,
86
159
  return cloneElement(child, {
87
160
  ref: composeRefs(hostRef, props['ref']),
88
161
  onMouseEnter: chain(props['onMouseEnter'], show),
89
- onMouseLeave: chain(props['onMouseLeave'], hide),
162
+ onMouseLeave: chain(props['onMouseLeave'], scheduleHide),
90
163
  onFocus: chain(props['onFocus'], show),
91
- onBlur: chain(props['onBlur'], hide),
164
+ onBlur: chain(props['onBlur'], onBlur),
92
165
  });
93
166
  }
94
167
  /** Handler dziecka najpierw, nasz po nim — jak w scalaniu propsów slotu. */
@@ -96,7 +169,7 @@ function chain(theirs, ours) {
96
169
  return (...args) => {
97
170
  if (typeof theirs === 'function')
98
171
  theirs(...args);
99
- ours();
172
+ ours(...args);
100
173
  };
101
174
  }
102
175
  /**
@@ -8,7 +8,7 @@ Import: `import { KptTree, type KptTreeNode } from '@konce-pt/react';`
8
8
  <ul class="kpt-tree" role="tree">
9
9
  <li class="kpt-tree__node" role="treeitem" aria-expanded="true">
10
10
  <div class="kpt-tree__row is-selected">
11
- <button class="kpt-tree__toggle">…chevron…</button>
11
+ <span class="kpt-tree__toggle" aria-hidden="true">…chevron…</span>
12
12
  <span class="kpt-tree__icon">…</span>
13
13
  <span class="kpt-tree__label">src</span>
14
14
  </div>
@@ -25,21 +25,33 @@ Import: `import { KptTree, type KptTreeNode } from '@konce-pt/react';`
25
25
  - `className`, `ref` and every other `<div>` prop go to the host
26
26
 
27
27
  ## Expansion is component state, not data
28
- The Angular port flips `node.expanded` in place and bumps a version signal. In React that would be
29
- a side effect on someone else's array during render, so the open branches live in a `Set` of node
30
- references instead — and `expanded` in the data goes back to being what it should be: the initial
31
- state, read once on mount.
28
+ `expanded` in a node is the initial state only. The component keeps what the user opened or closed
29
+ itself and never writes to the node objects — so two trees fed the same nodes open and close
30
+ independently, and replacing `nodes` with new objects brings their own initial state.
32
31
 
33
32
  ## Examples
34
33
  <KptTree nodes={tree} />
35
34
  <KptTree nodes={tree} selectable selected={node} onSelectedChange={setNode} />
36
35
 
36
+ ## Keyboard
37
+ The tree is one Tab stop (roving tabindex on the `treeitem` elements), following the WAI-ARIA tree view
38
+ pattern:
39
+ - `↓` / `↑` — the next / previous visible row, `Home` / `End` — the first / last;
40
+ - `→` — expands a closed branch, on an open one moves to its first child;
41
+ - `←` — collapses an open branch, otherwise moves to the parent;
42
+ - `Enter` / `Space` — the same as a click: selects (with `selectable`) or toggles the branch.
43
+ The logic is the pure function `treeKeyAction` in `tree-keys.ts`; the other port has a twin file.
44
+
37
45
  ## i18n
38
- The toggle is labelled from `tree.expand` / `tree.collapse`.
46
+ No labels of its own any more: the chevron is mouse-only and the keyboard expands with the arrows.
47
+ `locale` and `dictionary` are still accepted, so existing call sites keep compiling.
39
48
 
40
49
  ## Tokens
41
50
  The selected row `--kpt-color-primary`, the hover `--kpt-color-surface-variant`.
42
51
 
43
52
  ## Accessibility
44
- `role="tree"` / `role="treeitem"` with `aria-expanded` on branches only; the indentation is
45
- `padding-inline-start`, so it follows the writing direction.
53
+ `role="tree"`, `treeitem` and `group`; branches carry `aria-expanded`, every row `aria-level`,
54
+ selectable trees `aria-selected`, disabled rows `aria-disabled`. The chevron is mouse-only
55
+ (`aria-hidden`): a button inside a `treeitem` would be a nested control and a second Tab stop per
56
+ row, and the keyboard expands with the arrows anyway. The focus ring is drawn on the row, not on the
57
+ `<li>`, which would outline the whole subtree.
@@ -0,0 +1,27 @@
1
+ /** Widoczny wiersz drzewa w kolejności DOM (preorder, tylko rozwinięte gałęzie). */
2
+ export interface KptTreeRow {
3
+ /** Ma dzieci, więc da się go rozwinąć. */
4
+ expandable: boolean;
5
+ open: boolean;
6
+ /** Indeks rodzica w tej samej liście albo `-1` dla korzenia. */
7
+ parent: number;
8
+ }
9
+ /** Co zrobić po naciśnięciu klawisza: przenieść fokus, przełączyć gałąź albo aktywować wiersz. */
10
+ export type KptTreeKeyAction = {
11
+ type: 'focus';
12
+ index: number;
13
+ } | {
14
+ type: 'toggle';
15
+ index: number;
16
+ } | {
17
+ type: 'activate';
18
+ index: number;
19
+ };
20
+ /**
21
+ * ↓ / ↑ — następny / poprzedni widoczny wiersz (bez zawijania), `Home` / `End` — pierwszy / ostatni.
22
+ * → — zamkniętą gałąź rozwija, w otwartej przechodzi do pierwszego dziecka.
23
+ * ← — otwartą gałąź zwija, w pozostałych przypadkach przechodzi do rodzica.
24
+ * `Enter` / spacja — aktywuje wiersz (zaznaczenie albo przełączenie, jak kliknięcie).
25
+ * Zwraca `null` dla klawisza, który nic nie robi — wtedy nie blokujemy jego domyślnej akcji.
26
+ */
27
+ export declare function treeKeyAction(rows: readonly KptTreeRow[], current: number, key: string): KptTreeKeyAction | null;
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Klawiatura drzewa (wzorzec WAI-ARIA „tree view”) — czysta funkcja na spłaszczonej liście
3
+ * widocznych wierszy. Port Angulara ma bliźniaczy plik `lib/tree/tree-keys.ts` — zmiana zachowania w obu.
4
+ */
5
+ /**
6
+ * ↓ / ↑ — następny / poprzedni widoczny wiersz (bez zawijania), `Home` / `End` — pierwszy / ostatni.
7
+ * → — zamkniętą gałąź rozwija, w otwartej przechodzi do pierwszego dziecka.
8
+ * ← — otwartą gałąź zwija, w pozostałych przypadkach przechodzi do rodzica.
9
+ * `Enter` / spacja — aktywuje wiersz (zaznaczenie albo przełączenie, jak kliknięcie).
10
+ * Zwraca `null` dla klawisza, który nic nie robi — wtedy nie blokujemy jego domyślnej akcji.
11
+ */
12
+ export function treeKeyAction(rows, current, key) {
13
+ const row = rows[current];
14
+ if (!row)
15
+ return null;
16
+ switch (key) {
17
+ case 'ArrowDown':
18
+ return current < rows.length - 1 ? { type: 'focus', index: current + 1 } : null;
19
+ case 'ArrowUp':
20
+ return current > 0 ? { type: 'focus', index: current - 1 } : null;
21
+ case 'Home':
22
+ return { type: 'focus', index: 0 };
23
+ case 'End':
24
+ return { type: 'focus', index: rows.length - 1 };
25
+ case 'ArrowRight':
26
+ if (!row.expandable)
27
+ return null;
28
+ return row.open ? { type: 'focus', index: current + 1 } : { type: 'toggle', index: current };
29
+ case 'ArrowLeft':
30
+ if (row.expandable && row.open)
31
+ return { type: 'toggle', index: current };
32
+ return row.parent >= 0 ? { type: 'focus', index: row.parent } : null;
33
+ case 'Enter':
34
+ case ' ':
35
+ return { type: 'activate', index: current };
36
+ default:
37
+ return null;
38
+ }
39
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
- import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
2
+ import type { KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  export interface KptTreeNode<T = unknown> {
4
4
  label: string;
5
5
  value?: T;
@@ -21,11 +21,18 @@ export interface KptTreeProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onS
21
21
  /**
22
22
  * Drzewo hierarchiczne: rozwijanie, zaznaczanie, ikony.
23
23
  *
24
- * Stan rozwinięcia trzyma komponent w zbiorze węzłów, a nie pole `expanded` w danych: mutowanie
25
- * cudzej tablicy przy każdym kliknięciu byłoby w Reakcie efektem ubocznym renderu, a `expanded`
26
- * z danych zostaje tym, czym powinno być — stanem początkowym.
24
+ * Stan rozwinięcia trzyma komponent, a nie pole `expanded` w danych: mutowanie cudzej tablicy
25
+ * przy każdym kliknięciu byłoby w Reakcie efektem ubocznym renderu, a `expanded` z danych zostaje
26
+ * tym, czym powinno być — stanem początkowym.
27
+ *
28
+ * Klawiatura według wzorca WAI-ARIA „tree view”: jeden przystanek Tab na całe drzewo (roving
29
+ * tabindex), strzałki po widocznych wierszach, → / ← rozwijają i zwijają, `Enter` / spacja
30
+ * działają jak kliknięcie. Logika klawiszy siedzi w czystej funkcji `treeKeyAction`.
31
+ *
32
+ * `locale` i `dictionary` są przyjmowane dla zgodności — drzewo nie ma już własnych etykiet:
33
+ * przełącznik gałęzi jest tylko dla myszy, a z klawiatury rozwija się strzałkami.
27
34
  *
28
35
  * @example
29
36
  * <KptTree nodes={tree} selectable selected={node} onSelectedChange={setNode} />
30
37
  */
31
- export declare function KptTree<T>({ nodes, selectable, selected, defaultSelected, onSelectedChange, onNodeClick, locale, dictionary, className, ...rest }: Readonly<KptTreeProps<T>>): import("react").JSX.Element;
38
+ export declare function KptTree<T>({ nodes, selectable, selected, defaultSelected, onSelectedChange, onNodeClick, locale: _locale, dictionary: _dictionary, className, ...rest }: Readonly<KptTreeProps<T>>): import("react").JSX.Element;