@konce-pt/react 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accordion/accordion.js +7 -1
- package/dist/accordion/llms.txt +8 -4
- package/dist/alert/alert.d.ts +10 -4
- package/dist/alert/alert.js +13 -6
- package/dist/alert/llms.txt +8 -6
- package/dist/app-shell/app-shell.js +28 -2
- package/dist/app-shell/llms.txt +3 -0
- package/dist/autocomplete/autocomplete.d.ts +7 -2
- package/dist/autocomplete/autocomplete.js +46 -9
- package/dist/autocomplete/llms.txt +12 -0
- package/dist/avatar/avatar.d.ts +4 -2
- package/dist/avatar/avatar.js +8 -3
- package/dist/avatar/llms.txt +9 -5
- package/dist/avatar-group/avatar-group.d.ts +5 -2
- package/dist/avatar-group/avatar-group.js +10 -2
- package/dist/avatar-group/llms.txt +7 -4
- package/dist/badge/badge.d.ts +8 -1
- package/dist/badge/badge.js +3 -2
- package/dist/badge/llms.txt +11 -5
- package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
- package/dist/bottom-sheet/bottom-sheet.js +8 -2
- package/dist/bottom-sheet/llms.txt +8 -2
- package/dist/breadcrumb/breadcrumb.js +4 -4
- package/dist/breadcrumb/llms.txt +12 -5
- package/dist/card/card.d.ts +9 -1
- package/dist/card/card.js +2 -2
- package/dist/card/llms.txt +6 -2
- package/dist/carousel/carousel.d.ts +8 -1
- package/dist/carousel/carousel.js +9 -2
- package/dist/carousel/llms.txt +24 -10
- package/dist/checkbox/checkbox.d.ts +6 -1
- package/dist/checkbox/checkbox.js +11 -3
- package/dist/checkbox/llms.txt +11 -0
- package/dist/chip/chip.js +10 -1
- package/dist/chip/llms.txt +7 -4
- package/dist/chips-input/chips-input.d.ts +7 -1
- package/dist/chips-input/chips-input.js +21 -5
- package/dist/chips-input/llms.txt +12 -0
- package/dist/color-picker/color-picker.d.ts +18 -5
- package/dist/color-picker/color-picker.js +48 -14
- package/dist/color-picker/llms.txt +14 -0
- package/dist/confirm/confirm-container.d.ts +5 -1
- package/dist/confirm/confirm-container.js +15 -1
- package/dist/confirm/llms.txt +4 -2
- package/dist/context-menu/context-menu.d.ts +5 -0
- package/dist/context-menu/context-menu.js +48 -4
- package/dist/context-menu/llms.txt +11 -4
- package/dist/date-range/date-range.d.ts +14 -1
- package/dist/date-range/date-range.js +133 -17
- package/dist/date-range/llms.txt +26 -3
- package/dist/datepicker/datepicker.d.ts +10 -1
- package/dist/datepicker/datepicker.js +147 -14
- package/dist/datepicker/llms.txt +27 -3
- package/dist/dialog/dialog.d.ts +4 -0
- package/dist/dialog/dialog.js +7 -1
- package/dist/dialog/llms.txt +2 -0
- package/dist/drawer/drawer.js +7 -2
- package/dist/drawer/llms.txt +10 -5
- package/dist/empty/empty.d.ts +6 -1
- package/dist/empty/empty.js +2 -2
- package/dist/empty/llms.txt +4 -3
- package/dist/fab/llms.txt +2 -1
- package/dist/file-upload/file-upload.d.ts +5 -0
- package/dist/file-upload/file-upload.js +24 -3
- package/dist/file-upload/llms.txt +15 -4
- package/dist/form-field/form-field-context.d.ts +30 -1
- package/dist/form-field/form-field-context.js +27 -0
- package/dist/form-field/form-field.js +16 -2
- package/dist/form-field/llms.txt +10 -1
- package/dist/galleria/galleria.d.ts +6 -1
- package/dist/galleria/galleria.js +40 -1
- package/dist/galleria/llms.txt +17 -8
- package/dist/icon/icon.d.ts +7 -1
- package/dist/icon/icon.js +18 -4
- package/dist/icon/llms.txt +5 -2
- package/dist/image/image.d.ts +5 -0
- package/dist/image/image.js +38 -2
- package/dist/image/llms.txt +24 -17
- package/dist/input/input.d.ts +1 -1
- package/dist/input/input.js +4 -6
- package/dist/input/llms.txt +5 -0
- package/dist/input-mask/input-mask.d.ts +1 -1
- package/dist/input-mask/input-mask.js +8 -6
- package/dist/input-mask/llms.txt +11 -0
- package/dist/input-number/input-number.d.ts +13 -1
- package/dist/input-number/input-number.js +49 -9
- package/dist/input-number/llms.txt +21 -3
- package/dist/input-otp/input-otp.d.ts +7 -2
- package/dist/input-otp/input-otp.js +36 -7
- package/dist/input-otp/llms.txt +14 -0
- package/dist/internal/calendar-focus.d.ts +8 -0
- package/dist/internal/calendar-focus.js +38 -0
- package/dist/internal/option-nav.d.ts +24 -0
- package/dist/internal/option-nav.js +80 -0
- package/dist/internal/overlay.d.ts +7 -2
- package/dist/internal/overlay.js +4 -2
- package/dist/internal/roving.d.ts +9 -0
- package/dist/internal/roving.js +43 -0
- package/dist/internal/tween.d.ts +17 -0
- package/dist/internal/tween.js +40 -0
- package/dist/internal/use-modal-focus.d.ts +10 -0
- package/dist/internal/use-modal-focus.js +57 -0
- package/dist/knob/knob.d.ts +9 -1
- package/dist/knob/knob.js +41 -5
- package/dist/knob/llms.txt +15 -0
- package/dist/listbox/listbox.d.ts +13 -1
- package/dist/listbox/listbox.js +89 -6
- package/dist/listbox/llms.txt +21 -5
- package/dist/megamenu/llms.txt +28 -10
- package/dist/megamenu/megamenu.d.ts +10 -1
- package/dist/megamenu/megamenu.js +116 -5
- package/dist/menu/llms.txt +12 -1
- package/dist/menu/menu-keys.d.ts +15 -0
- package/dist/menu/menu-keys.js +53 -0
- package/dist/menu/menu-rows.js +1 -1
- package/dist/menu/menu.js +52 -4
- package/dist/menubar/llms.txt +24 -7
- package/dist/menubar/menubar.d.ts +8 -1
- package/dist/menubar/menubar.js +92 -6
- package/dist/meter-group/llms.txt +10 -3
- package/dist/meter-group/meter-group.d.ts +9 -2
- package/dist/meter-group/meter-group.js +8 -2
- package/dist/order-list/llms.txt +11 -3
- package/dist/order-list/order-list.d.ts +5 -1
- package/dist/order-list/order-list.js +46 -8
- package/dist/panel/llms.txt +9 -6
- package/dist/panel/panel.js +3 -1
- package/dist/password/llms.txt +8 -0
- package/dist/password/password.d.ts +6 -1
- package/dist/password/password.js +6 -5
- package/dist/pick-list/llms.txt +12 -3
- package/dist/pick-list/pick-list.d.ts +5 -1
- package/dist/pick-list/pick-list.js +52 -5
- package/dist/popover/llms.txt +9 -2
- package/dist/popover/popover.d.ts +9 -3
- package/dist/popover/popover.js +41 -6
- package/dist/public-api.d.ts +6 -6
- package/dist/public-api.js +1 -1
- package/dist/radio-group/llms.txt +11 -0
- package/dist/radio-group/radio-group.d.ts +9 -1
- package/dist/radio-group/radio-group.js +5 -2
- package/dist/rating/llms.txt +18 -5
- package/dist/rating/rating.d.ts +21 -3
- package/dist/rating/rating.js +87 -12
- package/dist/rich-text/llms.txt +20 -0
- package/dist/rich-text/rich-text.d.ts +7 -1
- package/dist/rich-text/rich-text.js +155 -28
- package/dist/scroll-top/llms.txt +5 -1
- package/dist/scroll-top/scroll-top.js +3 -1
- package/dist/select/llms.txt +21 -7
- package/dist/select/select.d.ts +12 -1
- package/dist/select/select.js +110 -38
- package/dist/sidenav/llms.txt +6 -0
- package/dist/skeleton/llms.txt +3 -2
- package/dist/skeleton/skeleton.d.ts +1 -1
- package/dist/skeleton/skeleton.js +2 -2
- package/dist/slider/llms.txt +14 -1
- package/dist/slider/slider.d.ts +11 -2
- package/dist/slider/slider.js +15 -2
- package/dist/speed-dial/llms.txt +6 -2
- package/dist/speed-dial/speed-dial.d.ts +1 -1
- package/dist/speed-dial/speed-dial.js +14 -3
- package/dist/spinner/llms.txt +12 -8
- package/dist/spinner/spinner.d.ts +5 -1
- package/dist/spinner/spinner.js +12 -2
- package/dist/split-button/llms.txt +11 -3
- package/dist/split-button/split-button.d.ts +3 -1
- package/dist/split-button/split-button.js +43 -3
- package/dist/splitter/llms.txt +14 -4
- package/dist/splitter/splitter.d.ts +9 -2
- package/dist/splitter/splitter.js +15 -2
- package/dist/stepper/llms.txt +8 -4
- package/dist/stepper/stepper.d.ts +3 -2
- package/dist/stepper/stepper.js +7 -2
- package/dist/switch/llms.txt +10 -0
- package/dist/switch/switch.d.ts +1 -1
- package/dist/switch/switch.js +4 -2
- package/dist/switch-group/llms.txt +11 -0
- package/dist/switch-group/switch-group.d.ts +5 -1
- package/dist/switch-group/switch-group.js +7 -2
- package/dist/tabs/llms.txt +10 -4
- package/dist/tabs/tabs.d.ts +4 -0
- package/dist/tabs/tabs.js +21 -2
- package/dist/textarea/llms.txt +10 -0
- package/dist/textarea/textarea.d.ts +3 -1
- package/dist/textarea/textarea.js +13 -8
- package/dist/toast/llms.txt +21 -7
- package/dist/toast/toast-container.d.ts +6 -1
- package/dist/toast/toast-container.js +52 -1
- package/dist/toast/toast-store.d.ts +12 -0
- package/dist/toast/toast-store.js +46 -7
- package/dist/tooltip/llms.txt +10 -3
- package/dist/tooltip/tooltip.d.ts +5 -0
- package/dist/tooltip/tooltip.js +80 -7
- package/dist/tree/llms.txt +20 -8
- package/dist/tree/tree-keys.d.ts +27 -0
- package/dist/tree/tree-keys.js +39 -0
- package/dist/tree/tree.d.ts +12 -5
- package/dist/tree/tree.js +75 -29
- package/package.json +12 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../internal/cn.js";
|
|
3
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
3
4
|
const PALETTE = [
|
|
4
5
|
'var(--kpt-color-primary)',
|
|
5
6
|
'var(--kpt-color-info)',
|
|
@@ -13,12 +14,17 @@ const PALETTE = [
|
|
|
13
14
|
* @example
|
|
14
15
|
* <KptMeterGroup values={[{ label: 'Użyte', value: 70 }, { label: 'Wolne', value: 30 }]} />
|
|
15
16
|
*/
|
|
16
|
-
export function KptMeterGroup({ values = [], max, showLegend = true, className, ...rest }) {
|
|
17
|
+
export function KptMeterGroup({ values = [], max, showLegend = true, label = '', legendValue = 'percent', unit = '', locale, dictionary, className, ...rest }) {
|
|
18
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
17
19
|
const sum = values.reduce((acc, item) => acc + item.value, 0);
|
|
18
20
|
// Bez `max` odniesieniem jest suma wartości; `|| 1` chroni przed dzieleniem przez zero,
|
|
19
21
|
// gdy wszystkie segmenty są zerowe.
|
|
20
22
|
const total = max ?? (sum || 1);
|
|
21
23
|
const percent = (item) => (item.value / total) * 100;
|
|
22
24
|
const color = (item, index) => item.color || PALETTE[index % PALETTE.length];
|
|
23
|
-
|
|
25
|
+
const withUnit = (value) => (unit ? `${value} ${unit}` : String(value));
|
|
26
|
+
const legendText = (item) => legendValue === 'value' ? withUnit(item.value) : `${Math.round(percent(item))}%`;
|
|
27
|
+
// Podział dla czytnika („Zdjęcia 42 GB, Filmy 28 GB…”) — sam `aria-valuenow` mówi tylko sumę.
|
|
28
|
+
const valueText = values.map((item) => `${item.label} ${withUnit(item.value)}`).join(', ');
|
|
29
|
+
return (_jsx("div", { className: cn('kpt-meter-group-host', className), ...rest, children: _jsxs("div", { className: "kpt-meter-group", children: [_jsx("div", { className: "kpt-meter-group__track", role: "meter", "aria-valuemin": 0, "aria-valuenow": sum, "aria-valuemax": total, "aria-valuetext": valueText, "aria-label": label || t('meterGroup.label'), children: values.map((item, index) => (_jsx("span", { className: "kpt-meter-group__segment", style: { width: `${percent(item)}%`, backgroundColor: color(item, index) }, title: item.label }, index))) }), showLegend ? (_jsx("ul", { className: "kpt-meter-group__legend", children: values.map((item, index) => (_jsxs("li", { className: "kpt-meter-group__legend-item", children: [_jsx("span", { className: "kpt-meter-group__dot", style: { backgroundColor: color(item, index) } }), _jsx("span", { children: item.label }), _jsx("span", { className: "kpt-meter-group__val", children: legendText(item) })] }, index))) })) : null] }) }));
|
|
24
30
|
}
|
package/dist/order-list/llms.txt
CHANGED
|
@@ -46,7 +46,15 @@ no dictionary keys.
|
|
|
46
46
|
## Tokens
|
|
47
47
|
The selection `--kpt-color-primary`, the buttons `--kpt-color-surface-raised`.
|
|
48
48
|
|
|
49
|
+
## Keyboard
|
|
50
|
+
The list is a `listbox` with a single Tab stop (roving tabindex), and the selection follows focus:
|
|
51
|
+
- `↓` / `↑` select the next / previous item (wrapping), `Home` / `End` the first / last;
|
|
52
|
+
- `Alt`+`↑` / `Alt`+`↓` move the selected item one place up / down — focus travels with it, so
|
|
53
|
+
repeated presses keep moving the same entry.
|
|
54
|
+
The up / down buttons stay and act on the same selection.
|
|
55
|
+
|
|
49
56
|
## Accessibility
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
The list is named after `header` (`aria-label`) and the selected item carries `aria-selected`.
|
|
58
|
+
Both buttons are labelled from the dictionary and go disabled while nothing is selected. Native
|
|
59
|
+
HTML5 drag and drop has no keyboard equivalent — `Alt`+arrows and the buttons cover it, and
|
|
60
|
+
`dragdrop` adds to them, it never replaces them.
|
|
@@ -14,7 +14,11 @@ export interface KptOrderListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
14
14
|
* OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
|
|
15
15
|
* w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
|
|
16
16
|
*
|
|
17
|
+
* Lista to `listbox` z jednym przystankiem Tab (roving tabindex), w którym zaznaczenie idzie
|
|
18
|
+
* za fokusem: strzałki / `Home` / `End` wybierają pozycję, `Alt`+↑/↓ przestawia ją o jedno
|
|
19
|
+
* miejsce (fokus jedzie razem z nią).
|
|
20
|
+
*
|
|
17
21
|
* @example
|
|
18
22
|
* <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
|
|
19
23
|
*/
|
|
20
|
-
export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
|
|
24
|
+
export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, dragdrop, renderItem, locale, dictionary, className, ref, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { composeRefs } from "../internal/compose-refs.js";
|
|
4
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
6
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
import { nextMenuIndex } from "../menu/menu-keys.js";
|
|
7
9
|
const EMPTY = [];
|
|
8
10
|
/**
|
|
9
11
|
* OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
|
|
10
12
|
* w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
|
|
11
13
|
*
|
|
14
|
+
* Lista to `listbox` z jednym przystankiem Tab (roving tabindex), w którym zaznaczenie idzie
|
|
15
|
+
* za fokusem: strzałki / `Home` / `End` wybierają pozycję, `Alt`+↑/↓ przestawia ją o jedno
|
|
16
|
+
* miejsce (fokus jedzie razem z nią).
|
|
17
|
+
*
|
|
12
18
|
* @example
|
|
13
19
|
* <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
|
|
14
20
|
*/
|
|
15
|
-
export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
|
|
21
|
+
export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ref, ...rest }) {
|
|
16
22
|
const { t } = useKptMessages({ locale, dictionary });
|
|
17
23
|
const [list, setList] = useControllableState({
|
|
18
24
|
value: items,
|
|
@@ -26,18 +32,50 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
|
|
|
26
32
|
const [overIndex, setOverIndex] = useState(null);
|
|
27
33
|
/** Kursor nad pustym obszarem listy — upuszczenie tam dopina na koniec. */
|
|
28
34
|
const [listOver, setListOver] = useState(false);
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
/** Pozycja, na którą po renderze trzeba przenieść fokus DOM (klawiatura). */
|
|
36
|
+
const [focusRequest, setFocusRequest] = useState(null);
|
|
37
|
+
const rootRef = useRef(null);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (focusRequest === null)
|
|
31
40
|
return;
|
|
32
|
-
|
|
41
|
+
rootRef.current?.querySelectorAll('[role="option"]')[focusRequest]?.focus();
|
|
42
|
+
setFocusRequest(null);
|
|
43
|
+
}, [focusRequest]);
|
|
44
|
+
/**
|
|
45
|
+
* Przesuwa pozycję spod `index` o jedno miejsce i zwraca jej nowy indeks. Jawny indeks zamiast
|
|
46
|
+
* `selected`: klawiatura zaznacza i przesuwa w jednym zdarzeniu, a stan z domknięcia byłby stary.
|
|
47
|
+
*/
|
|
48
|
+
const moveFrom = (index, dir) => {
|
|
49
|
+
const target = index + dir;
|
|
33
50
|
if (target < 0 || target >= list.length)
|
|
34
|
-
return;
|
|
51
|
+
return index;
|
|
35
52
|
const next = [...list];
|
|
36
|
-
[next[
|
|
53
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
37
54
|
setList(next);
|
|
38
55
|
// Zaznaczenie wędruje za elementem, nie zostaje na pozycji — inaczej kolejne kliknięcie
|
|
39
56
|
// przesuwałoby sąsiada zamiast tego samego wpisu.
|
|
40
57
|
setSelected(target);
|
|
58
|
+
return target;
|
|
59
|
+
};
|
|
60
|
+
const move = (dir) => {
|
|
61
|
+
if (selected !== null)
|
|
62
|
+
moveFrom(selected, dir);
|
|
63
|
+
};
|
|
64
|
+
/** Przystanek Tab: zaznaczona pozycja albo pierwsza. */
|
|
65
|
+
const tabStop = selected !== null && selected < list.length ? selected : 0;
|
|
66
|
+
// Strzałki wybierają pozycję (zaznaczenie idzie za fokusem), `Alt`+↑/↓ ją przestawia.
|
|
67
|
+
const onItemKeyDown = (index, event) => {
|
|
68
|
+
if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
setFocusRequest(moveFrom(index, event.key === 'ArrowUp' ? -1 : 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const next = nextMenuIndex(list.length, index, event.key);
|
|
74
|
+
if (next === null)
|
|
75
|
+
return;
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
setSelected(next);
|
|
78
|
+
setFocusRequest(next);
|
|
41
79
|
};
|
|
42
80
|
const clearDrag = () => {
|
|
43
81
|
setDragIndex(null);
|
|
@@ -98,5 +136,5 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
|
|
|
98
136
|
setList(next);
|
|
99
137
|
setSelected(next.length - 1);
|
|
100
138
|
};
|
|
101
|
-
return (_jsx("div", { className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
|
|
139
|
+
return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), role: "listbox", "aria-label": header || undefined, onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), role: "option", "aria-selected": selected === index, tabIndex: index === tabStop ? 0 : -1, onFocus: () => setSelected(index), onKeyDown: (event) => onItemKeyDown(index, event), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
|
|
102
140
|
}
|
package/dist/panel/llms.txt
CHANGED
|
@@ -9,9 +9,10 @@ Import: `import { KptPanel } from '@konce-pt/react';`
|
|
|
9
9
|
<div class="kpt-panel__header">
|
|
10
10
|
<span class="kpt-panel__title">Settings</span>
|
|
11
11
|
<span class="kpt-panel__actions">…actions…</span>
|
|
12
|
-
<button class="kpt-panel__toggle" aria-expanded="true" aria-
|
|
12
|
+
<button class="kpt-panel__toggle" aria-expanded="true" aria-controls="kpt-panel-body-r1"
|
|
13
|
+
aria-label="Collapse / expand Settings">…</button>
|
|
13
14
|
</div>
|
|
14
|
-
<div class="kpt-panel__collapse is-open">
|
|
15
|
+
<div class="kpt-panel__collapse is-open" id="kpt-panel-body-r1">
|
|
15
16
|
<div class="kpt-panel__collapse-inner">
|
|
16
17
|
<div class="kpt-panel__body">…children…</div>
|
|
17
18
|
</div>
|
|
@@ -36,7 +37,7 @@ nothing inside can take focus or be read out. That is also why a collapsed panel
|
|
|
36
37
|
whatever lives in it — a half-filled form inside survives a collapse.
|
|
37
38
|
|
|
38
39
|
## i18n
|
|
39
|
-
The toggle's label comes from `panel.toggle` in the dictionary.
|
|
40
|
+
The toggle's label comes from `panel.toggleNamed` (with `{header}`) or `panel.toggle` in the dictionary.
|
|
40
41
|
|
|
41
42
|
## Examples
|
|
42
43
|
<KptPanel header="Settings" toggleable>Panel content…</KptPanel>
|
|
@@ -50,6 +51,8 @@ The toggle's label comes from `panel.toggle` in the dictionary.
|
|
|
50
51
|
`--kpt-color-surface`, `--kpt-color-border`, `--kpt-radius-lg`, `--kpt-motion-easing-standard`.
|
|
51
52
|
|
|
52
53
|
## Accessibility
|
|
53
|
-
The toggle is a native button with `aria-expanded` and
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
The toggle is a native button with `aria-expanded` and `aria-controls` pointing at the body. Its
|
|
55
|
+
name says which panel it collapses — `panel.toggleNamed` (“Collapse / expand Settings”) when there
|
|
56
|
+
is a `header`, `panel.toggle` otherwise — so several panels on one page are told apart. A collapsed
|
|
57
|
+
body is `inert` and `aria-hidden`. The header title is not a heading — add your own when the panel
|
|
58
|
+
should appear in the document outline.
|
package/dist/panel/panel.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Children, isValidElement } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
6
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
7
|
import { KptIcon } from "../icon/icon.js";
|
|
7
8
|
/**
|
|
@@ -15,6 +16,7 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
15
16
|
*/
|
|
16
17
|
export function KptPanel({ header = '', toggleable = false, collapsed, defaultCollapsed = false, onCollapsedChange, animated = true, animationDuration = 500, locale, dictionary, className, style, children, ...rest }) {
|
|
17
18
|
const { t } = useKptMessages({ locale, dictionary });
|
|
19
|
+
const bodyId = useKptId(undefined, 'kpt-panel-body');
|
|
18
20
|
const [isCollapsed, setCollapsed] = useControllableState({
|
|
19
21
|
value: collapsed,
|
|
20
22
|
defaultValue: defaultCollapsed,
|
|
@@ -26,7 +28,7 @@ export function KptPanel({ header = '', toggleable = false, collapsed, defaultCo
|
|
|
26
28
|
const isActions = isValidElement(child) && child.type?.kptPanelSlot === 'actions';
|
|
27
29
|
(isActions ? actions : body).push(child);
|
|
28
30
|
});
|
|
29
|
-
return (_jsx("div", { className: cn('kpt-panel-host', className), style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsxs("div", { className: "kpt-panel", children: [_jsxs("div", { className: "kpt-panel__header", children: [_jsx("span", { className: "kpt-panel__title", children: header }), _jsx("span", { className: "kpt-panel__actions", children: actions }), toggleable ? (_jsx("button", { type: "button", className: "kpt-panel__toggle", onClick: () => setCollapsed(!isCollapsed), "aria-expanded": !isCollapsed, "aria-label": t('panel.toggle'), children: _jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }) })) : null] }), _jsx("div", { className: cn('kpt-panel__collapse', !isCollapsed && 'is-open'), inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-panel__collapse-inner", children: _jsx("div", { className: "kpt-panel__body", children: body }) }) })] }) }));
|
|
31
|
+
return (_jsx("div", { className: cn('kpt-panel-host', className), style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsxs("div", { className: "kpt-panel", children: [_jsxs("div", { className: "kpt-panel__header", children: [_jsx("span", { className: "kpt-panel__title", children: header }), _jsx("span", { className: "kpt-panel__actions", children: actions }), toggleable ? (_jsx("button", { type: "button", className: "kpt-panel__toggle", onClick: () => setCollapsed(!isCollapsed), "aria-expanded": !isCollapsed, "aria-controls": bodyId, "aria-label": header ? t('panel.toggleNamed', { header }) : t('panel.toggle'), children: _jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }) })) : null] }), _jsx("div", { className: cn('kpt-panel__collapse', !isCollapsed && 'is-open'), id: bodyId, inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-panel__collapse-inner", children: _jsx("div", { className: "kpt-panel__body", children: body }) }) })] }) }));
|
|
30
32
|
}
|
|
31
33
|
/** Akcje w nagłówku panelu, obok tytułu. */
|
|
32
34
|
export function KptPanelActions({ className, ...rest }) {
|
package/dist/password/llms.txt
CHANGED
|
@@ -36,6 +36,14 @@ the dictionary — indexed by the score, so a translation controls the wording w
|
|
|
36
36
|
<KptPassword value={pass} onValueChange={setPass} feedback />
|
|
37
37
|
<KptPassword value={pass} onValueChange={setPass} autoComplete="new-password" feedback />
|
|
38
38
|
|
|
39
|
+
## Label and description
|
|
40
|
+
`id` goes to the native field, not the host; without one it comes from `KptFormField` or is
|
|
41
|
+
generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
|
|
42
|
+
added to `aria-describedby` (merged with your own). Without a wrapper, name it with `ariaLabel`. The
|
|
43
|
+
host no longer carries the `id` (it used to, so `label[for]` pointed at `kpt-password` and the same
|
|
44
|
+
id existed twice). New: `ariaLabel`, `required`, `readonly`/`readOnly`, `aria-invalid` after touch;
|
|
45
|
+
with `feedback` the strength label is added to `aria-describedby`.
|
|
46
|
+
|
|
39
47
|
## Tokens
|
|
40
48
|
`--kpt-form-field-*` for the field, `--kpt-color-danger` / `-warning` / `-success` for the meter
|
|
41
49
|
bars by score.
|
|
@@ -14,6 +14,11 @@ export interface KptPasswordProps extends Omit<ComponentPropsWithRef<'div'>, 'on
|
|
|
14
14
|
autoComplete?: string | null;
|
|
15
15
|
/** Miernik siły pod polem. */
|
|
16
16
|
feedback?: boolean;
|
|
17
|
+
/** Nazwa dostępna, gdy pola nie opisuje etykieta (`<label for>` albo `KptFormField`). */
|
|
18
|
+
ariaLabel?: string | null;
|
|
19
|
+
/** Oznacza pole jako wymagane atrybutem `aria-required`. */
|
|
20
|
+
required?: boolean;
|
|
21
|
+
readOnly?: boolean;
|
|
17
22
|
disabled?: boolean;
|
|
18
23
|
invalid?: boolean;
|
|
19
24
|
touched?: boolean;
|
|
@@ -25,4 +30,4 @@ export interface KptPasswordProps extends Omit<ComponentPropsWithRef<'div'>, 'on
|
|
|
25
30
|
* @example
|
|
26
31
|
* <KptPassword value={password} onValueChange={setPassword} feedback autoComplete="new-password" />
|
|
27
32
|
*/
|
|
28
|
-
export declare function KptPassword({ value, defaultValue, onValueChange, placeholder, id, autoComplete, feedback, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptPasswordProps>): import("react").JSX.Element;
|
|
33
|
+
export declare function KptPassword({ value, defaultValue, onValueChange, placeholder, id, autoComplete, feedback, ariaLabel, required, readOnly, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptPasswordProps>): import("react").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { cn } from "../internal/cn.js";
|
|
|
4
4
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
5
|
import { useKptId } from "../internal/use-id.js";
|
|
6
6
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
8
8
|
import { KptIcon } from "../icon/icon.js";
|
|
9
9
|
/** Cztery niezależne kryteria; każde spełnione podnosi wynik o jeden. */
|
|
10
10
|
function scoreOf(password) {
|
|
@@ -25,13 +25,14 @@ function scoreOf(password) {
|
|
|
25
25
|
* @example
|
|
26
26
|
* <KptPassword value={password} onValueChange={setPassword} feedback autoComplete="new-password" />
|
|
27
27
|
*/
|
|
28
|
-
export function KptPassword({ value, defaultValue = '', onValueChange, placeholder = '', id, autoComplete = 'current-password', feedback = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
28
|
+
export function KptPassword({ value, defaultValue = '', onValueChange, placeholder = '', id, autoComplete = 'current-password', feedback = false, ariaLabel = null, required = false, readOnly = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
29
29
|
const { t, messages } = useKptMessages({ locale, dictionary });
|
|
30
|
-
const field =
|
|
31
|
-
const inputId =
|
|
30
|
+
const field = useKptFieldA11y(id, 'kpt-password');
|
|
31
|
+
const inputId = field.id;
|
|
32
|
+
const meterId = useKptId(undefined, 'kpt-password-strength');
|
|
32
33
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
33
34
|
const [visible, setVisible] = useState(false);
|
|
34
35
|
const showInvalid = invalid && (touched ?? true);
|
|
35
36
|
const score = scoreOf(current);
|
|
36
|
-
return (_jsxs("div", { className: cn('kpt-password-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-password', disabled && 'kpt-password--disabled', showInvalid && 'kpt-password--invalid'), children: [_jsx("input", { className: "kpt-password__input", id: inputId, type: visible ? 'text' : 'password', value: current, disabled: disabled, placeholder: placeholder, autoComplete: autoComplete ?? undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("button", { type: "button", className: "kpt-password__toggle", disabled: disabled, onClick: () => setVisible(!visible), "aria-label": visible ? t('password.hide') : t('password.show'), children: _jsx(KptIcon, { name: visible ? 'eye-off' : 'eye' }) })] }), feedback && current ? (_jsxs("div", { className: "kpt-password__meter", "data-score": score, children: [_jsx("div", { className: "kpt-password__bars", children: [0, 1, 2, 3].map((index) => (_jsx("span", { className: cn('kpt-password__bar', index < score && 'is-on') }, index))) }), _jsx("span", { className: "kpt-password__hint", children: messages.password.strength[score] })] })) : null] }));
|
|
37
|
+
return (_jsxs("div", { className: cn('kpt-password-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-password', disabled && 'kpt-password--disabled', showInvalid && 'kpt-password--invalid'), children: [_jsx("input", { className: "kpt-password__input", id: inputId, type: visible ? 'text' : 'password', value: current, disabled: disabled, readOnly: readOnly, "aria-invalid": showInvalid || undefined, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-describedby": joinIds(field.describedBy, feedback && current && meterId), placeholder: placeholder, autoComplete: autoComplete ?? undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("button", { type: "button", className: "kpt-password__toggle", disabled: disabled, onClick: () => setVisible(!visible), "aria-label": visible ? t('password.hide') : t('password.show'), children: _jsx(KptIcon, { name: visible ? 'eye-off' : 'eye' }) })] }), feedback && current ? (_jsxs("div", { className: "kpt-password__meter", id: meterId, "data-score": score, children: [_jsx("div", { className: "kpt-password__bars", "aria-hidden": "true", children: [0, 1, 2, 3].map((index) => (_jsx("span", { className: cn('kpt-password__bar', index < score && 'is-on') }, index))) }), _jsx("span", { className: "kpt-password__hint", children: messages.password.strength[score] })] })) : null] }));
|
|
37
38
|
}
|
package/dist/pick-list/llms.txt
CHANGED
|
@@ -60,7 +60,16 @@ double-click carry no visible label, so they add no dictionary keys.
|
|
|
60
60
|
## Tokens
|
|
61
61
|
The selection `--kpt-color-primary`, the headers `--kpt-color-surface-variant`.
|
|
62
62
|
|
|
63
|
+
## Keyboard
|
|
64
|
+
Each list is a `listbox` with `aria-multiselectable` and a single Tab stop (roving tabindex):
|
|
65
|
+
- `↓` / `↑` move between items (wrapping), `Home` / `End` jump to the ends;
|
|
66
|
+
- `Space` toggles the focused item's selection (`aria-selected`), the same as a click;
|
|
67
|
+
- `Enter` moves the focused item to the other list, the same as a double-click — focus stays at
|
|
68
|
+
that position in the list, on the next item.
|
|
69
|
+
The move buttons act on the same selection, so the whole control works without a mouse.
|
|
70
|
+
|
|
63
71
|
## Accessibility
|
|
64
|
-
|
|
65
|
-
"move right"
|
|
66
|
-
|
|
72
|
+
Each list is named after its header (`aria-label`). The four move buttons are labelled from the
|
|
73
|
+
dictionary, so "move all right" is distinguishable from "move right" without seeing the double
|
|
74
|
+
chevron. Native HTML5 drag and drop has no keyboard equivalent — `Enter` and the buttons cover
|
|
75
|
+
everything it does, and `dragdrop` adds to them, it never replaces them.
|
|
@@ -22,6 +22,10 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
22
22
|
* Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
|
|
23
23
|
* konkretną pozycję drugiego panelu.
|
|
24
24
|
*
|
|
25
|
+
* Każda lista to `listbox` z wielokrotnym wyborem i jednym przystankiem Tab (roving tabindex):
|
|
26
|
+
* strzałki / `Home` / `End` przechodzą po pozycjach, spacja zaznacza, `Enter` przenosi pozycję
|
|
27
|
+
* na drugą stronę (jak dwuklik).
|
|
28
|
+
*
|
|
25
29
|
* @example
|
|
26
30
|
* <KptPickList
|
|
27
31
|
* source={available} onSourceChange={setAvailable}
|
|
@@ -30,4 +34,4 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
30
34
|
* renderItem={(item) => item.name}
|
|
31
35
|
* />
|
|
32
36
|
*/
|
|
33
|
-
export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
|
|
37
|
+
export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, dragdrop, renderItem, locale, dictionary, className, ref, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { composeRefs } from "../internal/compose-refs.js";
|
|
4
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
6
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
import { nextMenuIndex } from "../menu/menu-keys.js";
|
|
7
9
|
const EMPTY = [];
|
|
8
10
|
/**
|
|
9
11
|
* PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
|
|
@@ -11,6 +13,10 @@ const EMPTY = [];
|
|
|
11
13
|
* Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
|
|
12
14
|
* konkretną pozycję drugiego panelu.
|
|
13
15
|
*
|
|
16
|
+
* Każda lista to `listbox` z wielokrotnym wyborem i jednym przystankiem Tab (roving tabindex):
|
|
17
|
+
* strzałki / `Home` / `End` przechodzą po pozycjach, spacja zaznacza, `Enter` przenosi pozycję
|
|
18
|
+
* na drugą stronę (jak dwuklik).
|
|
19
|
+
*
|
|
14
20
|
* @example
|
|
15
21
|
* <KptPickList
|
|
16
22
|
* source={available} onSourceChange={setAvailable}
|
|
@@ -19,7 +25,7 @@ const EMPTY = [];
|
|
|
19
25
|
* renderItem={(item) => item.name}
|
|
20
26
|
* />
|
|
21
27
|
*/
|
|
22
|
-
export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
|
|
28
|
+
export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ref, ...rest }) {
|
|
23
29
|
const { t } = useKptMessages({ locale, dictionary });
|
|
24
30
|
const [left, setLeft] = useControllableState({
|
|
25
31
|
value: source,
|
|
@@ -41,11 +47,33 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
|
|
|
41
47
|
const [over, setOver] = useState(null);
|
|
42
48
|
/** Panel, nad którego pustym obszarem jest kursor — upuszczenie tam dopina na koniec. */
|
|
43
49
|
const [listOver, setListOver] = useState(null);
|
|
50
|
+
/** Pozycja, która ostatnio miała fokus w każdej liście — ona jest przystankiem Tab. */
|
|
51
|
+
const [focusLeft, setFocusLeft] = useState(0);
|
|
52
|
+
const [focusRight, setFocusRight] = useState(0);
|
|
53
|
+
/** Pozycja, na którą po renderze trzeba przenieść fokus DOM (klawiatura). */
|
|
54
|
+
const [focusRequest, setFocusRequest] = useState(null);
|
|
55
|
+
const rootRef = useRef(null);
|
|
44
56
|
const listOf = (side) => (side === 'source' ? left : right);
|
|
45
57
|
const setListOf = (side) => (side === 'source' ? setLeft : setRight);
|
|
46
58
|
const selectionOf = (side) => (side === 'source' ? selLeft : selRight);
|
|
47
59
|
const setSelectionOf = (side) => side === 'source' ? setSelLeft : setSelRight;
|
|
48
60
|
const other = (side) => (side === 'source' ? 'target' : 'source');
|
|
61
|
+
const focusOf = (side) => (side === 'source' ? focusLeft : focusRight);
|
|
62
|
+
const setFocusOf = (side) => (side === 'source' ? setFocusLeft : setFocusRight);
|
|
63
|
+
/** Przystanek Tab listy — ostatnio skupiona pozycja, przycięta do długości listy. */
|
|
64
|
+
const tabIndexOf = (side) => Math.min(focusOf(side), listOf(side).length - 1);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!focusRequest)
|
|
67
|
+
return;
|
|
68
|
+
const list = rootRef.current?.querySelectorAll('.kpt-pick-list__items')[focusRequest.side === 'source' ? 0 : 1];
|
|
69
|
+
const options = list?.querySelectorAll('[role="option"]') ?? [];
|
|
70
|
+
options[Math.min(focusRequest.index, options.length - 1)]?.focus();
|
|
71
|
+
setFocusRequest(null);
|
|
72
|
+
}, [focusRequest]);
|
|
73
|
+
const focusItem = (side, index) => {
|
|
74
|
+
setFocusOf(side)(index);
|
|
75
|
+
setFocusRequest({ side, index });
|
|
76
|
+
};
|
|
49
77
|
const toggle = (side, item) => {
|
|
50
78
|
const next = new Set(selectionOf(side));
|
|
51
79
|
if (next.has(item))
|
|
@@ -91,6 +119,25 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
|
|
|
91
119
|
const to = other(from);
|
|
92
120
|
transfer(from, index, to, listOf(to).length);
|
|
93
121
|
};
|
|
122
|
+
const onItemKeyDown = (side, index, item, event) => {
|
|
123
|
+
if (event.key === ' ') {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
toggle(side, item);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (event.key === 'Enter') {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
moveOne(side, item);
|
|
131
|
+
// Pozycja zniknęła z listy — fokus zostaje w tym samym miejscu listy, na następnej.
|
|
132
|
+
focusItem(side, index);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const next = nextMenuIndex(listOf(side).length, index, event.key);
|
|
136
|
+
if (next === null)
|
|
137
|
+
return;
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
focusItem(side, next);
|
|
140
|
+
};
|
|
94
141
|
const clearDrag = () => {
|
|
95
142
|
setDrag(null);
|
|
96
143
|
setOver(null);
|
|
@@ -162,10 +209,10 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
|
|
|
162
209
|
const items = listOf(side);
|
|
163
210
|
const selection = selectionOf(side);
|
|
164
211
|
const header = side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
|
|
165
|
-
return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
|
|
212
|
+
return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), role: "listbox", "aria-multiselectable": "true", "aria-label": header, onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
|
|
166
213
|
const dragging = drag?.side === side && drag.index === index;
|
|
167
|
-
return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
|
|
214
|
+
return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), role: "option", "aria-selected": selection.has(item), tabIndex: index === tabIndexOf(side) ? 0 : -1, onFocus: () => setFocusOf(side)(index), onKeyDown: (event) => onItemKeyDown(side, index, item, event), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
|
|
168
215
|
}) })] }));
|
|
169
216
|
};
|
|
170
|
-
return (_jsx("div", { className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
|
|
217
|
+
return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
|
|
171
218
|
}
|
package/dist/popover/llms.txt
CHANGED
|
@@ -14,6 +14,7 @@ Import: `import { KptPopover } from '@konce-pt/react';`
|
|
|
14
14
|
- `trigger`: ReactNode — the element that opens the bubble, rendered in place
|
|
15
15
|
- `children`: ReactNode — the panel content
|
|
16
16
|
- `open` / `defaultOpen` / `onOpenChange` — controlled or uncontrolled
|
|
17
|
+
- `placement`: 'bottom' (default) | 'top' — the preferred side; it flips when there is no room
|
|
17
18
|
- `className`, `ref` and every other `<span>` prop go to the host
|
|
18
19
|
|
|
19
20
|
## The panel is a portal
|
|
@@ -38,5 +39,11 @@ backdrop takes that click, exactly as the CDK backdrop does.
|
|
|
38
39
|
`--kpt-z-overlay`.
|
|
39
40
|
|
|
40
41
|
## Accessibility
|
|
41
|
-
The panel is `role="dialog"`
|
|
42
|
-
|
|
42
|
+
The panel is `role="dialog"` with an `id` the trigger points to.
|
|
43
|
+
|
|
44
|
+
## Focus and keyboard
|
|
45
|
+
The control inside the trigger slot gets `aria-haspopup="dialog"`, a live `aria-expanded` and, while
|
|
46
|
+
open, `aria-controls` — on the real `<button>`, not on the wrapper. Opening moves focus to the first
|
|
47
|
+
interactive element of the bubble; `Escape` or a click outside closes it, and focus returns to the
|
|
48
|
+
trigger when it was inside the bubble. Focus is not trapped — the popover is supporting content, not
|
|
49
|
+
a modal.
|
|
@@ -7,17 +7,23 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
|
|
|
7
7
|
open?: boolean;
|
|
8
8
|
defaultOpen?: boolean;
|
|
9
9
|
onOpenChange?: (open: boolean) => void;
|
|
10
|
+
/** Preferowana strona dymka; gdy brakuje miejsca, przechodzi na drugą. */
|
|
11
|
+
placement?: 'bottom' | 'top';
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Popover — dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
|
|
13
15
|
*
|
|
14
16
|
* Port Angulara stoi na CDK Overlay; tutaj panel renderuje się w portalu do `document.body`,
|
|
15
|
-
* z tym samym zachowaniem: pozycja pod wyzwalaczem
|
|
16
|
-
* zamykanie kliknięciem poza panelem i klawiszem Escape.
|
|
17
|
+
* z tym samym zachowaniem: pozycja pod wyzwalaczem (albo nad nim przy `placement="top"`)
|
|
18
|
+
* z odbiciem przy braku miejsca, zamykanie kliknięciem poza panelem i klawiszem Escape.
|
|
19
|
+
*
|
|
20
|
+
* Dostępność (jak w Angularze): kontrolka w wyzwalaczu dostaje `aria-haspopup="dialog"`,
|
|
21
|
+
* `aria-expanded` i `aria-controls`; po otwarciu fokus przechodzi do pierwszego interaktywnego
|
|
22
|
+
* elementu dymka, a po zamknięciu wraca na wyzwalacz, jeśli był w dymku.
|
|
17
23
|
*
|
|
18
24
|
* @example
|
|
19
25
|
* <KptPopover trigger={<KptButton variant="outline">Szczegóły</KptButton>}>
|
|
20
26
|
* <div>Bogata treść popovera…</div>
|
|
21
27
|
* </KptPopover>
|
|
22
28
|
*/
|
|
23
|
-
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
|
29
|
+
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
package/dist/popover/popover.js
CHANGED
|
@@ -1,27 +1,62 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
7
|
+
/** Elementy, które same przyjmują fokus — pierwszy z nich dostaje fokus po otwarciu dymka. */
|
|
8
|
+
const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
6
9
|
/**
|
|
7
10
|
* Popover — dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
|
|
8
11
|
*
|
|
9
12
|
* Port Angulara stoi na CDK Overlay; tutaj panel renderuje się w portalu do `document.body`,
|
|
10
|
-
* z tym samym zachowaniem: pozycja pod wyzwalaczem
|
|
11
|
-
* zamykanie kliknięciem poza panelem i klawiszem Escape.
|
|
13
|
+
* z tym samym zachowaniem: pozycja pod wyzwalaczem (albo nad nim przy `placement="top"`)
|
|
14
|
+
* z odbiciem przy braku miejsca, zamykanie kliknięciem poza panelem i klawiszem Escape.
|
|
15
|
+
*
|
|
16
|
+
* Dostępność (jak w Angularze): kontrolka w wyzwalaczu dostaje `aria-haspopup="dialog"`,
|
|
17
|
+
* `aria-expanded` i `aria-controls`; po otwarciu fokus przechodzi do pierwszego interaktywnego
|
|
18
|
+
* elementu dymka, a po zamknięciu wraca na wyzwalacz, jeśli był w dymku.
|
|
12
19
|
*
|
|
13
20
|
* @example
|
|
14
21
|
* <KptPopover trigger={<KptButton variant="outline">Szczegóły</KptButton>}>
|
|
15
22
|
* <div>Bogata treść popovera…</div>
|
|
16
23
|
* </KptPopover>
|
|
17
24
|
*/
|
|
18
|
-
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, className, ...rest }) {
|
|
25
|
+
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', className, ...rest }) {
|
|
19
26
|
const triggerRef = useRef(null);
|
|
27
|
+
const panelRef = useRef(null);
|
|
28
|
+
const panelId = useKptId(undefined, 'kpt-popover');
|
|
20
29
|
const [isOpen, setOpen] = useControllableState({
|
|
21
30
|
value: open,
|
|
22
31
|
defaultValue: defaultOpen,
|
|
23
32
|
onChange: onOpenChange,
|
|
24
33
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
34
|
+
const findControl = () => triggerRef.current?.querySelector('button, a, [role="button"]') ?? null;
|
|
35
|
+
// ARIA na prawdziwej kontrolce w slocie, nie na opakowaniu `<span>` (patrz `KptMenu`).
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const control = findControl();
|
|
38
|
+
if (!control)
|
|
39
|
+
return;
|
|
40
|
+
control.setAttribute('aria-haspopup', 'dialog');
|
|
41
|
+
control.setAttribute('aria-expanded', String(isOpen));
|
|
42
|
+
if (isOpen)
|
|
43
|
+
control.setAttribute('aria-controls', panelId);
|
|
44
|
+
else
|
|
45
|
+
control.removeAttribute('aria-controls');
|
|
46
|
+
}, [isOpen, trigger, panelId]);
|
|
47
|
+
// Fokus do dymka klatkę później — `KptOverlay` trzyma panel niewidoczny, dopóki go nie zmierzy.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isOpen)
|
|
50
|
+
return;
|
|
51
|
+
const frame = requestAnimationFrame(() => panelRef.current?.querySelector(FOCUSABLE)?.focus());
|
|
52
|
+
return () => cancelAnimationFrame(frame);
|
|
53
|
+
}, [isOpen]);
|
|
54
|
+
const close = useCallback(() => {
|
|
55
|
+
const active = document.activeElement;
|
|
56
|
+
// Fokus wraca na wyzwalacz tylko wtedy, gdy był w dymku (albo przepadł na `<body>`).
|
|
57
|
+
if (!active || active === document.body || panelRef.current?.contains(active))
|
|
58
|
+
findControl()?.focus();
|
|
59
|
+
setOpen(false);
|
|
60
|
+
}, [setOpen]);
|
|
61
|
+
return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: triggerRef, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", children: children }) })] }));
|
|
27
62
|
}
|
package/dist/public-api.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ export type { KptConfirmOptions, KptConfirmRequest } from './confirm/confirm-sto
|
|
|
90
90
|
export { KptToastContainer } from './toast/toast-container.tsx';
|
|
91
91
|
export type { KptToastContainerProps } from './toast/toast-container.tsx';
|
|
92
92
|
export { kptToast, useKptToasts } from './toast/toast-store.ts';
|
|
93
|
-
export type { KptToast, KptToastOptions } from './toast/toast-store.ts';
|
|
93
|
+
export type { KptToast, KptToastAction, KptToastOptions } from './toast/toast-store.ts';
|
|
94
94
|
export { KptMenu } from './menu/menu.tsx';
|
|
95
95
|
export { KptMenubar } from './menubar/menubar.tsx';
|
|
96
96
|
export type { KptMenubarProps } from './menubar/menubar.tsx';
|
|
@@ -123,8 +123,8 @@ export type { KptSelectProps } from './select/select.tsx';
|
|
|
123
123
|
export type { KptAlertVariant, KptOption } from './shared/types.ts';
|
|
124
124
|
export { KptFormField } from './form-field/form-field.tsx';
|
|
125
125
|
export type { KptFormFieldLabelType, KptFormFieldProps } from './form-field/form-field.tsx';
|
|
126
|
-
export { useKptFormFieldContext, useKptFormFieldFilled } from './form-field/form-field-context.ts';
|
|
127
|
-
export type { KptFormFieldContextValue } from './form-field/form-field-context.ts';
|
|
126
|
+
export { useKptFieldA11y, useKptFormFieldContext, useKptFormFieldFilled } from './form-field/form-field-context.ts';
|
|
127
|
+
export type { KptFieldA11y, KptFormFieldContextValue } from './form-field/form-field-context.ts';
|
|
128
128
|
export { KptCheckbox } from './checkbox/checkbox.tsx';
|
|
129
129
|
export type { KptCheckboxProps } from './checkbox/checkbox.tsx';
|
|
130
130
|
export { KptAutocomplete } from './autocomplete/autocomplete.tsx';
|
|
@@ -156,7 +156,7 @@ export type { KptDateRangeMode, KptDateRangeProps, KptDateRangeValue } from './d
|
|
|
156
156
|
export { KptClock } from './clock/clock.tsx';
|
|
157
157
|
export type { KptClockProps } from './clock/clock.tsx';
|
|
158
158
|
export { KptColorPicker } from './color-picker/color-picker.tsx';
|
|
159
|
-
export type { KptColorPickerProps } from './color-picker/color-picker.tsx';
|
|
159
|
+
export type { KptColorPickerProps, KptColorPreset } from './color-picker/color-picker.tsx';
|
|
160
160
|
export { KptFileUpload } from './file-upload/file-upload.tsx';
|
|
161
161
|
export type { KptFileUploadProps } from './file-upload/file-upload.tsx';
|
|
162
162
|
export { KptSwitchGroup } from './switch-group/switch-group.tsx';
|
|
@@ -175,10 +175,10 @@ export type { KptIconButtonProps } from './icon-button/icon-button.tsx';
|
|
|
175
175
|
export { KptFab } from './fab/fab.tsx';
|
|
176
176
|
export type { KptFabProps } from './fab/fab.tsx';
|
|
177
177
|
export { KptSplitButton } from './split-button/split-button.tsx';
|
|
178
|
-
export type { KptSplitButtonProps } from './split-button/split-button.tsx';
|
|
178
|
+
export type { KptSplitButtonProps, KptSplitButtonVariant } from './split-button/split-button.tsx';
|
|
179
179
|
export type { KptButtonProps, KptButtonSize, KptButtonVariant } from './button/button.tsx';
|
|
180
180
|
export { KptCard, KptCardActions, KptCardFooter, KptCardHeader, KptCardLead, KptCardMedia, KptCardMeta, KptCardSection, KptCardSubtitle, KptCardTags, KptCardTitle, } from './card/card.tsx';
|
|
181
|
-
export type { KptCardProps, KptCardVariant } from './card/card.tsx';
|
|
181
|
+
export type { KptCardHeadingLevel, KptCardProps, KptCardVariant } from './card/card.tsx';
|
|
182
182
|
export { KptI18nProvider, useKptI18n, useKptMessages } from './i18n/i18n-context.tsx';
|
|
183
183
|
export type { KptI18nContextValue, KptI18nOverrides, KptI18nProviderProps } from './i18n/i18n-context.tsx';
|
|
184
184
|
export { KPT_MESSAGES_EN, KPT_MESSAGES_PL } from '@konce-pt/i18n';
|