@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.
- package/dist/accordion/accordion.js +7 -1
- package/dist/accordion/llms.txt +8 -4
- package/dist/alert/alert.d.ts +10 -4
- package/dist/alert/alert.js +13 -6
- package/dist/alert/llms.txt +8 -6
- package/dist/app-shell/app-shell.js +28 -2
- package/dist/app-shell/llms.txt +3 -0
- package/dist/autocomplete/autocomplete.d.ts +7 -2
- package/dist/autocomplete/autocomplete.js +46 -9
- package/dist/autocomplete/llms.txt +12 -0
- package/dist/avatar/avatar.d.ts +4 -2
- package/dist/avatar/avatar.js +8 -3
- package/dist/avatar/llms.txt +9 -5
- package/dist/avatar-group/avatar-group.d.ts +5 -2
- package/dist/avatar-group/avatar-group.js +10 -2
- package/dist/avatar-group/llms.txt +7 -4
- package/dist/badge/badge.d.ts +8 -1
- package/dist/badge/badge.js +3 -2
- package/dist/badge/llms.txt +11 -5
- package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
- package/dist/bottom-sheet/bottom-sheet.js +8 -2
- package/dist/bottom-sheet/llms.txt +8 -2
- package/dist/breadcrumb/breadcrumb.js +4 -4
- package/dist/breadcrumb/llms.txt +12 -5
- package/dist/card/card.d.ts +9 -1
- package/dist/card/card.js +2 -2
- package/dist/card/llms.txt +6 -2
- package/dist/carousel/carousel.d.ts +8 -1
- package/dist/carousel/carousel.js +9 -2
- package/dist/carousel/llms.txt +24 -10
- package/dist/checkbox/checkbox.d.ts +6 -1
- package/dist/checkbox/checkbox.js +11 -3
- package/dist/checkbox/llms.txt +11 -0
- package/dist/chip/chip.js +10 -1
- package/dist/chip/llms.txt +7 -4
- package/dist/chips-input/chips-input.d.ts +7 -1
- package/dist/chips-input/chips-input.js +21 -5
- package/dist/chips-input/llms.txt +12 -0
- package/dist/color-picker/color-picker.d.ts +18 -5
- package/dist/color-picker/color-picker.js +48 -14
- package/dist/color-picker/llms.txt +14 -0
- package/dist/confirm/confirm-container.d.ts +5 -1
- package/dist/confirm/confirm-container.js +15 -1
- package/dist/confirm/llms.txt +4 -2
- package/dist/context-menu/context-menu.d.ts +5 -0
- package/dist/context-menu/context-menu.js +48 -4
- package/dist/context-menu/llms.txt +11 -4
- package/dist/date-range/date-range.d.ts +14 -1
- package/dist/date-range/date-range.js +133 -17
- package/dist/date-range/llms.txt +26 -3
- package/dist/datepicker/datepicker.d.ts +10 -1
- package/dist/datepicker/datepicker.js +147 -14
- package/dist/datepicker/llms.txt +27 -3
- package/dist/dialog/dialog.d.ts +4 -0
- package/dist/dialog/dialog.js +7 -1
- package/dist/dialog/llms.txt +2 -0
- package/dist/drawer/drawer.js +7 -2
- package/dist/drawer/llms.txt +10 -5
- package/dist/empty/empty.d.ts +6 -1
- package/dist/empty/empty.js +2 -2
- package/dist/empty/llms.txt +4 -3
- package/dist/fab/llms.txt +2 -1
- package/dist/file-upload/file-upload.d.ts +5 -0
- package/dist/file-upload/file-upload.js +24 -3
- package/dist/file-upload/llms.txt +15 -4
- package/dist/form-field/form-field-context.d.ts +30 -1
- package/dist/form-field/form-field-context.js +27 -0
- package/dist/form-field/form-field.js +16 -2
- package/dist/form-field/llms.txt +10 -1
- package/dist/galleria/galleria.d.ts +6 -1
- package/dist/galleria/galleria.js +40 -1
- package/dist/galleria/llms.txt +17 -8
- package/dist/icon/icon.d.ts +7 -1
- package/dist/icon/icon.js +18 -4
- package/dist/icon/llms.txt +5 -2
- package/dist/image/image.d.ts +5 -0
- package/dist/image/image.js +38 -2
- package/dist/image/llms.txt +24 -17
- package/dist/input/input.d.ts +1 -1
- package/dist/input/input.js +4 -6
- package/dist/input/llms.txt +5 -0
- package/dist/input-mask/input-mask.d.ts +1 -1
- package/dist/input-mask/input-mask.js +8 -6
- package/dist/input-mask/llms.txt +11 -0
- package/dist/input-number/input-number.d.ts +13 -1
- package/dist/input-number/input-number.js +49 -9
- package/dist/input-number/llms.txt +21 -3
- package/dist/input-otp/input-otp.d.ts +7 -2
- package/dist/input-otp/input-otp.js +36 -7
- package/dist/input-otp/llms.txt +14 -0
- package/dist/internal/calendar-focus.d.ts +8 -0
- package/dist/internal/calendar-focus.js +38 -0
- package/dist/internal/option-nav.d.ts +24 -0
- package/dist/internal/option-nav.js +80 -0
- package/dist/internal/overlay.d.ts +7 -2
- package/dist/internal/overlay.js +4 -2
- package/dist/internal/roving.d.ts +9 -0
- package/dist/internal/roving.js +43 -0
- package/dist/internal/tween.d.ts +17 -0
- package/dist/internal/tween.js +40 -0
- package/dist/internal/use-modal-focus.d.ts +10 -0
- package/dist/internal/use-modal-focus.js +57 -0
- package/dist/knob/knob.d.ts +9 -1
- package/dist/knob/knob.js +41 -5
- package/dist/knob/llms.txt +15 -0
- package/dist/listbox/listbox.d.ts +13 -1
- package/dist/listbox/listbox.js +89 -6
- package/dist/listbox/llms.txt +21 -5
- package/dist/megamenu/llms.txt +28 -10
- package/dist/megamenu/megamenu.d.ts +10 -1
- package/dist/megamenu/megamenu.js +116 -5
- package/dist/menu/llms.txt +12 -1
- package/dist/menu/menu-keys.d.ts +15 -0
- package/dist/menu/menu-keys.js +53 -0
- package/dist/menu/menu-rows.js +1 -1
- package/dist/menu/menu.js +52 -4
- package/dist/menubar/llms.txt +24 -7
- package/dist/menubar/menubar.d.ts +8 -1
- package/dist/menubar/menubar.js +92 -6
- package/dist/meter-group/llms.txt +10 -3
- package/dist/meter-group/meter-group.d.ts +9 -2
- package/dist/meter-group/meter-group.js +8 -2
- package/dist/order-list/llms.txt +11 -3
- package/dist/order-list/order-list.d.ts +5 -1
- package/dist/order-list/order-list.js +46 -8
- package/dist/panel/llms.txt +9 -6
- package/dist/panel/panel.js +3 -1
- package/dist/password/llms.txt +8 -0
- package/dist/password/password.d.ts +6 -1
- package/dist/password/password.js +6 -5
- package/dist/pick-list/llms.txt +12 -3
- package/dist/pick-list/pick-list.d.ts +5 -1
- package/dist/pick-list/pick-list.js +52 -5
- package/dist/popover/llms.txt +9 -2
- package/dist/popover/popover.d.ts +9 -3
- package/dist/popover/popover.js +41 -6
- package/dist/public-api.d.ts +6 -6
- package/dist/public-api.js +1 -1
- package/dist/radio-group/llms.txt +11 -0
- package/dist/radio-group/radio-group.d.ts +9 -1
- package/dist/radio-group/radio-group.js +5 -2
- package/dist/rating/llms.txt +18 -5
- package/dist/rating/rating.d.ts +21 -3
- package/dist/rating/rating.js +87 -12
- package/dist/rich-text/llms.txt +20 -0
- package/dist/rich-text/rich-text.d.ts +7 -1
- package/dist/rich-text/rich-text.js +155 -28
- package/dist/scroll-top/llms.txt +5 -1
- package/dist/scroll-top/scroll-top.js +3 -1
- package/dist/select/llms.txt +21 -7
- package/dist/select/select.d.ts +12 -1
- package/dist/select/select.js +110 -38
- package/dist/sidenav/llms.txt +6 -0
- package/dist/skeleton/llms.txt +3 -2
- package/dist/skeleton/skeleton.d.ts +1 -1
- package/dist/skeleton/skeleton.js +2 -2
- package/dist/slider/llms.txt +14 -1
- package/dist/slider/slider.d.ts +11 -2
- package/dist/slider/slider.js +15 -2
- package/dist/speed-dial/llms.txt +6 -2
- package/dist/speed-dial/speed-dial.d.ts +1 -1
- package/dist/speed-dial/speed-dial.js +14 -3
- package/dist/spinner/llms.txt +12 -8
- package/dist/spinner/spinner.d.ts +5 -1
- package/dist/spinner/spinner.js +12 -2
- package/dist/split-button/llms.txt +11 -3
- package/dist/split-button/split-button.d.ts +3 -1
- package/dist/split-button/split-button.js +43 -3
- package/dist/splitter/llms.txt +14 -4
- package/dist/splitter/splitter.d.ts +9 -2
- package/dist/splitter/splitter.js +15 -2
- package/dist/stepper/llms.txt +8 -4
- package/dist/stepper/stepper.d.ts +3 -2
- package/dist/stepper/stepper.js +7 -2
- package/dist/switch/llms.txt +10 -0
- package/dist/switch/switch.d.ts +1 -1
- package/dist/switch/switch.js +4 -2
- package/dist/switch-group/llms.txt +11 -0
- package/dist/switch-group/switch-group.d.ts +5 -1
- package/dist/switch-group/switch-group.js +7 -2
- package/dist/tabs/llms.txt +10 -4
- package/dist/tabs/tabs.d.ts +4 -0
- package/dist/tabs/tabs.js +21 -2
- package/dist/textarea/llms.txt +10 -0
- package/dist/textarea/textarea.d.ts +3 -1
- package/dist/textarea/textarea.js +13 -8
- package/dist/toast/llms.txt +21 -7
- package/dist/toast/toast-container.d.ts +6 -1
- package/dist/toast/toast-container.js +52 -1
- package/dist/toast/toast-store.d.ts +12 -0
- package/dist/toast/toast-store.js +46 -7
- package/dist/tooltip/llms.txt +10 -3
- package/dist/tooltip/tooltip.d.ts +5 -0
- package/dist/tooltip/tooltip.js +80 -7
- package/dist/tree/llms.txt +20 -8
- package/dist/tree/tree-keys.d.ts +27 -0
- package/dist/tree/tree-keys.js +39 -0
- package/dist/tree/tree.d.ts +12 -5
- package/dist/tree/tree.js +75 -29
- package/package.json +12 -12
package/dist/toast/llms.txt
CHANGED
|
@@ -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"
|
|
9
|
-
<div class="kpt-toast" data-variant="success"
|
|
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
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/tooltip/llms.txt
CHANGED
|
@@ -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
|
|
41
|
-
|
|
42
|
-
|
|
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>
|
package/dist/tooltip/tooltip.js
CHANGED
|
@@ -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);
|
|
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'],
|
|
162
|
+
onMouseLeave: chain(props['onMouseLeave'], scheduleHide),
|
|
90
163
|
onFocus: chain(props['onFocus'], show),
|
|
91
|
-
onBlur: chain(props['onBlur'],
|
|
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
|
/**
|
package/dist/tree/llms.txt
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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"
|
|
45
|
-
`
|
|
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
|
+
}
|
package/dist/tree/tree.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
-
import {
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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;
|