@konce-pt/react 0.8.3 → 0.9.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/README.md +4 -4
- package/dist/backdrop/backdrop.d.ts +79 -0
- package/dist/backdrop/backdrop.js +195 -0
- package/dist/backdrop/llms.txt +132 -0
- package/dist/button/button.d.ts +23 -7
- package/dist/button/button.js +33 -9
- package/dist/button/llms.txt +48 -12
- package/dist/button-group/button-group.d.ts +7 -1
- package/dist/button-group/button-group.js +9 -4
- package/dist/button-group/llms.txt +30 -5
- package/dist/chat/chat.d.ts +53 -0
- package/dist/chat/chat.js +160 -0
- package/dist/chat/llms.txt +78 -0
- package/dist/chat-composer/chat-composer.d.ts +41 -0
- package/dist/chat-composer/chat-composer.js +81 -0
- package/dist/chat-composer/llms.txt +56 -0
- package/dist/chat-dock/chat-dock.d.ts +40 -0
- package/dist/chat-dock/chat-dock.js +67 -0
- package/dist/chat-dock/llms.txt +71 -0
- package/dist/chat-entry/public-api.d.ts +12 -0
- package/dist/chat-entry/public-api.js +10 -0
- package/dist/chat-message/chat-message.d.ts +53 -0
- package/dist/chat-message/chat-message.js +112 -0
- package/dist/chat-message/llms.txt +73 -0
- package/dist/chat-typing/chat-typing.d.ts +23 -0
- package/dist/chat-typing/chat-typing.js +30 -0
- package/dist/chat-typing/llms.txt +41 -0
- package/dist/chip/chip.d.ts +10 -1
- package/dist/chip/chip.js +3 -2
- package/dist/chip/llms.txt +9 -0
- package/dist/data-view/data-view.d.ts +15 -3
- package/dist/data-view/data-view.js +22 -5
- package/dist/data-view/llms.txt +20 -7
- package/dist/dialog/dialog.d.ts +9 -1
- package/dist/dialog/dialog.js +2 -2
- package/dist/internal/dev.d.ts +1 -0
- package/dist/internal/dev.js +3 -0
- package/dist/internal/overlay.d.ts +6 -1
- package/dist/internal/overlay.js +5 -3
- package/dist/mention/llms.txt +60 -0
- package/dist/mention/mention.d.ts +38 -0
- package/dist/mention/mention.js +29 -0
- package/dist/mention/mentions.d.ts +31 -0
- package/dist/mention/mentions.js +24 -0
- package/dist/order-list/llms.txt +16 -5
- package/dist/order-list/order-list.d.ts +5 -3
- package/dist/order-list/order-list.js +69 -4
- package/dist/peek/llms.txt +42 -0
- package/dist/peek/peek.d.ts +35 -0
- package/dist/peek/peek.js +127 -0
- package/dist/pick-list/llms.txt +19 -5
- package/dist/pick-list/pick-list.d.ts +7 -2
- package/dist/pick-list/pick-list.js +123 -17
- package/dist/progress/llms.txt +70 -22
- package/dist/progress/progress.d.ts +34 -7
- package/dist/progress/progress.js +46 -9
- package/dist/public-api.d.ts +15 -3
- package/dist/public-api.js +6 -0
- package/dist/rich-text/llms.txt +10 -0
- package/dist/rich-text/rich-text.d.ts +11 -1
- package/dist/rich-text/rich-text.js +157 -5
- package/dist/rich-text/sanitize.js +10 -1
- package/dist/roadmap/roadmap.js +1 -3
- package/dist/shared/types.d.ts +2 -0
- package/package.json +16 -9
|
@@ -7,12 +7,12 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
7
7
|
const EMPTY = [];
|
|
8
8
|
/**
|
|
9
9
|
* OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
|
|
10
|
-
* w górę i w dół.
|
|
10
|
+
* w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
|
-
* <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" renderItem={(t) => t.name} />
|
|
13
|
+
* <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
|
|
14
14
|
*/
|
|
15
|
-
export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', renderItem, locale, dictionary, className, ...rest }) {
|
|
15
|
+
export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
|
|
16
16
|
const { t } = useKptMessages({ locale, dictionary });
|
|
17
17
|
const [list, setList] = useControllableState({
|
|
18
18
|
value: items,
|
|
@@ -20,6 +20,12 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
|
|
|
20
20
|
onChange: onItemsChange,
|
|
21
21
|
});
|
|
22
22
|
const [selected, setSelected] = useState(null);
|
|
23
|
+
/** Indeks przeciąganej pozycji; `null` gdy nic nie jest przeciągane. */
|
|
24
|
+
const [dragIndex, setDragIndex] = useState(null);
|
|
25
|
+
/** Indeks pozycji pod kursorem — rysuje wskaźnik miejsca upuszczenia. */
|
|
26
|
+
const [overIndex, setOverIndex] = useState(null);
|
|
27
|
+
/** Kursor nad pustym obszarem listy — upuszczenie tam dopina na koniec. */
|
|
28
|
+
const [listOver, setListOver] = useState(false);
|
|
23
29
|
const move = (dir) => {
|
|
24
30
|
if (selected === null)
|
|
25
31
|
return;
|
|
@@ -33,5 +39,64 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
|
|
|
33
39
|
// przesuwałoby sąsiada zamiast tego samego wpisu.
|
|
34
40
|
setSelected(target);
|
|
35
41
|
};
|
|
36
|
-
|
|
42
|
+
const clearDrag = () => {
|
|
43
|
+
setDragIndex(null);
|
|
44
|
+
setOverIndex(null);
|
|
45
|
+
setListOver(false);
|
|
46
|
+
};
|
|
47
|
+
const onDragStart = (index, event) => {
|
|
48
|
+
if (!dragdrop)
|
|
49
|
+
return;
|
|
50
|
+
setDragIndex(index);
|
|
51
|
+
event.dataTransfer.setData('text/plain', String(index));
|
|
52
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
53
|
+
};
|
|
54
|
+
const onDragOver = (index, event) => {
|
|
55
|
+
if (dragIndex === null)
|
|
56
|
+
return;
|
|
57
|
+
// Zatrzymujemy propagację, żeby `<ul>` nie uznał, że kursor jest nad pustym obszarem.
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
setOverIndex(index);
|
|
61
|
+
setListOver(false);
|
|
62
|
+
};
|
|
63
|
+
const onDrop = (index, event) => {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
const from = dragIndex;
|
|
67
|
+
clearDrag();
|
|
68
|
+
if (from === null || from === index)
|
|
69
|
+
return;
|
|
70
|
+
// Indeks celu liczony na tablicy sprzed usunięcia — element ląduje na pozycji tego,
|
|
71
|
+
// na który go upuszczono (ten sam wzór co przestawianie kolumn w KptDataTable).
|
|
72
|
+
const next = [...list];
|
|
73
|
+
const [moved] = next.splice(from, 1);
|
|
74
|
+
if (moved === undefined)
|
|
75
|
+
return;
|
|
76
|
+
next.splice(index, 0, moved);
|
|
77
|
+
setList(next);
|
|
78
|
+
setSelected(index);
|
|
79
|
+
};
|
|
80
|
+
const onListDragOver = (event) => {
|
|
81
|
+
if (dragIndex === null)
|
|
82
|
+
return;
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
setListOver(true);
|
|
85
|
+
setOverIndex(null);
|
|
86
|
+
};
|
|
87
|
+
const onListDrop = (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
const from = dragIndex;
|
|
90
|
+
clearDrag();
|
|
91
|
+
if (from === null)
|
|
92
|
+
return;
|
|
93
|
+
const next = [...list];
|
|
94
|
+
const [moved] = next.splice(from, 1);
|
|
95
|
+
if (moved === undefined)
|
|
96
|
+
return;
|
|
97
|
+
next.push(moved);
|
|
98
|
+
setList(next);
|
|
99
|
+
setSelected(next.length - 1);
|
|
100
|
+
};
|
|
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))) })] })] }) }));
|
|
37
102
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# KptPeek (@konce-pt/react)
|
|
2
|
+
|
|
3
|
+
A peek card — content tied to what you are looking at, shown without leaving the text. Hover opens
|
|
4
|
+
it, the card itself is interactive, and a click pins it.
|
|
5
|
+
Import: `import { KptPeek } from '@konce-pt/react';`
|
|
6
|
+
|
|
7
|
+
## DOM
|
|
8
|
+
<span class="kpt-peek-host">
|
|
9
|
+
<a class="kpt-peek__trigger" aria-expanded="true" aria-controls="…">…trigger…</a>
|
|
10
|
+
</span>
|
|
11
|
+
<!-- while open, in a portal on document.body: -->
|
|
12
|
+
<div class="kpt-peek__panel" role="dialog" tabindex="-1" data-pinned>…children…</div>
|
|
13
|
+
|
|
14
|
+
## Props
|
|
15
|
+
- `trigger`: ReactNode — the element the card hangs on; a mention, an avatar, a chip, a link
|
|
16
|
+
- `children`: ReactNode — the card content
|
|
17
|
+
- `placement`: 'top' | 'bottom' (default 'bottom')
|
|
18
|
+
- `openDelay`: number (default 300), `closeDelay`: number (default 200)
|
|
19
|
+
- `disabled`: boolean — the card never opens; the trigger stays a plain link or button
|
|
20
|
+
- `label`: string — the card's accessible name; without it the trigger's text is used
|
|
21
|
+
- `onOpenChange`: `(open: boolean) => void` — the moment to fetch the card's data
|
|
22
|
+
- `className`, `ref` and every other `<span>` prop go to the host
|
|
23
|
+
|
|
24
|
+
## The props land on the trigger itself
|
|
25
|
+
A valid element passed as `trigger` is cloned with the handlers and `aria-expanded` merged in, so
|
|
26
|
+
the state sits on the element a screen reader actually reads — not on some `<span>` next to it.
|
|
27
|
+
Anything else (a bare string) gets wrapped in a `<span>`.
|
|
28
|
+
|
|
29
|
+
## Hover is not the only way in
|
|
30
|
+
Hover opens after `openDelay` and leaving closes after `closeDelay` — the pointer needs time to
|
|
31
|
+
travel from the trigger to the card, so entering the card cancels the close. Focus opens it with no
|
|
32
|
+
delay. A click, Enter or a tap **pins** the card: it stays after the pointer leaves and focus moves
|
|
33
|
+
inside it. That is the only route to the card's content where there is no hover at all. Escape
|
|
34
|
+
closes it and hands focus back to the trigger; so does a pointer press outside.
|
|
35
|
+
|
|
36
|
+
## Which of the three
|
|
37
|
+
`KptTooltip` carries plain text and cannot be clicked. `KptPopover` opens on click and puts a
|
|
38
|
+
backdrop over the page. `KptPeek` opens on hover, the card is interactive, and nothing is covered.
|
|
39
|
+
|
|
40
|
+
## Tokens
|
|
41
|
+
`--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-focus-ring`,
|
|
42
|
+
`--kpt-z-overlay`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
/** Z której strony wyzwalacza wychodzi karta. */
|
|
3
|
+
export type KptPeekPlacement = 'top' | 'bottom';
|
|
4
|
+
export interface KptPeekProps extends Omit<ComponentPropsWithRef<'span'>, 'content'> {
|
|
5
|
+
/** Element, na którym wisi karta — wzmianka, awatar, chip albo zwykły link. */
|
|
6
|
+
trigger: ReactNode;
|
|
7
|
+
/** Treść karty. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
placement?: KptPeekPlacement;
|
|
10
|
+
/** Zwłoka otwarcia po najechaniu (ms) — bez niej karty mrugają przy przejeżdżaniu przez tekst. */
|
|
11
|
+
openDelay?: number;
|
|
12
|
+
/** Zwłoka zamknięcia po zjeździe kursora (ms) — tyle jest na przejazd z wyzwalacza na kartę. */
|
|
13
|
+
closeDelay?: number;
|
|
14
|
+
/** Wyłączona karta nie otwiera się wcale; wyzwalacz zostaje zwykłym linkiem czy przyciskiem. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Nazwa karty dla czytnika ekranu; bez niej bierze się tekst wyzwalacza. */
|
|
17
|
+
label?: string;
|
|
18
|
+
onOpenChange?: (open: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Karta podglądu pod kursorem — treść powiązana z tym, na co się patrzy, bez opuszczania tekstu.
|
|
22
|
+
* Tym różni się od sąsiadów: `KptTooltip` niesie sam tekst i nie da się w nim kliknąć,
|
|
23
|
+
* a `KptPopover` otwiera dopiero klik i zasłania stronę backdropem.
|
|
24
|
+
*
|
|
25
|
+
* Hover pokazuje kartę po `openDelay`, zjazd kursora chowa ją po `closeDelay` — kursor musi mieć
|
|
26
|
+
* czas przejechać z wyzwalacza na kartę, więc najazd na kartę anuluje zamknięcie. Fokus otwiera
|
|
27
|
+
* kartę od razu, a klik, Enter albo dotyk **przypina** ją: karta zostaje mimo zjazdu kursora,
|
|
28
|
+
* a fokus wchodzi do środka. To jedyna droga do treści karty tam, gdzie hovera nie ma w ogóle.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* <KptPeek trigger={<a href="/ludzie/anna">@Anna Kowalska</a>}>
|
|
32
|
+
* <KptCard>Profil, projekty, kontakt…</KptCard>
|
|
33
|
+
* </KptPeek>
|
|
34
|
+
*/
|
|
35
|
+
export declare function KptPeek({ trigger, children, placement, openDelay, closeDelay, disabled, label, onOpenChange, className, ...rest }: Readonly<KptPeekProps>): import("react").JSX.Element;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useId, useRef, useState, } from 'react';
|
|
3
|
+
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { KptOverlay } from "../internal/overlay.js";
|
|
5
|
+
import { mergeSlotProps } from "../internal/slot.js";
|
|
6
|
+
/**
|
|
7
|
+
* Karta podglądu pod kursorem — treść powiązana z tym, na co się patrzy, bez opuszczania tekstu.
|
|
8
|
+
* Tym różni się od sąsiadów: `KptTooltip` niesie sam tekst i nie da się w nim kliknąć,
|
|
9
|
+
* a `KptPopover` otwiera dopiero klik i zasłania stronę backdropem.
|
|
10
|
+
*
|
|
11
|
+
* Hover pokazuje kartę po `openDelay`, zjazd kursora chowa ją po `closeDelay` — kursor musi mieć
|
|
12
|
+
* czas przejechać z wyzwalacza na kartę, więc najazd na kartę anuluje zamknięcie. Fokus otwiera
|
|
13
|
+
* kartę od razu, a klik, Enter albo dotyk **przypina** ją: karta zostaje mimo zjazdu kursora,
|
|
14
|
+
* a fokus wchodzi do środka. To jedyna droga do treści karty tam, gdzie hovera nie ma w ogóle.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <KptPeek trigger={<a href="/ludzie/anna">@Anna Kowalska</a>}>
|
|
18
|
+
* <KptCard>Profil, projekty, kontakt…</KptCard>
|
|
19
|
+
* </KptPeek>
|
|
20
|
+
*/
|
|
21
|
+
export function KptPeek({ trigger, children, placement = 'bottom', openDelay = 300, closeDelay = 200, disabled = false, label, onOpenChange, className, ...rest }) {
|
|
22
|
+
const hostRef = useRef(null);
|
|
23
|
+
const triggerRef = useRef(null);
|
|
24
|
+
const panelRef = useRef(null);
|
|
25
|
+
const timer = useRef(undefined);
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
/** Przypięta karta zostaje po zjeździe kursora — przypina ją klik, Enter albo dotyk. */
|
|
28
|
+
const [pinned, setPinned] = useState(false);
|
|
29
|
+
const panelId = useId();
|
|
30
|
+
const stop = useCallback(() => {
|
|
31
|
+
if (timer.current)
|
|
32
|
+
clearTimeout(timer.current);
|
|
33
|
+
timer.current = undefined;
|
|
34
|
+
}, []);
|
|
35
|
+
const schedule = useCallback((action, delay) => {
|
|
36
|
+
stop();
|
|
37
|
+
if (delay <= 0)
|
|
38
|
+
action();
|
|
39
|
+
else
|
|
40
|
+
timer.current = setTimeout(action, delay);
|
|
41
|
+
}, [stop]);
|
|
42
|
+
const show = useCallback(() => {
|
|
43
|
+
setOpen((was) => {
|
|
44
|
+
if (!was)
|
|
45
|
+
onOpenChange?.(true);
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
}, [onOpenChange]);
|
|
49
|
+
const close = useCallback(() => {
|
|
50
|
+
stop();
|
|
51
|
+
setPinned(false);
|
|
52
|
+
setOpen((was) => {
|
|
53
|
+
if (was)
|
|
54
|
+
onOpenChange?.(false);
|
|
55
|
+
return false;
|
|
56
|
+
});
|
|
57
|
+
}, [onOpenChange, stop]);
|
|
58
|
+
const hoverIn = useCallback(() => {
|
|
59
|
+
if (disabled)
|
|
60
|
+
return;
|
|
61
|
+
schedule(show, open ? 0 : openDelay);
|
|
62
|
+
}, [disabled, open, openDelay, schedule, show]);
|
|
63
|
+
const hoverOut = useCallback(() => {
|
|
64
|
+
if (pinned)
|
|
65
|
+
return;
|
|
66
|
+
schedule(close, closeDelay);
|
|
67
|
+
}, [close, closeDelay, pinned, schedule]);
|
|
68
|
+
const focusIn = useCallback(() => {
|
|
69
|
+
if (disabled)
|
|
70
|
+
return;
|
|
71
|
+
schedule(show, 0);
|
|
72
|
+
}, [disabled, schedule, show]);
|
|
73
|
+
const focusOut = useCallback(() => {
|
|
74
|
+
// Sprawdzenie po klatce: w chwili `blur` aktywnym elementem jest jeszcze `body`, więc
|
|
75
|
+
// przejście Tabem z wyzwalacza do karty wyglądałoby jak wyjście na zewnątrz.
|
|
76
|
+
schedule(() => {
|
|
77
|
+
const active = document.activeElement;
|
|
78
|
+
if (hostRef.current?.contains(active) || panelRef.current?.contains(active))
|
|
79
|
+
return;
|
|
80
|
+
close();
|
|
81
|
+
}, 0);
|
|
82
|
+
}, [close, schedule]);
|
|
83
|
+
/** Klik, Enter albo dotyk: karta zostaje i przyjmuje fokus. */
|
|
84
|
+
const pin = useCallback(() => {
|
|
85
|
+
if (disabled)
|
|
86
|
+
return;
|
|
87
|
+
setPinned(true);
|
|
88
|
+
show();
|
|
89
|
+
// Panel powstaje dopiero po przerysowaniu portalu, więc fokus idzie w następnej klatce.
|
|
90
|
+
setTimeout(() => panelRef.current?.focus({ preventScroll: true }));
|
|
91
|
+
}, [disabled, show]);
|
|
92
|
+
useEffect(() => stop, [stop]);
|
|
93
|
+
// Przypiętej karty nie zamyka zjazd kursora, więc kliknięcie obok musi ją zdjąć. Faza
|
|
94
|
+
// przechwytywania i `pointerdown`: własne `onClick` w karcie zdąży się wykonać wcześniej.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!open)
|
|
97
|
+
return;
|
|
98
|
+
const outside = (event) => {
|
|
99
|
+
const target = event.target;
|
|
100
|
+
if (hostRef.current?.contains(target) || panelRef.current?.contains(target))
|
|
101
|
+
return;
|
|
102
|
+
close();
|
|
103
|
+
};
|
|
104
|
+
document.addEventListener('pointerdown', outside, true);
|
|
105
|
+
return () => document.removeEventListener('pointerdown', outside, true);
|
|
106
|
+
}, [close, open]);
|
|
107
|
+
const triggerProps = {
|
|
108
|
+
className: 'kpt-peek__trigger',
|
|
109
|
+
ref: triggerRef,
|
|
110
|
+
onMouseEnter: hoverIn,
|
|
111
|
+
onMouseLeave: hoverOut,
|
|
112
|
+
onFocus: focusIn,
|
|
113
|
+
onBlur: focusOut,
|
|
114
|
+
onClick: pin,
|
|
115
|
+
'aria-expanded': open,
|
|
116
|
+
'aria-controls': open ? panelId : undefined,
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs("span", { ref: hostRef, className: cn('kpt-peek-host', className), ...rest, children: [isValidElement(trigger) ? (
|
|
119
|
+
// Propsy idą na sam wyzwalacz, nie na opakowanie: `aria-expanded` ma siedzieć na tym
|
|
120
|
+
// elemencie, po którym jeździ czytnik ekranu, a nie na obojętnym `<span>` obok.
|
|
121
|
+
cloneElement(trigger, mergeSlotProps(triggerProps, trigger.props))) : (_jsx("span", { ...triggerProps, children: trigger })), _jsx(KptOverlay, { open: open, anchor: triggerRef.current, onClose: close, offset: 8, withBackdrop: false, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-peek__panel", role: "dialog", tabIndex: -1, "aria-label": label || triggerRef.current?.textContent?.trim() || undefined, "data-pinned": pinned ? '' : undefined, onMouseEnter: hoverIn, onMouseLeave: hoverOut, onFocus: focusIn, onBlur: focusOut, onKeyDown: (event) => {
|
|
122
|
+
if (event.key !== 'Escape')
|
|
123
|
+
return;
|
|
124
|
+
close();
|
|
125
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
126
|
+
}, children: children }) })] }));
|
|
127
|
+
}
|
package/dist/pick-list/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# KptPickList (@konce-pt/react)
|
|
2
2
|
|
|
3
3
|
Transfers items between two lists. Click to select, the middle buttons move the selection or
|
|
4
|
-
everything.
|
|
4
|
+
everything, a double-click sends one row across, and `dragdrop` adds dragging.
|
|
5
5
|
Import: `import { KptPickList } from '@konce-pt/react';`
|
|
6
6
|
|
|
7
7
|
## DOM
|
|
@@ -22,13 +22,24 @@ Import: `import { KptPickList } from '@konce-pt/react';`
|
|
|
22
22
|
- `source` / `defaultSource` / `onSourceChange` — the left list
|
|
23
23
|
- `target` / `defaultTarget` / `onTargetChange` — the right list
|
|
24
24
|
- `sourceHeader`, `targetHeader` — without them `pickList.available` / `pickList.selected`
|
|
25
|
+
- `dragdrop` — `false` by default; enables dragging (opt-in, nothing changes without it)
|
|
25
26
|
- `renderItem(item, index)` — required; the equivalent of `<ng-template let-item>`
|
|
26
27
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
27
28
|
|
|
28
29
|
## The selection is kept by reference
|
|
29
30
|
Selected items live in a `Set` of references, not of indexes: after a move the indexes on both
|
|
30
|
-
sides shift, while a reference stays the same.
|
|
31
|
-
|
|
31
|
+
sides shift, while a reference stays the same. The buttons clear the whole selection of the side
|
|
32
|
+
they empty; a double-click or a drag only drops the one item that left.
|
|
33
|
+
|
|
34
|
+
## A double-click moves one item
|
|
35
|
+
It sends exactly the row you clicked to the other panel and appends it at the end, whatever else is
|
|
36
|
+
selected. It does not need `dragdrop` — the two clicks underneath cancel each other out, so the rest
|
|
37
|
+
of the selection is left alone.
|
|
38
|
+
|
|
39
|
+
## Where a dragged item lands
|
|
40
|
+
Dropping onto a row puts the dragged item at that row's position: inside one panel that reorders,
|
|
41
|
+
across panels it transfers. Dropping on the empty part of a panel appends at the end. While a drag
|
|
42
|
+
is running the row under the cursor carries a `--dragover` modifier and the dragged one `is-dragging`.
|
|
32
43
|
|
|
33
44
|
## Two lists, two controlled values
|
|
34
45
|
Both sides are independent controllable values, so the component works uncontrolled (`default*`),
|
|
@@ -38,15 +49,18 @@ half-controlled, or fully controlled by the form.
|
|
|
38
49
|
<KptPickList
|
|
39
50
|
source={available} onSourceChange={setAvailable}
|
|
40
51
|
target={selected} onTargetChange={setSelected}
|
|
52
|
+
dragdrop
|
|
41
53
|
renderItem={(item) => item.name}
|
|
42
54
|
/>
|
|
43
55
|
|
|
44
56
|
## i18n
|
|
45
|
-
The headers and the four move buttons come from the `pickList` namespace.
|
|
57
|
+
The headers and the four move buttons come from the `pickList` namespace. Dragging and the
|
|
58
|
+
double-click carry no visible label, so they add no dictionary keys.
|
|
46
59
|
|
|
47
60
|
## Tokens
|
|
48
61
|
The selection `--kpt-color-primary`, the headers `--kpt-color-surface-variant`.
|
|
49
62
|
|
|
50
63
|
## Accessibility
|
|
51
64
|
Every move button is labelled from the dictionary, so "move all right" is distinguishable from
|
|
52
|
-
"move right" without seeing the double chevron.
|
|
65
|
+
"move right" without seeing the double chevron. Native HTML5 drag and drop has no keyboard
|
|
66
|
+
equivalent, so those buttons stay the full keyboard path — `dragdrop` never replaces them.
|
|
@@ -11,18 +11,23 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
11
11
|
sourceHeader?: string;
|
|
12
12
|
/** Nagłówek listy docelowej; bez niego `pickList.selected`. */
|
|
13
13
|
targetHeader?: string;
|
|
14
|
+
/** Włącza porządkowanie i przenoszenie elementów metodą przeciągnij-i-upuść. */
|
|
15
|
+
dragdrop?: boolean;
|
|
14
16
|
/** Szablon elementu — odpowiednik `<ng-template let-item>` z Angulara. */
|
|
15
17
|
renderItem: (item: T, index: number) => ReactNode;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
|
|
19
|
-
* przenoszą zaznaczone albo wszystko
|
|
21
|
+
* przenoszą zaznaczone albo wszystko, dwuklik przerzuca pojedynczy element na drugą stronę.
|
|
22
|
+
* Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
|
|
23
|
+
* konkretną pozycję drugiego panelu.
|
|
20
24
|
*
|
|
21
25
|
* @example
|
|
22
26
|
* <KptPickList
|
|
23
27
|
* source={available} onSourceChange={setAvailable}
|
|
24
28
|
* target={selected} onTargetChange={setSelected}
|
|
29
|
+
* dragdrop
|
|
25
30
|
* renderItem={(item) => item.name}
|
|
26
31
|
* />
|
|
27
32
|
*/
|
|
28
|
-
export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, renderItem, locale, dictionary, className, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
|
|
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;
|
|
@@ -7,16 +7,19 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
7
7
|
const EMPTY = [];
|
|
8
8
|
/**
|
|
9
9
|
* PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
|
|
10
|
-
* przenoszą zaznaczone albo wszystko
|
|
10
|
+
* przenoszą zaznaczone albo wszystko, dwuklik przerzuca pojedynczy element na drugą stronę.
|
|
11
|
+
* Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
|
|
12
|
+
* konkretną pozycję drugiego panelu.
|
|
11
13
|
*
|
|
12
14
|
* @example
|
|
13
15
|
* <KptPickList
|
|
14
16
|
* source={available} onSourceChange={setAvailable}
|
|
15
17
|
* target={selected} onTargetChange={setSelected}
|
|
18
|
+
* dragdrop
|
|
16
19
|
* renderItem={(item) => item.name}
|
|
17
20
|
* />
|
|
18
21
|
*/
|
|
19
|
-
export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, renderItem, locale, dictionary, className, ...rest }) {
|
|
22
|
+
export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
|
|
20
23
|
const { t } = useKptMessages({ locale, dictionary });
|
|
21
24
|
const [left, setLeft] = useControllableState({
|
|
22
25
|
value: source,
|
|
@@ -32,34 +35,137 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
|
|
|
32
35
|
// po obu stronach się przesuwają, a referencja zostaje ta sama.
|
|
33
36
|
const [selLeft, setSelLeft] = useState(new Set());
|
|
34
37
|
const [selRight, setSelRight] = useState(new Set());
|
|
38
|
+
/** Przeciągany element — strona i pozycja w jej tablicy. */
|
|
39
|
+
const [drag, setDrag] = useState(null);
|
|
40
|
+
/** Element pod kursorem — rysuje wskaźnik miejsca upuszczenia. */
|
|
41
|
+
const [over, setOver] = useState(null);
|
|
42
|
+
/** Panel, nad którego pustym obszarem jest kursor — upuszczenie tam dopina na koniec. */
|
|
43
|
+
const [listOver, setListOver] = useState(null);
|
|
44
|
+
const listOf = (side) => (side === 'source' ? left : right);
|
|
45
|
+
const setListOf = (side) => (side === 'source' ? setLeft : setRight);
|
|
46
|
+
const selectionOf = (side) => (side === 'source' ? selLeft : selRight);
|
|
47
|
+
const setSelectionOf = (side) => side === 'source' ? setSelLeft : setSelRight;
|
|
48
|
+
const other = (side) => (side === 'source' ? 'target' : 'source');
|
|
35
49
|
const toggle = (side, item) => {
|
|
36
|
-
const
|
|
37
|
-
const next = new Set(selection);
|
|
50
|
+
const next = new Set(selectionOf(side));
|
|
38
51
|
if (next.has(item))
|
|
39
52
|
next.delete(item);
|
|
40
53
|
else
|
|
41
54
|
next.add(item);
|
|
42
|
-
|
|
55
|
+
setSelectionOf(side)(next);
|
|
43
56
|
};
|
|
44
57
|
const move = (from, all) => {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const setTo = from === 'source' ? setRight : setLeft;
|
|
49
|
-
const selection = from === 'source' ? selLeft : selRight;
|
|
50
|
-
const setSelection = from === 'source' ? setSelLeft : setSelRight;
|
|
58
|
+
const to = other(from);
|
|
59
|
+
const fromItems = listOf(from);
|
|
60
|
+
const selection = selectionOf(from);
|
|
51
61
|
const moving = all ? fromItems : fromItems.filter((item) => selection.has(item));
|
|
52
62
|
if (!moving.length)
|
|
53
63
|
return;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
setListOf(from)(fromItems.filter((item) => !moving.includes(item)));
|
|
65
|
+
setListOf(to)([...listOf(to), ...moving]);
|
|
66
|
+
setSelectionOf(from)(new Set());
|
|
67
|
+
};
|
|
68
|
+
/** Przenosi jeden element między panelami, wstawiając go pod wskazanym indeksem. */
|
|
69
|
+
const transfer = (fromSide, fromIndex, toSide, toIndex) => {
|
|
70
|
+
const fromItems = [...listOf(fromSide)];
|
|
71
|
+
const [moved] = fromItems.splice(fromIndex, 1);
|
|
72
|
+
if (moved === undefined)
|
|
73
|
+
return;
|
|
74
|
+
const toItems = [...listOf(toSide)];
|
|
75
|
+
toItems.splice(toIndex, 0, moved);
|
|
76
|
+
setListOf(fromSide)(fromItems);
|
|
77
|
+
setListOf(toSide)(toItems);
|
|
78
|
+
// Element opuszcza panel — jego zaznaczenie po tamtej stronie przestaje cokolwiek znaczyć.
|
|
79
|
+
const selection = selectionOf(fromSide);
|
|
80
|
+
if (selection.has(moved)) {
|
|
81
|
+
const next = new Set(selection);
|
|
82
|
+
next.delete(moved);
|
|
83
|
+
setSelectionOf(fromSide)(next);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** Dwuklik: przenosi wyłącznie klikniętą pozycję, niezależnie od zaznaczenia. */
|
|
87
|
+
const moveOne = (from, item) => {
|
|
88
|
+
const index = listOf(from).indexOf(item);
|
|
89
|
+
if (index === -1)
|
|
90
|
+
return;
|
|
91
|
+
const to = other(from);
|
|
92
|
+
transfer(from, index, to, listOf(to).length);
|
|
93
|
+
};
|
|
94
|
+
const clearDrag = () => {
|
|
95
|
+
setDrag(null);
|
|
96
|
+
setOver(null);
|
|
97
|
+
setListOver(null);
|
|
98
|
+
};
|
|
99
|
+
const onDragStart = (side, index, event) => {
|
|
100
|
+
if (!dragdrop)
|
|
101
|
+
return;
|
|
102
|
+
setDrag({ side, index });
|
|
103
|
+
event.dataTransfer.setData('text/plain', String(index));
|
|
104
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
105
|
+
};
|
|
106
|
+
const onDragOver = (side, index, event) => {
|
|
107
|
+
if (drag === null)
|
|
108
|
+
return;
|
|
109
|
+
// Zatrzymujemy propagację, żeby `<ul>` nie uznał, że kursor jest nad pustym obszarem.
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
setOver({ side, index });
|
|
113
|
+
setListOver(null);
|
|
114
|
+
};
|
|
115
|
+
const onDrop = (side, index, event) => {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
const from = drag;
|
|
119
|
+
clearDrag();
|
|
120
|
+
if (from === null)
|
|
121
|
+
return;
|
|
122
|
+
if (from.side !== side) {
|
|
123
|
+
transfer(from.side, from.index, side, index);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (from.index === index)
|
|
127
|
+
return;
|
|
128
|
+
// Indeks celu liczony na tablicy sprzed usunięcia — element ląduje na pozycji tego,
|
|
129
|
+
// na który go upuszczono (ten sam wzór co przestawianie kolumn w KptDataTable).
|
|
130
|
+
const next = [...listOf(side)];
|
|
131
|
+
const [moved] = next.splice(from.index, 1);
|
|
132
|
+
if (moved === undefined)
|
|
133
|
+
return;
|
|
134
|
+
next.splice(index, 0, moved);
|
|
135
|
+
setListOf(side)(next);
|
|
136
|
+
};
|
|
137
|
+
const onListDragOver = (side, event) => {
|
|
138
|
+
if (drag === null)
|
|
139
|
+
return;
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
setListOver(side);
|
|
142
|
+
setOver(null);
|
|
143
|
+
};
|
|
144
|
+
const onListDrop = (side, event) => {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
const from = drag;
|
|
147
|
+
clearDrag();
|
|
148
|
+
if (from === null)
|
|
149
|
+
return;
|
|
150
|
+
if (from.side !== side) {
|
|
151
|
+
transfer(from.side, from.index, side, listOf(side).length);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const next = [...listOf(side)];
|
|
155
|
+
const [moved] = next.splice(from.index, 1);
|
|
156
|
+
if (moved === undefined)
|
|
157
|
+
return;
|
|
158
|
+
next.push(moved);
|
|
159
|
+
setListOf(side)(next);
|
|
57
160
|
};
|
|
58
161
|
const column = (side) => {
|
|
59
|
-
const items = side
|
|
60
|
-
const selection = side
|
|
162
|
+
const items = listOf(side);
|
|
163
|
+
const selection = selectionOf(side);
|
|
61
164
|
const header = side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
|
|
62
|
-
return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className:
|
|
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) => {
|
|
166
|
+
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));
|
|
168
|
+
}) })] }));
|
|
63
169
|
};
|
|
64
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')] }) }));
|
|
65
171
|
}
|