@konce-pt/react 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/roving.d.ts +9 -0
- package/dist/internal/roving.js +43 -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 +5 -1
- package/dist/knob/knob.js +4 -2
- package/dist/knob/llms.txt +8 -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
|
@@ -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;
|
|
@@ -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
|
}
|