@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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
/** Elementy z kolejki Tab — na nich zawija się pułapka. */
|
|
3
|
+
const TABBABLE = 'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
|
|
4
|
+
function tabbables(panel) {
|
|
5
|
+
return [...panel.querySelectorAll(TABBABLE)].filter((el) => !el.closest('[hidden], [inert]'));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fokus panelu modalnego (`KptDrawer`, `KptBottomSheet`): przy otwarciu zapamiętuje element
|
|
9
|
+
* z fokusem, przenosi fokus do panelu (pierwszy element z kolejki Tab albo sam panel) i zawija
|
|
10
|
+
* `Tab` / `Shift+Tab` w jego obrębie; przy zamknięciu oddaje fokus zapamiętanemu elementowi.
|
|
11
|
+
* Bez tego `aria-modal="true"` obiecywał czytnikowi okno, z którego Tab po cichu wychodził.
|
|
12
|
+
*
|
|
13
|
+
* Port Angulara robi to samo na pułapce CDK (`shared/modal-focus.ts`) — zmiana zachowania w obu.
|
|
14
|
+
*/
|
|
15
|
+
export function useModalFocus(open, panelRef) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const panel = panelRef.current;
|
|
18
|
+
if (!open || !panel)
|
|
19
|
+
return;
|
|
20
|
+
const active = document.activeElement;
|
|
21
|
+
const returnTo = active instanceof HTMLElement && !panel.contains(active) ? active : null;
|
|
22
|
+
// Klatkę później: panel dostaje `is-open` (i `visibility: visible`) w tym samym commicie,
|
|
23
|
+
// a przeglądarka nie skupi elementu, który w chwili wywołania jest jeszcze niewidoczny.
|
|
24
|
+
const frame = requestAnimationFrame(() => {
|
|
25
|
+
const first = tabbables(panel)[0];
|
|
26
|
+
(first ?? panel).focus();
|
|
27
|
+
});
|
|
28
|
+
const onKeyDown = (event) => {
|
|
29
|
+
if (event.key !== 'Tab')
|
|
30
|
+
return;
|
|
31
|
+
const items = tabbables(panel);
|
|
32
|
+
if (!items.length) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
panel.focus();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const first = items[0];
|
|
38
|
+
const last = items[items.length - 1];
|
|
39
|
+
const current = document.activeElement;
|
|
40
|
+
if (event.shiftKey && (current === first || !panel.contains(current))) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
last.focus();
|
|
43
|
+
}
|
|
44
|
+
else if (!event.shiftKey && (current === last || !panel.contains(current))) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
first.focus();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('keydown', onKeyDown);
|
|
50
|
+
return () => {
|
|
51
|
+
cancelAnimationFrame(frame);
|
|
52
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
53
|
+
if (returnTo?.isConnected)
|
|
54
|
+
returnTo.focus();
|
|
55
|
+
};
|
|
56
|
+
}, [open, panelRef]);
|
|
57
|
+
}
|
package/dist/knob/knob.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface KptKnobProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
|
|
|
13
13
|
/** Dopisek za wartością w środku (np. '%'). */
|
|
14
14
|
suffix?: string;
|
|
15
15
|
showValue?: boolean;
|
|
16
|
+
/** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
|
|
19
|
+
ariaLabel?: string | null;
|
|
16
20
|
disabled?: boolean;
|
|
17
21
|
invalid?: boolean;
|
|
18
22
|
touched?: boolean;
|
|
@@ -21,7 +25,11 @@ export interface KptKnobProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
|
|
|
21
25
|
/**
|
|
22
26
|
* Pokrętło — wybór wartości po łuku, przeciąganiem albo strzałkami.
|
|
23
27
|
*
|
|
28
|
+
* Skok wartości (strzałka, `Home`/`End`, klik w łuk, zmiana z zewnątrz) łuk i licznik pokonują
|
|
29
|
+
* płynnie; przy przeciąganiu idą za kursorem bez opóźnienia. `prefers-reduced-motion` wyłącza
|
|
30
|
+
* animację, a `aria-valuenow` zawsze niesie wartość docelową, nie pośrednią.
|
|
31
|
+
*
|
|
24
32
|
* @example
|
|
25
33
|
* <KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
|
|
26
34
|
*/
|
|
27
|
-
export declare function KptKnob({ value, defaultValue, onValueChange, min, max, step, size, stroke, suffix, showValue, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptKnobProps>): import("react").JSX.Element;
|
|
35
|
+
export declare function KptKnob({ value, defaultValue, onValueChange, min, max, step, size, stroke, suffix, showValue, id, ariaLabel, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptKnobProps>): import("react").JSX.Element;
|
package/dist/knob/knob.js
CHANGED
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { kptStepDecimals, kptTween } from "../internal/tween.js";
|
|
4
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
5
7
|
const TAU = Math.PI * 2;
|
|
6
8
|
/** Przerwa u dołu — kąt „otwarcia" łuku, żeby początek i koniec były rozróżnialne. */
|
|
7
9
|
const GAP = Math.PI / 4;
|
|
8
10
|
const START_ANGLE = Math.PI / 2 + GAP;
|
|
9
11
|
const SWEEP = TAU - GAP * 2;
|
|
12
|
+
/** Czas dojścia łuku i licznika po skoku wartości (klawisz, klik, zmiana z zewnątrz). */
|
|
13
|
+
const TWEEN_MS = 220;
|
|
10
14
|
/**
|
|
11
15
|
* Pokrętło — wybór wartości po łuku, przeciąganiem albo strzałkami.
|
|
12
16
|
*
|
|
17
|
+
* Skok wartości (strzałka, `Home`/`End`, klik w łuk, zmiana z zewnątrz) łuk i licznik pokonują
|
|
18
|
+
* płynnie; przy przeciąganiu idą za kursorem bez opóźnienia. `prefers-reduced-motion` wyłącza
|
|
19
|
+
* animację, a `aria-valuenow` zawsze niesie wartość docelową, nie pośrednią.
|
|
20
|
+
*
|
|
13
21
|
* @example
|
|
14
22
|
* <KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
|
|
15
23
|
*/
|
|
16
|
-
export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max = 100, step = 1, size = 96, stroke = 10, suffix = '', showValue = true, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
|
|
24
|
+
export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max = 100, step = 1, size = 96, stroke = 10, suffix = '', showValue = true, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
|
|
25
|
+
const field = useKptFieldA11y(id, 'kpt-knob');
|
|
17
26
|
const svgRef = useRef(null);
|
|
18
27
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
19
28
|
const showInvalid = invalid && (touched ?? true);
|
|
20
29
|
const range = max - min;
|
|
21
|
-
|
|
30
|
+
/** Wartość na rysunku — w trakcie animacji pośrednia, poza nią równa `current`. */
|
|
31
|
+
const [shown, setShown] = useState(current);
|
|
32
|
+
const shownRef = useRef(current);
|
|
33
|
+
/** Przeciąganie trwa — wtedy rysunek idzie za kursorem bez animacji. */
|
|
34
|
+
const dragging = useRef(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const paint = (v) => {
|
|
37
|
+
shownRef.current = v;
|
|
38
|
+
setShown(v);
|
|
39
|
+
};
|
|
40
|
+
if (dragging.current) {
|
|
41
|
+
paint(current);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
return kptTween(shownRef.current, current, TWEEN_MS, paint);
|
|
45
|
+
}, [current]);
|
|
46
|
+
const ratio = range <= 0 ? 0 : Math.min(1, Math.max(0, (shown - min) / range));
|
|
22
47
|
/** Wycinek łuku jako ścieżka SVG; `to <= from` daje samo `M`, czyli nic widocznego. */
|
|
23
48
|
const arc = (from, to) => {
|
|
24
49
|
const r = (size - stroke) / 2;
|
|
@@ -52,17 +77,28 @@ export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max =
|
|
|
52
77
|
const onPointerDown = (event) => {
|
|
53
78
|
if (disabled)
|
|
54
79
|
return;
|
|
80
|
+
// Bez tego przeciąganie zaznacza tekst: licznik w środku i — przez SVG — całe koło, jak po
|
|
81
|
+
// `Ctrl+A`. Zablokowanie domyślnej akcji zabiera też fokus, więc oddajemy go ręcznie.
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.currentTarget.focus();
|
|
84
|
+
// Klik w łuk to skok — animowany. Dopiero ruch po nim jest przeciąganiem.
|
|
55
85
|
setFromPoint(event.clientX, event.clientY);
|
|
56
86
|
// Nasłuch wisi na oknie, a nie na pokrętle: przeciąganie ma nie gubić wartości,
|
|
57
87
|
// gdy kursor wyjedzie poza koło — a wyjeżdża praktycznie zawsze.
|
|
58
|
-
const move = (e) =>
|
|
88
|
+
const move = (e) => {
|
|
89
|
+
dragging.current = true;
|
|
90
|
+
setFromPoint(e.clientX, e.clientY);
|
|
91
|
+
};
|
|
59
92
|
const up = () => {
|
|
60
93
|
window.removeEventListener('pointermove', move);
|
|
61
94
|
window.removeEventListener('pointerup', up);
|
|
95
|
+
window.removeEventListener('pointercancel', up);
|
|
96
|
+
dragging.current = false;
|
|
62
97
|
onTouch?.();
|
|
63
98
|
};
|
|
64
99
|
window.addEventListener('pointermove', move);
|
|
65
100
|
window.addEventListener('pointerup', up);
|
|
101
|
+
window.addEventListener('pointercancel', up);
|
|
66
102
|
};
|
|
67
103
|
const onKeyDown = (event) => {
|
|
68
104
|
if (disabled)
|
|
@@ -82,5 +118,5 @@ export function KptKnob({ value, defaultValue = 0, onValueChange, min = 0, max =
|
|
|
82
118
|
setCurrent(Math.min(max, Math.max(min, next)));
|
|
83
119
|
onTouch?.();
|
|
84
120
|
};
|
|
85
|
-
return (_jsx("div", { className: cn('kpt-knob-host', className), ...rest, children: _jsx("div", { className: cn('kpt-knob', showInvalid && 'kpt-knob--invalid'), role: "slider", tabIndex: disabled ? -1 : 0, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-disabled": disabled || undefined, onPointerDown: onPointerDown, onKeyDown: onKeyDown, children: _jsxs("svg", { ref: svgRef, viewBox: `0 0 ${size} ${size}`, style: { width: size, height: size }, children: [_jsx("path", { className: "kpt-knob__range", d: arc(0, 1), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), _jsx("path", { className: "kpt-knob__value", d: arc(0, ratio), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), showValue ? (_jsxs("text", { className: "kpt-knob__text", x: size / 2, y: size / 2, textAnchor: "middle", dominantBaseline: "central", children: [
|
|
121
|
+
return (_jsx("div", { className: cn('kpt-knob-host', className), ...rest, children: _jsx("div", { className: cn('kpt-knob', showInvalid && 'kpt-knob--invalid'), role: "slider", id: field.id, tabIndex: disabled ? -1 : 0, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": current, "aria-valuetext": suffix ? `${current}${suffix}` : undefined, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onPointerDown: onPointerDown, onKeyDown: onKeyDown, children: _jsxs("svg", { ref: svgRef, viewBox: `0 0 ${size} ${size}`, style: { width: size, height: size }, children: [_jsx("path", { className: "kpt-knob__range", d: arc(0, 1), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), _jsx("path", { className: "kpt-knob__value", d: arc(0, ratio), fill: "none", strokeWidth: stroke, strokeLinecap: "round" }), showValue ? (_jsxs("text", { className: "kpt-knob__text", x: size / 2, y: size / 2, textAnchor: "middle", dominantBaseline: "central", children: [shown.toFixed(kptStepDecimals(step)), suffix] })) : null] }) }) }));
|
|
86
122
|
}
|
package/dist/knob/llms.txt
CHANGED
|
@@ -36,10 +36,25 @@ on `pointerup`, which is also where `onTouch` fires — one touch per drag, not
|
|
|
36
36
|
<KptKnob value={level} onValueChange={setLevel} min={0} max={100} step={5} suffix="%" />
|
|
37
37
|
<KptKnob value={level} onValueChange={setLevel} size={64} stroke={6} showValue={false} />
|
|
38
38
|
|
|
39
|
+
## Label and description
|
|
40
|
+
`id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
|
|
41
|
+
generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
|
|
42
|
+
`aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
|
|
43
|
+
is added to `aria-describedby`. New: `id`, `ariaLabel`; `aria-valuetext` includes `suffix` ("40%"),
|
|
44
|
+
and a disabled knob leaves the Tab order. Use `ariaLabel`, not `aria-label` on the host — that one
|
|
45
|
+
lands on the wrapper.
|
|
46
|
+
|
|
39
47
|
## Tokens
|
|
40
48
|
The background arc `--kpt-color-border`, the value `--kpt-color-primary`, the text
|
|
41
49
|
`--kpt-color-on-surface`.
|
|
42
50
|
|
|
51
|
+
## Motion
|
|
52
|
+
A jump in value — an arrow key, Home/End, a click on the arc, a change from outside — moves the arc
|
|
53
|
+
and counts the number up or down over ~220 ms (ease-out). Dragging follows the pointer with no
|
|
54
|
+
delay, so it never feels laggy. `prefers-reduced-motion: reduce` turns the animation off.
|
|
55
|
+
`aria-valuenow` always carries the target value, never an in-between frame. Dragging does not
|
|
56
|
+
select the number or the arc.
|
|
57
|
+
|
|
43
58
|
## Accessibility
|
|
44
59
|
`role="slider"` with `aria-valuemin` / `aria-valuemax` / `aria-valuenow`; arrows step by `step`,
|
|
45
60
|
Home and End jump to the bounds. `disabled` sets `aria-disabled` and drops the knob out of the
|
|
@@ -9,6 +9,12 @@ export interface KptListboxProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
9
9
|
multiple?: boolean;
|
|
10
10
|
/** Pole filtrowania nad listą. */
|
|
11
11
|
filter?: boolean;
|
|
12
|
+
/** Id listy (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Nazwa dostępna listy, gdy nie opisuje jej etykieta (`KptFormField`). */
|
|
15
|
+
ariaLabel?: string | null;
|
|
16
|
+
/** Oznacza listę jako wymaganą atrybutem `aria-required`. */
|
|
17
|
+
required?: boolean;
|
|
12
18
|
disabled?: boolean;
|
|
13
19
|
invalid?: boolean;
|
|
14
20
|
touched?: boolean;
|
|
@@ -21,8 +27,14 @@ export interface KptListboxProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
21
27
|
* W trybie pojedynczym ponowny klik w zaznaczoną opcję odznacza ją — lista bywa filtrem,
|
|
22
28
|
* a filtr trzeba dać się wyczyścić bez sięgania po osobny przycisk.
|
|
23
29
|
*
|
|
30
|
+
* Klawiatura według wzorca WAI-ARIA „listbox”: lista to jeden przystanek Tab, a aktywną opcję
|
|
31
|
+
* ogłasza `aria-activedescendant`. Strzałki, `Home`/`End`, `PageUp`/`PageDown` przesuwają aktywną
|
|
32
|
+
* opcję (wyłączone są pomijane), pisanie liter skacze do kolejnej pasującej (bez względu na
|
|
33
|
+
* diakrytyki), `Spacja`/`Enter` wybiera, a w trybie wielokrotnym `Ctrl`/`⌘`+`A` zaznacza wszystkie
|
|
34
|
+
* widoczne. Pole filtra stoi przed listą; `↓` przenosi z niego fokus do listy.
|
|
35
|
+
*
|
|
24
36
|
* @example
|
|
25
37
|
* <KptListbox options={roles} value={role} onValueChange={setRole} filter />
|
|
26
38
|
* <KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
|
|
27
39
|
*/
|
|
28
|
-
export declare function KptListbox<T>({ options, value, defaultValue, onValueChange, multiple, filter, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptListboxProps<T>>): import("react").JSX.Element;
|
|
40
|
+
export declare function KptListbox<T>({ options, value, defaultValue, onValueChange, multiple, filter, id, ariaLabel, required, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptListboxProps<T>>): import("react").JSX.Element;
|
package/dist/listbox/listbox.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { foldMentionText } from '@konce-pt/mention';
|
|
3
4
|
import { cn } from "../internal/cn.js";
|
|
4
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { KptTypeahead, OPTION_NAV_KEYS, isTypeaheadKey, nextOptionIndex, typeaheadIndex } from "../internal/option-nav.js";
|
|
5
7
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
8
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
6
9
|
import { KptIcon } from "../icon/icon.js";
|
|
7
10
|
/**
|
|
8
11
|
* Lista wyboru widoczna w całości, bez rozwijania. Pojedynczy albo wielokrotny wybór,
|
|
@@ -11,23 +14,44 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
11
14
|
* W trybie pojedynczym ponowny klik w zaznaczoną opcję odznacza ją — lista bywa filtrem,
|
|
12
15
|
* a filtr trzeba dać się wyczyścić bez sięgania po osobny przycisk.
|
|
13
16
|
*
|
|
17
|
+
* Klawiatura według wzorca WAI-ARIA „listbox”: lista to jeden przystanek Tab, a aktywną opcję
|
|
18
|
+
* ogłasza `aria-activedescendant`. Strzałki, `Home`/`End`, `PageUp`/`PageDown` przesuwają aktywną
|
|
19
|
+
* opcję (wyłączone są pomijane), pisanie liter skacze do kolejnej pasującej (bez względu na
|
|
20
|
+
* diakrytyki), `Spacja`/`Enter` wybiera, a w trybie wielokrotnym `Ctrl`/`⌘`+`A` zaznacza wszystkie
|
|
21
|
+
* widoczne. Pole filtra stoi przed listą; `↓` przenosi z niego fokus do listy.
|
|
22
|
+
*
|
|
14
23
|
* @example
|
|
15
24
|
* <KptListbox options={roles} value={role} onValueChange={setRole} filter />
|
|
16
25
|
* <KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
|
|
17
26
|
*/
|
|
18
|
-
export function KptListbox({ options = [], value, defaultValue = null, onValueChange, multiple = false, filter = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
27
|
+
export function KptListbox({ options = [], value, defaultValue = null, onValueChange, multiple = false, filter = false, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
19
28
|
const { t } = useKptMessages({ locale, dictionary });
|
|
29
|
+
/** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
30
|
+
const field = useKptFieldA11y(id, 'kpt-listbox');
|
|
20
31
|
const [current, setCurrent] = useControllableState({
|
|
21
32
|
value,
|
|
22
33
|
defaultValue,
|
|
23
34
|
onChange: onValueChange,
|
|
24
35
|
});
|
|
25
36
|
const [query, setQuery] = useState('');
|
|
37
|
+
/** Aktywna opcja (indeks w `visible`); `-1` = żadna. */
|
|
38
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
39
|
+
const listRef = useRef(null);
|
|
40
|
+
const typeahead = useRef(new KptTypeahead()).current;
|
|
26
41
|
const showInvalid = invalid && (touched ?? true);
|
|
42
|
+
/** Filtr bez względu na wielkość liter i diakrytyki („lodz” znajduje „Łódź”). */
|
|
27
43
|
const visible = useMemo(() => {
|
|
28
|
-
const q = query.trim()
|
|
29
|
-
return q ? options.filter((option) => option.label
|
|
44
|
+
const q = foldMentionText(query.trim());
|
|
45
|
+
return q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
|
|
30
46
|
}, [options, query]);
|
|
47
|
+
const disabledMap = visible.map((option) => disabled || !!option.disabled);
|
|
48
|
+
const optionId = (index) => `${field.id}-opt-${index}`;
|
|
49
|
+
const activeOptionId = activeIndex >= 0 && activeIndex < visible.length ? optionId(activeIndex) : undefined;
|
|
50
|
+
// Aktywna opcja zawsze w polu widzenia listy (przewijana `max-height`).
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (activeOptionId)
|
|
53
|
+
document.getElementById(activeOptionId)?.scrollIntoView({ block: 'nearest' });
|
|
54
|
+
}, [activeOptionId]);
|
|
31
55
|
const isSelected = (candidate) => multiple ? Array.isArray(current) && current.includes(candidate) : current === candidate;
|
|
32
56
|
const toggle = (candidate) => {
|
|
33
57
|
if (multiple) {
|
|
@@ -37,7 +61,66 @@ export function KptListbox({ options = [], value, defaultValue = null, onValueCh
|
|
|
37
61
|
else {
|
|
38
62
|
setCurrent(current === candidate ? null : candidate);
|
|
39
63
|
}
|
|
40
|
-
onTouch?.();
|
|
41
64
|
};
|
|
42
|
-
|
|
65
|
+
const pick = (index) => {
|
|
66
|
+
const option = visible[index];
|
|
67
|
+
if (!option || disabledMap[index])
|
|
68
|
+
return;
|
|
69
|
+
setActiveIndex(index);
|
|
70
|
+
toggle(option.value);
|
|
71
|
+
};
|
|
72
|
+
/** Wejście fokusem: aktywna zostaje wybrana opcja, a bez niej pierwsza dozwolona. */
|
|
73
|
+
const onFocus = () => {
|
|
74
|
+
if (activeIndex >= 0 && activeIndex < visible.length)
|
|
75
|
+
return;
|
|
76
|
+
const selected = visible.findIndex((option, index) => isSelected(option.value) && !disabledMap[index]);
|
|
77
|
+
setActiveIndex(selected >= 0 ? selected : (nextOptionIndex(disabledMap, -1, 'Home') ?? -1));
|
|
78
|
+
};
|
|
79
|
+
const onQuery = (text) => {
|
|
80
|
+
setQuery(text);
|
|
81
|
+
const q = foldMentionText(text.trim());
|
|
82
|
+
const next = q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
|
|
83
|
+
setActiveIndex(nextOptionIndex(next.map((option) => disabled || !!option.disabled), -1, 'Home') ?? -1);
|
|
84
|
+
};
|
|
85
|
+
const onKeyDown = (event) => {
|
|
86
|
+
if (disabled)
|
|
87
|
+
return;
|
|
88
|
+
if (OPTION_NAV_KEYS.has(event.key)) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
const next = nextOptionIndex(disabledMap, activeIndex, event.key);
|
|
91
|
+
if (next !== null)
|
|
92
|
+
setActiveIndex(next);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
96
|
+
if (activeIndex < 0)
|
|
97
|
+
return;
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
pick(activeIndex);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (multiple && (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const all = visible.filter((_, index) => !disabledMap[index]).map((option) => option.value);
|
|
105
|
+
const list = Array.isArray(current) ? current : [];
|
|
106
|
+
// Drugi raz `Ctrl+A` przy wszystkich zaznaczonych — odznacza widoczne.
|
|
107
|
+
const allOn = all.every((item) => list.includes(item));
|
|
108
|
+
setCurrent(allOn ? list.filter((item) => !all.includes(item)) : [...list, ...all.filter((item) => !list.includes(item))]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (isTypeaheadKey(event.nativeEvent)) {
|
|
112
|
+
const hit = typeaheadIndex(visible.map((option) => option.label), disabledMap, activeIndex, typeahead.push(event.key));
|
|
113
|
+
if (hit !== null)
|
|
114
|
+
setActiveIndex(hit);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return (_jsx("div", { className: cn('kpt-listbox-host', className), ...rest, children: _jsxs("div", { className: cn('kpt-listbox', showInvalid && 'kpt-listbox--invalid', disabled && 'kpt-listbox--disabled'), children: [filter ? (_jsxs("div", { className: "kpt-listbox__filter", children: [_jsx(KptIcon, { name: "search" }), _jsx("input", { type: "text", placeholder: t('listbox.search'), "aria-label": t('listbox.search'), "aria-controls": field.id, disabled: disabled, value: query, onChange: (event) => onQuery(event.target.value), onKeyDown: (event) => {
|
|
118
|
+
if (event.key !== 'ArrowDown')
|
|
119
|
+
return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
listRef.current?.focus();
|
|
122
|
+
} })] })) : null, _jsx("div", { ref: listRef, className: "kpt-listbox__items", role: "listbox", id: field.id, tabIndex: disabled ? -1 : 0, "aria-multiselectable": multiple || undefined, "aria-activedescendant": activeOptionId, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: () => onTouch?.(), children: visible.length ? (visible.map((option, index) => (_jsxs("div", { role: "option", id: optionId(index), className: cn('kpt-listbox__item', isSelected(option.value) && 'is-selected', index === activeIndex && 'is-active'), "aria-selected": isSelected(option.value), "aria-disabled": disabledMap[index] || undefined, onMouseDown: (event) => {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
listRef.current?.focus();
|
|
125
|
+
}, onClick: () => pick(index), children: [_jsx("span", { children: option.label }), isSelected(option.value) ? _jsx(KptIcon, { name: "check" }) : null] }, String(option.value))))) : (_jsx("div", { className: "kpt-listbox__empty", role: "status", children: t('listbox.empty') })) })] }) }));
|
|
43
126
|
}
|
package/dist/listbox/llms.txt
CHANGED
|
@@ -5,10 +5,10 @@ Import: `import { KptListbox } from '@konce-pt/react';`
|
|
|
5
5
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-listbox-host">
|
|
8
|
-
<div class="kpt-listbox"
|
|
8
|
+
<div class="kpt-listbox">
|
|
9
9
|
<div class="kpt-listbox__filter">…icon + input…</div>
|
|
10
|
-
<div class="kpt-listbox__items">
|
|
11
|
-
<
|
|
10
|
+
<div class="kpt-listbox__items" role="listbox" tabindex="0" aria-activedescendant="…-opt-0">
|
|
11
|
+
<div class="kpt-listbox__item is-selected is-active" role="option" id="…-opt-0" aria-selected="true">Admin</div>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
@@ -17,6 +17,7 @@ Import: `import { KptListbox } from '@konce-pt/react';`
|
|
|
17
17
|
- `options`: `KptOption<T>[]`
|
|
18
18
|
- `value` / `defaultValue` / `onValueChange` — `T` (single) or `T[]` (multiple)
|
|
19
19
|
- `multiple`, `filter`, `disabled`, `invalid`, `touched`, `onTouch()`
|
|
20
|
+
- `id`, `ariaLabel`, `required`
|
|
20
21
|
- `locale`, `dictionary` — i18n overrides for this component only
|
|
21
22
|
|
|
22
23
|
## Clicking the selection clears it
|
|
@@ -30,10 +31,25 @@ filter has to be clearable without a separate button.
|
|
|
30
31
|
<KptListbox options={roles} value={role} onValueChange={setRole} filter />
|
|
31
32
|
<KptListbox options={tags} value={tags} onValueChange={setTags} multiple />
|
|
32
33
|
|
|
34
|
+
## Keyboard
|
|
35
|
+
The WAI-ARIA listbox pattern: the list is one Tab stop and the active option is announced through
|
|
36
|
+
`aria-activedescendant`. `↑`/`↓`, `Home`/`End` and `PageUp`/`PageDown` move the active option,
|
|
37
|
+
skipping disabled ones (no wrapping); typing letters jumps to the next match, diacritics ignored;
|
|
38
|
+
`Space`/`Enter` picks; in `multiple` mode `Ctrl`/`⌘`+`A` selects every visible option (again —
|
|
39
|
+
clears them). The filter field sits before the list (outside the `listbox`) and `↓` moves focus
|
|
40
|
+
from it into the list; the filter itself ignores diacritics ("lodz" finds "Łódź").
|
|
41
|
+
|
|
42
|
+
## Label and description
|
|
43
|
+
`id` goes to the list (`role="listbox"`), never the host. A `div` with a role is not named by
|
|
44
|
+
`label[for]`, so inside `KptFormField` the list gets `aria-labelledby` pointing at the wrapper's
|
|
45
|
+
label (unless `ariaLabel` is set) and the hint or error in `aria-describedby`. New: `id`,
|
|
46
|
+
`ariaLabel`, `required` (→ `aria-required`); `aria-invalid` after touch; `touch` fires on blur of the list.
|
|
47
|
+
|
|
33
48
|
## Tokens
|
|
34
49
|
`--kpt-color-surface`, `--kpt-color-border`, `--kpt-color-primary-subtle` (the selected row),
|
|
35
50
|
`--kpt-form-field-border-error`.
|
|
36
51
|
|
|
37
52
|
## Accessibility
|
|
38
|
-
`role="listbox"` with `aria-multiselectable
|
|
39
|
-
`role="option"`
|
|
53
|
+
The list is `role="listbox"` with `aria-multiselectable`, `aria-activedescendant` and a name from
|
|
54
|
+
`ariaLabel` or `KptFormField`; options are `role="option"` with `aria-selected`/`aria-disabled`.
|
|
55
|
+
The keyboard is described above.
|
package/dist/megamenu/llms.txt
CHANGED
|
@@ -5,21 +5,23 @@ Import: `import { KptMegamenu } from '@konce-pt/react';`
|
|
|
5
5
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-megamenu-host">
|
|
8
|
-
<
|
|
9
|
-
<button class="kpt-megamenu__top is-open" role="menuitem"
|
|
10
|
-
|
|
8
|
+
<div class="kpt-megamenu" role="menubar" aria-orientation="horizontal" aria-label="Main menu">
|
|
9
|
+
<button class="kpt-megamenu__top is-open" role="menuitem" tabindex="0"
|
|
10
|
+
aria-haspopup="menu" aria-expanded="true" aria-controls="kpt-megamenu-r1">Products …caret…</button>
|
|
11
|
+
</div>
|
|
11
12
|
</div>
|
|
12
13
|
<!-- while open, in a portal: -->
|
|
13
|
-
<div class="kpt-megamenu__panel" role="menu">
|
|
14
|
-
<div class="kpt-megamenu__column">
|
|
15
|
-
<div class="kpt-megamenu__col-title">Components</div>
|
|
16
|
-
<button class="kpt-megamenu__link">Forms</button>
|
|
14
|
+
<div class="kpt-megamenu__panel" role="menu" id="kpt-megamenu-r1" aria-label="Products">
|
|
15
|
+
<div class="kpt-megamenu__column" role="group" aria-labelledby="kpt-megamenu-r1-col-0">
|
|
16
|
+
<div class="kpt-megamenu__col-title" role="presentation" id="kpt-megamenu-r1-col-0">Components</div>
|
|
17
|
+
<button class="kpt-megamenu__link" role="menuitem" tabindex="-1">Forms</button>
|
|
17
18
|
</div>
|
|
18
19
|
</div>
|
|
19
20
|
|
|
20
21
|
## Props
|
|
21
22
|
- `items`: `KptMenuItem<T>[]` — three levels: top item → `items` (columns) → column `items` (links)
|
|
22
|
-
- `onSelect(link)` — a picked link from a column
|
|
23
|
+
- `onSelect(link)` — a picked link from a column, or a top item without columns
|
|
24
|
+
- `ariaLabel`: string — the bar's name for screen readers, e.g. "Main menu"
|
|
23
25
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
24
26
|
|
|
25
27
|
## The panel is anchored to the whole bar
|
|
@@ -39,10 +41,26 @@ without closing the panel, so browsing across sections costs one click each.
|
|
|
39
41
|
],
|
|
40
42
|
}];
|
|
41
43
|
|
|
44
|
+
## Links (`url`)
|
|
45
|
+
An entry with `url` and no children (and not disabled) renders as a real `<a href>` with the same
|
|
46
|
+
look, so the browser handles navigation — opening in a new tab, the status-bar preview. `onSelect`
|
|
47
|
+
still fires before the browser follows the link.
|
|
48
|
+
|
|
42
49
|
## Tokens
|
|
43
50
|
`--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`,
|
|
44
51
|
`--kpt-color-on-surface-muted` (the column titles).
|
|
45
52
|
|
|
46
53
|
## Accessibility
|
|
47
|
-
The bar is `role="menubar"`
|
|
48
|
-
|
|
54
|
+
The bar is `role="menubar"` (`aria-orientation="horizontal"`, named by `ariaLabel`) with
|
|
55
|
+
`role="menuitem"` buttons; those with columns carry `aria-haspopup="menu"`, `aria-expanded` and
|
|
56
|
+
`aria-controls`. The panel is `role="menu"` named after the open item, each column is
|
|
57
|
+
`role="group"` named by its title, and links are `role="menuitem"`. Column titles are not headings,
|
|
58
|
+
so they do not disturb the page outline. A top item without columns fires `onSelect` instead of
|
|
59
|
+
opening an empty panel.
|
|
60
|
+
|
|
61
|
+
## Keyboard
|
|
62
|
+
The same as `KptMenubar`: one `Tab` stop for the bar, `ArrowLeft`/`ArrowRight` and `Home`/`End`
|
|
63
|
+
along it, `ArrowDown` (or `Enter`/`Space`) opens the panel on the first link, `ArrowUp` on the last.
|
|
64
|
+
In the panel `ArrowDown`/`ArrowUp`/`Home`/`End` walk through all links column by column,
|
|
65
|
+
`ArrowLeft`/`ArrowRight` show the neighbouring item's panel, `Escape` or `Tab` close it and return
|
|
66
|
+
focus to the top item.
|
|
@@ -3,15 +3,24 @@ import type { KptMenuItem } from '../menu/menu-item.ts';
|
|
|
3
3
|
export interface KptMegamenuProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
|
|
4
4
|
items?: readonly KptMenuItem<T>[];
|
|
5
5
|
onSelect?: (item: KptMenuItem<T>) => void;
|
|
6
|
+
/** Nazwa belki dla czytników ekranu, np. „Menu główne”. */
|
|
7
|
+
ariaLabel?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Mega-menu — pozioma belka z szerokim, wielokolumnowym panelem.
|
|
9
11
|
* Struktura: pozycja górna → `items` (kolumny) → `items` kolumny (odnośniki).
|
|
12
|
+
* Pozycja górna bez kolumn działa jak akcja — wywołuje `onSelect` zamiast otwierać pusty panel.
|
|
10
13
|
*
|
|
11
14
|
* Panel kotwiczy się do CAŁEJ belki, nie do pojedynczego przycisku: jest szeroki, więc ma
|
|
12
15
|
* zaczynać się przy krawędzi menu, a nie przy przypadkowej pozycji, w którą kliknięto.
|
|
13
16
|
*
|
|
17
|
+
* Klawiatura jak w `KptMenubar`: belka to jeden przystanek Tab, strzałki w lewo/prawo
|
|
18
|
+
* i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera panel
|
|
19
|
+
* z fokusem na pierwszym odnośniku. W panelu strzałki w górę/dół przechodzą po wszystkich
|
|
20
|
+
* odnośnikach kolumna po kolumnie, w lewo/prawo — do panelu sąsiedniej pozycji, a `Escape`
|
|
21
|
+
* zamyka panel i oddaje fokus belce.
|
|
22
|
+
*
|
|
14
23
|
* @example
|
|
15
24
|
* <KptMegamenu items={items} onSelect={onSelect} />
|
|
16
25
|
*/
|
|
17
|
-
export declare function KptMegamenu<T>({ items, onSelect, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;
|
|
26
|
+
export declare function KptMegamenu<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;
|