@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
package/dist/select/select.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
8
8
|
multiple?: boolean;
|
|
9
9
|
/** Pole wyszukiwania nad listą. */
|
|
10
10
|
filter?: boolean;
|
|
11
|
+
/** Id wyzwalacza; bez niego generowane, w `KptFormField` — z wrappera. */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** Oznacza pole jako wymagane atrybutem `aria-required`. */
|
|
14
|
+
required?: boolean;
|
|
11
15
|
disabled?: boolean;
|
|
12
16
|
invalid?: boolean;
|
|
13
17
|
touched?: boolean;
|
|
@@ -26,6 +30,13 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
26
30
|
* Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
|
|
27
31
|
* i wielokrotny (`multiple`, chipy).
|
|
28
32
|
*
|
|
33
|
+
* Klawiatura według wzorca WAI-ARIA „select-only combobox”: fokus zostaje na polu, a aktywna opcja
|
|
34
|
+
* idzie przez `aria-activedescendant`. `↓`/`↑`/`Enter`/`Spacja` otwierają listę na wybranej opcji,
|
|
35
|
+
* strzałki, `Home`/`End`, `PageUp`/`PageDown` chodzą po opcjach (z pominięciem wyłączonych),
|
|
36
|
+
* wpisywane litery skaczą do pasującej opcji (bez polskich znaków), `Enter` wybiera, `Escape`
|
|
37
|
+
* zamyka. Z `filter` fokus przechodzi do pola wyszukiwania, które obsługuje te same klawisze.
|
|
38
|
+
* W trybie `multiple` `Backspace` na zamkniętym polu usuwa ostatni wybór.
|
|
39
|
+
*
|
|
29
40
|
* Wewnątrz `KptFormField` zgłasza wypełnienie hookiem `useKptFormFieldFilled`, więc etykieta
|
|
30
41
|
* Float Label unosi się nad wybraną wartością — natywnego pola, z którego wrapper mógłby to
|
|
31
42
|
* odczytać, tu nie ma.
|
|
@@ -34,4 +45,4 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
34
45
|
* <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz…" />
|
|
35
46
|
* <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
|
|
36
47
|
*/
|
|
37
|
-
export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
|
|
48
|
+
export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
|
package/dist/select/select.js
CHANGED
|
@@ -3,13 +3,22 @@ import { useCallback, useEffect, useMemo, useRef, useState, } 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 { KptTypeahead, OPTION_NAV_KEYS, isTypeaheadKey, nextOptionIndex, typeaheadIndex } from "../internal/option-nav.js";
|
|
6
8
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
|
-
import { useKptFormFieldFilled } from "../form-field/form-field-context.js";
|
|
9
|
+
import { joinIds, useKptFieldA11y, useKptFormFieldFilled } from "../form-field/form-field-context.js";
|
|
8
10
|
import { KptIcon } from "../icon/icon.js";
|
|
9
11
|
/**
|
|
10
12
|
* Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
|
|
11
13
|
* i wielokrotny (`multiple`, chipy).
|
|
12
14
|
*
|
|
15
|
+
* Klawiatura według wzorca WAI-ARIA „select-only combobox”: fokus zostaje na polu, a aktywna opcja
|
|
16
|
+
* idzie przez `aria-activedescendant`. `↓`/`↑`/`Enter`/`Spacja` otwierają listę na wybranej opcji,
|
|
17
|
+
* strzałki, `Home`/`End`, `PageUp`/`PageDown` chodzą po opcjach (z pominięciem wyłączonych),
|
|
18
|
+
* wpisywane litery skaczą do pasującej opcji (bez polskich znaków), `Enter` wybiera, `Escape`
|
|
19
|
+
* zamyka. Z `filter` fokus przechodzi do pola wyszukiwania, które obsługuje te same klawisze.
|
|
20
|
+
* W trybie `multiple` `Backspace` na zamkniętym polu usuwa ostatni wybór.
|
|
21
|
+
*
|
|
13
22
|
* Wewnątrz `KptFormField` zgłasza wypełnienie hookiem `useKptFormFieldFilled`, więc etykieta
|
|
14
23
|
* Float Label unosi się nad wybraną wartością — natywnego pola, z którego wrapper mógłby to
|
|
15
24
|
* odczytać, tu nie ma.
|
|
@@ -18,9 +27,16 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
18
27
|
* <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz…" />
|
|
19
28
|
* <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
|
|
20
29
|
*/
|
|
21
|
-
export function KptSelect({ options = [], placeholder = '', multiple = false, filter = false, disabled = false, invalid = false, touched, value, defaultValue = null, onValueChange, open, defaultOpen = false, onOpenChange, onTouch, locale, dictionary, className, onBlur, ...rest }) {
|
|
30
|
+
export function KptSelect({ options = [], placeholder = '', multiple = false, filter = false, disabled = false, invalid = false, touched, value, defaultValue = null, onValueChange, open, defaultOpen = false, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }) {
|
|
31
|
+
/** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
32
|
+
const field = useKptFieldA11y(id, 'kpt-select-trigger');
|
|
22
33
|
const { t } = useKptMessages({ locale, dictionary });
|
|
23
34
|
const triggerRef = useRef(null);
|
|
35
|
+
const searchRef = useRef(null);
|
|
36
|
+
const typeahead = useRef(new KptTypeahead());
|
|
37
|
+
const uid = useKptId(undefined, 'kpt-select');
|
|
38
|
+
const listId = `${uid}-list`;
|
|
39
|
+
const optionId = (index) => `${uid}-opt-${index}`;
|
|
24
40
|
const [current, setCurrent] = useControllableState({
|
|
25
41
|
value,
|
|
26
42
|
defaultValue,
|
|
@@ -33,14 +49,6 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
33
49
|
});
|
|
34
50
|
const [filterText, setFilterText] = useState('');
|
|
35
51
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
36
|
-
// Panel może zostać otwarty także z zewnątrz (`open`), nie tylko klikiem w trigger —
|
|
37
|
-
// filtr i aktywną pozycję resetujemy więc przy każdym otwarciu, nie przy przełączaniu.
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (!isOpen)
|
|
40
|
-
return;
|
|
41
|
-
setFilterText('');
|
|
42
|
-
setActiveIndex(-1);
|
|
43
|
-
}, [isOpen]);
|
|
44
52
|
const showInvalid = invalid && (touched ?? true);
|
|
45
53
|
const selectedValues = useMemo(() => {
|
|
46
54
|
if (multiple)
|
|
@@ -48,19 +56,47 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
48
56
|
return current == null ? [] : [current];
|
|
49
57
|
}, [current, multiple]);
|
|
50
58
|
const selectedOptions = useMemo(() => options.filter((option) => selectedValues.includes(option.value)), [options, selectedValues]);
|
|
51
|
-
const
|
|
52
|
-
const q =
|
|
59
|
+
const filterOptions = (text) => {
|
|
60
|
+
const q = text.trim().toLowerCase();
|
|
53
61
|
return q ? options.filter((option) => option.label.toLowerCase().includes(q)) : options;
|
|
54
|
-
}
|
|
62
|
+
};
|
|
63
|
+
const filteredOptions = useMemo(() => filterOptions(filterText), [options, filterText]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
64
|
+
const disabledMap = filteredOptions.map((option) => !!option.disabled);
|
|
55
65
|
const filled = selectedOptions.length > 0;
|
|
56
66
|
useKptFormFieldFilled(filled);
|
|
57
67
|
const isSelected = (option) => selectedValues.includes(option.value);
|
|
68
|
+
// Panel może zostać otwarty także z zewnątrz (`open`), nie tylko z pola — filtr i aktywną
|
|
69
|
+
// pozycję ustawiamy więc przy każdym otwarciu. Lista otwiera się na wybranej opcji; z `filter`
|
|
70
|
+
// fokus idzie do wyszukiwarki (klatkę później: `KptOverlay` mierzy panel w `visibility: hidden`).
|
|
71
|
+
// Tylko na otwarcie — zmiana wyboru w trybie `multiple` nie może przesuwać aktywnej opcji.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isOpen)
|
|
74
|
+
return;
|
|
75
|
+
setFilterText('');
|
|
76
|
+
const selected = options.findIndex((option) => selectedValues.includes(option.value));
|
|
77
|
+
const firstEnabled = nextOptionIndex(options.map((o) => !!o.disabled), -1, 'ArrowDown') ?? -1;
|
|
78
|
+
setActiveIndex(selected >= 0 ? selected : firstEnabled);
|
|
79
|
+
if (!filter)
|
|
80
|
+
return;
|
|
81
|
+
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
82
|
+
return () => cancelAnimationFrame(frame);
|
|
83
|
+
}, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
84
|
+
// Aktywna opcja zawsze w polu widzenia listy.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (isOpen && activeIndex >= 0)
|
|
87
|
+
document.getElementById(`${uid}-opt-${activeIndex}`)?.scrollIntoView({ block: 'nearest' });
|
|
88
|
+
}, [isOpen, activeIndex, uid]);
|
|
58
89
|
const close = useCallback(() => {
|
|
59
90
|
if (!isOpen)
|
|
60
91
|
return;
|
|
61
92
|
setOpen(false);
|
|
62
93
|
onTouch?.();
|
|
63
94
|
}, [isOpen, setOpen, onTouch]);
|
|
95
|
+
/** Zamknięcie z powrotem fokusu na pole — po wyborze, `Escape`, `Tab` w wyszukiwarce. */
|
|
96
|
+
const closeAndFocus = () => {
|
|
97
|
+
close();
|
|
98
|
+
triggerRef.current?.focus();
|
|
99
|
+
};
|
|
64
100
|
const pick = (option) => {
|
|
65
101
|
if (option.disabled)
|
|
66
102
|
return;
|
|
@@ -69,49 +105,85 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
69
105
|
}
|
|
70
106
|
else {
|
|
71
107
|
setCurrent(option.value);
|
|
72
|
-
|
|
108
|
+
closeAndFocus();
|
|
73
109
|
}
|
|
74
110
|
};
|
|
75
|
-
const
|
|
111
|
+
const jumpTo = (char) => {
|
|
112
|
+
const labels = filteredOptions.map((option) => option.label);
|
|
113
|
+
const found = typeaheadIndex(labels, disabledMap, activeIndex, typeahead.current.push(char));
|
|
114
|
+
if (found !== null)
|
|
115
|
+
setActiveIndex(found);
|
|
116
|
+
};
|
|
117
|
+
/** Jeden obsługiwacz dla pola i dla wyszukiwarki w panelu — różnią się tylko `Spacją` i `Tab`. */
|
|
118
|
+
const onKeyDown = (event, source) => {
|
|
76
119
|
if (disabled)
|
|
77
120
|
return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
121
|
+
const key = event.key;
|
|
122
|
+
if (!isOpen) {
|
|
123
|
+
if (['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End'].includes(key)) {
|
|
124
|
+
event.preventDefault();
|
|
81
125
|
setOpen(true);
|
|
126
|
+
if (key === 'ArrowUp' || key === 'End' || key === 'Home') {
|
|
127
|
+
// Efekt otwarcia ustawia wybraną opcję; skrajne klawisze nadpisują to klatkę później.
|
|
128
|
+
const target = nextOptionIndex(disabledMap, -1, key === 'Home' ? 'Home' : 'End');
|
|
129
|
+
requestAnimationFrame(() => {
|
|
130
|
+
if (target !== null)
|
|
131
|
+
setActiveIndex(target);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if ((key === 'Backspace' || key === 'Delete') && multiple && selectedValues.length) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
setCurrent(selectedValues.slice(0, -1));
|
|
138
|
+
}
|
|
139
|
+
else if (isTypeaheadKey(event.nativeEvent) && !filter) {
|
|
140
|
+
setOpen(true);
|
|
141
|
+
jumpTo(key);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
82
144
|
}
|
|
83
|
-
|
|
84
|
-
close();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const onPanelKeyDown = (event) => {
|
|
88
|
-
if (event.key === 'Escape') {
|
|
145
|
+
if (key === 'Escape') {
|
|
89
146
|
event.preventDefault();
|
|
90
|
-
|
|
147
|
+
closeAndFocus();
|
|
91
148
|
}
|
|
92
|
-
else if (
|
|
93
|
-
|
|
94
|
-
|
|
149
|
+
else if (key === 'Tab') {
|
|
150
|
+
// Z wyszukiwarki (w portalu na końcu strony) Tab uciekłby w próżnię — wracamy na pole.
|
|
151
|
+
if (source === 'search') {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
closeAndFocus();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
close();
|
|
157
|
+
}
|
|
95
158
|
}
|
|
96
|
-
else if (
|
|
159
|
+
else if (OPTION_NAV_KEYS.has(key)) {
|
|
97
160
|
event.preventDefault();
|
|
98
|
-
|
|
161
|
+
const next = nextOptionIndex(disabledMap, activeIndex, key);
|
|
162
|
+
if (next !== null)
|
|
163
|
+
setActiveIndex(next);
|
|
99
164
|
}
|
|
100
|
-
else if (
|
|
165
|
+
else if (key === 'Enter' || (key === ' ' && source === 'trigger')) {
|
|
101
166
|
event.preventDefault();
|
|
102
167
|
const option = filteredOptions[activeIndex];
|
|
103
|
-
if (option)
|
|
168
|
+
if (option && !option.disabled)
|
|
104
169
|
pick(option);
|
|
105
170
|
}
|
|
171
|
+
else if (source === 'trigger' && isTypeaheadKey(event.nativeEvent)) {
|
|
172
|
+
jumpTo(key);
|
|
173
|
+
}
|
|
106
174
|
};
|
|
107
|
-
return (_jsxs("span", { className: "kpt-select-host", children: [_jsxs("div", { ref: triggerRef, className: cn('kpt-select', isOpen && 'kpt-select--open', disabled && 'kpt-select--disabled', showInvalid && 'kpt-select--invalid', className), role: "combobox", tabIndex: 0, "aria-expanded": isOpen, "aria-disabled": disabled || undefined, onClick: () => !disabled && setOpen(!isOpen), onKeyDown:
|
|
175
|
+
return (_jsxs("span", { className: "kpt-select-host", children: [_jsxs("div", { ref: triggerRef, className: cn('kpt-select', isOpen && 'kpt-select--open', disabled && 'kpt-select--disabled', showInvalid && 'kpt-select--invalid', className), role: "combobox", id: field.id, tabIndex: 0, "aria-haspopup": "listbox", "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), "aria-required": required || undefined, "aria-expanded": isOpen, "aria-controls": isOpen ? listId : undefined, "aria-activedescendant": isOpen && !filter && activeIndex >= 0 ? optionId(activeIndex) : undefined, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, onClick: () => !disabled && setOpen(!isOpen), onKeyDown: (event) => onKeyDown(event, 'trigger'), onBlur: (event) => {
|
|
108
176
|
onBlur?.(event);
|
|
109
|
-
|
|
110
|
-
|
|
177
|
+
// Przy otwartym panelu fokus idzie do pola szukania — „dotknięcie” wyśle dopiero zamknięcie.
|
|
178
|
+
if (!isOpen)
|
|
179
|
+
onTouch?.();
|
|
180
|
+
}, ...rest, children: [_jsx("div", { className: "kpt-select__value", children: multiple && selectedOptions.length ? (selectedOptions.map((option) => (_jsxs("span", { className: "kpt-select__chip", children: [option.label, _jsx("button", { type: "button", className: "kpt-select__chip-remove", tabIndex: -1, "aria-label": `${t('select.removeChip')}: ${option.label}`, onClick: (event) => {
|
|
111
181
|
event.stopPropagation();
|
|
112
182
|
setCurrent(selectedValues.filter((v) => v !== option.value));
|
|
113
|
-
}, children: _jsx(KptIcon, { name: "x" }) })] }, String(option.value))))) : !multiple && filled ? (_jsx("span", { className: "kpt-select__single", children: selectedOptions[0]?.label })) : (_jsx("span", { className: "kpt-select__placeholder", children: placeholder })) }), _jsx(KptIcon, { className: "kpt-select__chevron", name: "chevron-down" })] }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, matchAnchorWidth: true, children: _jsxs("div", { className: "kpt-select__panel",
|
|
183
|
+
}, children: _jsx(KptIcon, { name: "x" }) })] }, String(option.value))))) : !multiple && filled ? (_jsx("span", { className: "kpt-select__single", children: selectedOptions[0]?.label })) : (_jsx("span", { className: "kpt-select__placeholder", children: placeholder })) }), _jsx(KptIcon, { className: "kpt-select__chevron", name: "chevron-down" })] }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, matchAnchorWidth: true, children: _jsxs("div", { className: "kpt-select__panel", children: [filter ? (_jsxs("div", { className: "kpt-select__search", children: [_jsx(KptIcon, { name: "search" }), _jsx("input", { ref: searchRef, type: "text", className: "kpt-select__search-input", role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-controls": listId, "aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined, "aria-label": t('select.search'), value: filterText, placeholder: t('select.search'), onKeyDown: (event) => onKeyDown(event, 'search'), onChange: (event) => {
|
|
114
184
|
setFilterText(event.target.value);
|
|
115
|
-
|
|
116
|
-
|
|
185
|
+
// Po zawężeniu aktywna jest pierwsza pasująca opcja — `Enter` wybiera ją od razu.
|
|
186
|
+
const narrowed = filterOptions(event.target.value).map((o) => !!o.disabled);
|
|
187
|
+
setActiveIndex(nextOptionIndex(narrowed, -1, 'ArrowDown') ?? -1);
|
|
188
|
+
} })] })) : null, _jsx("ul", { className: "kpt-select__list", role: "listbox", id: listId, "aria-multiselectable": multiple, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, children: filteredOptions.length ? (filteredOptions.map((option, index) => (_jsxs("li", { id: optionId(index), className: cn('kpt-select__option', isSelected(option) && 'kpt-select__option--selected', index === activeIndex && 'kpt-select__option--active', option.disabled && 'kpt-select__option--disabled'), role: "option", "aria-selected": isSelected(option), "aria-disabled": option.disabled || undefined, onMouseDown: (event) => event.preventDefault(), onClick: () => pick(option), onMouseEnter: () => setActiveIndex(index), children: [multiple ? (_jsx("span", { className: cn('kpt-select__check', isSelected(option) && 'is-on'), children: _jsx(KptIcon, { name: "check" }) })) : null, _jsx("span", { className: "kpt-select__option-label", children: option.label }), !multiple && isSelected(option) ? (_jsx(KptIcon, { className: "kpt-select__option-tick", name: "check" })) : null] }, String(option.value))))) : (_jsx("li", { className: "kpt-select__empty", role: "status", children: t('select.noResults') })) })] }) })] }));
|
|
117
189
|
}
|
package/dist/sidenav/llms.txt
CHANGED
|
@@ -44,6 +44,12 @@ attribute names in CSS selectors are case-insensitive in HTML, so one rule serve
|
|
|
44
44
|
the components rather than writing the attribute by hand — JSX rejects an unknown camelCase
|
|
45
45
|
attribute, and a hand-written one is easy to misspell.
|
|
46
46
|
|
|
47
|
+
## Links
|
|
48
|
+
Links in the scroll area are `display: flex` with a gap, so an icon and a label never glue together
|
|
49
|
+
even when the template compiler drops the whitespace between them. A link with
|
|
50
|
+
`aria-current="page"` gets the muted background and a medium weight — mark the current page
|
|
51
|
+
yourself, the component does not know which one it is.
|
|
52
|
+
|
|
47
53
|
## Tokens
|
|
48
54
|
`--kpt-sidenav-bg`, `--kpt-sidenav-fg`, `--kpt-sidenav-border`, plus `--kpt-sidenav-width` and
|
|
49
55
|
`--kpt-sidenav-rail-width`, which are read by `KptAppShell`.
|
package/dist/skeleton/llms.txt
CHANGED
|
@@ -17,7 +17,8 @@ Import: `import { KptSkeleton } from '@konce-pt/react';`
|
|
|
17
17
|
|
|
18
18
|
## The last line is shorter
|
|
19
19
|
In the `text` variant the last line is 60% wide. A stack of full-width rectangles does not read as
|
|
20
|
-
a paragraph, and a paragraph is exactly what it stands in for.
|
|
20
|
+
a paragraph, and a paragraph is exactly what it stands in for. A single line (`lines={1}`) keeps
|
|
21
|
+
the full `width` — there is no paragraph to imitate.
|
|
21
22
|
|
|
22
23
|
## Examples
|
|
23
24
|
<KptSkeleton variant="text" lines={3} />
|
|
@@ -25,7 +26,7 @@ a paragraph, and a paragraph is exactly what it stands in for.
|
|
|
25
26
|
<KptSkeleton variant="rect" width="100%" height="8rem" />
|
|
26
27
|
|
|
27
28
|
## Tokens
|
|
28
|
-
`--kpt-color-muted
|
|
29
|
+
The base `--kpt-color-muted`; the shimmer is a lighter band mixed with `--kpt-color-surface`; corners `--kpt-radius-md` (blocks) / `--kpt-radius-sm` (text lines).
|
|
29
30
|
|
|
30
31
|
## Accessibility
|
|
31
32
|
The host is `aria-hidden="true"` — a placeholder has nothing to announce, and reading out empty
|
|
@@ -11,7 +11,7 @@ export interface KptSkeletonProps extends Omit<ComponentPropsWithRef<'span'>, 'c
|
|
|
11
11
|
* Placeholder ładowania z animacją shimmer.
|
|
12
12
|
*
|
|
13
13
|
* Wariant `text` skraca ostatnią linię do 60% — pełny prostokąt nie wygląda jak akapit, a to
|
|
14
|
-
* właśnie akapit ma udawać.
|
|
14
|
+
* właśnie akapit ma udawać. Przy jednej linii nie ma czego udawać, więc ta ma pełną szerokość.
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* <KptSkeleton variant="text" lines={3} />
|
|
@@ -4,12 +4,12 @@ import { cn } from "../internal/cn.js";
|
|
|
4
4
|
* Placeholder ładowania z animacją shimmer.
|
|
5
5
|
*
|
|
6
6
|
* Wariant `text` skraca ostatnią linię do 60% — pełny prostokąt nie wygląda jak akapit, a to
|
|
7
|
-
* właśnie akapit ma udawać.
|
|
7
|
+
* właśnie akapit ma udawać. Przy jednej linii nie ma czego udawać, więc ta ma pełną szerokość.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* <KptSkeleton variant="text" lines={3} />
|
|
11
11
|
* <KptSkeleton variant="circle" width="3rem" height="3rem" />
|
|
12
12
|
*/
|
|
13
13
|
export function KptSkeleton({ variant = 'rect', width = '100%', height = '1rem', lines = 3, className, ...rest }) {
|
|
14
|
-
return (_jsx("span", { className: cn('kpt-skeleton-host', className), "aria-hidden": "true", ...rest, children: variant === 'text' ? (Array.from({ length: lines }, (_, index) => (_jsx("span", { className: "kpt-skeleton kpt-skeleton--text", style: { width: index === lines - 1 ? '60%' : width } }, index)))) : (_jsx("span", { className: "kpt-skeleton", "data-variant": variant, style: { width, height } })) }));
|
|
14
|
+
return (_jsx("span", { className: cn('kpt-skeleton-host', className), "aria-hidden": "true", ...rest, children: variant === 'text' ? (Array.from({ length: lines }, (_, index) => (_jsx("span", { className: "kpt-skeleton kpt-skeleton--text", style: { width: lines > 1 && index === lines - 1 ? '60%' : width } }, index)))) : (_jsx("span", { className: "kpt-skeleton", "data-variant": variant, style: { width, height } })) }));
|
|
15
15
|
}
|
package/dist/slider/llms.txt
CHANGED
|
@@ -16,7 +16,7 @@ Import: `import { KptSlider } from '@konce-pt/react';`
|
|
|
16
16
|
<div class="kpt-slider__rail"></div>
|
|
17
17
|
<div class="kpt-slider__fill" style="left: 20%; right: 20%"></div>
|
|
18
18
|
<input class="kpt-slider__thumb kpt-slider__thumb--low" type="range" aria-label="Minimum">
|
|
19
|
-
<input class="kpt-slider__thumb kpt-slider__thumb--high" type="range" aria-label="
|
|
19
|
+
<input class="kpt-slider__thumb kpt-slider__thumb--high" type="range" aria-label="Maximum">
|
|
20
20
|
</div>
|
|
21
21
|
<span class="kpt-slider__value">20–80</span>
|
|
22
22
|
</div>
|
|
@@ -42,6 +42,19 @@ pair therefore always stays ordered, so the fill never inverts.
|
|
|
42
42
|
<KptSlider value={volume} onValueChange={(v) => setVolume(v as number)} showValue />
|
|
43
43
|
<KptSlider range value={span} onValueChange={(v) => setSpan(v as [number, number])} showValue />
|
|
44
44
|
|
|
45
|
+
## Screen reader and keyboard
|
|
46
|
+
`suffix`: string (" zł", "%", " km") — shown after the value and put into `aria-valuetext` ("40
|
|
47
|
+
zł"), so the unit is read too. `aria-invalid` after touch.
|
|
48
|
+
|
|
49
|
+
## Label and description
|
|
50
|
+
`id` goes to the native field, not the host; without one it comes from `KptFormField` or is
|
|
51
|
+
generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
|
|
52
|
+
added to `aria-describedby` (merged with your own). Without a wrapper, name it with `ariaLabel`.
|
|
53
|
+
With `range` the thumbs are named "Minimum"/"Maximum" from i18n (`slider.min`/`slider.max`,
|
|
54
|
+
previously hard-coded and mixed-language); inside `kpt-form-field` each thumb reads "<label>
|
|
55
|
+
Minimum" through `aria-labelledby`, without one `ariaLabel` gives "<ariaLabel>, Minimum". `id` goes
|
|
56
|
+
to the single input or the lower thumb.
|
|
57
|
+
|
|
45
58
|
## Tokens
|
|
46
59
|
`--kpt-color-primary` (the fill and the handles), `--kpt-color-muted` (the rail).
|
|
47
60
|
|
package/dist/slider/slider.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
2
3
|
/** Wartość suwaka: pojedyncza liczba albo para `[min, max]` w trybie `range`. */
|
|
3
4
|
export type KptSliderValue = number | [number, number];
|
|
4
|
-
export interface KptSliderProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange' | 'defaultValue'
|
|
5
|
+
export interface KptSliderProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange' | 'defaultValue'>, KptI18nOverrides {
|
|
5
6
|
value?: KptSliderValue;
|
|
6
7
|
defaultValue?: KptSliderValue;
|
|
7
8
|
onValueChange?: (value: KptSliderValue) => void;
|
|
@@ -12,6 +13,14 @@ export interface KptSliderProps extends Omit<ComponentPropsWithRef<'div'>, 'onCh
|
|
|
12
13
|
step?: number;
|
|
13
14
|
/** Pokazuje bieżącą wartość obok suwaka. */
|
|
14
15
|
showValue?: boolean;
|
|
16
|
+
/** Jednostka za liczbą („ zł”, „%”, „ km”) — w pokazanej wartości i w `aria-valuetext`. */
|
|
17
|
+
suffix?: string;
|
|
18
|
+
invalid?: boolean;
|
|
19
|
+
touched?: boolean;
|
|
20
|
+
/** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
21
|
+
id?: string;
|
|
22
|
+
/** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
|
|
23
|
+
ariaLabel?: string | null;
|
|
15
24
|
disabled?: boolean;
|
|
16
25
|
onTouch?: () => void;
|
|
17
26
|
}
|
|
@@ -25,4 +34,4 @@ export interface KptSliderProps extends Omit<ComponentPropsWithRef<'div'>, 'onCh
|
|
|
25
34
|
* <KptSlider value={volume} onValueChange={setVolume} min={0} max={100} step={5} showValue />
|
|
26
35
|
* <KptSlider range value={range} onValueChange={setRange} showValue />
|
|
27
36
|
*/
|
|
28
|
-
export declare function KptSlider({ value, defaultValue, onValueChange, range, min, max, step, showValue, disabled, onTouch, className, ...rest }: Readonly<KptSliderProps>): import("react").JSX.Element;
|
|
37
|
+
export declare function KptSlider({ value, defaultValue, onValueChange, range, min, max, step, showValue, suffix, invalid, touched, id, ariaLabel, disabled, onTouch, locale, dictionary, className, ...rest }: Readonly<KptSliderProps>): import("react").JSX.Element;
|
package/dist/slider/slider.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../internal/cn.js";
|
|
3
3
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
4
|
+
import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
5
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
4
6
|
/**
|
|
5
7
|
* Suwak wartości. W trybie `range` udostępnia dwa uchwyty, a wartością jest para `[min, max]`.
|
|
6
8
|
*
|
|
@@ -11,7 +13,18 @@ import { useControllableState } from "../internal/use-controllable-state.js";
|
|
|
11
13
|
* <KptSlider value={volume} onValueChange={setVolume} min={0} max={100} step={5} showValue />
|
|
12
14
|
* <KptSlider range value={range} onValueChange={setRange} showValue />
|
|
13
15
|
*/
|
|
14
|
-
export function KptSlider({ value, defaultValue, onValueChange, range = false, min = 0, max = 100, step = 1, showValue = false, disabled = false, onTouch, className, ...rest }) {
|
|
16
|
+
export function KptSlider({ value, defaultValue, onValueChange, range = false, min = 0, max = 100, step = 1, showValue = false, suffix = '', invalid = false, touched, id, ariaLabel = null, disabled = false, onTouch, locale, dictionary, className, ...rest }) {
|
|
17
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
18
|
+
const field = useKptFieldA11y(id, 'kpt-slider');
|
|
19
|
+
/** Uchwyt zakresu bez etykiety pola: „Cena, Minimum” z `ariaLabel` albo samo „Minimum”. */
|
|
20
|
+
const thumbLabel = (end) => {
|
|
21
|
+
if (field.labelledBy)
|
|
22
|
+
return undefined;
|
|
23
|
+
const name = t(end === 'min' ? 'slider.min' : 'slider.max');
|
|
24
|
+
return ariaLabel ? `${ariaLabel}, ${name}` : name;
|
|
25
|
+
};
|
|
26
|
+
/** Uchwyt zakresu w `KptFormField`: etykieta pola + nazwa uchwytu. */
|
|
27
|
+
const thumbLabelledBy = (end) => field.labelledBy ? joinIds(field.labelledBy, `${field.id}-${end}-name`) : undefined;
|
|
15
28
|
const [current, setCurrent] = useControllableState({
|
|
16
29
|
value,
|
|
17
30
|
defaultValue: defaultValue ?? (range ? [min, max] : 0),
|
|
@@ -26,5 +39,5 @@ export function KptSlider({ value, defaultValue, onValueChange, range = false, m
|
|
|
26
39
|
// Uchwyty nie mogą się wyminąć — dolny nie przekracza górnego i odwrotnie.
|
|
27
40
|
const setLow = (next) => setCurrent([Math.min(next, high), high]);
|
|
28
41
|
const setHigh = (next) => setCurrent([low, Math.max(next, low)]);
|
|
29
|
-
return (_jsx("div", { className: cn('kpt-slider-host', className), ...rest, children: range ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-slider__range", children: [_jsx("div", { className: "kpt-slider__rail" }), _jsx("div", { className: "kpt-slider__fill", style: { left: `${pct(low)}%`, right: `${100 - pct(high)}%` } }), _jsx("input", { className: "kpt-slider__thumb kpt-slider__thumb--low", type: "range", min: min, max: max, step: step, value: low, disabled: disabled, "aria-label": "
|
|
42
|
+
return (_jsx("div", { className: cn('kpt-slider-host', className), ...rest, children: range ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-slider__range", children: [_jsx("span", { className: "kpt-slider__sr", id: `${field.id}-min-name`, children: t('slider.min') }), _jsx("span", { className: "kpt-slider__sr", id: `${field.id}-max-name`, children: t('slider.max') }), _jsx("div", { className: "kpt-slider__rail" }), _jsx("div", { className: "kpt-slider__fill", style: { left: `${pct(low)}%`, right: `${100 - pct(high)}%` } }), _jsx("input", { className: "kpt-slider__thumb kpt-slider__thumb--low", type: "range", min: min, max: max, step: step, value: low, "aria-valuetext": suffix ? `${low}${suffix}` : undefined, "aria-invalid": invalid && (touched ?? true) ? true : undefined, id: field.id, disabled: disabled, "aria-label": thumbLabel('min'), "aria-labelledby": thumbLabelledBy('min'), "aria-describedby": field.describedBy, onChange: (event) => setLow(Number(event.target.value)), onBlur: () => onTouch?.() }), _jsx("input", { className: "kpt-slider__thumb kpt-slider__thumb--high", type: "range", min: min, max: max, step: step, value: high, "aria-valuetext": suffix ? `${high}${suffix}` : undefined, "aria-invalid": invalid && (touched ?? true) ? true : undefined, disabled: disabled, "aria-label": thumbLabel('max'), "aria-labelledby": thumbLabelledBy('max'), "aria-describedby": field.describedBy, onChange: (event) => setHigh(Number(event.target.value)), onBlur: () => onTouch?.() })] }), showValue ? (_jsxs("span", { className: "kpt-slider__value", children: [low, suffix, "\u2013", high, suffix] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("input", { className: "kpt-slider__input", type: "range", min: min, max: max, step: step, value: single, "aria-valuetext": suffix ? `${single}${suffix}` : undefined, "aria-invalid": invalid && (touched ?? true) ? true : undefined, id: field.id, disabled: disabled, "aria-label": ariaLabel ?? undefined, "aria-describedby": field.describedBy, style: { ['--_pct']: `${pct(single)}%` }, onChange: (event) => setCurrent(Number(event.target.value)), onBlur: () => onTouch?.() }), showValue ? (_jsxs("span", { className: "kpt-slider__value", children: [single, suffix] })) : null] })) }));
|
|
30
43
|
}
|
package/dist/speed-dial/llms.txt
CHANGED
|
@@ -42,5 +42,9 @@ The main button comes from `KptFab`; the actions use `--kpt-color-surface-raised
|
|
|
42
42
|
`--kpt-elevation-2`.
|
|
43
43
|
|
|
44
44
|
## Accessibility
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
- The main button carries `aria-expanded` and `aria-controls` pointing at the actions container.
|
|
46
|
+
- Closed actions are `visibility: hidden` (switched after the fade-out), so they are out of the Tab
|
|
47
|
+
order and the accessibility tree — not just transparent.
|
|
48
|
+
- `Escape` anywhere in the dial closes it and returns focus to the main button.
|
|
49
|
+
- Every action is a `<button>` labelled with its own `label`, so the fan works from the keyboard even
|
|
50
|
+
when the actions show icons only.
|
|
@@ -17,4 +17,4 @@ export interface KptSpeedDialProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
17
17
|
* @example
|
|
18
18
|
* <KptSpeedDial items={actions} direction="up" onSelect={onAction} />
|
|
19
19
|
*/
|
|
20
|
-
export declare function KptSpeedDial<T>({ items, direction, icon, ariaLabel, onSelect, locale, dictionary, className, ...rest }: Readonly<KptSpeedDialProps<T>>): import("react").JSX.Element;
|
|
20
|
+
export declare function KptSpeedDial<T>({ items, direction, icon, ariaLabel, onSelect, locale, dictionary, className, onKeyDown: onKeyDownProp, ...rest }: Readonly<KptSpeedDialProps<T>>): import("react").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { useKptId } from "../internal/use-id.js";
|
|
4
5
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
5
6
|
import { KptFab } from "../fab/fab.js";
|
|
6
7
|
import { KptIcon } from "../icon/icon.js";
|
|
@@ -10,14 +11,24 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
10
11
|
* @example
|
|
11
12
|
* <KptSpeedDial items={actions} direction="up" onSelect={onAction} />
|
|
12
13
|
*/
|
|
13
|
-
export function KptSpeedDial({ items = [], direction = 'up', icon = 'plus', ariaLabel, onSelect, locale, dictionary, className, ...rest }) {
|
|
14
|
+
export function KptSpeedDial({ items = [], direction = 'up', icon = 'plus', ariaLabel, onSelect, locale, dictionary, className, onKeyDown: onKeyDownProp, ...rest }) {
|
|
14
15
|
const { t } = useKptMessages({ locale, dictionary });
|
|
15
16
|
const [open, setOpen] = useState(false);
|
|
17
|
+
const actionsId = useKptId(undefined, 'kpt-speed-dial-actions');
|
|
18
|
+
const hostRef = useRef(null);
|
|
19
|
+
/** Escape z dowolnego miejsca w wachlarzu: zamyka i zwraca fokus na główny przycisk. */
|
|
20
|
+
const onKeyDown = (event) => {
|
|
21
|
+
onKeyDownProp?.(event);
|
|
22
|
+
if (event.key !== 'Escape' || !open)
|
|
23
|
+
return;
|
|
24
|
+
setOpen(false);
|
|
25
|
+
hostRef.current?.querySelector('.kpt-fab')?.focus();
|
|
26
|
+
};
|
|
16
27
|
const pick = (item) => {
|
|
17
28
|
if (item.disabled)
|
|
18
29
|
return;
|
|
19
30
|
onSelect?.(item);
|
|
20
31
|
setOpen(false);
|
|
21
32
|
};
|
|
22
|
-
return (_jsxs("div", { className: cn('kpt-speed-dial-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-speed-dial', open && 'is-open'), "data-direction": direction, children: [_jsx("div", { className: "kpt-speed-dial__actions", children: items.map((item, index) => (_jsxs("button", { type: "button", className: "kpt-speed-dial__action", disabled: item.disabled, "aria-label": item.label, onClick: () => pick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, item.label ? _jsx("span", { className: "kpt-speed-dial__label", children: item.label }) : null] }, index))) }), _jsx(KptFab, { "aria-label": open ? t('common.close') : (ariaLabel ?? t('speedDial.actions')), onClick: () => setOpen(!open), children: _jsx("span", { className: "kpt-speed-dial__trigger-icon", children: _jsx(KptIcon, { name: icon }) }) })] }), open ? _jsx("div", { className: "kpt-speed-dial__backdrop", onClick: () => setOpen(false) }) : null] }));
|
|
33
|
+
return (_jsxs("div", { ref: hostRef, className: cn('kpt-speed-dial-host', className), onKeyDown: onKeyDown, ...rest, children: [_jsxs("div", { className: cn('kpt-speed-dial', open && 'is-open'), "data-direction": direction, children: [_jsx("div", { className: "kpt-speed-dial__actions", id: actionsId, children: items.map((item, index) => (_jsxs("button", { type: "button", className: "kpt-speed-dial__action", disabled: item.disabled, "aria-label": item.label, onClick: () => pick(item), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, item.label ? _jsx("span", { className: "kpt-speed-dial__label", children: item.label }) : null] }, index))) }), _jsx(KptFab, { "aria-label": open ? t('common.close') : (ariaLabel ?? t('speedDial.actions')), "aria-expanded": open, "aria-controls": actionsId, onClick: () => setOpen(!open), children: _jsx("span", { className: "kpt-speed-dial__trigger-icon", children: _jsx(KptIcon, { name: icon }) }) })] }), open ? _jsx("div", { className: "kpt-speed-dial__backdrop", onClick: () => setOpen(false) }) : null] }));
|
|
23
34
|
}
|
package/dist/spinner/llms.txt
CHANGED
|
@@ -4,7 +4,8 @@ A loading indicator. Import: `import { KptSpinner } from '@konce-pt/react';`
|
|
|
4
4
|
|
|
5
5
|
## DOM
|
|
6
6
|
<span class="kpt-spinner-host">
|
|
7
|
-
<span class="kpt-spinner"
|
|
7
|
+
<span class="kpt-spinner" aria-hidden="true" style="width: 1.75rem; height: 1.75rem"></span>
|
|
8
|
+
<span class="kpt-spinner__label" role="status">Loading</span>
|
|
8
9
|
</span>
|
|
9
10
|
|
|
10
11
|
## Props
|
|
@@ -14,9 +15,10 @@ A loading indicator. Import: `import { KptSpinner } from '@konce-pt/react';`
|
|
|
14
15
|
- `className`, `ref` and every other `<span>` prop go to the host
|
|
15
16
|
|
|
16
17
|
## The colour is inherited
|
|
17
|
-
The
|
|
18
|
-
|
|
19
|
-
colour
|
|
18
|
+
The host sets `color: var(--kpt-color-primary)`, and the ring is drawn in `currentColor` — so the
|
|
19
|
+
spinner is primary by default, and anything that sets `color` on the host changes it. Pass
|
|
20
|
+
`style={{ color: 'inherit' }}` (or a class) to take the colour of the text around it, e.g. inside a
|
|
21
|
+
filled chip. There is no colour prop, on purpose.
|
|
20
22
|
|
|
21
23
|
## Examples
|
|
22
24
|
<KptSpinner />
|
|
@@ -24,10 +26,12 @@ colour prop, on purpose.
|
|
|
24
26
|
<KptButton disabled><KptSpinner size="1em" /> Saving…</KptButton>
|
|
25
27
|
|
|
26
28
|
## Tokens
|
|
27
|
-
None of its own — the
|
|
29
|
+
None of its own — the colour is `--kpt-color-primary` on the host (drawn in `currentColor`), the
|
|
30
|
+
size comes from the prop, the ring width is 8% of the size (at least 2px), the speed from
|
|
28
31
|
`--kpt-motion-duration-*`.
|
|
29
32
|
|
|
30
33
|
## Accessibility
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
The ring is `aria-hidden`; next to it sits a visually hidden `.kpt-spinner__label` with
|
|
35
|
+
`role="status"`. Its text — `label`, or `spinner.label` — is written a frame after the spinner
|
|
36
|
+
appears, because a status region that is born already filled in is skipped by most screen readers.
|
|
37
|
+
Under `prefers-reduced-motion` the rotation slows down instead of stopping.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
2
|
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
3
3
|
export interface KptSpinnerProps extends Omit<ComponentPropsWithRef<'span'>, 'children'>, KptI18nOverrides {
|
|
4
4
|
size?: string;
|
|
@@ -8,6 +8,10 @@ export interface KptSpinnerProps extends Omit<ComponentPropsWithRef<'span'>, 'ch
|
|
|
8
8
|
/**
|
|
9
9
|
* Wskaźnik ładowania. Kolor bierze z `currentColor`, więc dopasowuje się do treści wokół.
|
|
10
10
|
*
|
|
11
|
+
* Pierścień jest dekoracją (`aria-hidden`); dla czytnika jest osobny, wizualnie ukryty region
|
|
12
|
+
* `role="status"` z etykietą. Tekst trafia do regionu klatkę po pojawieniu się spinnera: region,
|
|
13
|
+
* który powstaje od razu z treścią, większość czytników przemilcza — ogłaszają dopiero zmianę.
|
|
14
|
+
*
|
|
11
15
|
* @example
|
|
12
16
|
* <KptSpinner />
|
|
13
17
|
* <KptSpinner size="2rem" label="Wczytywanie danych" />
|
package/dist/spinner/spinner.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
2
3
|
import { cn } from "../internal/cn.js";
|
|
3
4
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
4
5
|
/**
|
|
5
6
|
* Wskaźnik ładowania. Kolor bierze z `currentColor`, więc dopasowuje się do treści wokół.
|
|
6
7
|
*
|
|
8
|
+
* Pierścień jest dekoracją (`aria-hidden`); dla czytnika jest osobny, wizualnie ukryty region
|
|
9
|
+
* `role="status"` z etykietą. Tekst trafia do regionu klatkę po pojawieniu się spinnera: region,
|
|
10
|
+
* który powstaje od razu z treścią, większość czytników przemilcza — ogłaszają dopiero zmianę.
|
|
11
|
+
*
|
|
7
12
|
* @example
|
|
8
13
|
* <KptSpinner />
|
|
9
14
|
* <KptSpinner size="2rem" label="Wczytywanie danych" />
|
|
10
15
|
*/
|
|
11
16
|
export function KptSpinner({ size = '1.5rem', label, locale, dictionary, className, ...rest }) {
|
|
12
17
|
const { t } = useKptMessages({ locale, dictionary });
|
|
13
|
-
|
|
18
|
+
const [ready, setReady] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const frame = requestAnimationFrame(() => setReady(true));
|
|
21
|
+
return () => cancelAnimationFrame(frame);
|
|
22
|
+
}, []);
|
|
23
|
+
return (_jsxs("span", { className: cn('kpt-spinner-host', className), ...rest, children: [_jsx("span", { className: "kpt-spinner", "aria-hidden": "true", style: { width: size, height: size, borderWidth: `max(2px, calc(${size} * 0.08))` } }), _jsx("span", { className: "kpt-spinner__label", role: "status", children: ready ? (label ?? t('spinner.label')) : '' })] }));
|
|
14
24
|
}
|
|
@@ -14,7 +14,9 @@ Import: `import { KptSplitButton } from '@konce-pt/react';`
|
|
|
14
14
|
<div class="kpt-split-button__panel" role="menu">…rows…</div>
|
|
15
15
|
|
|
16
16
|
## Props
|
|
17
|
-
- `variant`:
|
|
17
|
+
- `variant`: `KptSplitButtonVariant` = 'filled' | 'tonal' | 'outline' | 'text' (default 'filled') —
|
|
18
|
+
the subset of `KptButtonVariant` that has a style; `outline` and `text` use the `--kpt-button-outline-*`
|
|
19
|
+
and `--kpt-button-text-*` tokens, so they match `kpt-button` in the same row
|
|
18
20
|
- `items`: `KptMenuItem<T>[]` — the same shape as `KptMenu`
|
|
19
21
|
- `disabled`: boolean — disables both buttons
|
|
20
22
|
- `onPrimaryClick()` — the main action
|
|
@@ -35,6 +37,12 @@ the `danger` modifier all behave identically, only the BEM block differs.
|
|
|
35
37
|
## i18n
|
|
36
38
|
The toggle's label comes from `splitButton.more` in the dictionary.
|
|
37
39
|
|
|
40
|
+
## Keyboard
|
|
41
|
+
The toggle is a menu button: `aria-haspopup="menu"`, a live `aria-expanded`, `aria-controls` while
|
|
42
|
+
open. `↓` / `↑` on it open the menu on the first / last item; inside, `↓` / `↑` wrap, `Home` / `End`
|
|
43
|
+
jump, `Escape` / `Tab` close, and closing or picking returns focus to the toggle. Same rules as the
|
|
44
|
+
menu — see the `KptMenu` docs.
|
|
45
|
+
|
|
38
46
|
## Examples
|
|
39
47
|
<KptSplitButton items={items} onPrimaryClick={save} onSelect={onItem}>Save</KptSplitButton>
|
|
40
48
|
|
|
@@ -50,5 +58,5 @@ The `--kpt-button-*` tokens of the chosen variant, plus `--kpt-color-surface-rai
|
|
|
50
58
|
`--kpt-elevation-3` for the panel.
|
|
51
59
|
|
|
52
60
|
## Accessibility
|
|
53
|
-
Two native buttons; the toggle carries `aria-label
|
|
54
|
-
`role="menuitem"` rows.
|
|
61
|
+
Two native buttons; the toggle carries `aria-label` plus the menu-button attributes above, the
|
|
62
|
+
panel is `role="menu"` with `role="menuitem"` rows.
|