@konce-pt/react 0.11.2 → 0.12.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 +8 -2
- package/dist/accordion/accordion.js +11 -10
- package/dist/alert/alert.js +1 -0
- package/dist/alert/llms.txt +1 -0
- package/dist/app-shell/app-shell.js +12 -5
- package/dist/app-shell/llms.txt +5 -4
- package/dist/auth/auth.js +17 -8
- package/dist/auth/llms.txt +3 -2
- package/dist/autocomplete/autocomplete.d.ts +1 -1
- package/dist/autocomplete/autocomplete.js +12 -4
- package/dist/autocomplete/llms.txt +3 -0
- package/dist/avatar/avatar.js +1 -0
- package/dist/avatar-group/avatar-group.js +1 -0
- package/dist/backdrop/backdrop.d.ts +0 -19
- package/dist/backdrop/backdrop.js +18 -13
- package/dist/bottom-sheet/bottom-sheet.d.ts +6 -1
- package/dist/bottom-sheet/bottom-sheet.js +3 -2
- package/dist/bottom-sheet/llms.txt +3 -0
- package/dist/breadcrumb/breadcrumb.d.ts +7 -1
- package/dist/breadcrumb/breadcrumb.js +10 -2
- package/dist/breadcrumb/llms.txt +8 -0
- package/dist/button/button.js +2 -1
- package/dist/button-group/button-group.d.ts +14 -1
- package/dist/button-group/button-group.js +30 -4
- package/dist/button-group/llms.txt +9 -6
- package/dist/card/card.js +4 -3
- package/dist/carousel/carousel.js +4 -1
- package/dist/charts/chart-legend.js +1 -0
- package/dist/charts/chart.d.ts +3 -2
- package/dist/charts/chart.js +12 -4
- package/dist/charts/llms.txt +6 -3
- package/dist/chat/chat.js +39 -9
- package/dist/chat/llms.txt +9 -0
- package/dist/chat-composer/chat-composer.d.ts +5 -2
- package/dist/chat-composer/chat-composer.js +10 -1
- package/dist/chat-dock/chat-dock.js +1 -0
- package/dist/chat-message/chat-message.js +2 -1
- package/dist/chat-typing/chat-typing.js +1 -0
- package/dist/checkbox/checkbox.d.ts +1 -1
- package/dist/checkbox/checkbox.js +9 -2
- package/dist/checkbox/llms.txt +3 -0
- package/dist/chip/chip.js +1 -0
- package/dist/chips-input/chips-input.d.ts +3 -1
- package/dist/chips-input/chips-input.js +14 -5
- package/dist/chips-input/llms.txt +3 -0
- package/dist/clock/clock.d.ts +9 -4
- package/dist/clock/clock.js +35 -5
- package/dist/clock/llms.txt +6 -2
- package/dist/color-picker/color-picker.d.ts +5 -1
- package/dist/color-picker/color-picker.js +36 -17
- package/dist/color-picker/llms.txt +3 -0
- package/dist/confirm/confirm-container.js +1 -0
- package/dist/confirm/confirm-store.js +6 -0
- package/dist/confirm/llms.txt +2 -1
- package/dist/context-menu/context-menu.js +35 -12
- package/dist/context-menu/llms.txt +3 -0
- package/dist/data-table/data-table.d.ts +12 -1
- package/dist/data-table/data-table.js +113 -51
- package/dist/data-table/export-csv.d.ts +2 -2
- package/dist/data-table/export-csv.js +7 -9
- package/dist/data-table/llms.txt +19 -4
- package/dist/data-view/data-view.js +1 -0
- package/dist/date-range/date-range.d.ts +16 -4
- package/dist/date-range/date-range.js +69 -46
- package/dist/date-range/llms.txt +17 -3
- package/dist/datepicker/datepicker.d.ts +17 -3
- package/dist/datepicker/datepicker.js +86 -54
- package/dist/datepicker/llms.txt +17 -3
- package/dist/dialog/dialog.js +29 -8
- package/dist/dialog/llms.txt +3 -0
- package/dist/divider/divider.d.ts +7 -1
- package/dist/divider/divider.js +3 -2
- package/dist/divider/llms.txt +1 -0
- package/dist/drawer/drawer.d.ts +6 -1
- package/dist/drawer/drawer.js +3 -2
- package/dist/drawer/llms.txt +3 -0
- package/dist/fieldset/fieldset.js +4 -1
- package/dist/file-upload/file-upload.d.ts +7 -1
- package/dist/file-upload/file-upload.js +18 -7
- package/dist/file-upload/llms.txt +6 -3
- package/dist/form-field/form-field.js +1 -0
- package/dist/galleria/galleria.js +4 -1
- package/dist/i18n/i18n-context.d.ts +3 -3
- package/dist/i18n/i18n-context.js +25 -3
- package/dist/icon/icon.js +4 -1
- package/dist/image/image.js +4 -4
- package/dist/input/input.js +1 -0
- package/dist/input-mask/input-mask.d.ts +7 -2
- package/dist/input-mask/input-mask.js +15 -41
- package/dist/input-mask/llms.txt +3 -0
- package/dist/input-number/input-number.d.ts +3 -1
- package/dist/input-number/input-number.js +15 -11
- package/dist/input-number/llms.txt +5 -2
- package/dist/input-otp/input-otp.d.ts +16 -3
- package/dist/input-otp/input-otp.js +43 -19
- package/dist/input-otp/llms.txt +11 -0
- package/dist/internal/mask.d.ts +14 -0
- package/dist/internal/mask.js +62 -0
- package/dist/internal/number-group.d.ts +22 -0
- package/dist/internal/number-group.js +53 -0
- package/dist/internal/option-key.d.ts +6 -0
- package/dist/internal/option-key.js +8 -0
- package/dist/internal/overlay.js +37 -7
- package/dist/internal/password-strength.d.ts +6 -0
- package/dist/internal/password-strength.js +74 -0
- package/dist/internal/scroll-lock.d.ts +6 -0
- package/dist/internal/scroll-lock.js +35 -0
- package/dist/internal/slot-children.d.ts +13 -0
- package/dist/internal/slot-children.js +31 -0
- package/dist/internal/step.d.ts +12 -0
- package/dist/internal/step.js +31 -0
- package/dist/internal/tween.js +2 -3
- package/dist/internal/use-controllable-state.d.ts +5 -0
- package/dist/internal/use-controllable-state.js +25 -4
- package/dist/internal/use-modal-focus.d.ts +1 -0
- package/dist/internal/use-modal-focus.js +12 -1
- package/dist/knob/knob.d.ts +5 -2
- package/dist/knob/knob.js +15 -14
- package/dist/knob/llms.txt +4 -2
- package/dist/listbox/listbox.d.ts +3 -1
- package/dist/listbox/listbox.js +30 -10
- package/dist/listbox/llms.txt +3 -0
- package/dist/map/llms.txt +5 -1
- package/dist/map/map-create-dialog.js +30 -22
- package/dist/map/map-hours-field.js +1 -0
- package/dist/map/map-image-field.js +2 -1
- package/dist/map/map-legend.js +1 -0
- package/dist/map/map-poi-card.js +2 -1
- package/dist/map/map-search.js +54 -30
- package/dist/map/map.d.ts +6 -1
- package/dist/map/map.js +62 -34
- package/dist/megamenu/llms.txt +11 -0
- package/dist/megamenu/megamenu.d.ts +7 -1
- package/dist/megamenu/megamenu.js +15 -6
- package/dist/menu/menu-rows.js +1 -0
- package/dist/menu/menu.js +10 -3
- package/dist/menubar/llms.txt +11 -0
- package/dist/menubar/menubar.d.ts +7 -1
- package/dist/menubar/menubar.js +15 -3
- package/dist/meter-group/meter-group.js +1 -0
- package/dist/order-list/llms.txt +3 -0
- package/dist/order-list/order-list.js +21 -6
- package/dist/paginator/paginator.js +7 -4
- package/dist/panel/panel.js +5 -3
- package/dist/password/llms.txt +6 -2
- package/dist/password/password.js +4 -14
- package/dist/peek/llms.txt +1 -1
- package/dist/peek/peek.js +37 -15
- package/dist/pick-list/llms.txt +3 -0
- package/dist/pick-list/pick-list.js +19 -3
- package/dist/popover/llms.txt +3 -0
- package/dist/popover/popover.d.ts +3 -1
- package/dist/popover/popover.js +10 -3
- package/dist/progress/progress.js +1 -0
- package/dist/public-api.d.ts +1 -1
- package/dist/radio-group/llms.txt +3 -0
- package/dist/radio-group/radio-group.d.ts +3 -1
- package/dist/radio-group/radio-group.js +13 -2
- package/dist/rating/rating.js +1 -0
- package/dist/rich-text/llms.txt +14 -0
- package/dist/rich-text/rich-text.d.ts +3 -9
- package/dist/rich-text/rich-text.js +241 -96
- package/dist/rich-text/sanitize.d.ts +9 -0
- package/dist/rich-text/sanitize.js +77 -29
- package/dist/roadmap/llms.txt +9 -3
- package/dist/roadmap/roadmap.js +88 -11
- package/dist/scroll-top/scroll-top.js +1 -0
- package/dist/select/llms.txt +3 -0
- package/dist/select/select.d.ts +3 -1
- package/dist/select/select.js +46 -32
- package/dist/shared/types.d.ts +21 -0
- package/dist/shared/types.js +9 -1
- package/dist/sidenav/sidenav.d.ts +1 -2
- package/dist/sidenav/sidenav.js +4 -3
- package/dist/slider/llms.txt +3 -0
- package/dist/slider/slider.d.ts +3 -1
- package/dist/slider/slider.js +15 -4
- package/dist/speed-dial/speed-dial.js +2 -1
- package/dist/spinner/spinner.js +1 -0
- package/dist/split-button/split-button.js +10 -3
- package/dist/splitter/splitter.js +23 -8
- package/dist/stepper/llms.txt +3 -0
- package/dist/stepper/stepper.js +9 -5
- package/dist/switch/llms.txt +3 -0
- package/dist/switch/switch.d.ts +1 -1
- package/dist/switch/switch.js +9 -2
- package/dist/switch-group/llms.txt +3 -0
- package/dist/switch-group/switch-group.d.ts +3 -1
- package/dist/switch-group/switch-group.js +5 -3
- package/dist/tabs/llms.txt +3 -0
- package/dist/tabs/tabs.js +30 -14
- package/dist/textarea/textarea.js +1 -0
- package/dist/toast/llms.txt +3 -0
- package/dist/toast/toast-container.js +1 -0
- package/dist/toast/toast-store.d.ts +5 -1
- package/dist/toast/toast-store.js +1 -1
- package/dist/tooltip/llms.txt +5 -3
- package/dist/tooltip/tooltip.d.ts +3 -2
- package/dist/tooltip/tooltip.js +90 -31
- package/dist/tree/llms.txt +1 -0
- package/dist/tree/tree.d.ts +6 -1
- package/dist/tree/tree.js +14 -7
- package/package.json +15 -14
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Sanityzacja treści edytora. Kod nie zna
|
|
3
|
-
*
|
|
4
|
-
* (jak `@konce-pt/datetime`).
|
|
2
|
+
* Sanityzacja treści edytora. Kod nie zna frameworka — operuje wyłącznie na DOM. Bliźniak
|
|
3
|
+
* w porcie Angulara: `rich-text/sanitize.ts` — zmiana w obu (docelowo wspólna paczka pure-TS).
|
|
5
4
|
*/
|
|
6
5
|
/** Znaczniki dopuszczone w treści; pozostałe rozwijamy do dzieci, nie usuwając tekstu. */
|
|
7
6
|
const ALLOWED_TAGS = new Set([
|
|
@@ -17,15 +16,29 @@ const ALLOWED_STYLES = new Set([
|
|
|
17
16
|
* `data-mention` i `contenteditable` są tu dla wzmianek: identyfikator musi przeżyć zapis treści,
|
|
18
17
|
* a `contenteditable="false"` trzyma wzmiankę jako jeden kawałek, w którego środku nie da się
|
|
19
18
|
* pisać. Oba atrybuty są bierne — nie wykonują niczego i nie wskazują zasobów, więc wpuszczenie
|
|
20
|
-
* ich nie otwiera nowej furtki.
|
|
19
|
+
* ich nie otwiera nowej furtki. `class` przechodzi osobno, tylko z klasami samego edytora.
|
|
21
20
|
*/
|
|
22
21
|
const ALLOWED_ATTRS = new Set([
|
|
23
|
-
'colspan', 'rowspan', 'span', 'width', '
|
|
22
|
+
'colspan', 'rowspan', 'span', 'width', 'target', 'rel', 'title', 'color',
|
|
24
23
|
'data-mention', 'contenteditable',
|
|
25
24
|
]);
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Klasy, które zostawia sam edytor (`kpt-rt-table`, `kpt-mention`). Obce klasy z wklejki — aplikacji,
|
|
27
|
+
* biblioteki (`kpt-dialog`, `kpt-visually-hidden`) czy Tailwinda — przejęłyby ich wygląd tam,
|
|
28
|
+
* gdzie aplikacja wyświetla treść.
|
|
29
|
+
*/
|
|
30
|
+
const EDITOR_CLASS = /^kpt-(rt-[a-z0-9-]+|mention)$/;
|
|
31
|
+
// Schematy dopuszczone w `href` — ta sama allowlista dla narzędzia „link” i dla wklejonego HTML.
|
|
27
32
|
const SAFE_URL_SCHEME = /^(https?:|mailto:|tel:)/i;
|
|
28
33
|
const HAS_URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
34
|
+
/** Znaki sterujące, które przeglądarka pomija przy parsowaniu URL-a (`jav\tascript:`). */
|
|
35
|
+
// eslint-disable-next-line no-control-regex -- celowo: właśnie te znaki trzeba wyciąć przed testem schematu
|
|
36
|
+
const URL_CONTROL = /[\x00-\x1f]/g;
|
|
37
|
+
/** Czy adres z treści może zostać: względny (bez schematu) albo ze schematu z allowlisty. */
|
|
38
|
+
function isSafeHref(value) {
|
|
39
|
+
const compact = value.replace(URL_CONTROL, '').trim();
|
|
40
|
+
return !HAS_URL_SCHEME.test(compact) || SAFE_URL_SCHEME.test(compact);
|
|
41
|
+
}
|
|
29
42
|
/** Deklaracje `style` z allowlisty; `null`, gdy nie zostaje nic. */
|
|
30
43
|
function sanitizeStyleAttr(value) {
|
|
31
44
|
const kept = value
|
|
@@ -34,56 +47,91 @@ function sanitizeStyleAttr(value) {
|
|
|
34
47
|
.filter((decl) => ALLOWED_STYLES.has(decl.split(':')[0]?.trim().toLowerCase() ?? ''));
|
|
35
48
|
return kept.length ? kept.join('; ') : null;
|
|
36
49
|
}
|
|
37
|
-
function sanitizeAttribute(el, attr) {
|
|
50
|
+
function sanitizeAttribute(el, attr, report) {
|
|
38
51
|
const name = attr.name.toLowerCase();
|
|
39
|
-
|
|
52
|
+
const remove = () => {
|
|
40
53
|
el.removeAttribute(attr.name);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (/^\s*javascript:/i.test(attr.value.replace(/[\x00-\x1f]/g, '')))
|
|
47
|
-
el.removeAttribute(attr.name);
|
|
54
|
+
report.changed = true;
|
|
55
|
+
};
|
|
56
|
+
if (name === 'href') {
|
|
57
|
+
if (!isSafeHref(attr.value))
|
|
58
|
+
remove();
|
|
48
59
|
}
|
|
49
60
|
else if (name === 'style') {
|
|
50
61
|
const kept = sanitizeStyleAttr(attr.value);
|
|
51
|
-
|
|
62
|
+
const declared = attr.value.split(';').filter((decl) => decl.trim()).length;
|
|
63
|
+
if (kept === null)
|
|
64
|
+
remove();
|
|
65
|
+
else if (kept.split('; ').length !== declared) {
|
|
66
|
+
// Zmiana tylko wtedy, gdy wypadła deklaracja — samo przeformatowanie zapisu się nie liczy.
|
|
52
67
|
el.setAttribute('style', kept);
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
report.changed = true;
|
|
69
|
+
}
|
|
55
70
|
}
|
|
56
|
-
else if (
|
|
57
|
-
|
|
71
|
+
else if (name === 'class') {
|
|
72
|
+
const tokens = attr.value.split(/\s+/).filter(Boolean);
|
|
73
|
+
const kept = tokens.filter((token) => EDITOR_CLASS.test(token));
|
|
74
|
+
if (kept.length === 0)
|
|
75
|
+
remove();
|
|
76
|
+
else if (kept.length !== tokens.length) {
|
|
77
|
+
el.setAttribute('class', kept.join(' '));
|
|
78
|
+
report.changed = true;
|
|
79
|
+
}
|
|
58
80
|
}
|
|
81
|
+
else if (!ALLOWED_ATTRS.has(name)) {
|
|
82
|
+
// Także `on*` i `src` — żaden dopuszczony znacznik nie ładuje zasobów.
|
|
83
|
+
remove();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Link otwierany w nowej karcie dostaje `noopener noreferrer` — bez tego karta sięga do `opener`. */
|
|
87
|
+
function secureTarget(el, report) {
|
|
88
|
+
if (el.tagName !== 'A' || !el.hasAttribute('target'))
|
|
89
|
+
return;
|
|
90
|
+
const rel = new Set((el.getAttribute('rel') ?? '').split(/\s+/).filter(Boolean));
|
|
91
|
+
if (rel.has('noopener') && rel.has('noreferrer'))
|
|
92
|
+
return;
|
|
93
|
+
rel.add('noopener');
|
|
94
|
+
rel.add('noreferrer');
|
|
95
|
+
el.setAttribute('rel', [...rel].join(' '));
|
|
96
|
+
report.changed = true;
|
|
59
97
|
}
|
|
60
98
|
/** Tag spoza allowlisty rozwijamy do dzieci; atrybuty czyścimy po kolei. */
|
|
61
|
-
function sanitizeElement(el) {
|
|
99
|
+
function sanitizeElement(el, report) {
|
|
62
100
|
if (!ALLOWED_TAGS.has(el.tagName)) {
|
|
63
101
|
el.replaceWith(...Array.from(el.childNodes));
|
|
102
|
+
report.changed = true;
|
|
64
103
|
return;
|
|
65
104
|
}
|
|
66
105
|
for (const attr of Array.from(el.attributes))
|
|
67
|
-
sanitizeAttribute(el, attr);
|
|
106
|
+
sanitizeAttribute(el, attr, report);
|
|
107
|
+
secureTarget(el, report);
|
|
68
108
|
}
|
|
69
109
|
/**
|
|
70
|
-
* Czyści HTML
|
|
71
|
-
*
|
|
110
|
+
* Czyści HTML i mówi, czy coś usunął. `changed: false` znaczy, że treść była czysta — wtedy
|
|
111
|
+
* wołający nie musi nadpisywać wartości modelu (serializacja DOM potrafi zmienić zapis, np.
|
|
112
|
+
* cudzysłowy, bez zmiany treści, a taka różnica nie może brudzić formularza).
|
|
72
113
|
*/
|
|
73
|
-
export function
|
|
114
|
+
export function sanitizeHtmlReport(html) {
|
|
74
115
|
const template = document.createElement('template');
|
|
75
116
|
template.innerHTML = html;
|
|
117
|
+
const report = { changed: false };
|
|
76
118
|
for (const el of Array.from(template.content.querySelectorAll('*')))
|
|
77
|
-
sanitizeElement(el);
|
|
78
|
-
return template.innerHTML;
|
|
119
|
+
sanitizeElement(el, report);
|
|
120
|
+
return { html: template.innerHTML, changed: report.changed };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Czyści HTML wchodzący do edytora. Dotyczy **każdego** zapisu do DOM edytora, nie tylko
|
|
124
|
+
* wklejania: `value` z aplikacji też bywa niezaufane (baza, CMS, inny użytkownik).
|
|
125
|
+
*/
|
|
126
|
+
export function sanitizeHtml(html) {
|
|
127
|
+
return sanitizeHtmlReport(html).html;
|
|
79
128
|
}
|
|
80
129
|
/**
|
|
81
130
|
* Waliduje URL z narzędzia „link". Brak schematu → dopisujemy `https://`; `#` i `/` przechodzą
|
|
82
131
|
* bez zmian; schemat spoza allowlisty → `null`, czyli link nie zostaje wstawiony.
|
|
83
132
|
*/
|
|
84
133
|
export function sanitizeUrl(url) {
|
|
85
|
-
|
|
86
|
-
const trimmed = url.trim().replace(/[\x00-\x1f]/g, '');
|
|
134
|
+
const trimmed = url.trim().replace(URL_CONTROL, '');
|
|
87
135
|
if (!trimmed)
|
|
88
136
|
return null;
|
|
89
137
|
if (trimmed.startsWith('#') || trimmed.startsWith('/'))
|
package/dist/roadmap/llms.txt
CHANGED
|
@@ -69,7 +69,7 @@ frame without arrows beats arrows in the wrong place.
|
|
|
69
69
|
- Entries with an invalid date or a duplicate `id` are skipped, with a warning in a development build.
|
|
70
70
|
|
|
71
71
|
## i18n
|
|
72
|
-
The `roadmap.*` keys: `empty`, `week`, `today`, `milestone`, `expandGroup`, `collapseGroup
|
|
72
|
+
The `roadmap.*` keys: `empty`, `week`, `today`, `milestone`, `expandGroup`, `collapseGroup`, `progress` (`{percent}`).
|
|
73
73
|
Month and weekday names come from `Intl` per `config.locale` or the locale from `KptI18nProvider`.
|
|
74
74
|
|
|
75
75
|
## Tokens
|
|
@@ -78,5 +78,11 @@ bars per variant `--kpt-color-{primary,success,danger,warning,info,muted}`.
|
|
|
78
78
|
Sizes: `--kpt-roadmap-label-w`, `--kpt-roadmap-cell-w` (both can be overridden from outside).
|
|
79
79
|
|
|
80
80
|
## Accessibility
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
Bars and milestones are `role="button"` with an accessible name built from the label, sublabel, date range
|
|
82
|
+
and progress (`Design, 3–14 Mar 2026, 40% complete`) — the end is exclusive, so the last occupied day is
|
|
83
|
+
announced. The whole bar grid is one Tab stop (roving tabindex): Left/Right move within a row, Up/Down go to
|
|
84
|
+
the bar with the nearest start in the neighbouring row, Home/End jump to the row's edge, Ctrl+Home/End to the
|
|
85
|
+
first/last bar; Enter and Space act like a click. With `virtual` on, moving to a row outside the window
|
|
86
|
+
scrolls it in first. Disabled items get `aria-disabled`. A group toggle is named by the group label and
|
|
87
|
+
carries `aria-expanded`; "Expand/Collapse group" is only its tooltip. The background grid and the dependency
|
|
88
|
+
layer are `aria-hidden`.
|
package/dist/roadmap/roadmap.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { buildRows, buildTimeline, linkGeometry, resolveLinks, resolveRange, sliceRows, } from '@konce-pt/roadmap';
|
|
4
|
+
import { buildRows, buildTimeline, linkGeometry, resolveLinks, resolveRange, roadmapItemDates, roadmapNavTarget, sliceRows, } from '@konce-pt/roadmap';
|
|
4
5
|
import { cn } from "../internal/cn.js";
|
|
5
6
|
import { isDevBuild } from "../internal/dev.js";
|
|
6
7
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
@@ -44,9 +45,9 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
44
45
|
* efekt zrobiłby to samo o jeden render za późno, czyli po mignięciu rozwiniętej grupy.
|
|
45
46
|
*/
|
|
46
47
|
const [fromConfig, setFromConfig] = useState(() => declaredCollapsed(config.groups));
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
48
|
+
const [prevGroups, setPrevGroups] = useState(config.groups);
|
|
49
|
+
if (prevGroups !== config.groups) {
|
|
50
|
+
setPrevGroups(config.groups);
|
|
50
51
|
setFromConfig(declaredCollapsed(config.groups));
|
|
51
52
|
}
|
|
52
53
|
/** Suma obu źródeł — stan faktycznie użyty do budowy wierszy. */
|
|
@@ -68,6 +69,28 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
68
69
|
weekPrefix: config.timeline?.weekPrefix ?? t('roadmap.week'),
|
|
69
70
|
}), [range, zoom, config, locale, t]);
|
|
70
71
|
const layout = useMemo(() => buildRows(config.items, config.groups, collapsedIds, config.rows), [config.items, config.groups, config.rows, collapsedIds]);
|
|
72
|
+
const itemLocale = config.locale ?? config.timeline?.locale ?? locale;
|
|
73
|
+
/**
|
|
74
|
+
* Paski widoczne na osi, wiersz po wierszu — mapa dla nawigacji strzałkami. Liczona ze wszystkich
|
|
75
|
+
* wierszy, nie z okna wirtualizacji: strzałka w dół z ostatniego wyrenderowanego wiersza ma dokąd iść.
|
|
76
|
+
*/
|
|
77
|
+
const navGrid = useMemo(() => layout.rows.map((row) => row.kind === 'group'
|
|
78
|
+
? []
|
|
79
|
+
: row.items
|
|
80
|
+
.filter((item) => timeline.span(item.start, item.end).visible)
|
|
81
|
+
.map((item) => ({ id: item.id, start: item.start }))
|
|
82
|
+
.sort((a, b) => a.start - b.start)), [layout, timeline]);
|
|
83
|
+
/** Pasek, na którym ostatnio był fokus. */
|
|
84
|
+
const [activeId, setActiveId] = useState(null);
|
|
85
|
+
/**
|
|
86
|
+
* Jedyny przystanek Tab w siatce pasków (roving tabindex) — ostatnio aktywny pasek, a gdy zniknął
|
|
87
|
+
* (zwinięta grupa, inne dane), pierwszy. Bez tego roadmapa ze stu zadaniami to sto przystanków Tab.
|
|
88
|
+
*/
|
|
89
|
+
const tabStopId = useMemo(() => {
|
|
90
|
+
if (activeId && navGrid.some((row) => row.some((item) => item.id === activeId)))
|
|
91
|
+
return activeId;
|
|
92
|
+
return navGrid.find((row) => row.length > 0)?.[0].id ?? null;
|
|
93
|
+
}, [navGrid, activeId]);
|
|
71
94
|
/** Wiersze faktycznie renderowane — całość albo okno wirtualizacji. */
|
|
72
95
|
const visibleRows = useMemo(() => {
|
|
73
96
|
if (!virtual || viewportHeight <= 0)
|
|
@@ -110,6 +133,21 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
110
133
|
const total = row.laneCount * barHeight + (row.laneCount - 1) * LANE_GAP;
|
|
111
134
|
return Math.max(0, (row.height - total) / 2) + lane * laneStep;
|
|
112
135
|
}, [barHeight, laneStep]);
|
|
136
|
+
/** Nazwa dostępna paska: „Etykieta, 3–14 mar 2026, ukończono 40%". */
|
|
137
|
+
const itemAriaLabel = useCallback((item) => {
|
|
138
|
+
const source = item.source;
|
|
139
|
+
const dates = roadmapItemDates(item.start, item.end, itemLocale, {
|
|
140
|
+
isMilestone: item.isMilestone,
|
|
141
|
+
withTime: zoom === 'hour',
|
|
142
|
+
});
|
|
143
|
+
const parts = [item.isMilestone ? `${t('roadmap.milestone')}: ${source.label}` : source.label, dates];
|
|
144
|
+
if (source.sublabel)
|
|
145
|
+
parts.splice(1, 0, source.sublabel);
|
|
146
|
+
if (source.progress != null && !item.isMilestone) {
|
|
147
|
+
parts.push(t('roadmap.progress', { percent: Math.round(source.progress * 100) }));
|
|
148
|
+
}
|
|
149
|
+
return parts.join(', ');
|
|
150
|
+
}, [itemLocale, zoom, t]);
|
|
113
151
|
/** Paski danego wiersza gotowe do renderu. */
|
|
114
152
|
const bars = useCallback((row) => {
|
|
115
153
|
const result = [];
|
|
@@ -132,10 +170,11 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
132
170
|
isMilestone: item.isMilestone,
|
|
133
171
|
clippedStart: geom.clippedStart,
|
|
134
172
|
clippedEnd: geom.clippedEnd,
|
|
173
|
+
ariaLabel: itemAriaLabel(item),
|
|
135
174
|
});
|
|
136
175
|
}
|
|
137
176
|
return result;
|
|
138
|
-
}, [timeline, barTop, barHeight]);
|
|
177
|
+
}, [timeline, barTop, barHeight, itemAriaLabel]);
|
|
139
178
|
/** Pasek zbiorczy wiersza grupy. */
|
|
140
179
|
const summary = useCallback((row) => {
|
|
141
180
|
const span = row.groupState?.span;
|
|
@@ -208,11 +247,49 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
208
247
|
return;
|
|
209
248
|
onItemClick?.(item.source);
|
|
210
249
|
};
|
|
250
|
+
/** Przy wirtualizacji przewija tak, żeby wiersz z paskiem wszedł do okna i się wyrenderował. */
|
|
251
|
+
const revealRow = (scroll, id) => {
|
|
252
|
+
if (!virtual)
|
|
253
|
+
return;
|
|
254
|
+
const row = layout.rows.find((candidate) => candidate.items.some((item) => item.id === id));
|
|
255
|
+
if (!row)
|
|
256
|
+
return;
|
|
257
|
+
const header = scroll.querySelector('.kpt-roadmap__head')?.offsetHeight ?? 0;
|
|
258
|
+
const viewport = scroll.clientHeight - header;
|
|
259
|
+
let top = scroll.scrollTop;
|
|
260
|
+
if (row.top < top)
|
|
261
|
+
top = row.top;
|
|
262
|
+
else if (row.top + row.height > top + viewport)
|
|
263
|
+
top = row.top + row.height - viewport;
|
|
264
|
+
if (top === scroll.scrollTop)
|
|
265
|
+
return;
|
|
266
|
+
scroll.scrollTop = top;
|
|
267
|
+
setScrollTop(top);
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Klawiatura na pasku: Enter/Spacja działają jak kliknięcie, strzałki przenoszą fokus po siatce
|
|
271
|
+
* (w poziomie po wierszu, w pionie między wierszami), Home/End na skraj wiersza, z Ctrl — roadmapy.
|
|
272
|
+
*/
|
|
211
273
|
const handleItemKeydown = (event, item) => {
|
|
212
|
-
if (event.key
|
|
274
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
handleItemClick(item);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const target = roadmapNavTarget(navGrid, item.id, event.key, event.ctrlKey || event.metaKey);
|
|
280
|
+
if (!target)
|
|
213
281
|
return;
|
|
214
282
|
event.preventDefault();
|
|
215
|
-
|
|
283
|
+
setActiveId(target);
|
|
284
|
+
const scroll = event.currentTarget.closest('.kpt-roadmap__scroll');
|
|
285
|
+
if (!scroll)
|
|
286
|
+
return;
|
|
287
|
+
revealRow(scroll, target);
|
|
288
|
+
// Pasek z innego wiersza może jeszcze nie istnieć w DOM (wirtualizacja). Aktualizacja ze zdarzenia
|
|
289
|
+
// klawiatury jest zatwierdzana synchronicznie, więc w następnej klatce pasek już jest.
|
|
290
|
+
requestAnimationFrame(() => {
|
|
291
|
+
scroll.querySelector(`[data-kpt-item="${CSS.escape(target)}"]`)?.focus();
|
|
292
|
+
});
|
|
216
293
|
};
|
|
217
294
|
/**
|
|
218
295
|
* Treść dymka dla kolumny etykiet. Pusta tam, gdzie dymek nie miałby czego
|
|
@@ -242,25 +319,25 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
|
|
|
242
319
|
const group = row.group;
|
|
243
320
|
const first = row.items[0];
|
|
244
321
|
const sum = row.kind === 'group' ? summary(row) : null;
|
|
245
|
-
return (_jsxs("div", { className: cn('kpt-roadmap__row', row.kind === 'group' && 'kpt-roadmap__row--group'), style: { insetBlockStart: `${row.top}px`, blockSize: `${row.height}px` }, children: [_jsx(KptTooltip, { text: labelTooltip(row), onlyWhenTruncated: true, children: _jsx("div", { className: cn('kpt-roadmap__label', row.kind === 'group' && 'kpt-roadmap__label--group'), style: { paddingInlineStart: `${0.75 + row.depth * 1}rem` }, children: group ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "kpt-roadmap__toggle", "aria-expanded": !isCollapsed(group), "aria-label":
|
|
322
|
+
return (_jsxs("div", { className: cn('kpt-roadmap__row', row.kind === 'group' && 'kpt-roadmap__row--group'), style: { insetBlockStart: `${row.top}px`, blockSize: `${row.height}px` }, children: [_jsx(KptTooltip, { text: labelTooltip(row), onlyWhenTruncated: true, children: _jsx("div", { className: cn('kpt-roadmap__label', row.kind === 'group' && 'kpt-roadmap__label--group'), style: { paddingInlineStart: `${0.75 + row.depth * 1}rem` }, children: group ? (_jsxs(_Fragment, { children: [_jsx(KptTooltip, { text: isCollapsed(group) ? t('roadmap.expandGroup') : t('roadmap.collapseGroup'), children: _jsx("button", { type: "button", className: "kpt-roadmap__toggle", "aria-expanded": !isCollapsed(group), "aria-label": group.label, onClick: () => toggleGroup(group), children: _jsx("span", { className: cn('kpt-roadmap__toggle-icon', !isCollapsed(group) && 'kpt-roadmap__toggle-icon--open') }) }) }), _jsx(KptTooltip, { text: group.sublabel ? `${group.label} — ${group.sublabel}` : group.label, onlyWhenTruncated: true, children: _jsxs("span", { className: "kpt-roadmap__label-stack", children: [_jsx("span", { className: "kpt-roadmap__label-text kpt-roadmap__label-text--group", children: group.label }), group.sublabel && _jsx("span", { className: "kpt-roadmap__label-sub", children: group.sublabel })] }) }), row.groupState?.itemCount ? (_jsx("span", { className: "kpt-roadmap__label-count", children: row.groupState.itemCount })) : null] })) : (first && (_jsxs(_Fragment, { children: [_jsx("span", { className: "kpt-roadmap__label-text", children: first.source.label }), row.items.length === 1 && first.source.sublabel && (_jsx("span", { className: "kpt-roadmap__label-sub", children: first.source.sublabel }))] }))) }) }), _jsx("div", { className: "kpt-roadmap__lane", children: row.kind === 'group'
|
|
246
323
|
? sum && (_jsx("div", { className: "kpt-roadmap__summary", "data-variant": sum.variant, style: {
|
|
247
324
|
insetInlineStart: `${sum.left}%`,
|
|
248
325
|
inlineSize: `${sum.width}%`,
|
|
249
326
|
backgroundColor: sum.color,
|
|
250
327
|
} }))
|
|
251
|
-
: bars(row).map((bar) => bar.isMilestone ? (_jsx(KptTooltip, { text: bar.title, children: _jsx("div", { className: "kpt-roadmap__milestone", "data-variant": bar.variant,
|
|
328
|
+
: bars(row).map((bar) => bar.isMilestone ? (_jsx(KptTooltip, { text: bar.title, children: _jsx("div", { className: "kpt-roadmap__milestone", "data-variant": bar.variant, style: {
|
|
252
329
|
insetInlineStart: `${bar.left}%`,
|
|
253
330
|
insetBlockStart: `${bar.top}px`,
|
|
254
331
|
inlineSize: `${bar.height}px`,
|
|
255
332
|
blockSize: `${bar.height}px`,
|
|
256
333
|
backgroundColor: bar.color,
|
|
257
|
-
}, tabIndex: 0, onClick: () => handleItemClick(bar.item), onKeyDown: (event) => handleItemKeydown(event, bar.item) }) }, bar.item.id)) : (_jsx(KptTooltip, { text: bar.title, onlyWhenTruncated: true, children: _jsxs("div", { className: cn('kpt-roadmap__bar', bar.clippedStart && 'kpt-roadmap__bar--clipped-start', bar.clippedEnd && 'kpt-roadmap__bar--clipped-end', bar.item.source.disabled && 'kpt-roadmap__bar--disabled'), "data-variant": bar.variant, style: {
|
|
334
|
+
}, role: "button", "aria-label": bar.ariaLabel, "aria-disabled": bar.item.source.disabled || undefined, "data-kpt-item": bar.item.id, tabIndex: bar.item.id === tabStopId ? 0 : -1, onClick: () => handleItemClick(bar.item), onFocus: () => setActiveId(bar.item.id), onKeyDown: (event) => handleItemKeydown(event, bar.item) }) }, bar.item.id)) : (_jsx(KptTooltip, { text: bar.title, onlyWhenTruncated: true, children: _jsxs("div", { className: cn('kpt-roadmap__bar', bar.clippedStart && 'kpt-roadmap__bar--clipped-start', bar.clippedEnd && 'kpt-roadmap__bar--clipped-end', bar.item.source.disabled && 'kpt-roadmap__bar--disabled'), "data-variant": bar.variant, style: {
|
|
258
335
|
insetInlineStart: `${bar.left}%`,
|
|
259
336
|
inlineSize: `${bar.width}%`,
|
|
260
337
|
insetBlockStart: `${bar.top}px`,
|
|
261
338
|
blockSize: `${bar.height}px`,
|
|
262
339
|
backgroundColor: bar.color,
|
|
263
|
-
}, tabIndex: 0, onClick: () => handleItemClick(bar.item), onKeyDown: (event) => handleItemKeydown(event, bar.item), children: [bar.progress !== null && (_jsx("span", { className: "kpt-roadmap__bar-progress", style: { inlineSize: `${bar.progress * 100}%` } })), _jsx("span", { className: "kpt-roadmap__bar-label", children: bar.label })] }) }, bar.item.id))) })] }, row.key));
|
|
340
|
+
}, role: "button", "aria-label": bar.ariaLabel, "aria-disabled": bar.item.source.disabled || undefined, "data-kpt-item": bar.item.id, tabIndex: bar.item.id === tabStopId ? 0 : -1, onClick: () => handleItemClick(bar.item), onFocus: () => setActiveId(bar.item.id), onKeyDown: (event) => handleItemKeydown(event, bar.item), children: [bar.progress !== null && (_jsx("span", { className: "kpt-roadmap__bar-progress", style: { inlineSize: `${bar.progress * 100}%` } })), _jsx("span", { className: "kpt-roadmap__bar-label", children: bar.label })] }) }, bar.item.id))) })] }, row.key));
|
|
264
341
|
})] }))] }) }));
|
|
265
342
|
}
|
|
266
343
|
/** Identyfikatory grup zwiniętych deklaratywnie w danych. */
|
package/dist/select/llms.txt
CHANGED
|
@@ -68,3 +68,6 @@ The trigger is `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded`
|
|
|
68
68
|
and `aria-activedescendant`; the list is `role="listbox"` with `aria-multiselectable`, and each
|
|
69
69
|
option is `role="option"` with `aria-selected` and `aria-disabled`. Chip remove buttons sit outside
|
|
70
70
|
the Tab order and are named with the option label.
|
|
71
|
+
|
|
72
|
+
## Read-only and required
|
|
73
|
+
`readOnly` shows the choice, but the panel does not open and the chips have no remove button (`aria-readonly`). The filter ignores case and diacritics ("lodz" finds "Łódź"), as in `listbox`. "No results" sits next to the list, not inside it (a listbox may contain only options).
|
package/dist/select/select.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
13
13
|
/** Oznacza pole jako wymagane atrybutem `aria-required`. */
|
|
14
14
|
required?: boolean;
|
|
15
15
|
disabled?: boolean;
|
|
16
|
+
/** Tylko do odczytu — wybór widać, panel się nie otwiera (`aria-readonly`). */
|
|
17
|
+
readOnly?: boolean;
|
|
16
18
|
invalid?: boolean;
|
|
17
19
|
touched?: boolean;
|
|
18
20
|
/** Wartość: pojedyncza (T) albo tablica (T[]) w trybie `multiple`. */
|
|
@@ -45,4 +47,4 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
|
|
|
45
47
|
* <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz…" />
|
|
46
48
|
* <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
|
|
47
49
|
*/
|
|
48
|
-
export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
|
|
50
|
+
export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, readOnly, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
|
package/dist/select/select.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { foldMentionText } from '@konce-pt/mention';
|
|
3
5
|
import { cn } from "../internal/cn.js";
|
|
6
|
+
import { optionKey } from "../internal/option-key.js";
|
|
4
7
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
8
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
9
|
import { useKptId } from "../internal/use-id.js";
|
|
@@ -8,6 +11,11 @@ import { KptTypeahead, OPTION_NAV_KEYS, isTypeaheadKey, nextOptionIndex, typeahe
|
|
|
8
11
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
9
12
|
import { joinIds, useKptFieldA11y, useKptFormFieldFilled } from "../form-field/form-field-context.js";
|
|
10
13
|
import { KptIcon } from "../icon/icon.js";
|
|
14
|
+
/** Filtr opcji bez wielkości liter i diakrytyków, jak w `KptListbox`: „lodz” znajduje „Łódź”. */
|
|
15
|
+
function filterSelectOptions(options, text) {
|
|
16
|
+
const q = foldMentionText(text.trim());
|
|
17
|
+
return q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
|
|
18
|
+
}
|
|
11
19
|
/**
|
|
12
20
|
* Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
|
|
13
21
|
* i wielokrotny (`multiple`, chipy).
|
|
@@ -27,11 +35,13 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
27
35
|
* <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz…" />
|
|
28
36
|
* <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
|
|
29
37
|
*/
|
|
30
|
-
export function KptSelect({ options = [], placeholder = '', multiple = false, filter = false, disabled = false, invalid = false, touched, value, defaultValue = null, onValueChange, open, defaultOpen = false, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }) {
|
|
38
|
+
export function KptSelect({ options = [], placeholder = '', multiple = false, filter = false, disabled = false, readOnly = false, invalid = false, touched, value, defaultValue = null, onValueChange, open, defaultOpen = false, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }) {
|
|
31
39
|
/** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
32
40
|
const field = useKptFieldA11y(id, 'kpt-select-trigger');
|
|
33
41
|
const { t } = useKptMessages({ locale, dictionary });
|
|
34
42
|
const triggerRef = useRef(null);
|
|
43
|
+
/** Kotwica nakładki w stanie (callback ref), nie `triggerRef.current` czytane w renderze. */
|
|
44
|
+
const [anchor, setAnchor] = useState(null);
|
|
35
45
|
const searchRef = useRef(null);
|
|
36
46
|
const typeahead = useRef(new KptTypeahead());
|
|
37
47
|
const uid = useKptId(undefined, 'kpt-select');
|
|
@@ -52,35 +62,41 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
52
62
|
const showInvalid = invalid && (touched ?? true);
|
|
53
63
|
const selectedValues = useMemo(() => {
|
|
54
64
|
if (multiple)
|
|
55
|
-
return Array.isArray(current) ? current : [];
|
|
65
|
+
return Array.isArray(current) ? (current) : [];
|
|
56
66
|
return current == null ? [] : [current];
|
|
57
67
|
}, [current, multiple]);
|
|
58
68
|
const selectedOptions = useMemo(() => options.filter((option) => selectedValues.includes(option.value)), [options, selectedValues]);
|
|
59
|
-
const
|
|
60
|
-
const q = text.trim().toLowerCase();
|
|
61
|
-
return q ? options.filter((option) => option.label.toLowerCase().includes(q)) : options;
|
|
62
|
-
};
|
|
63
|
-
const filteredOptions = useMemo(() => filterOptions(filterText), [options, filterText]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
+
const filteredOptions = useMemo(() => filterSelectOptions(options, filterText), [options, filterText]);
|
|
64
70
|
const disabledMap = filteredOptions.map((option) => !!option.disabled);
|
|
65
71
|
const filled = selectedOptions.length > 0;
|
|
66
72
|
useKptFormFieldFilled(filled);
|
|
67
73
|
const isSelected = (option) => selectedValues.includes(option.value);
|
|
74
|
+
/** Skrajna opcja, na której ma się otworzyć lista (`Home`/`End`/`↑` na zamkniętym polu). */
|
|
75
|
+
const [openEdge, setOpenEdge] = useState(null);
|
|
68
76
|
// Panel może zostać otwarty także z zewnątrz (`open`), nie tylko z pola — filtr i aktywną
|
|
69
|
-
// pozycję ustawiamy więc przy każdym
|
|
70
|
-
//
|
|
71
|
-
//
|
|
77
|
+
// pozycję ustawiamy więc przy każdym przejściu w stan otwarty, wzorcem „poprzednia wartość
|
|
78
|
+
// w stanie” (w tym samym renderze, bez efektu). Lista otwiera się na wybranej opcji; zmiana
|
|
79
|
+
// wyboru w trybie `multiple` przy otwartym panelu nie przesuwa aktywnej opcji.
|
|
80
|
+
const [wasOpen, setWasOpen] = useState(isOpen);
|
|
81
|
+
if (wasOpen !== isOpen) {
|
|
82
|
+
setWasOpen(isOpen);
|
|
83
|
+
if (isOpen) {
|
|
84
|
+
setFilterText('');
|
|
85
|
+
const disabledAll = options.map((o) => !!o.disabled);
|
|
86
|
+
const selected = options.findIndex((option) => selectedValues.includes(option.value));
|
|
87
|
+
const firstEnabled = nextOptionIndex(disabledAll, -1, 'ArrowDown') ?? -1;
|
|
88
|
+
setActiveIndex(openEdge ? (nextOptionIndex(disabledAll, -1, openEdge) ?? -1) : selected >= 0 ? selected : firstEnabled);
|
|
89
|
+
setOpenEdge(null);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Z `filter` fokus idzie do wyszukiwarki — klatkę później, bo `KptOverlay` mierzy panel
|
|
93
|
+
// w `visibility: hidden`.
|
|
72
94
|
useEffect(() => {
|
|
73
|
-
if (!isOpen)
|
|
74
|
-
return;
|
|
75
|
-
setFilterText('');
|
|
76
|
-
const selected = options.findIndex((option) => selectedValues.includes(option.value));
|
|
77
|
-
const firstEnabled = nextOptionIndex(options.map((o) => !!o.disabled), -1, 'ArrowDown') ?? -1;
|
|
78
|
-
setActiveIndex(selected >= 0 ? selected : firstEnabled);
|
|
79
|
-
if (!filter)
|
|
95
|
+
if (!isOpen || !filter)
|
|
80
96
|
return;
|
|
81
97
|
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
82
98
|
return () => cancelAnimationFrame(frame);
|
|
83
|
-
}, [isOpen]);
|
|
99
|
+
}, [isOpen, filter]);
|
|
84
100
|
// Aktywna opcja zawsze w polu widzenia listy.
|
|
85
101
|
useEffect(() => {
|
|
86
102
|
if (isOpen && activeIndex >= 0)
|
|
@@ -116,21 +132,16 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
116
132
|
};
|
|
117
133
|
/** Jeden obsługiwacz dla pola i dla wyszukiwarki w panelu — różnią się tylko `Spacją` i `Tab`. */
|
|
118
134
|
const onKeyDown = (event, source) => {
|
|
119
|
-
if (disabled)
|
|
135
|
+
if (disabled || readOnly)
|
|
120
136
|
return;
|
|
121
137
|
const key = event.key;
|
|
122
138
|
if (!isOpen) {
|
|
123
139
|
if (['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End'].includes(key)) {
|
|
124
140
|
event.preventDefault();
|
|
141
|
+
// Skrajne klawisze otwierają listę na pierwszej albo ostatniej opcji zamiast na wybranej.
|
|
142
|
+
if (key === 'ArrowUp' || key === 'End' || key === 'Home')
|
|
143
|
+
setOpenEdge(key === 'Home' ? 'Home' : 'End');
|
|
125
144
|
setOpen(true);
|
|
126
|
-
if (key === 'ArrowUp' || key === 'End' || key === 'Home') {
|
|
127
|
-
// Efekt otwarcia ustawia wybraną opcję; skrajne klawisze nadpisują to klatkę później.
|
|
128
|
-
const target = nextOptionIndex(disabledMap, -1, key === 'Home' ? 'Home' : 'End');
|
|
129
|
-
requestAnimationFrame(() => {
|
|
130
|
-
if (target !== null)
|
|
131
|
-
setActiveIndex(target);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
145
|
}
|
|
135
146
|
else if ((key === 'Backspace' || key === 'Delete') && multiple && selectedValues.length) {
|
|
136
147
|
event.preventDefault();
|
|
@@ -172,18 +183,21 @@ export function KptSelect({ options = [], placeholder = '', multiple = false, fi
|
|
|
172
183
|
jumpTo(key);
|
|
173
184
|
}
|
|
174
185
|
};
|
|
175
|
-
return (_jsxs("span", { className: "kpt-select-host", children: [_jsxs("div", { ref:
|
|
186
|
+
return (_jsxs("span", { className: "kpt-select-host", children: [_jsxs("div", { ref: (el) => {
|
|
187
|
+
triggerRef.current = el;
|
|
188
|
+
setAnchor(el);
|
|
189
|
+
}, className: cn('kpt-select', isOpen && 'kpt-select--open', disabled && 'kpt-select--disabled', readOnly && 'kpt-select--readonly', showInvalid && 'kpt-select--invalid', className), role: "combobox", id: field.id, tabIndex: 0, "aria-haspopup": "listbox", "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), "aria-required": required || undefined, "aria-readonly": readOnly || undefined, "aria-expanded": isOpen, "aria-controls": isOpen ? listId : undefined, "aria-activedescendant": isOpen && !filter && activeIndex >= 0 ? optionId(activeIndex) : undefined, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, onClick: () => !disabled && !readOnly && setOpen(!isOpen), onKeyDown: (event) => onKeyDown(event, 'trigger'), onBlur: (event) => {
|
|
176
190
|
onBlur?.(event);
|
|
177
191
|
// Przy otwartym panelu fokus idzie do pola szukania — „dotknięcie” wyśle dopiero zamknięcie.
|
|
178
192
|
if (!isOpen)
|
|
179
193
|
onTouch?.();
|
|
180
|
-
}, ...rest, children: [_jsx("div", { className: "kpt-select__value", children: multiple && selectedOptions.length ? (selectedOptions.map((option) => (_jsxs("span", { className: "kpt-select__chip", children: [option.label, _jsx("button", { type: "button", className: "kpt-select__chip-remove", tabIndex: -1, "aria-label": `${t('select.removeChip')}: ${option.label}`, onClick: (event) => {
|
|
194
|
+
}, ...rest, children: [_jsx("div", { className: "kpt-select__value", children: multiple && selectedOptions.length ? (selectedOptions.map((option, index) => (_jsxs("span", { className: "kpt-select__chip", children: [option.label, readOnly ? null : (_jsx("button", { type: "button", className: "kpt-select__chip-remove", tabIndex: -1, "aria-label": `${t('select.removeChip')}: ${option.label}`, onClick: (event) => {
|
|
181
195
|
event.stopPropagation();
|
|
182
196
|
setCurrent(selectedValues.filter((v) => v !== option.value));
|
|
183
|
-
}, children: _jsx(KptIcon, { name: "x" }) })] },
|
|
197
|
+
}, children: _jsx(KptIcon, { name: "x" }) }))] }, optionKey(option.value, index))))) : !multiple && filled ? (_jsx("span", { className: "kpt-select__single", children: selectedOptions[0]?.label })) : (_jsx("span", { className: "kpt-select__placeholder", children: placeholder })) }), _jsx(KptIcon, { className: "kpt-select__chevron", name: "chevron-down" })] }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, minAnchorWidth: true, children: _jsxs("div", { className: "kpt-select__panel", children: [filter ? (_jsxs("div", { className: "kpt-select__search", children: [_jsx(KptIcon, { name: "search" }), _jsx("input", { ref: searchRef, type: "text", className: "kpt-select__search-input", role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-controls": listId, "aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined, "aria-label": t('select.search'), value: filterText, placeholder: t('select.search'), onKeyDown: (event) => onKeyDown(event, 'search'), onChange: (event) => {
|
|
184
198
|
setFilterText(event.target.value);
|
|
185
199
|
// Po zawężeniu aktywna jest pierwsza pasująca opcja — `Enter` wybiera ją od razu.
|
|
186
|
-
const narrowed =
|
|
200
|
+
const narrowed = filterSelectOptions(options, event.target.value).map((o) => !!o.disabled);
|
|
187
201
|
setActiveIndex(nextOptionIndex(narrowed, -1, 'ArrowDown') ?? -1);
|
|
188
|
-
} })] })) : null, _jsx("ul", { className: "kpt-select__list", role: "listbox", id: listId, "aria-multiselectable": multiple, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, children: filteredOptions.
|
|
202
|
+
} })] })) : null, _jsx("ul", { className: "kpt-select__list", role: "listbox", id: listId, "aria-multiselectable": multiple, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, children: filteredOptions.map((option, index) => (_jsxs("li", { id: optionId(index), className: cn('kpt-select__option', isSelected(option) && 'kpt-select__option--selected', index === activeIndex && 'kpt-select__option--active', option.disabled && 'kpt-select__option--disabled'), role: "option", "aria-selected": isSelected(option), "aria-disabled": option.disabled || undefined, onMouseDown: (event) => event.preventDefault(), onClick: () => pick(option), onMouseEnter: () => setActiveIndex(index), children: [multiple ? (_jsx("span", { className: cn('kpt-select__check', isSelected(option) && 'is-on'), children: _jsx(KptIcon, { name: "check" }) })) : null, _jsx("span", { className: "kpt-select__option-label", children: option.label }), !multiple && isSelected(option) ? (_jsx(KptIcon, { className: "kpt-select__option-tick", name: "check" })) : null] }, optionKey(option.value, index)))) }), filteredOptions.length === 0 ? (_jsx("div", { className: "kpt-select__empty", role: "status", children: t('select.noResults') })) : null] }) })] }));
|
|
189
203
|
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -8,3 +8,24 @@ export interface KptOption<T = unknown> {
|
|
|
8
8
|
}
|
|
9
9
|
/** Wariant komunikatu — wspólny dla alertu i toastów. */
|
|
10
10
|
export type KptAlertVariant = 'info' | 'success' | 'warning' | 'danger';
|
|
11
|
+
/**
|
|
12
|
+
* Klik w odnośnik komponentu nawigacji (`kpt-breadcrumb`, `kpt-menubar`, `kpt-megamenu`). Linki
|
|
13
|
+
* są zwykłym `<a href>`, więc bez routera klik przeładowuje stronę. Aplikacja z routerem łapie to
|
|
14
|
+
* zdarzenie, woła `event.preventDefault()` i nawiguje sama — biblioteka nie zależy od żadnego routera.
|
|
15
|
+
* Zdarzenie leci tylko dla zwykłego kliku: Ctrl/⌘/Shift/środkowy przycisk i `target="_blank"`
|
|
16
|
+
* zostają natywne (nowa karta, nowe okno).
|
|
17
|
+
*/
|
|
18
|
+
export interface KptNavigateEvent<T = unknown> {
|
|
19
|
+
/** Pozycja, w którą kliknięto. */
|
|
20
|
+
item: T;
|
|
21
|
+
/** Adres z `href`. */
|
|
22
|
+
url: string;
|
|
23
|
+
/** Oryginalne zdarzenie — `preventDefault()` zatrzymuje przeładowanie strony. */
|
|
24
|
+
event: MouseEvent;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Czy klik to zwykła nawigacja w tej samej karcie: lewy przycisk, bez modyfikatorów, a odnośnik
|
|
28
|
+
* nie ma `target` innego niż `_self`. `target` podaje wołający — w React `currentTarget`
|
|
29
|
+
* zdarzenia natywnego to korzeń aplikacji, nie kliknięty odnośnik.
|
|
30
|
+
*/
|
|
31
|
+
export declare function kptIsPlainClick(event: Pick<MouseEvent, 'button' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'>, target?: string): boolean;
|
package/dist/shared/types.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Czy klik to zwykła nawigacja w tej samej karcie: lewy przycisk, bez modyfikatorów, a odnośnik
|
|
3
|
+
* nie ma `target` innego niż `_self`. `target` podaje wołający — w React `currentTarget`
|
|
4
|
+
* zdarzenia natywnego to korzeń aplikacji, nie kliknięty odnośnik.
|
|
5
|
+
*/
|
|
6
|
+
export function kptIsPlainClick(event, target = '') {
|
|
7
|
+
const modified = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
|
|
8
|
+
return event.button === 0 && !modified && (target === '' || target === '_self');
|
|
9
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
-
export
|
|
3
|
-
}
|
|
2
|
+
export type KptSidenavProps = ComponentPropsWithRef<'div'>;
|
|
4
3
|
/**
|
|
5
4
|
* Panel boczny (sidenav) — stylowany kontener nawigacji, zwykle podawany do slotu
|
|
6
5
|
* `sidenav` komponentu `<KptAppShell>`.
|
package/dist/sidenav/sidenav.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { isValidElement } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { slotChildren } from "../internal/slot-children.js";
|
|
4
5
|
/*
|
|
5
6
|
* Panel odtwarza projekcję z Angulara: treść domyślna ląduje w obszarze przewijanym,
|
|
6
7
|
* a `<KptSidenav.Footer>` jest wynoszona POZA ten obszar. Dzięki temu stopka zostaje
|
|
@@ -20,14 +21,14 @@ import { cn } from "../internal/cn.js";
|
|
|
20
21
|
export function KptSidenav({ className, children, ...rest }) {
|
|
21
22
|
const scroll = [];
|
|
22
23
|
const footers = [];
|
|
23
|
-
|
|
24
|
+
for (const child of slotChildren(children)) {
|
|
24
25
|
if (isValidElement(child) && child.type?.kptSidenavSlot === 'footer') {
|
|
25
26
|
footers.push(child);
|
|
26
27
|
}
|
|
27
28
|
else {
|
|
28
29
|
scroll.push(child);
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
|
+
}
|
|
31
32
|
return (_jsxs("div", { className: cn('kpt-sidenav', className), ...rest, children: [_jsx("div", { className: "kpt-sidenav__scroll", children: scroll }), footers] }));
|
|
32
33
|
}
|
|
33
34
|
/**
|