@konce-pt/react 0.11.1 → 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 +15 -2
- package/dist/button-group/button-group.js +31 -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 +148 -55
- 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
package/dist/map/map-search.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import { cn } from "../internal/cn.js";
|
|
4
5
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
5
6
|
import { KptIcon } from "../icon/icon.js";
|
|
@@ -25,7 +26,6 @@ export function KptMapSearch({ geocoder = null, placeholder = '', minLength = 3,
|
|
|
25
26
|
*/
|
|
26
27
|
const [term, setTerm] = useState('');
|
|
27
28
|
const [results, setResults] = useState([]);
|
|
28
|
-
const [loading, setLoading] = useState(false);
|
|
29
29
|
/** Trwa dociąganie współrzędnych wybranej podpowiedzi — inny moment niż czekanie na listę. */
|
|
30
30
|
const [resolving, setResolving] = useState(false);
|
|
31
31
|
/** Czy jakieś zapytanie już wróciło — bez tego „brak wyników" mignąłby przed pierwszym. */
|
|
@@ -37,40 +37,64 @@ export function KptMapSearch({ geocoder = null, placeholder = '', minLength = 3,
|
|
|
37
37
|
/** Przerywacz dociągania w locie — kolejny wybór i czyszczenie pola go używają. */
|
|
38
38
|
const pending = useRef(null);
|
|
39
39
|
const canSearch = !!geocoder?.search;
|
|
40
|
+
/*
|
|
41
|
+
* Zapytanie do geokodera jako wartość: nowa tożsamość = nowe zapytanie. `viewbox` i `countryCodes`
|
|
42
|
+
* wchodzą jako klucze tekstowe — rodzic podaje je zwykle świeżą tablicą w każdym renderze, a to
|
|
43
|
+
* restartowałoby odliczanie przy każdym ruchu mapy i zapytanie nigdy by nie wyszło.
|
|
44
|
+
*/
|
|
45
|
+
const needle = term.trim();
|
|
46
|
+
const viewboxKey = viewbox ? JSON.stringify(viewbox) : '';
|
|
47
|
+
const countryKey = countryCodes?.join(',') ?? '';
|
|
48
|
+
const request = useMemo(() => {
|
|
49
|
+
if (!geocoder?.search || needle.length < minLength)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
geocoder,
|
|
53
|
+
needle,
|
|
54
|
+
options: {
|
|
55
|
+
language: locale,
|
|
56
|
+
limit,
|
|
57
|
+
viewbox: viewboxKey ? JSON.parse(viewboxKey) : undefined,
|
|
58
|
+
countryCodes: countryKey ? countryKey.split(',') : undefined,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}, [geocoder, needle, minLength, locale, limit, viewboxKey, countryKey]);
|
|
62
|
+
/** Zapytanie, na które przyszła już odpowiedź (albo błąd). */
|
|
63
|
+
const [settled, setSettled] = useState(null);
|
|
64
|
+
const loading = request !== null && settled !== request;
|
|
65
|
+
// Fraza za krótka (albo brak geokodera) — lista i stan wracają do zera. W renderze, nie w efekcie,
|
|
66
|
+
// żeby nie mignęła jedna klatka starych wyników.
|
|
67
|
+
const [hadRequest, setHadRequest] = useState(request !== null);
|
|
68
|
+
if (hadRequest !== (request !== null)) {
|
|
69
|
+
setHadRequest(request !== null);
|
|
70
|
+
if (!request) {
|
|
71
|
+
setResults([]);
|
|
72
|
+
setSearched(false);
|
|
73
|
+
setActiveIndex(-1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
40
76
|
const busy = loading || resolving;
|
|
41
77
|
const tooShort = query.trim().length > 0 && query.trim().length < minLength;
|
|
42
78
|
const hasPanel = open && (tooShort || busy || results.length > 0 || searched);
|
|
43
|
-
const applyResults = useCallback((found) => {
|
|
44
|
-
// Podpowiedź bez współrzędnych ma sens tylko wtedy, gdy port umie ją uzupełnić —
|
|
45
|
-
// pozycja, która nie ma dokąd polecieć, jest gorsza od jej braku.
|
|
46
|
-
setResults(geocoder?.resolve ? found : found.filter((suggestion) => suggestion.at));
|
|
47
|
-
setLoading(false);
|
|
48
|
-
setSearched(true);
|
|
49
|
-
setActiveIndex(-1);
|
|
50
|
-
setOpen(true);
|
|
51
|
-
}, [geocoder]);
|
|
52
79
|
// Odpytanie geokodera po odczekaniu `debounce` od ostatniego znaku. Sprzątanie kasuje
|
|
53
80
|
// zarówno niewystrzelony timer, jak i zapytanie w locie — adapter przerywa wtedy `fetch`.
|
|
54
81
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
if (!geocoder?.search || needle.length < minLength) {
|
|
57
|
-
setResults([]);
|
|
58
|
-
setLoading(false);
|
|
59
|
-
setSearched(false);
|
|
60
|
-
setActiveIndex(-1);
|
|
82
|
+
if (!request)
|
|
61
83
|
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
84
|
+
const { geocoder: port, needle: phrase, options } = request;
|
|
85
|
+
const applyResults = (found) => {
|
|
86
|
+
// Podpowiedź bez współrzędnych ma sens tylko wtedy, gdy port umie ją uzupełnić —
|
|
87
|
+
// pozycja, która nie ma dokąd polecieć, jest gorsza od jej braku.
|
|
88
|
+
setResults(port.resolve ? found : found.filter((suggestion) => suggestion.at));
|
|
89
|
+
setSettled(request);
|
|
90
|
+
setSearched(true);
|
|
91
|
+
setActiveIndex(-1);
|
|
92
|
+
setOpen(true);
|
|
93
|
+
};
|
|
64
94
|
const controller = new AbortController();
|
|
65
95
|
const timer = setTimeout(() => {
|
|
66
|
-
|
|
67
|
-
.search(
|
|
68
|
-
language: locale,
|
|
69
|
-
limit,
|
|
70
|
-
viewbox: viewbox ?? undefined,
|
|
71
|
-
countryCodes: countryCodes ?? undefined,
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
})
|
|
96
|
+
port
|
|
97
|
+
.search(phrase, { ...options, signal: controller.signal })
|
|
74
98
|
.then((found) => {
|
|
75
99
|
if (!controller.signal.aborted)
|
|
76
100
|
applyResults(found);
|
|
@@ -85,7 +109,7 @@ export function KptMapSearch({ geocoder = null, placeholder = '', minLength = 3,
|
|
|
85
109
|
clearTimeout(timer);
|
|
86
110
|
controller.abort();
|
|
87
111
|
};
|
|
88
|
-
}, [
|
|
112
|
+
}, [request, debounce]);
|
|
89
113
|
useEffect(() => () => pending.current?.abort(), []);
|
|
90
114
|
const status = (() => {
|
|
91
115
|
if (tooShort)
|
|
@@ -189,12 +213,12 @@ export function KptMapSearch({ geocoder = null, placeholder = '', minLength = 3,
|
|
|
189
213
|
}
|
|
190
214
|
};
|
|
191
215
|
const optionId = (index) => `${uid}-opt-${index}`;
|
|
192
|
-
return (_jsxs("div", { className: cn('kpt-map-search', className), ...rest, children: [_jsxs("div", { className: "kpt-map-search__field", children: [_jsx("span", { className: "kpt-map-search__icon", "aria-hidden": "true", children: _jsx(KptIcon, { name: "search" }) }), _jsx("input", { className: "kpt-map-search__input", type: "text", autoComplete: "off", autoCapitalize: "off", spellCheck: false, role: "combobox", "aria-autocomplete": "list", id: uid, value: query, placeholder: placeholder || t('map.search.placeholder'), disabled: disabled || !canSearch, "aria-label": t('map.search.label'), "aria-expanded": hasPanel, "aria-busy": busy || undefined, "aria-controls": hasPanel ? `${uid}-
|
|
216
|
+
return (_jsxs("div", { className: cn('kpt-map-search', className), ...rest, children: [_jsxs("div", { className: "kpt-map-search__field", children: [_jsx("span", { className: "kpt-map-search__icon", "aria-hidden": "true", children: _jsx(KptIcon, { name: "search" }) }), _jsx("input", { className: "kpt-map-search__input", type: "text", autoComplete: "off", autoCapitalize: "off", spellCheck: false, role: "combobox", "aria-autocomplete": "list", id: uid, value: query, placeholder: placeholder || t('map.search.placeholder'), disabled: disabled || !canSearch, "aria-label": t('map.search.label'), "aria-expanded": hasPanel, "aria-busy": busy || undefined, "aria-controls": hasPanel && results.length && !tooShort && !busy ? `${uid}-list` : undefined, "aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined, onChange: (event) => {
|
|
193
217
|
setQuery(event.target.value);
|
|
194
218
|
setTerm(event.target.value);
|
|
195
219
|
setOpen(true);
|
|
196
220
|
setUnresolved(false);
|
|
197
|
-
}, onFocus: () => setOpen(true), onBlur: () => setOpen(false), onKeyDown: onKeyDown }), query ? (_jsx("button", { className: "kpt-map-search__clear", type: "button", "aria-label": t('map.search.clear'), title: t('map.search.clear'), onClick: clear, children: _jsx(KptIcon, { name: "x" }) })) : null] }), hasPanel ? (_jsx("div", { className: "kpt-map-search__panel", id: `${uid}-panel`, children: tooShort ? (_jsx("p", { className: "kpt-map-search__note", children: t('map.search.minLength', { count: minLength }) })) : busy ? (_jsx("p", { className: "kpt-map-search__note", children: t('map.search.searching') })) : results.length ? (_jsx("ul", { className: "kpt-map-search__list", role: "listbox", "aria-label": t('map.search.results'), children: results.map((result, index) => (_jsxs("li", { className: cn('kpt-map-search__option', index === activeIndex && 'is-active'), role: "option", id: optionId(index), "aria-selected": index === activeIndex, onMouseDown: (event) => {
|
|
221
|
+
}, onFocus: () => setOpen(true), onBlur: () => setOpen(false), onKeyDown: onKeyDown }), query ? (_jsx("button", { className: "kpt-map-search__clear", type: "button", "aria-label": t('map.search.clear'), title: t('map.search.clear'), onClick: clear, children: _jsx(KptIcon, { name: "x" }) })) : null] }), hasPanel ? (_jsx("div", { className: "kpt-map-search__panel", id: `${uid}-panel`, children: tooShort ? (_jsx("p", { className: "kpt-map-search__note", children: t('map.search.minLength', { count: minLength }) })) : busy ? (_jsx("p", { className: "kpt-map-search__note", children: t('map.search.searching') })) : results.length ? (_jsx("ul", { className: "kpt-map-search__list", id: `${uid}-list`, role: "listbox", "aria-label": t('map.search.results'), children: results.map((result, index) => (_jsxs("li", { className: cn('kpt-map-search__option', index === activeIndex && 'is-active'), role: "option", id: optionId(index), "aria-selected": index === activeIndex, onMouseDown: (event) => {
|
|
198
222
|
event.preventDefault();
|
|
199
223
|
pick(result);
|
|
200
224
|
}, onMouseEnter: () => setActiveIndex(index), children: [_jsx("span", { className: "kpt-map-search__pin", "aria-hidden": "true", children: _jsx(KptIcon, { name: "map-pin" }) }), _jsx("span", { className: "kpt-map-search__label", children: result.label })] }, result.id))) })) : (_jsx("p", { className: "kpt-map-search__note", children: t('map.search.noResults') })) })) : null, _jsx("p", { className: "kpt-map-search__status", "aria-live": "polite", children: status })] }));
|
package/dist/map/map.d.ts
CHANGED
|
@@ -93,6 +93,11 @@ export interface KptMapProps<T = unknown> extends Omit<ComponentPropsWithRef<'di
|
|
|
93
93
|
}) => void;
|
|
94
94
|
onUserPositionChange?: (position: KptMapUserPosition | null) => void;
|
|
95
95
|
onUserLocationError?: (error: KptMapGeolocationError) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Silnika nie udało się uruchomić — brak paczki `leaflet`, odrzucony klucz Google. Błąd i tak
|
|
98
|
+
* trafia do konsoli; callback pozwala aplikacji pokazać własny komunikat zamiast szarego prostokąta.
|
|
99
|
+
*/
|
|
100
|
+
onEngineError?: (error: unknown) => void;
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
98
103
|
* Mapa z konfigurowalnymi znacznikami: kategorie z kolorem i ikoną, grupowanie z licznikiem,
|
|
@@ -110,4 +115,4 @@ export interface KptMapProps<T = unknown> extends Omit<ComponentPropsWithRef<'di
|
|
|
110
115
|
* renderPopup={({ location, data }) => <strong>{location.title}</strong>}
|
|
111
116
|
* />
|
|
112
117
|
*/
|
|
113
|
-
export declare function KptMap<T = unknown>({ locations, categories, country, view, bounds, restrictToCountry, cluster, clusterOptions, clusterColorBy, legend, legendCounts, hiddenCategories, defaultHiddenCategories, onHiddenCategoriesChange, showUserLocation, followUser, geolocation, allowCreate, search, searchPlaceholder, searchOptions, searchZoom, searchMarker, geocoder, popupLayout, distanceFrom, height, tileUrl, attribution, engine, renderPopup, renderCreateForm, onLocationClick, onClusterClick, onLocationCreate, onSearchSelect, onViewChange, onUserPositionChange, onUserLocationError, locale: localeOverride, dictionary, className, style, ...rest }: Readonly<KptMapProps<T>>): import("react").JSX.Element;
|
|
118
|
+
export declare function KptMap<T = unknown>({ locations, categories, country, view, bounds, restrictToCountry, cluster, clusterOptions, clusterColorBy, legend, legendCounts, hiddenCategories, defaultHiddenCategories, onHiddenCategoriesChange, showUserLocation, followUser, geolocation, allowCreate, search, searchPlaceholder, searchOptions, searchZoom, searchMarker, geocoder, popupLayout, distanceFrom, height, tileUrl, attribution, engine, renderPopup, renderCreateForm, onLocationClick, onClusterClick, onLocationCreate, onSearchSelect, onViewChange, onUserPositionChange, onUserLocationError, onEngineError, locale: localeOverride, dictionary, className, style, ...rest }: Readonly<KptMapProps<T>>): import("react").JSX.Element;
|
package/dist/map/map.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
|
-
import { KPT_MAP_DEFAULT_ATTRIBUTION, KPT_MAP_DEFAULT_TILE_URL,
|
|
3
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { KPT_MAP_DEFAULT_ATTRIBUTION, KPT_MAP_DEFAULT_TILE_URL, clusterLocations, countByCategory, haversine, isCluster, isSplittable, renderClusterHtml, renderMarkerHtml, renderUserMarkerHtml, resolveCountryView, } from '@konce-pt/map';
|
|
4
|
+
import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
5
|
import { cn } from "../internal/cn.js";
|
|
5
6
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
7
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
@@ -42,7 +43,7 @@ const NO_ENGINE_ERROR = 'KptMap: brak silnika mapy. Podaj fabrykę przez prop `e
|
|
|
42
43
|
* renderPopup={({ location, data }) => <strong>{location.title}</strong>}
|
|
43
44
|
* />
|
|
44
45
|
*/
|
|
45
|
-
export function KptMap({ locations, categories, country = 'PL', view = null, bounds = null, restrictToCountry = false, cluster = true, clusterOptions, clusterColorBy = 'none', legend = true, legendCounts = true, hiddenCategories, defaultHiddenCategories = [], onHiddenCategoriesChange, showUserLocation = false, followUser = false, geolocation = null, allowCreate = false, search = false, searchPlaceholder = '', searchOptions, searchZoom = 16, searchMarker = true, geocoder = null, popupLayout = 'plain', distanceFrom = null, height = '480px', tileUrl = null, attribution = null, engine, renderPopup, renderCreateForm, onLocationClick, onClusterClick, onLocationCreate, onSearchSelect, onViewChange, onUserPositionChange, onUserLocationError, locale: localeOverride, dictionary, className, style, ...rest }) {
|
|
46
|
+
export function KptMap({ locations, categories, country = 'PL', view = null, bounds = null, restrictToCountry = false, cluster = true, clusterOptions, clusterColorBy = 'none', legend = true, legendCounts = true, hiddenCategories, defaultHiddenCategories = [], onHiddenCategoriesChange, showUserLocation = false, followUser = false, geolocation = null, allowCreate = false, search = false, searchPlaceholder = '', searchOptions, searchZoom = 16, searchMarker = true, geocoder = null, popupLayout = 'plain', distanceFrom = null, height = '480px', tileUrl = null, attribution = null, engine, renderPopup, renderCreateForm, onLocationClick, onClusterClick, onLocationCreate, onSearchSelect, onViewChange, onUserPositionChange, onUserLocationError, onEngineError, locale: localeOverride, dictionary, className, style, ...rest }) {
|
|
46
47
|
const { t } = useKptMessages({ locale: localeOverride, dictionary });
|
|
47
48
|
const canvasRef = useRef(null);
|
|
48
49
|
const popupRef = useRef(null);
|
|
@@ -53,8 +54,12 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
53
54
|
* montaż musi więc poczekać, aż pierwszy skończy sprzątać.
|
|
54
55
|
*/
|
|
55
56
|
const mountChain = useRef(Promise.resolve());
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Silnik po udanym `init()` — w stanie, nie tylko w refie, bo render czyta z niego pozycje
|
|
59
|
+
* nakładek (`project`). Ref zostaje dla handlerów zdarzeń, które żyją dłużej niż render.
|
|
60
|
+
*/
|
|
61
|
+
const [liveEngine, setLiveEngine] = useState(null);
|
|
62
|
+
const ready = liveEngine !== null;
|
|
58
63
|
/** Zoom trzymamy w stanie, bo od niego zależy wynik grupowania. */
|
|
59
64
|
const [zoom, setZoom] = useState(6);
|
|
60
65
|
/** Rośnie przy każdym ruchu mapy — wymusza przeliczenie pozycji nakładek. */
|
|
@@ -184,7 +189,11 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
184
189
|
* a jego callbacki żyją dłużej niż render, w którym powstały.
|
|
185
190
|
*/
|
|
186
191
|
const latest = useRef({ entries, allowCreate, popup, contextTarget, onLocationClick, onClusterClick, onViewChange });
|
|
187
|
-
|
|
192
|
+
// Odświeżane po zatwierdzeniu renderu, nie w jego trakcie — przerwany render nie zostawi w refie danych,
|
|
193
|
+
// których nigdy nie było na ekranie.
|
|
194
|
+
useLayoutEffect(() => {
|
|
195
|
+
latest.current = { entries, allowCreate, popup, contextTarget, onLocationClick, onClusterClick, onViewChange };
|
|
196
|
+
});
|
|
188
197
|
const emitViewChange = useCallback(() => {
|
|
189
198
|
const map = engineRef.current;
|
|
190
199
|
if (!map)
|
|
@@ -222,11 +231,14 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
222
231
|
* „w locie", przed rozwiązaniem obietnicy, zostawiłoby oznaczony kontener i drugi montaż
|
|
223
232
|
* kończyłby się błędem „Map container is already initialized".
|
|
224
233
|
*/
|
|
234
|
+
const reportEngineError = useEffectEvent((error) => {
|
|
235
|
+
console.error(error);
|
|
236
|
+
onEngineError?.(error);
|
|
237
|
+
});
|
|
225
238
|
useEffect(() => {
|
|
226
239
|
let disposed = false;
|
|
227
240
|
const map = engine();
|
|
228
241
|
engineRef.current = map;
|
|
229
|
-
setEngineKind(map.kind);
|
|
230
242
|
const preset = resolveCountryView(country);
|
|
231
243
|
const canvas = canvasRef.current;
|
|
232
244
|
if (!canvas)
|
|
@@ -265,18 +277,24 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
265
277
|
});
|
|
266
278
|
map.on('markerclick', (id) => onMarkerClick(id));
|
|
267
279
|
setZoom(map.getZoom());
|
|
268
|
-
|
|
280
|
+
setLiveEngine(map);
|
|
269
281
|
emitViewChange();
|
|
282
|
+
})
|
|
283
|
+
// Bez tego odrzucony `init()` to „Uncaught (in promise)” i mapa bez słowa wyjaśnienia.
|
|
284
|
+
.catch((error) => {
|
|
285
|
+
if (!disposed)
|
|
286
|
+
reportEngineError(error);
|
|
270
287
|
});
|
|
271
288
|
return () => {
|
|
272
289
|
disposed = true;
|
|
273
290
|
engineRef.current = null;
|
|
274
|
-
|
|
291
|
+
setLiveEngine(null);
|
|
275
292
|
// Zniszczenie dopinamy do łańcucha: dopiero po `init()`, bo inaczej `destroy()` trafiłby
|
|
276
293
|
// w mapę, której jeszcze nie ma, a kontener zostałby oznaczony jako zajęty na zawsze.
|
|
277
294
|
mountChain.current = mountChain.current.finally(() => map.destroy());
|
|
278
295
|
};
|
|
279
296
|
// Silnik i widok startowy czytamy raz — podmiana po starcie nie przeładowuje mapy.
|
|
297
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- montaż raz na życie komponentu (patrz wyżej)
|
|
280
298
|
}, []);
|
|
281
299
|
// Bez tego Leaflet zostawia szare pola po każdej zmianie układu strony.
|
|
282
300
|
useLayoutEffect(() => {
|
|
@@ -307,27 +325,37 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
307
325
|
return;
|
|
308
326
|
engineRef.current?.setUserPosition(userPosition, renderUserMarkerHtml({ label: t('map.myLocation'), iconSvgInner: getKptIcon('user') }));
|
|
309
327
|
}, [ready, userPosition, t]);
|
|
328
|
+
/*
|
|
329
|
+
* Włączenie albo podmiana nasłuchu pozycji zeruje jego stan — liczone w renderze (wzorzec
|
|
330
|
+
* poprzedniej wartości), a nie w efekcie, który pokazałby jedną klatkę starej pozycji.
|
|
331
|
+
*/
|
|
332
|
+
const watching = showUserLocation && !!geolocation;
|
|
333
|
+
const [watchSource, setWatchSource] = useState({ watching, geolocation });
|
|
334
|
+
if (watchSource.watching !== watching || watchSource.geolocation !== geolocation) {
|
|
335
|
+
setWatchSource({ watching, geolocation });
|
|
336
|
+
setUserPosition(null);
|
|
337
|
+
setLocationBlocked(false);
|
|
338
|
+
}
|
|
339
|
+
// Handlery nasłuchu widzą zawsze świeże `followUser` i callbacki, a ich zmiana nie restartuje nasłuchu.
|
|
340
|
+
const handlePosition = useEffectEvent((position) => {
|
|
341
|
+
setUserPosition(position);
|
|
342
|
+
onUserPositionChange?.(position);
|
|
343
|
+
if (position && followUser) {
|
|
344
|
+
const map = engineRef.current;
|
|
345
|
+
if (map)
|
|
346
|
+
map.setView(position.at, Math.max(map.getZoom(), 13));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const handleLocationError = useEffectEvent((error) => {
|
|
350
|
+
setUserPosition(null);
|
|
351
|
+
setLocationBlocked(true);
|
|
352
|
+
onUserLocationError?.(error);
|
|
353
|
+
});
|
|
310
354
|
// Śledzenie pozycji użytkownika. Odmowa zgody wyłącza kontrolkę, ale nie wywraca komponentu.
|
|
311
355
|
useEffect(() => {
|
|
312
|
-
if (!showUserLocation || !geolocation)
|
|
313
|
-
setUserPosition(null);
|
|
356
|
+
if (!showUserLocation || !geolocation)
|
|
314
357
|
return;
|
|
315
|
-
|
|
316
|
-
setLocationBlocked(false);
|
|
317
|
-
return geolocation.watch((position) => {
|
|
318
|
-
setUserPosition(position);
|
|
319
|
-
onUserPositionChange?.(position);
|
|
320
|
-
if (position && followUser) {
|
|
321
|
-
const map = engineRef.current;
|
|
322
|
-
if (map)
|
|
323
|
-
map.setView(position.at, Math.max(map.getZoom(), 13));
|
|
324
|
-
}
|
|
325
|
-
}, (error) => {
|
|
326
|
-
setUserPosition(null);
|
|
327
|
-
setLocationBlocked(true);
|
|
328
|
-
onUserLocationError?.(error);
|
|
329
|
-
});
|
|
330
|
-
// `followUser` czytamy w callbacku, więc nie restartuje nasłuchu.
|
|
358
|
+
return geolocation.watch((position) => handlePosition(position), (error) => handleLocationError(error));
|
|
331
359
|
}, [showUserLocation, geolocation]);
|
|
332
360
|
const popupCategory = popup ? categoryById.get(popup.location.categoryId) : undefined;
|
|
333
361
|
/**
|
|
@@ -343,16 +371,16 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
343
371
|
/** Pozycje nakładek w pikselach kontenera; zależą od `viewportTick`, czyli od ruchu mapy. */
|
|
344
372
|
const popupPosition = useMemo(() => {
|
|
345
373
|
void viewportTick;
|
|
346
|
-
if (!popup || !
|
|
374
|
+
if (!popup || !liveEngine)
|
|
347
375
|
return null;
|
|
348
|
-
return
|
|
349
|
-
}, [viewportTick, popup,
|
|
376
|
+
return liveEngine.project(popup.location.at);
|
|
377
|
+
}, [viewportTick, popup, liveEngine]);
|
|
350
378
|
const contextPosition = useMemo(() => {
|
|
351
379
|
void viewportTick;
|
|
352
|
-
if (!contextTarget || !
|
|
380
|
+
if (!contextTarget || !liveEngine)
|
|
353
381
|
return null;
|
|
354
|
-
return
|
|
355
|
-
}, [viewportTick, contextTarget,
|
|
382
|
+
return liveEngine.project(contextTarget.at);
|
|
383
|
+
}, [viewportTick, contextTarget, liveEngine]);
|
|
356
384
|
/** Kategorie oferowane w menu „dodaj" — ukryte w legendzie odpadają. */
|
|
357
385
|
const creatableCategories = useMemo(() => {
|
|
358
386
|
const hiddenSet = new Set(hidden);
|
|
@@ -403,7 +431,7 @@ export function KptMap({ locations, categories, country = 'PL', view = null, bou
|
|
|
403
431
|
map?.setView(result.at, searchZoom);
|
|
404
432
|
onSearchSelect?.(result);
|
|
405
433
|
};
|
|
406
|
-
return (_jsxs("div", { className: cn('kpt-map', className), "data-engine":
|
|
434
|
+
return (_jsxs("div", { className: cn('kpt-map', className), "data-engine": liveEngine?.kind, style: { ...style, '--kpt-map-height': height }, ...rest, children: [_jsxs("div", { className: "kpt-map__viewport", children: [_jsx("div", { ref: canvasRef, className: "kpt-map__canvas" }), _jsxs("div", { className: "kpt-map__controls", role: "group", "aria-label": t('map.legend'), children: [_jsx("button", { className: "kpt-map__control", type: "button", "aria-label": t('map.zoomIn'), title: t('map.zoomIn'), disabled: !ready, onClick: () => zoomBy(1), children: _jsx("span", { "aria-hidden": "true", children: "+" }) }), _jsx("button", { className: "kpt-map__control", type: "button", "aria-label": t('map.zoomOut'), title: t('map.zoomOut'), disabled: !ready, onClick: () => zoomBy(-1), children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) }), showUserLocation ? (_jsx("button", { className: "kpt-map__control kpt-map__control--locate", type: "button", "aria-label": locationBlocked ? t('map.locationDenied') : t('map.locateMe'), title: locationBlocked ? t('map.locationDenied') : t('map.locateMe'), disabled: !userPosition, onClick: centreOnUser, children: _jsx("span", { className: "kpt-map__crosshair", "aria-hidden": "true" }) })) : null] }), searchEnabled ? (_jsx("div", { className: "kpt-map__search", children: _jsx(KptMapSearch, { geocoder: geocoder, placeholder: searchPlaceholder, minLength: searchOptions?.minLength ?? 3, debounce: searchOptions?.debounce ?? 300, limit: searchOptions?.limit ?? 5, viewbox: searchViewbox, countryCodes: searchCountryCodes, onSelect: handleSearchSelect, onClear: () => setSearchPin(null) }) })) : null, allowCreate && contextTarget && contextPosition ? (_jsxs("div", { className: "kpt-map__context", role: "menu", "aria-label": t('map.addLocation'), style: { '--_kpt-map-x': `${contextPosition.x}px`, '--_kpt-map-y': `${contextPosition.y}px` }, children: [_jsx("p", { className: "kpt-map__context-title", children: t('map.chooseCategory') }), creatableCategories.length === 0 ? (_jsx("p", { className: "kpt-map__context-empty", children: t('map.empty') })) : (creatableCategories.map((category) => (_jsxs("button", { className: "kpt-map__context-item", type: "button", role: "menuitem", onClick: () => {
|
|
407
435
|
setContextTarget(null);
|
|
408
436
|
setCreateFor({ at: contextTarget.at, category });
|
|
409
437
|
setCreateOpen(true);
|
package/dist/megamenu/llms.txt
CHANGED
|
@@ -64,3 +64,14 @@ along it, `ArrowDown` (or `Enter`/`Space`) opens the panel on the first link, `A
|
|
|
64
64
|
In the panel `ArrowDown`/`ArrowUp`/`Home`/`End` walk through all links column by column,
|
|
65
65
|
`ArrowLeft`/`ArrowRight` show the neighbouring item's panel, `Escape` or `Tab` close it and return
|
|
66
66
|
focus to the top item.
|
|
67
|
+
|
|
68
|
+
## Router
|
|
69
|
+
Links are plain `<a href>`, so without a router a click reloads the page. `onNavigate` fires on a plain
|
|
70
|
+
click (left button, no Ctrl/⌘/Shift, no `target="_blank"`) with `{ item, url, event }`: call
|
|
71
|
+
`event.preventDefault()` and navigate with your router. Modified clicks stay native (a new tab, a new
|
|
72
|
+
window). The library depends on no router.
|
|
73
|
+
|
|
74
|
+
<KptMegamenu items={items} onNavigate={({ event, url }) => { event.preventDefault(); navigate(url); }} />
|
|
75
|
+
|
|
76
|
+
## When to use it
|
|
77
|
+
The `menubar`/`menu` roles switch a screen reader into application mode: page navigation by headings and links stops, and arrow keys are expected. That fits an application menu (File, Edit, View) or a large product menu. For plain site navigation (a few links in a header) the WAI-ARIA APG recommends the disclosure pattern instead — a `<nav>` with buttons carrying `aria-expanded` and ordinary link lists — which you can build from `kpt-button` and lists. The menu does not render a third level: nested `items` of a submenu item are ignored.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptNavigateEvent } from '../shared/types.ts';
|
|
2
3
|
import type { KptMenuItem } from '../menu/menu-item.ts';
|
|
3
4
|
export interface KptMegamenuProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
|
|
4
5
|
items?: readonly KptMenuItem<T>[];
|
|
5
6
|
onSelect?: (item: KptMenuItem<T>) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Zwykły klik w pozycję z `url`. Aplikacja z routerem woła `event.preventDefault()` i nawiguje
|
|
9
|
+
* sama — bez tego `<a href>` przeładowuje stronę.
|
|
10
|
+
*/
|
|
11
|
+
onNavigate?: (event: KptNavigateEvent<KptMenuItem<T>>) => void;
|
|
6
12
|
/** Nazwa belki dla czytników ekranu, np. „Menu główne”. */
|
|
7
13
|
ariaLabel?: string;
|
|
8
14
|
}
|
|
@@ -23,4 +29,4 @@ export interface KptMegamenuProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
23
29
|
* @example
|
|
24
30
|
* <KptMegamenu items={items} onSelect={onSelect} />
|
|
25
31
|
*/
|
|
26
|
-
export declare function KptMegamenu<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;
|
|
32
|
+
export declare function KptMegamenu<T>({ items, onSelect, onNavigate, ariaLabel, className, ...rest }: Readonly<KptMegamenuProps<T>>): import("react").JSX.Element;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { cn } from "../internal/cn.js";
|
|
@@ -5,6 +6,7 @@ import { KptOverlay } from "../internal/overlay.js";
|
|
|
5
6
|
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
6
7
|
import { useKptId } from "../internal/use-id.js";
|
|
7
8
|
import { KptIcon } from "../icon/icon.js";
|
|
9
|
+
import { kptIsPlainClick } from "../shared/types.js";
|
|
8
10
|
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
9
11
|
/**
|
|
10
12
|
* Mega-menu — pozioma belka z szerokim, wielokolumnowym panelem.
|
|
@@ -23,8 +25,9 @@ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
|
23
25
|
* @example
|
|
24
26
|
* <KptMegamenu items={items} onSelect={onSelect} />
|
|
25
27
|
*/
|
|
26
|
-
export function KptMegamenu({ items = [], onSelect, ariaLabel, className, ...rest }) {
|
|
27
|
-
|
|
28
|
+
export function KptMegamenu({ items = [], onSelect, onNavigate, ariaLabel, className, ...rest }) {
|
|
29
|
+
/** Pasek jako kotwica panelu — w stanie, bo czyta go render. */
|
|
30
|
+
const [bar, setBar] = useState(null);
|
|
28
31
|
const panelRef = useRef(null);
|
|
29
32
|
const buttonsRef = useRef([]);
|
|
30
33
|
const [openIndex, setOpenIndex] = useState(null);
|
|
@@ -122,18 +125,24 @@ export function KptMegamenu({ items = [], onSelect, ariaLabel, className, ...res
|
|
|
122
125
|
focusTop(top);
|
|
123
126
|
}
|
|
124
127
|
};
|
|
125
|
-
|
|
128
|
+
/** Zwykły klik w odnośnik → `onNavigate` (Ctrl/⌘/Shift, środkowy przycisk i `_blank` zostają natywne). */
|
|
129
|
+
const linkClick = (event, item) => {
|
|
130
|
+
if (item.url && onNavigate && kptIsPlainClick(event, event.currentTarget.target)) {
|
|
131
|
+
onNavigate({ item, url: item.url, event: event.nativeEvent });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return (_jsxs("div", { className: cn('kpt-megamenu-host', className), ...rest, children: [_jsx("div", { className: "kpt-megamenu", role: "menubar", "aria-orientation": "horizontal", "aria-label": ariaLabel || undefined, ref: setBar, children: items.map((item, index) => {
|
|
126
135
|
const hasMenu = !!item.items?.length;
|
|
127
136
|
if (isLink(item)) {
|
|
128
137
|
return (_jsxs("a", { ref: (el) => {
|
|
129
138
|
buttonsRef.current[index] = el;
|
|
130
|
-
}, className: "kpt-megamenu__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, 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 })] }, index));
|
|
139
|
+
}, className: "kpt-megamenu__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: (event) => { linkClick(event, item); 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 })] }, index));
|
|
131
140
|
}
|
|
132
141
|
return (_jsxs("button", { ref: (el) => {
|
|
133
142
|
buttonsRef.current[index] = el;
|
|
134
143
|
}, type: "button", className: cn('kpt-megamenu__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 : 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-megamenu__caret" }) : null] }, index));
|
|
135
|
-
}) }), _jsx(KptOverlay, { open: openIndex !== null, anchor:
|
|
144
|
+
}) }), _jsx(KptOverlay, { open: openIndex !== null, anchor: bar, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-megamenu__panel", role: "menu", tabIndex: -1, id: panelId, "aria-label": openIndex === null ? undefined : items[openIndex]?.label, onKeyDown: onPanelKeyDown, children: columns.map((column, columnIndex) => {
|
|
136
145
|
const titleId = `${panelId}-col-${columnIndex}`;
|
|
137
|
-
return (_jsxs("div", { className: "kpt-megamenu__column", role: "group", "aria-labelledby": column.label ? titleId : undefined, children: [column.label ? (_jsx("div", { className: "kpt-megamenu__col-title", role: "presentation", id: titleId, children: column.label })) : null, (column.items ?? []).map((link, linkIndex) => isLink(link) ? (_jsxs("a", { className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, href: link.url, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)) : (_jsxs("button", { type: "button", className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, disabled: link.disabled, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)))] }, columnIndex));
|
|
146
|
+
return (_jsxs("div", { className: "kpt-megamenu__column", role: "group", "aria-labelledby": column.label ? titleId : undefined, children: [column.label ? (_jsx("div", { className: "kpt-megamenu__col-title", role: "presentation", id: titleId, children: column.label })) : null, (column.items ?? []).map((link, linkIndex) => isLink(link) ? (_jsxs("a", { className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, href: link.url, onClick: (event) => { linkClick(event, link); pick(link); }, children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)) : (_jsxs("button", { type: "button", className: "kpt-megamenu__link", role: "menuitem", tabIndex: -1, disabled: link.disabled, onClick: () => pick(link), children: [link.icon ? _jsx(KptIcon, { name: link.icon }) : null, _jsx("span", { children: link.label })] }, linkIndex)))] }, columnIndex));
|
|
138
147
|
}) }) })] }));
|
|
139
148
|
}
|
package/dist/menu/menu-rows.js
CHANGED
package/dist/menu/menu.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { cn } from "../internal/cn.js";
|
|
4
5
|
import { KptOverlay } from "../internal/overlay.js";
|
|
5
6
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
@@ -17,6 +18,12 @@ import { MenuRows } from "./menu-rows.js";
|
|
|
17
18
|
*/
|
|
18
19
|
export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = false, onOpenChange, className, ...rest }) {
|
|
19
20
|
const triggerRef = useRef(null);
|
|
21
|
+
/** Kotwica panelu — w stanie, bo czyta ją render; ref zostaje dla handlerów. */
|
|
22
|
+
const [anchor, setAnchor] = useState(null);
|
|
23
|
+
const setTrigger = useCallback((element) => {
|
|
24
|
+
triggerRef.current = element;
|
|
25
|
+
setAnchor(element);
|
|
26
|
+
}, []);
|
|
20
27
|
const panelRef = useRef(null);
|
|
21
28
|
/** Którą pozycję skupić po otwarciu — strzałka w górę na wyzwalaczu otwiera „od końca”. */
|
|
22
29
|
const focusEdge = useRef('first');
|
|
@@ -83,8 +90,8 @@ export function KptMenu({ items = [], trigger, onSelect, open, defaultOpen = fal
|
|
|
83
90
|
onSelect?.(item);
|
|
84
91
|
closeAndRestoreFocus();
|
|
85
92
|
};
|
|
86
|
-
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref:
|
|
93
|
+
return (_jsxs("span", { className: cn('kpt-menu-host', className), ...rest, children: [_jsx("span", { className: "kpt-menu__trigger", ref: setTrigger, onClick: () => {
|
|
87
94
|
focusEdge.current = 'first';
|
|
88
95
|
setOpen(!isOpen);
|
|
89
|
-
}, onKeyDown: onTriggerKeyDown, children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor:
|
|
96
|
+
}, onKeyDown: onTriggerKeyDown, children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-menu__panel", role: "menu", tabIndex: -1, onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
|
|
90
97
|
}
|
package/dist/menubar/llms.txt
CHANGED
|
@@ -60,3 +60,14 @@ The bar is a single `Tab` stop (roving `tabindex`), following the WAI-ARIA menub
|
|
|
60
60
|
its first row, `ArrowUp` on its last one.
|
|
61
61
|
- In a dropdown: `ArrowDown`/`ArrowUp`/`Home`/`End` move between rows, `ArrowLeft`/`ArrowRight`
|
|
62
62
|
open the neighbouring dropdown, `Escape` and `Tab` close it and return focus to the top item.
|
|
63
|
+
|
|
64
|
+
## Router
|
|
65
|
+
Links are plain `<a href>`, so without a router a click reloads the page. `onNavigate` fires on a plain
|
|
66
|
+
click (left button, no Ctrl/⌘/Shift, no `target="_blank"`) with `{ item, url, event }`: call
|
|
67
|
+
`event.preventDefault()` and navigate with your router. Modified clicks stay native (a new tab, a new
|
|
68
|
+
window). The library depends on no router.
|
|
69
|
+
|
|
70
|
+
<KptMenubar items={items} onNavigate={({ event, url }) => { event.preventDefault(); navigate(url); }} />
|
|
71
|
+
|
|
72
|
+
## When to use it
|
|
73
|
+
The `menubar`/`menu` roles switch a screen reader into application mode: page navigation by headings and links stops, and arrow keys are expected. That fits an application menu (File, Edit, View) or a large product menu. For plain site navigation (a few links in a header) the WAI-ARIA APG recommends the disclosure pattern instead — a `<nav>` with buttons carrying `aria-expanded` and ordinary link lists — which you can build from `kpt-button` and lists. The menu does not render a third level: nested `items` of a submenu item are ignored.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptNavigateEvent } from '../shared/types.ts';
|
|
2
3
|
import type { KptMenuItem } from '../menu/menu-item.ts';
|
|
3
4
|
export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'onSelect'> {
|
|
4
5
|
items?: readonly KptMenuItem<T>[];
|
|
5
6
|
onSelect?: (item: KptMenuItem<T>) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Zwykły klik w pozycję z `url`. Aplikacja z routerem woła `event.preventDefault()` i nawiguje
|
|
9
|
+
* sama — bez tego `<a href>` przeładowuje stronę.
|
|
10
|
+
*/
|
|
11
|
+
onNavigate?: (event: KptNavigateEvent<KptMenuItem<T>>) => void;
|
|
6
12
|
/** Nazwa belki dla czytników ekranu, np. „Menu edytora”. */
|
|
7
13
|
ariaLabel?: string;
|
|
8
14
|
}
|
|
@@ -20,4 +26,4 @@ export interface KptMenubarProps<T> extends Omit<ComponentPropsWithRef<'div'>, '
|
|
|
20
26
|
* @example
|
|
21
27
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
22
28
|
*/
|
|
23
|
-
export declare function KptMenubar<T>({ items, onSelect, ariaLabel, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
|
29
|
+
export declare function KptMenubar<T>({ items, onSelect, onNavigate, ariaLabel, className, ...rest }: Readonly<KptMenubarProps<T>>): import("react").JSX.Element;
|
package/dist/menubar/menubar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { useEffect, useRef, useState } from 'react';
|
|
3
4
|
import { cn } from "../internal/cn.js";
|
|
@@ -5,6 +6,7 @@ import { KptOverlay } from "../internal/overlay.js";
|
|
|
5
6
|
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
6
7
|
import { useKptId } from "../internal/use-id.js";
|
|
7
8
|
import { KptIcon } from "../icon/icon.js";
|
|
9
|
+
import { kptIsPlainClick } from "../shared/types.js";
|
|
8
10
|
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
9
11
|
/**
|
|
10
12
|
* Pozioma belka menu z rozwijanymi pod-menu. Pozycja bez `items` działa jak akcja lub link.
|
|
@@ -20,8 +22,10 @@ import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
|
20
22
|
* @example
|
|
21
23
|
* <KptMenubar items={items} onSelect={onSelect} />
|
|
22
24
|
*/
|
|
23
|
-
export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest }) {
|
|
25
|
+
export function KptMenubar({ items = [], onSelect, onNavigate, ariaLabel, className, ...rest }) {
|
|
24
26
|
const [openIndex, setOpenIndex] = useState(null);
|
|
27
|
+
/** Przycisk otwartego pod-menu — w stanie, bo render podaje go nakładce jako kotwicę. */
|
|
28
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
25
29
|
// Ostatnio skupiona pozycja górna — ona jest przystankiem Tab belki.
|
|
26
30
|
const [focusIndex, setFocusIndex] = useState(0);
|
|
27
31
|
const buttonsRef = useRef([]);
|
|
@@ -59,6 +63,7 @@ export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest
|
|
|
59
63
|
focusMenuEdge(panelRef.current, edge);
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
66
|
+
setAnchorEl(buttonsRef.current[index] ?? null);
|
|
62
67
|
setOpenIndex(hasMenu ? index : null);
|
|
63
68
|
focusTop(index);
|
|
64
69
|
};
|
|
@@ -67,6 +72,7 @@ export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest
|
|
|
67
72
|
return;
|
|
68
73
|
if (item.items?.length) {
|
|
69
74
|
focusEdge.current = 'first';
|
|
75
|
+
setAnchorEl(buttonsRef.current[index] ?? null);
|
|
70
76
|
setOpenIndex((current) => (current === index ? null : index));
|
|
71
77
|
}
|
|
72
78
|
else {
|
|
@@ -114,12 +120,18 @@ export function KptMenubar({ items = [], onSelect, ariaLabel, className, ...rest
|
|
|
114
120
|
focusTop(index);
|
|
115
121
|
}
|
|
116
122
|
};
|
|
123
|
+
/** Zwykły klik w odnośnik → `onNavigate` (Ctrl/⌘/Shift, środkowy przycisk i `_blank` zostają natywne). */
|
|
124
|
+
const linkClick = (event, item) => {
|
|
125
|
+
if (item.url && onNavigate && kptIsPlainClick(event, event.currentTarget.target)) {
|
|
126
|
+
onNavigate({ item, url: item.url, event: event.nativeEvent });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
117
129
|
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
130
|
const hasMenu = !!item.items?.length;
|
|
119
131
|
return (_jsxs("div", { className: "kpt-menubar__group", role: "none", children: [isLink(item) ? (_jsxs("a", { ref: (el) => {
|
|
120
132
|
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) => {
|
|
133
|
+
}, className: "kpt-menubar__top", role: "menuitem", href: item.url, tabIndex: index === tabStop ? 0 : -1, onClick: (event) => { linkClick(event, item); 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
134
|
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:
|
|
135
|
+
}, 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: openIndex === index ? anchorEl : null, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-menubar__dropdown", role: "menu", tabIndex: -1, 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: (event) => { linkClick(event, sub); 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
136
|
}) }) }));
|
|
125
137
|
}
|
package/dist/order-list/llms.txt
CHANGED
|
@@ -58,3 +58,6 @@ The list is named after `header` (`aria-label`) and the selected item carries `a
|
|
|
58
58
|
Both buttons are labelled from the dictionary and go disabled while nothing is selected. Native
|
|
59
59
|
HTML5 drag and drop has no keyboard equivalent — `Alt`+arrows and the buttons cover it, and
|
|
60
60
|
`dragdrop` adds to them, it never replaces them.
|
|
61
|
+
|
|
62
|
+
## Announcements
|
|
63
|
+
Every reorder (buttons, Alt+↑/↓, drag and drop) is announced in a polite live region: `orderList.moved` with `{position}` and `{total}` — focus does not move, so a screen reader would not hear the new place otherwise.
|