@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
|
@@ -1,28 +1,139 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
|
+
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
6
9
|
/**
|
|
7
10
|
* Mega-menu — pozioma belka z szerokim, wielokolumnowym panelem.
|
|
8
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.
|
|
9
13
|
*
|
|
10
14
|
* Panel kotwiczy się do CAŁEJ belki, nie do pojedynczego przycisku: jest szeroki, więc ma
|
|
11
15
|
* zaczynać się przy krawędzi menu, a nie przy przypadkowej pozycji, w którą kliknięto.
|
|
12
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
|
+
*
|
|
13
23
|
* @example
|
|
14
24
|
* <KptMegamenu items={items} onSelect={onSelect} />
|
|
15
25
|
*/
|
|
16
|
-
export function KptMegamenu({ items = [], onSelect, className, ...rest }) {
|
|
17
|
-
const
|
|
26
|
+
export function KptMegamenu({ items = [], onSelect, ariaLabel, className, ...rest }) {
|
|
27
|
+
const barRef = useRef(null);
|
|
28
|
+
const panelRef = useRef(null);
|
|
29
|
+
const buttonsRef = useRef([]);
|
|
18
30
|
const [openIndex, setOpenIndex] = useState(null);
|
|
19
|
-
|
|
31
|
+
// Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
|
|
32
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
33
|
+
// Który odnośnik skupić po otwarciu — strzałka w górę otwiera „od końca”.
|
|
34
|
+
const focusEdge = useRef('first');
|
|
35
|
+
const panelId = useKptId(undefined, 'kpt-megamenu');
|
|
36
|
+
const disabled = items.map((item) => !!item.disabled);
|
|
37
|
+
// Pozycja z `url` (bez kolumn, niewyłączona) to `<a href>` — nawigację robi przeglądarka,
|
|
38
|
+
// więc działa nowa karta i podgląd adresu. `onSelect` i tak jest wołane.
|
|
39
|
+
const isLink = (item) => !!item.url && !item.items?.length && !item.disabled;
|
|
40
|
+
const tabStop = rovingStop(disabled, focusIndex);
|
|
20
41
|
const columns = openIndex === null ? [] : (items[openIndex]?.items ?? []);
|
|
42
|
+
// Po otwarciu i po każdej zmianie pozycji fokus na skrajny odnośnik. Klatkę później:
|
|
43
|
+
// `KptOverlay` trzyma panel w `visibility: hidden`, dopóki go nie zmierzy.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (openIndex === null)
|
|
46
|
+
return;
|
|
47
|
+
const frame = requestAnimationFrame(() => {
|
|
48
|
+
if (panelRef.current)
|
|
49
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
50
|
+
});
|
|
51
|
+
return () => cancelAnimationFrame(frame);
|
|
52
|
+
}, [openIndex]);
|
|
53
|
+
const closeAll = () => setOpenIndex(null);
|
|
54
|
+
const focusTop = (index) => {
|
|
55
|
+
setFocusIndex(index);
|
|
56
|
+
buttonsRef.current[index]?.focus();
|
|
57
|
+
};
|
|
58
|
+
/** Skupia pozycję górną i pokazuje jej panel, jeśli ma kolumny (inaczej zamyka otwarty). */
|
|
59
|
+
const openAt = (index, edge) => {
|
|
60
|
+
focusEdge.current = edge;
|
|
61
|
+
const hasMenu = !!items[index]?.items?.length;
|
|
62
|
+
if (hasMenu && openIndex === index && panelRef.current) {
|
|
63
|
+
focusMenuEdge(panelRef.current, edge);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setOpenIndex(hasMenu ? index : null);
|
|
67
|
+
focusTop(index);
|
|
68
|
+
};
|
|
69
|
+
const toggle = (index, item) => {
|
|
70
|
+
if (item.disabled)
|
|
71
|
+
return;
|
|
72
|
+
if (item.items?.length) {
|
|
73
|
+
focusEdge.current = 'first';
|
|
74
|
+
setOpenIndex((current) => (current === index ? null : index));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
onSelect?.(item);
|
|
78
|
+
closeAll();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
21
81
|
const pick = (link) => {
|
|
22
82
|
if (link.disabled)
|
|
23
83
|
return;
|
|
84
|
+
const top = openIndex;
|
|
24
85
|
onSelect?.(link);
|
|
25
86
|
closeAll();
|
|
87
|
+
if (top !== null)
|
|
88
|
+
focusTop(top);
|
|
89
|
+
};
|
|
90
|
+
const onTopKeyDown = (event, index) => {
|
|
91
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
92
|
+
if (!items[index]?.items?.length)
|
|
93
|
+
return;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
openAt(index, event.key === 'ArrowUp' ? 'last' : 'first');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const next = nextRovingIndex(disabled, index, event.key);
|
|
99
|
+
if (next === null)
|
|
100
|
+
return;
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
// Przy otwartym panelu przejście w bok pokazuje panel sąsiada — jak w menu aplikacji.
|
|
103
|
+
if (openIndex === null)
|
|
104
|
+
focusTop(next);
|
|
105
|
+
else
|
|
106
|
+
openAt(next, 'first');
|
|
107
|
+
};
|
|
108
|
+
const onPanelKeyDown = (event) => {
|
|
109
|
+
if (openIndex === null)
|
|
110
|
+
return;
|
|
111
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
112
|
+
const next = nextRovingIndex(disabled, openIndex, event.key);
|
|
113
|
+
if (next === null)
|
|
114
|
+
return;
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
openAt(next, 'first');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close') {
|
|
120
|
+
const top = openIndex;
|
|
121
|
+
closeAll();
|
|
122
|
+
focusTop(top);
|
|
123
|
+
}
|
|
26
124
|
};
|
|
27
|
-
return (_jsxs("div", { className: cn('kpt-megamenu-host', className), ...rest, children: [_jsx("
|
|
125
|
+
return (_jsxs("div", { className: cn('kpt-megamenu-host', className), ...rest, children: [_jsx("div", { className: "kpt-megamenu", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, ref: barRef, children: items.map((item, index) => {
|
|
126
|
+
const hasMenu = !!item.items?.length;
|
|
127
|
+
if (isLink(item)) {
|
|
128
|
+
return (_jsxs("a", { ref: (el) => {
|
|
129
|
+
buttonsRef.current[index] = el;
|
|
130
|
+
}, className: "kpt-megamenu__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: () => toggle(index, item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label })] }, index));
|
|
131
|
+
}
|
|
132
|
+
return (_jsxs("button", { ref: (el) => {
|
|
133
|
+
buttonsRef.current[index] = el;
|
|
134
|
+
}, type: "button", className: cn('kpt-megamenu__top', openIndex === index && 'is-open'), role: "menuitem", disabled: item.disabled, tabIndex: index === tabStop ? 0 : -1, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? openIndex === index : undefined, "aria-controls": openIndex === index ? panelId : undefined, onClick: () => toggle(index, item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), hasMenu ? _jsx(KptIcon, { name: "chevron-down", className: "kpt-megamenu__caret" }) : null] }, index));
|
|
135
|
+
}) }), _jsx(KptOverlay, { open: openIndex !== null, anchor: barRef.current, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-megamenu__panel", role: "menu", id: panelId, "aria-label": openIndex === null ? undefined : items[openIndex]?.label, onKeyDown: onPanelKeyDown, children: columns.map((column, columnIndex) => {
|
|
136
|
+
const titleId = `${panelId}-col-${columnIndex}`;
|
|
137
|
+
return (_jsxs("div", { className: "kpt-megamenu__column", role: "group", "aria-labelledby": column.label ? titleId : undefined, children: [column.label ? (_jsx("div", { className: "kpt-megamenu__col-title", role: "presentation", id: titleId, children: column.label })) : null, (column.items ?? []).map((link, linkIndex) => isLink(link) ? (_jsxs("a", { className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, href: link.url, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)) : (_jsxs("button", { type: "button", className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, disabled: link.disabled, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)))] }, columnIndex));
|
|
138
|
+
}) }) })] }));
|
|
28
139
|
}
|
package/dist/menu/llms.txt
CHANGED
|
@@ -45,12 +45,23 @@ Angular port, which stands on CDK. Position, backdrop and Escape behave the same
|
|
|
45
45
|
`--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-danger`
|
|
46
46
|
(the `danger` item), `--kpt-z-overlay`.
|
|
47
47
|
|
|
48
|
+
## Keyboard
|
|
49
|
+
- `↓` / `↑` on the trigger open the menu with focus on the first / last item; a click, `Enter`
|
|
50
|
+
or `Space` opens it on the first.
|
|
51
|
+
- Inside: `↓` / `↑` move between items and wrap around, `Home` / `End` jump to the ends;
|
|
52
|
+
disabled items, separators and headers are skipped.
|
|
53
|
+
- `Escape` and `Tab` close the menu, and so does picking an item — focus returns to the trigger.
|
|
54
|
+
- Items carry `tabindex="-1"` (roving focus), so an open menu is one Tab stop, not one per item.
|
|
55
|
+
|
|
56
|
+
The logic lives in `menu-keys.ts`, shared with `KptSplitButton`; the other port has a twin file.
|
|
57
|
+
|
|
48
58
|
## Accessibility
|
|
49
59
|
The panel is `role="menu"`, items are native buttons with `role="menuitem"`, separators carry
|
|
50
60
|
`role="separator"` and header rows `role="presentation"`, so a screen reader skips them instead of
|
|
51
61
|
announcing them as actions.
|
|
52
62
|
|
|
53
|
-
The trigger itself gets `aria-haspopup="menu"
|
|
63
|
+
The trigger itself gets `aria-haspopup="menu"`, a live `aria-expanded` and, while open,
|
|
64
|
+
`aria-controls` pointing at the panel. They are placed on the
|
|
54
65
|
interactive control INSIDE the slot (the projected `<button>`/`<a>`), never on the
|
|
55
66
|
`.kpt-menu__trigger` wrapper — giving that wrapper a role would create a nested control and a second
|
|
56
67
|
tab stop, and state attributes on a roleless element violate `aria-allowed-attr`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indeks pozycji, na którą ma przejść fokus. `count` to liczba **aktywnych** pozycji (wyłączone,
|
|
3
|
+
* separatory i nagłówki są już odfiltrowane), `current` to indeks obecnie skupionej albo `-1`.
|
|
4
|
+
* Strzałki zawijają się na końcach. Zwraca `null` dla klawisza, który nie nawiguje.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nextMenuIndex(count: number, current: number, key: string): number | null;
|
|
7
|
+
/** Aktywne pozycje panelu w kolejności DOM. */
|
|
8
|
+
export declare function menuItems(panel: HTMLElement): HTMLElement[];
|
|
9
|
+
/** Skupia pierwszą albo ostatnią aktywną pozycję — po otwarciu menu z klawiatury lub myszy. */
|
|
10
|
+
export declare function focusMenuEdge(panel: HTMLElement, edge: 'first' | 'last'): void;
|
|
11
|
+
/**
|
|
12
|
+
* Obsługa klawisza w otwartym panelu. Zwraca `'close'`, gdy menu ma się zamknąć z powrotem
|
|
13
|
+
* fokusu na przycisk (Escape, Tab), `true`, gdy klawisz przesunął fokus, `false` — inny klawisz.
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleMenuKeydown(panel: HTMLElement, event: KeyboardEvent): 'close' | boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Klawiatura panelu menu (wzorzec WAI-ARIA „menu button”), wspólna dla `kpt-menu`
|
|
3
|
+
* i `kpt-split-button` — oba mają ten sam panel z pozycjami `role="menuitem"`.
|
|
4
|
+
* Port Angulara ma bliźniaczy plik `lib/menu/menu-keys.ts` — zmiana zachowania w obu.
|
|
5
|
+
*/
|
|
6
|
+
/** Klawisze przesuwające fokus po pozycjach. */
|
|
7
|
+
const NAV_KEYS = new Set(['ArrowDown', 'ArrowUp', 'Home', 'End']);
|
|
8
|
+
/**
|
|
9
|
+
* Indeks pozycji, na którą ma przejść fokus. `count` to liczba **aktywnych** pozycji (wyłączone,
|
|
10
|
+
* separatory i nagłówki są już odfiltrowane), `current` to indeks obecnie skupionej albo `-1`.
|
|
11
|
+
* Strzałki zawijają się na końcach. Zwraca `null` dla klawisza, który nie nawiguje.
|
|
12
|
+
*/
|
|
13
|
+
export function nextMenuIndex(count, current, key) {
|
|
14
|
+
if (!NAV_KEYS.has(key) || count === 0)
|
|
15
|
+
return null;
|
|
16
|
+
switch (key) {
|
|
17
|
+
case 'Home':
|
|
18
|
+
return 0;
|
|
19
|
+
case 'End':
|
|
20
|
+
return count - 1;
|
|
21
|
+
case 'ArrowDown':
|
|
22
|
+
return current < 0 ? 0 : (current + 1) % count;
|
|
23
|
+
default:
|
|
24
|
+
return current < 0 ? count - 1 : (current - 1 + count) % count;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Aktywne pozycje panelu w kolejności DOM. */
|
|
28
|
+
export function menuItems(panel) {
|
|
29
|
+
return [...panel.querySelectorAll('[role="menuitem"]:not(:disabled)')];
|
|
30
|
+
}
|
|
31
|
+
/** Skupia pierwszą albo ostatnią aktywną pozycję — po otwarciu menu z klawiatury lub myszy. */
|
|
32
|
+
export function focusMenuEdge(panel, edge) {
|
|
33
|
+
const items = menuItems(panel);
|
|
34
|
+
(edge === 'first' ? items[0] : items[items.length - 1])?.focus();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Obsługa klawisza w otwartym panelu. Zwraca `'close'`, gdy menu ma się zamknąć z powrotem
|
|
38
|
+
* fokusu na przycisk (Escape, Tab), `true`, gdy klawisz przesunął fokus, `false` — inny klawisz.
|
|
39
|
+
*/
|
|
40
|
+
export function handleMenuKeydown(panel, event) {
|
|
41
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
return 'close';
|
|
44
|
+
}
|
|
45
|
+
const items = menuItems(panel);
|
|
46
|
+
const current = items.indexOf(document.activeElement);
|
|
47
|
+
const next = nextMenuIndex(items.length, current, event.key);
|
|
48
|
+
if (next === null)
|
|
49
|
+
return false;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
items[next].focus();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/dist/menu/menu-rows.js
CHANGED
|
@@ -10,6 +10,6 @@ export function MenuRows({ items, block, onPick }) {
|
|
|
10
10
|
if (item.header) {
|
|
11
11
|
return (_jsxs("div", { className: `${block}__header`, role: "presentation", children: [item.avatar ? (_jsx(KptAvatar, { src: item.avatar.src ?? null, name: item.avatar.name ?? '', icon: item.avatar.icon ?? null, size: "sm" })) : item.icon ? (_jsx(KptIcon, { name: item.icon })) : null, _jsxs("span", { className: `${block}__text`, children: [item.label ? _jsx("span", { className: `${block}__label`, children: item.label }) : null, item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] })] }, index));
|
|
12
12
|
}
|
|
13
|
-
return (_jsxs("button", { type: "button", className: `${block}__item${item.danger ? ` ${block}__item--danger` : ''}`, role: "menuitem", disabled: item.disabled, onClick: () => onPick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsxs("span", { className: `${block}__text`, children: [_jsx("span", { className: `${block}__label`, children: item.label }), item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] }), item.badge !== undefined && item.badge !== null ? (_jsx(KptBadge, { className: `${block}__badge`, value: item.badge, variant: item.badgeVariant ?? 'primary' })) : null] }, index));
|
|
13
|
+
return (_jsxs("button", { type: "button", className: `${block}__item${item.danger ? ` ${block}__item--danger` : ''}`, role: "menuitem", tabIndex: -1, disabled: item.disabled, onClick: () => onPick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsxs("span", { className: `${block}__text`, children: [_jsx("span", { className: `${block}__label`, children: item.label }), item.description ? _jsx("span", { className: `${block}__description`, children: item.description }) : null] }), item.badge !== undefined && item.badge !== null ? (_jsx(KptBadge, { className: `${block}__badge`, value: item.badge, variant: item.badgeVariant ?? 'primary' })) : null] }, index));
|
|
14
14
|
});
|
|
15
15
|
}
|
package/dist/menu/menu.js
CHANGED
|
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
7
|
+
import { focusMenuEdge, handleMenuKeydown } from "./menu-keys.js";
|
|
6
8
|
import { MenuRows } from "./menu-rows.js";
|
|
7
9
|
/**
|
|
8
10
|
* Menu rozwijane (dropdown). Pozycje przez `items`, wyzwalacz przez prop `trigger`.
|
|
@@ -15,6 +17,10 @@ import { MenuRows } from "./menu-rows.js";
|
|
|
15
17
|
*/
|
|
16
18
|
export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = false, onOpenChange, className, ...rest }) {
|
|
17
19
|
const triggerRef = useRef(null);
|
|
20
|
+
const panelRef = useRef(null);
|
|
21
|
+
/** Którą pozycję skupić po otwarciu — strzałka w górę na wyzwalaczu otwiera „od końca”. */
|
|
22
|
+
const focusEdge = useRef('first');
|
|
23
|
+
const panelId = useKptId(undefined, 'kpt-menu');
|
|
18
24
|
const [isOpen, setOpen] = useControllableState({
|
|
19
25
|
value: open,
|
|
20
26
|
defaultValue: defaultOpen,
|
|
@@ -26,17 +32,59 @@ export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = fal
|
|
|
26
32
|
// zagnieżdżoną kontrolkę i drugi tab stop. `cloneElement` też odpada — `trigger` bywa
|
|
27
33
|
// komponentem, który nie przekazuje nieznanych propsów dalej na DOM.
|
|
28
34
|
useEffect(() => {
|
|
29
|
-
const control =
|
|
35
|
+
const control = findControl();
|
|
30
36
|
if (!control)
|
|
31
37
|
return;
|
|
32
38
|
control.setAttribute('aria-haspopup', 'menu');
|
|
33
39
|
control.setAttribute('aria-expanded', String(isOpen));
|
|
34
|
-
|
|
40
|
+
if (isOpen)
|
|
41
|
+
control.setAttribute('aria-controls', panelId);
|
|
42
|
+
else
|
|
43
|
+
control.removeAttribute('aria-controls');
|
|
44
|
+
}, [isOpen, trigger, panelId]);
|
|
45
|
+
// Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
|
|
46
|
+
// panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isOpen)
|
|
49
|
+
return;
|
|
50
|
+
const frame = requestAnimationFrame(() => {
|
|
51
|
+
if (panelRef.current)
|
|
52
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
53
|
+
});
|
|
54
|
+
return () => cancelAnimationFrame(frame);
|
|
55
|
+
}, [isOpen]);
|
|
56
|
+
/** Prawdziwa kontrolka w slocie — tam idą ARIA i powrót fokusu. */
|
|
57
|
+
function findControl() {
|
|
58
|
+
return triggerRef.current?.querySelector('button, a, [role="button"]') ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** Fokus wraca na kontrolkę — inaczej po zniknięciu panelu spadłby na `<body>`. */
|
|
61
|
+
const closeAndRestoreFocus = () => {
|
|
62
|
+
close();
|
|
63
|
+
findControl()?.focus();
|
|
64
|
+
};
|
|
65
|
+
const onTriggerKeyDown = (event) => {
|
|
66
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
67
|
+
return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
focusEdge.current = event.key === 'ArrowUp' ? 'last' : 'first';
|
|
70
|
+
if (isOpen && panelRef.current)
|
|
71
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
72
|
+
else
|
|
73
|
+
setOpen(true);
|
|
74
|
+
};
|
|
75
|
+
const onPanelKeyDown = (event) => {
|
|
76
|
+
if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close') {
|
|
77
|
+
closeAndRestoreFocus();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
35
80
|
const pick = (item) => {
|
|
36
81
|
if (item.disabled)
|
|
37
82
|
return;
|
|
38
83
|
onSelect?.(item);
|
|
39
|
-
|
|
84
|
+
closeAndRestoreFocus();
|
|
40
85
|
};
|
|
41
|
-
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () =>
|
|
86
|
+
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () => {
|
|
87
|
+
focusEdge.current = 'first';
|
|
88
|
+
setOpen(!isOpen);
|
|
89
|
+
}, onKeyDown: onTriggerKeyDown, children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-menu__panel", role: "menu", onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
|
|
42
90
|
}
|
package/dist/menubar/llms.txt
CHANGED
|
@@ -5,18 +5,20 @@ Import: `import { KptMenubar } from '@konce-pt/react';`
|
|
|
5
5
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-menubar-host">
|
|
8
|
-
<
|
|
9
|
-
<div class="kpt-menubar__group">
|
|
10
|
-
<button class="kpt-menubar__top is-open" role="menuitem"
|
|
8
|
+
<div class="kpt-menubar" role="menubar" aria-orientation="horizontal" aria-label="Editor menu">
|
|
9
|
+
<div class="kpt-menubar__group" role="none">
|
|
10
|
+
<button class="kpt-menubar__top is-open" role="menuitem" tabindex="0"
|
|
11
|
+
aria-haspopup="menu" aria-expanded="true" aria-controls="kpt-menubar-r1-menu-0">File …caret…</button>
|
|
11
12
|
</div>
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
</div>
|
|
14
15
|
<!-- while open, in a portal: -->
|
|
15
|
-
<div class="kpt-menubar__dropdown" role="menu">…items and separators…</div>
|
|
16
|
+
<div class="kpt-menubar__dropdown" role="menu" id="kpt-menubar-r1-menu-0" aria-label="File">…items and separators…</div>
|
|
16
17
|
|
|
17
18
|
## Props
|
|
18
19
|
- `items`: `KptMenuItem<T>[]` — a top item's `items` become its dropdown
|
|
19
20
|
- `onSelect(item)` — an action item, or a pick from a dropdown
|
|
21
|
+
- `ariaLabel`: string — the bar's name for screen readers, e.g. "Editor menu"
|
|
20
22
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
21
23
|
|
|
22
24
|
## The dropdown is a portal
|
|
@@ -36,10 +38,25 @@ bar can mix menus and plain actions with no extra flag.
|
|
|
36
38
|
{ label: 'Help' },
|
|
37
39
|
];
|
|
38
40
|
|
|
41
|
+
## Links (`url`)
|
|
42
|
+
An entry with `url` and no children (and not disabled) renders as a real `<a href>` with the same
|
|
43
|
+
look, so the browser handles navigation — opening in a new tab, the status-bar preview. `onSelect`
|
|
44
|
+
still fires before the browser follows the link.
|
|
45
|
+
|
|
39
46
|
## Tokens
|
|
40
47
|
`--kpt-color-surface`, `--kpt-color-muted` (the open top item), `--kpt-color-surface-raised` and
|
|
41
48
|
`--kpt-elevation-3` for the dropdown, `--kpt-color-danger`.
|
|
42
49
|
|
|
43
50
|
## Accessibility
|
|
44
|
-
The bar is `role="menubar"
|
|
45
|
-
`role="separator"`.
|
|
51
|
+
The bar is `role="menubar"` (`aria-orientation="horizontal"`, named by `ariaLabel`), top items and
|
|
52
|
+
dropdown rows are `role="menuitem"`, separators carry `role="separator"`. Top items with a dropdown
|
|
53
|
+
carry `aria-haspopup="menu"`, `aria-expanded` and, while open, `aria-controls`. The bar is a plain
|
|
54
|
+
`<div>`, not `<nav>` — wrap it in your own `<nav aria-label="…">` when it is the site navigation.
|
|
55
|
+
|
|
56
|
+
## Keyboard
|
|
57
|
+
The bar is a single `Tab` stop (roving `tabindex`), following the WAI-ARIA menubar pattern.
|
|
58
|
+
- On a top item: `ArrowLeft`/`ArrowRight` move along the bar (wrapping, skipping disabled items),
|
|
59
|
+
`Home`/`End` jump to the ends; `ArrowDown`, `Enter` or `Space` open the dropdown with focus on
|
|
60
|
+
its first row, `ArrowUp` on its last one.
|
|
61
|
+
- In a dropdown: `ArrowDown`/`ArrowUp`/`Home`/`End` move between rows, `ArrowLeft`/`ArrowRight`
|
|
62
|
+
open the neighbouring dropdown, `Escape` and `Tab` close it and return focus to the top item.
|
|
@@ -3,6 +3,8 @@ import type { KptMenuItem } from '../menu/menu-item.ts';
|
|
|
3
3
|
export interface KptMenubarProps<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 edytora”. */
|
|
7
|
+
ariaLabel?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
|
|
@@ -10,7 +12,12 @@ export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
10
12
|
* Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
|
|
11
13
|
* korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
|
|
12
14
|
*
|
|
15
|
+
* Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
|
|
16
|
+
* i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
|
|
17
|
+
* pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
|
|
18
|
+
* w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
|
|
19
|
+
*
|
|
13
20
|
* @example
|
|
14
21
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
15
22
|
*/
|
|
16
|
-
export declare function KptMenubar<T>({ items, onSelect, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
|
23
|
+
export declare function KptMenubar<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
package/dist/menubar/menubar.js
CHANGED
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
|
+
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
6
9
|
/**
|
|
7
10
|
* Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
|
|
8
11
|
*
|
|
9
12
|
* Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
|
|
10
13
|
* korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
|
|
11
14
|
*
|
|
15
|
+
* Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
|
|
16
|
+
* i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
|
|
17
|
+
* pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
|
|
18
|
+
* w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
|
|
19
|
+
*
|
|
12
20
|
* @example
|
|
13
21
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
14
22
|
*/
|
|
15
|
-
export function KptMenubar({ items = [], onSelect, className, ...rest }) {
|
|
23
|
+
export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest }) {
|
|
16
24
|
const [openIndex, setOpenIndex] = useState(null);
|
|
25
|
+
// Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
|
|
26
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
17
27
|
const buttonsRef = useRef([]);
|
|
28
|
+
const panelRef = useRef(null);
|
|
29
|
+
// Którą pozycję pod-menu skupić po otwarciu — strzałka w górę otwiera „od końca”.
|
|
30
|
+
const focusEdge = useRef('first');
|
|
31
|
+
const baseId = useKptId(undefined, 'kpt-menubar');
|
|
32
|
+
const panelId = (index) => `${baseId}-menu-${index}`;
|
|
33
|
+
const disabled = items.map((item) => !!item.disabled);
|
|
34
|
+
// Pozycja z `url` (bez pod-menu, niewyłączona) to `<a href>` — nawigację robi przeglądarka,
|
|
35
|
+
// więc działa nowa karta i podgląd adresu. `onSelect` i tak jest wołane.
|
|
36
|
+
const isLink = (item) => !!item.url && !item.items?.length && !item.disabled;
|
|
37
|
+
const tabStop = rovingStop(disabled, focusIndex);
|
|
38
|
+
// Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
|
|
39
|
+
// panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (openIndex === null)
|
|
42
|
+
return;
|
|
43
|
+
const frame = requestAnimationFrame(() => {
|
|
44
|
+
if (panelRef.current)
|
|
45
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
46
|
+
});
|
|
47
|
+
return () => cancelAnimationFrame(frame);
|
|
48
|
+
}, [openIndex]);
|
|
18
49
|
const closeAll = () => setOpenIndex(null);
|
|
50
|
+
const focusTop = (index) => {
|
|
51
|
+
setFocusIndex(index);
|
|
52
|
+
buttonsRef.current[index]?.focus();
|
|
53
|
+
};
|
|
54
|
+
/** Skupia pozycję górną i otwiera jej pod-menu, jeśli je ma (inaczej zamyka otwarte). */
|
|
55
|
+
const openAt = (index, edge) => {
|
|
56
|
+
focusEdge.current = edge;
|
|
57
|
+
const hasMenu = !!items[index]?.items?.length;
|
|
58
|
+
if (hasMenu && openIndex === index && panelRef.current) {
|
|
59
|
+
focusMenuEdge(panelRef.current, edge);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setOpenIndex(hasMenu ? index : null);
|
|
63
|
+
focusTop(index);
|
|
64
|
+
};
|
|
19
65
|
const toggle = (index, item) => {
|
|
20
66
|
if (item.disabled)
|
|
21
67
|
return;
|
|
22
68
|
if (item.items?.length) {
|
|
69
|
+
focusEdge.current = 'first';
|
|
23
70
|
setOpenIndex((current) => (current === index ? null : index));
|
|
24
71
|
}
|
|
25
72
|
else {
|
|
@@ -27,13 +74,52 @@ export function KptMenubar({ items = [], onSelect, className, ...rest }) {
|
|
|
27
74
|
closeAll();
|
|
28
75
|
}
|
|
29
76
|
};
|
|
30
|
-
const pick = (item) => {
|
|
77
|
+
const pick = (item, index) => {
|
|
31
78
|
if (item.disabled)
|
|
32
79
|
return;
|
|
33
80
|
onSelect?.(item);
|
|
34
81
|
closeAll();
|
|
82
|
+
// Fokus wraca na pozycję górną — inaczej po zniknięciu pod-menu spadłby na `<body>`.
|
|
83
|
+
focusTop(index);
|
|
84
|
+
};
|
|
85
|
+
const onTopKeyDown = (event, index) => {
|
|
86
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
87
|
+
if (!items[index]?.items?.length)
|
|
88
|
+
return;
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
openAt(index, event.key === 'ArrowUp' ? 'last' : 'first');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const next = nextRovingIndex(disabled, index, event.key);
|
|
94
|
+
if (next === null)
|
|
95
|
+
return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
// Przy otwartym pod-menu przejście w bok otwiera pod-menu sąsiada — jak w menu aplikacji.
|
|
98
|
+
if (openIndex === null)
|
|
99
|
+
focusTop(next);
|
|
100
|
+
else
|
|
101
|
+
openAt(next, 'first');
|
|
102
|
+
};
|
|
103
|
+
const onPanelKeyDown = (event, index) => {
|
|
104
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
105
|
+
const next = nextRovingIndex(disabled, index, event.key);
|
|
106
|
+
if (next === null)
|
|
107
|
+
return;
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
openAt(next, 'first');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close') {
|
|
113
|
+
closeAll();
|
|
114
|
+
focusTop(index);
|
|
115
|
+
}
|
|
35
116
|
};
|
|
36
|
-
return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("div", { className: "kpt-menubar", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, children: items.map((item, index) => {
|
|
118
|
+
const hasMenu = !!item.items?.length;
|
|
119
|
+
return (_jsxs("div", { className: "kpt-menubar__group", role: "none", children: [isLink(item) ? (_jsxs("a", { ref: (el) => {
|
|
120
|
+
buttonsRef.current[index] = el;
|
|
121
|
+
}, className: "kpt-menubar__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: () => pick(item, index), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label })] })) : (_jsxs("button", { ref: (el) => {
|
|
122
|
+
buttonsRef.current[index] = el;
|
|
123
|
+
}, type: "button", className: cn('kpt-menubar__top', openIndex === index && 'is-open'), role: "menuitem", disabled: item.disabled, tabIndex: index === tabStop ? 0 : -1, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? openIndex === index : undefined, "aria-controls": openIndex === index ? panelId(index) : undefined, onClick: () => toggle(index, item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), hasMenu ? _jsx(KptIcon, { name: "chevron-down", className: "kpt-menubar__caret" }) : null] })), _jsx(KptOverlay, { open: openIndex === index && hasMenu, anchor: buttonsRef.current[index] ?? null, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-menubar__dropdown", role: "menu", id: panelId(index), "aria-label": item.label, onKeyDown: (event) => onPanelKeyDown(event, index), children: (item.items ?? []).map((sub, subIndex) => sub.separator ? (_jsx("div", { className: "kpt-menubar__separator", role: "separator" }, subIndex)) : isLink(sub) ? (_jsxs("a", { className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, href: sub.url, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex)) : (_jsxs("button", { type: "button", className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, disabled: sub.disabled, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex))) }) })] }, index));
|
|
124
|
+
}) }) }));
|
|
39
125
|
}
|
|
@@ -6,7 +6,8 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-meter-group-host">
|
|
8
8
|
<div class="kpt-meter-group">
|
|
9
|
-
<div class="kpt-meter-group__track" role="meter" aria-
|
|
9
|
+
<div class="kpt-meter-group__track" role="meter" aria-label="Usage" aria-valuemin="0"
|
|
10
|
+
aria-valuenow="100" aria-valuemax="100" aria-valuetext="Documents 45, …">
|
|
10
11
|
<span class="kpt-meter-group__segment" style="width:45%" title="Documents"></span>…
|
|
11
12
|
</div>
|
|
12
13
|
<ul class="kpt-meter-group__legend">
|
|
@@ -22,6 +23,9 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
|
|
|
22
23
|
- `values`: `KptMeterItem[]` — `{ label, value, color? }`
|
|
23
24
|
- `max`: number — the reference total; without it the values' sum is 100%
|
|
24
25
|
- `showLegend`: boolean (default true)
|
|
26
|
+
- `label`: string — the meter's name; empty = `meterGroup.label`
|
|
27
|
+
- `legendValue`: 'percent' | 'value' (default 'percent') — what the legend shows next to each label
|
|
28
|
+
- `unit`: string — appended to the values in the legend and in `aria-valuetext` (e.g. 'GB')
|
|
25
29
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
26
30
|
|
|
27
31
|
## Percentages come from the sum, not from 100
|
|
@@ -43,5 +47,8 @@ The palette `--kpt-color-{primary,info,success,warning,danger}`, the track
|
|
|
43
47
|
`--kpt-color-surface-sunken`.
|
|
44
48
|
|
|
45
49
|
## Accessibility
|
|
46
|
-
The track is `role="meter"`
|
|
47
|
-
|
|
50
|
+
The track is `role="meter"` named by `label` (or `meterGroup.label`), with `aria-valuemin="0"`,
|
|
51
|
+
`aria-valuenow` set to the sum and `aria-valuemax` set to the real total — `max`, or the sum without
|
|
52
|
+
it. `aria-valuetext` spells out the split (“Photos 42 GB, Videos 28 GB, …”), so the segments reach
|
|
53
|
+
the screen reader even with the legend hidden. Keep the legend visible anyway: it is the text
|
|
54
|
+
alternative to the colours.
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
2
3
|
export interface KptMeterItem {
|
|
3
4
|
label: string;
|
|
4
5
|
value: number;
|
|
5
6
|
/** Własny kolor segmentu; bez niego wchodzi kolejny z palety. */
|
|
6
7
|
color?: string;
|
|
7
8
|
}
|
|
8
|
-
export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'
|
|
9
|
+
export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
|
|
9
10
|
values?: readonly KptMeterItem[];
|
|
10
11
|
/** Suma odniesienia; domyślnie suma wartości, czyli segmenty wypełniają całość. */
|
|
11
12
|
max?: number;
|
|
12
13
|
showLegend?: boolean;
|
|
14
|
+
/** Nazwa paska dla czytników ekranu, np. „Miejsce na dysku”; domyślnie `meterGroup.label`. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Co pokazuje legenda przy etykiecie: udział procentowy albo samą wartość (z `unit`). */
|
|
17
|
+
legendValue?: 'percent' | 'value';
|
|
18
|
+
/** Jednostka dopisywana do wartości — w legendzie (`legendValue="value"`) i w `aria-valuetext`. */
|
|
19
|
+
unit?: string;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Wielosegmentowy miernik z opcjonalną legendą.
|
|
@@ -17,4 +24,4 @@ export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'> {
|
|
|
17
24
|
* @example
|
|
18
25
|
* <KptMeterGroup values={[{ label: 'Użyte', value: 70 }, { label: 'Wolne', value: 30 }]} />
|
|
19
26
|
*/
|
|
20
|
-
export declare function KptMeterGroup({ values, max, showLegend, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
|
|
27
|
+
export declare function KptMeterGroup({ values, max, showLegend, label, legendValue, unit, locale, dictionary, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
|