@konce-pt/react 0.9.1 → 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/menu/menu.js
CHANGED
|
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
5
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
7
|
+
import { focusMenuEdge, handleMenuKeydown } from "./menu-keys.js";
|
|
6
8
|
import { MenuRows } from "./menu-rows.js";
|
|
7
9
|
/**
|
|
8
10
|
* Menu rozwijane (dropdown). Pozycje przez `items`, wyzwalacz przez prop `trigger`.
|
|
@@ -15,6 +17,10 @@ import { MenuRows } from "./menu-rows.js";
|
|
|
15
17
|
*/
|
|
16
18
|
export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = false, onOpenChange, className, ...rest }) {
|
|
17
19
|
const triggerRef = useRef(null);
|
|
20
|
+
const panelRef = useRef(null);
|
|
21
|
+
/** Którą pozycję skupić po otwarciu — strzałka w górę na wyzwalaczu otwiera „od końca”. */
|
|
22
|
+
const focusEdge = useRef('first');
|
|
23
|
+
const panelId = useKptId(undefined, 'kpt-menu');
|
|
18
24
|
const [isOpen, setOpen] = useControllableState({
|
|
19
25
|
value: open,
|
|
20
26
|
defaultValue: defaultOpen,
|
|
@@ -26,17 +32,59 @@ export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = fal
|
|
|
26
32
|
// zagnieżdżoną kontrolkę i drugi tab stop. `cloneElement` też odpada — `trigger` bywa
|
|
27
33
|
// komponentem, który nie przekazuje nieznanych propsów dalej na DOM.
|
|
28
34
|
useEffect(() => {
|
|
29
|
-
const control =
|
|
35
|
+
const control = findControl();
|
|
30
36
|
if (!control)
|
|
31
37
|
return;
|
|
32
38
|
control.setAttribute('aria-haspopup', 'menu');
|
|
33
39
|
control.setAttribute('aria-expanded', String(isOpen));
|
|
34
|
-
|
|
40
|
+
if (isOpen)
|
|
41
|
+
control.setAttribute('aria-controls', panelId);
|
|
42
|
+
else
|
|
43
|
+
control.removeAttribute('aria-controls');
|
|
44
|
+
}, [isOpen, trigger, panelId]);
|
|
45
|
+
// Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
|
|
46
|
+
// panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!isOpen)
|
|
49
|
+
return;
|
|
50
|
+
const frame = requestAnimationFrame(() => {
|
|
51
|
+
if (panelRef.current)
|
|
52
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
53
|
+
});
|
|
54
|
+
return () => cancelAnimationFrame(frame);
|
|
55
|
+
}, [isOpen]);
|
|
56
|
+
/** Prawdziwa kontrolka w slocie — tam idą ARIA i powrót fokusu. */
|
|
57
|
+
function findControl() {
|
|
58
|
+
return triggerRef.current?.querySelector('button, a, [role="button"]') ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** Fokus wraca na kontrolkę — inaczej po zniknięciu panelu spadłby na `<body>`. */
|
|
61
|
+
const closeAndRestoreFocus = () => {
|
|
62
|
+
close();
|
|
63
|
+
findControl()?.focus();
|
|
64
|
+
};
|
|
65
|
+
const onTriggerKeyDown = (event) => {
|
|
66
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
67
|
+
return;
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
focusEdge.current = event.key === 'ArrowUp' ? 'last' : 'first';
|
|
70
|
+
if (isOpen && panelRef.current)
|
|
71
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
72
|
+
else
|
|
73
|
+
setOpen(true);
|
|
74
|
+
};
|
|
75
|
+
const onPanelKeyDown = (event) => {
|
|
76
|
+
if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close') {
|
|
77
|
+
closeAndRestoreFocus();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
35
80
|
const pick = (item) => {
|
|
36
81
|
if (item.disabled)
|
|
37
82
|
return;
|
|
38
83
|
onSelect?.(item);
|
|
39
|
-
|
|
84
|
+
closeAndRestoreFocus();
|
|
40
85
|
};
|
|
41
|
-
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () =>
|
|
86
|
+
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: triggerRef, onClick: () => {
|
|
87
|
+
focusEdge.current = 'first';
|
|
88
|
+
setOpen(!isOpen);
|
|
89
|
+
}, onKeyDown: onTriggerKeyDown, children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-menu__panel", role: "menu", onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
|
|
42
90
|
}
|
package/dist/menubar/llms.txt
CHANGED
|
@@ -5,18 +5,20 @@ Import: `import { KptMenubar } from '@konce-pt/react';`
|
|
|
5
5
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-menubar-host">
|
|
8
|
-
<
|
|
9
|
-
<div class="kpt-menubar__group">
|
|
10
|
-
<button class="kpt-menubar__top is-open" role="menuitem"
|
|
8
|
+
<div class="kpt-menubar" role="menubar" aria-orientation="horizontal" aria-label="Editor menu">
|
|
9
|
+
<div class="kpt-menubar__group" role="none">
|
|
10
|
+
<button class="kpt-menubar__top is-open" role="menuitem" tabindex="0"
|
|
11
|
+
aria-haspopup="menu" aria-expanded="true" aria-controls="kpt-menubar-r1-menu-0">File …caret…</button>
|
|
11
12
|
</div>
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
</div>
|
|
14
15
|
<!-- while open, in a portal: -->
|
|
15
|
-
<div class="kpt-menubar__dropdown" role="menu">…items and separators…</div>
|
|
16
|
+
<div class="kpt-menubar__dropdown" role="menu" id="kpt-menubar-r1-menu-0" aria-label="File">…items and separators…</div>
|
|
16
17
|
|
|
17
18
|
## Props
|
|
18
19
|
- `items`: `KptMenuItem<T>[]` — a top item's `items` become its dropdown
|
|
19
20
|
- `onSelect(item)` — an action item, or a pick from a dropdown
|
|
21
|
+
- `ariaLabel`: string — the bar's name for screen readers, e.g. "Editor menu"
|
|
20
22
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
21
23
|
|
|
22
24
|
## The dropdown is a portal
|
|
@@ -36,10 +38,25 @@ bar can mix menus and plain actions with no extra flag.
|
|
|
36
38
|
{ label: 'Help' },
|
|
37
39
|
];
|
|
38
40
|
|
|
41
|
+
## Links (`url`)
|
|
42
|
+
An entry with `url` and no children (and not disabled) renders as a real `<a href>` with the same
|
|
43
|
+
look, so the browser handles navigation — opening in a new tab, the status-bar preview. `onSelect`
|
|
44
|
+
still fires before the browser follows the link.
|
|
45
|
+
|
|
39
46
|
## Tokens
|
|
40
47
|
`--kpt-color-surface`, `--kpt-color-muted` (the open top item), `--kpt-color-surface-raised` and
|
|
41
48
|
`--kpt-elevation-3` for the dropdown, `--kpt-color-danger`.
|
|
42
49
|
|
|
43
50
|
## Accessibility
|
|
44
|
-
The bar is `role="menubar"
|
|
45
|
-
`role="separator"`.
|
|
51
|
+
The bar is `role="menubar"` (`aria-orientation="horizontal"`, named by `ariaLabel`), top items and
|
|
52
|
+
dropdown rows are `role="menuitem"`, separators carry `role="separator"`. Top items with a dropdown
|
|
53
|
+
carry `aria-haspopup="menu"`, `aria-expanded` and, while open, `aria-controls`. The bar is a plain
|
|
54
|
+
`<div>`, not `<nav>` — wrap it in your own `<nav aria-label="…">` when it is the site navigation.
|
|
55
|
+
|
|
56
|
+
## Keyboard
|
|
57
|
+
The bar is a single `Tab` stop (roving `tabindex`), following the WAI-ARIA menubar pattern.
|
|
58
|
+
- On a top item: `ArrowLeft`/`ArrowRight` move along the bar (wrapping, skipping disabled items),
|
|
59
|
+
`Home`/`End` jump to the ends; `ArrowDown`, `Enter` or `Space` open the dropdown with focus on
|
|
60
|
+
its first row, `ArrowUp` on its last one.
|
|
61
|
+
- In a dropdown: `ArrowDown`/`ArrowUp`/`Home`/`End` move between rows, `ArrowLeft`/`ArrowRight`
|
|
62
|
+
open the neighbouring dropdown, `Escape` and `Tab` close it and return focus to the top item.
|
|
@@ -3,6 +3,8 @@ import type { KptMenuItem } from '../menu/menu-item.ts';
|
|
|
3
3
|
export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
|
|
4
4
|
items?: readonly KptMenuItem<T>[];
|
|
5
5
|
onSelect?: (item: KptMenuItem<T>) => void;
|
|
6
|
+
/** Nazwa belki dla czytników ekranu, np. „Menu edytora”. */
|
|
7
|
+
ariaLabel?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
|
|
@@ -10,7 +12,12 @@ export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
10
12
|
* Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
|
|
11
13
|
* korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
|
|
12
14
|
*
|
|
15
|
+
* Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
|
|
16
|
+
* i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
|
|
17
|
+
* pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
|
|
18
|
+
* w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
|
|
19
|
+
*
|
|
13
20
|
* @example
|
|
14
21
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
15
22
|
*/
|
|
16
|
-
export declare function KptMenubar<T>({ items, onSelect, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
|
23
|
+
export declare function KptMenubar<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
package/dist/menubar/menubar.js
CHANGED
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
|
+
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
6
|
+
import { useKptId } from "../internal/use-id.js";
|
|
5
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
6
9
|
/**
|
|
7
10
|
* Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
|
|
8
11
|
*
|
|
9
12
|
* Pod-menu renderuje się w portalu, więc rodzic z `overflow: hidden` go nie przycina — to ta sama
|
|
10
13
|
* korzyść, dla której port Angulara sięga po CDK Overlay, tylko bez CDK.
|
|
11
14
|
*
|
|
15
|
+
* Klawiatura według wzorca WAI-ARIA „menubar”: belka to jeden przystanek Tab, strzałki w lewo/prawo
|
|
16
|
+
* i `Home`/`End` chodzą po pozycjach górnych, strzałka w dół (albo `Enter`/spacja) otwiera
|
|
17
|
+
* pod-menu z fokusem na pierwszej pozycji, w górę — na ostatniej. W otwartym pod-menu strzałki
|
|
18
|
+
* w lewo/prawo przechodzą do sąsiedniego, a `Escape` zamyka je i oddaje fokus belce.
|
|
19
|
+
*
|
|
12
20
|
* @example
|
|
13
21
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
14
22
|
*/
|
|
15
|
-
export function KptMenubar({ items = [], onSelect, className, ...rest }) {
|
|
23
|
+
export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest }) {
|
|
16
24
|
const [openIndex, setOpenIndex] = useState(null);
|
|
25
|
+
// Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
|
|
26
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
17
27
|
const buttonsRef = useRef([]);
|
|
28
|
+
const panelRef = useRef(null);
|
|
29
|
+
// Którą pozycję pod-menu skupić po otwarciu — strzałka w górę otwiera „od końca”.
|
|
30
|
+
const focusEdge = useRef('first');
|
|
31
|
+
const baseId = useKptId(undefined, 'kpt-menubar');
|
|
32
|
+
const panelId = (index) => `${baseId}-menu-${index}`;
|
|
33
|
+
const disabled = items.map((item) => !!item.disabled);
|
|
34
|
+
// Pozycja z `url` (bez pod-menu, niewyłączona) to `<a href>` — nawigację robi przeglądarka,
|
|
35
|
+
// więc działa nowa karta i podgląd adresu. `onSelect` i tak jest wołane.
|
|
36
|
+
const isLink = (item) => !!item.url && !item.items?.length && !item.disabled;
|
|
37
|
+
const tabStop = rovingStop(disabled, focusIndex);
|
|
38
|
+
// Po otwarciu fokus na pierwszą (albo ostatnią) pozycję. Klatkę później: `KptOverlay` trzyma
|
|
39
|
+
// panel w `visibility: hidden`, dopóki go nie zmierzy, a niewidocznego elementu nie da się skupić.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (openIndex === null)
|
|
42
|
+
return;
|
|
43
|
+
const frame = requestAnimationFrame(() => {
|
|
44
|
+
if (panelRef.current)
|
|
45
|
+
focusMenuEdge(panelRef.current, focusEdge.current);
|
|
46
|
+
});
|
|
47
|
+
return () => cancelAnimationFrame(frame);
|
|
48
|
+
}, [openIndex]);
|
|
18
49
|
const closeAll = () => setOpenIndex(null);
|
|
50
|
+
const focusTop = (index) => {
|
|
51
|
+
setFocusIndex(index);
|
|
52
|
+
buttonsRef.current[index]?.focus();
|
|
53
|
+
};
|
|
54
|
+
/** Skupia pozycję górną i otwiera jej pod-menu, jeśli je ma (inaczej zamyka otwarte). */
|
|
55
|
+
const openAt = (index, edge) => {
|
|
56
|
+
focusEdge.current = edge;
|
|
57
|
+
const hasMenu = !!items[index]?.items?.length;
|
|
58
|
+
if (hasMenu && openIndex === index && panelRef.current) {
|
|
59
|
+
focusMenuEdge(panelRef.current, edge);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setOpenIndex(hasMenu ? index : null);
|
|
63
|
+
focusTop(index);
|
|
64
|
+
};
|
|
19
65
|
const toggle = (index, item) => {
|
|
20
66
|
if (item.disabled)
|
|
21
67
|
return;
|
|
22
68
|
if (item.items?.length) {
|
|
69
|
+
focusEdge.current = 'first';
|
|
23
70
|
setOpenIndex((current) => (current === index ? null : index));
|
|
24
71
|
}
|
|
25
72
|
else {
|
|
@@ -27,13 +74,52 @@ export function KptMenubar({ items = [], onSelect, className, ...rest }) {
|
|
|
27
74
|
closeAll();
|
|
28
75
|
}
|
|
29
76
|
};
|
|
30
|
-
const pick = (item) => {
|
|
77
|
+
const pick = (item, index) => {
|
|
31
78
|
if (item.disabled)
|
|
32
79
|
return;
|
|
33
80
|
onSelect?.(item);
|
|
34
81
|
closeAll();
|
|
82
|
+
// Fokus wraca na pozycję górną — inaczej po zniknięciu pod-menu spadłby na `<body>`.
|
|
83
|
+
focusTop(index);
|
|
84
|
+
};
|
|
85
|
+
const onTopKeyDown = (event, index) => {
|
|
86
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
87
|
+
if (!items[index]?.items?.length)
|
|
88
|
+
return;
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
openAt(index, event.key === 'ArrowUp' ? 'last' : 'first');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const next = nextRovingIndex(disabled, index, event.key);
|
|
94
|
+
if (next === null)
|
|
95
|
+
return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
// Przy otwartym pod-menu przejście w bok otwiera pod-menu sąsiada — jak w menu aplikacji.
|
|
98
|
+
if (openIndex === null)
|
|
99
|
+
focusTop(next);
|
|
100
|
+
else
|
|
101
|
+
openAt(next, 'first');
|
|
102
|
+
};
|
|
103
|
+
const onPanelKeyDown = (event, index) => {
|
|
104
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
105
|
+
const next = nextRovingIndex(disabled, index, event.key);
|
|
106
|
+
if (next === null)
|
|
107
|
+
return;
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
openAt(next, 'first');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close') {
|
|
113
|
+
closeAll();
|
|
114
|
+
focusTop(index);
|
|
115
|
+
}
|
|
35
116
|
};
|
|
36
|
-
return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
return (_jsx("div", { className: cn('kpt-menubar-host', className), ...rest, children: _jsx("div", { className: "kpt-menubar", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, children: items.map((item, index) => {
|
|
118
|
+
const hasMenu = !!item.items?.length;
|
|
119
|
+
return (_jsxs("div", { className: "kpt-menubar__group", role: "none", children: [isLink(item) ? (_jsxs("a", { ref: (el) => {
|
|
120
|
+
buttonsRef.current[index] = el;
|
|
121
|
+
}, className: "kpt-menubar__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: () => pick(item, index), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label })] })) : (_jsxs("button", { ref: (el) => {
|
|
122
|
+
buttonsRef.current[index] = el;
|
|
123
|
+
}, type: "button", className: cn('kpt-menubar__top', openIndex === index && 'is-open'), role: "menuitem", disabled: item.disabled, tabIndex: index === tabStop ? 0 : -1, "aria-haspopup": hasMenu ? 'menu' : undefined, "aria-expanded": hasMenu ? openIndex === index : undefined, "aria-controls": openIndex === index ? panelId(index) : undefined, onClick: () => toggle(index, item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => onTopKeyDown(event, index), children: [item.icon ? _jsx(KptIcon, { name: item.icon }) : null, _jsx("span", { children: item.label }), hasMenu ? _jsx(KptIcon, { name: "chevron-down", className: "kpt-menubar__caret" }) : null] })), _jsx(KptOverlay, { open: openIndex === index && hasMenu, anchor: buttonsRef.current[index] ?? null, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-menubar__dropdown", role: "menu", id: panelId(index), "aria-label": item.label, onKeyDown: (event) => onPanelKeyDown(event, index), children: (item.items ?? []).map((sub, subIndex) => sub.separator ? (_jsx("div", { className: "kpt-menubar__separator", role: "separator" }, subIndex)) : isLink(sub) ? (_jsxs("a", { className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, href: sub.url, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex)) : (_jsxs("button", { type: "button", className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, disabled: sub.disabled, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex))) }) })] }, index));
|
|
124
|
+
}) }) }));
|
|
39
125
|
}
|
|
@@ -6,7 +6,8 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-meter-group-host">
|
|
8
8
|
<div class="kpt-meter-group">
|
|
9
|
-
<div class="kpt-meter-group__track" role="meter" aria-
|
|
9
|
+
<div class="kpt-meter-group__track" role="meter" aria-label="Usage" aria-valuemin="0"
|
|
10
|
+
aria-valuenow="100" aria-valuemax="100" aria-valuetext="Documents 45, …">
|
|
10
11
|
<span class="kpt-meter-group__segment" style="width:45%" title="Documents"></span>…
|
|
11
12
|
</div>
|
|
12
13
|
<ul class="kpt-meter-group__legend">
|
|
@@ -22,6 +23,9 @@ Import: `import { KptMeterGroup } from '@konce-pt/react';`
|
|
|
22
23
|
- `values`: `KptMeterItem[]` — `{ label, value, color? }`
|
|
23
24
|
- `max`: number — the reference total; without it the values' sum is 100%
|
|
24
25
|
- `showLegend`: boolean (default true)
|
|
26
|
+
- `label`: string — the meter's name; empty = `meterGroup.label`
|
|
27
|
+
- `legendValue`: 'percent' | 'value' (default 'percent') — what the legend shows next to each label
|
|
28
|
+
- `unit`: string — appended to the values in the legend and in `aria-valuetext` (e.g. 'GB')
|
|
25
29
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
26
30
|
|
|
27
31
|
## Percentages come from the sum, not from 100
|
|
@@ -43,5 +47,8 @@ The palette `--kpt-color-{primary,info,success,warning,danger}`, the track
|
|
|
43
47
|
`--kpt-color-surface-sunken`.
|
|
44
48
|
|
|
45
49
|
## Accessibility
|
|
46
|
-
The track is `role="meter"`
|
|
47
|
-
|
|
50
|
+
The track is `role="meter"` named by `label` (or `meterGroup.label`), with `aria-valuemin="0"`,
|
|
51
|
+
`aria-valuenow` set to the sum and `aria-valuemax` set to the real total — `max`, or the sum without
|
|
52
|
+
it. `aria-valuetext` spells out the split (“Photos 42 GB, Videos 28 GB, …”), so the segments reach
|
|
53
|
+
the screen reader even with the legend hidden. Keep the legend visible anyway: it is the text
|
|
54
|
+
alternative to the colours.
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
2
3
|
export interface KptMeterItem {
|
|
3
4
|
label: string;
|
|
4
5
|
value: number;
|
|
5
6
|
/** Własny kolor segmentu; bez niego wchodzi kolejny z palety. */
|
|
6
7
|
color?: string;
|
|
7
8
|
}
|
|
8
|
-
export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'
|
|
9
|
+
export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
|
|
9
10
|
values?: readonly KptMeterItem[];
|
|
10
11
|
/** Suma odniesienia; domyślnie suma wartości, czyli segmenty wypełniają całość. */
|
|
11
12
|
max?: number;
|
|
12
13
|
showLegend?: boolean;
|
|
14
|
+
/** Nazwa paska dla czytników ekranu, np. „Miejsce na dysku”; domyślnie `meterGroup.label`. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Co pokazuje legenda przy etykiecie: udział procentowy albo samą wartość (z `unit`). */
|
|
17
|
+
legendValue?: 'percent' | 'value';
|
|
18
|
+
/** Jednostka dopisywana do wartości — w legendzie (`legendValue="value"`) i w `aria-valuetext`. */
|
|
19
|
+
unit?: string;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Wielosegmentowy miernik z opcjonalną legendą.
|
|
@@ -17,4 +24,4 @@ export interface KptMeterGroupProps extends ComponentPropsWithRef<'div'> {
|
|
|
17
24
|
* @example
|
|
18
25
|
* <KptMeterGroup values={[{ label: 'Użyte', value: 70 }, { label: 'Wolne', value: 30 }]} />
|
|
19
26
|
*/
|
|
20
|
-
export declare function KptMeterGroup({ values, max, showLegend, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
|
|
27
|
+
export declare function KptMeterGroup({ values, max, showLegend, label, legendValue, unit, locale, dictionary, className, ...rest }: Readonly<KptMeterGroupProps>): import("react").JSX.Element;
|
|
@@ -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;
|