@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.
Files changed (203) hide show
  1. package/README.md +8 -2
  2. package/dist/accordion/accordion.js +11 -10
  3. package/dist/alert/alert.js +1 -0
  4. package/dist/alert/llms.txt +1 -0
  5. package/dist/app-shell/app-shell.js +12 -5
  6. package/dist/app-shell/llms.txt +5 -4
  7. package/dist/auth/auth.js +17 -8
  8. package/dist/auth/llms.txt +3 -2
  9. package/dist/autocomplete/autocomplete.d.ts +1 -1
  10. package/dist/autocomplete/autocomplete.js +12 -4
  11. package/dist/autocomplete/llms.txt +3 -0
  12. package/dist/avatar/avatar.js +1 -0
  13. package/dist/avatar-group/avatar-group.js +1 -0
  14. package/dist/backdrop/backdrop.d.ts +0 -19
  15. package/dist/backdrop/backdrop.js +18 -13
  16. package/dist/bottom-sheet/bottom-sheet.d.ts +6 -1
  17. package/dist/bottom-sheet/bottom-sheet.js +3 -2
  18. package/dist/bottom-sheet/llms.txt +3 -0
  19. package/dist/breadcrumb/breadcrumb.d.ts +7 -1
  20. package/dist/breadcrumb/breadcrumb.js +10 -2
  21. package/dist/breadcrumb/llms.txt +8 -0
  22. package/dist/button/button.js +2 -1
  23. package/dist/button-group/button-group.d.ts +15 -2
  24. package/dist/button-group/button-group.js +31 -4
  25. package/dist/button-group/llms.txt +9 -6
  26. package/dist/card/card.js +4 -3
  27. package/dist/carousel/carousel.js +4 -1
  28. package/dist/charts/chart-legend.js +1 -0
  29. package/dist/charts/chart.d.ts +3 -2
  30. package/dist/charts/chart.js +12 -4
  31. package/dist/charts/llms.txt +6 -3
  32. package/dist/chat/chat.js +39 -9
  33. package/dist/chat/llms.txt +9 -0
  34. package/dist/chat-composer/chat-composer.d.ts +5 -2
  35. package/dist/chat-composer/chat-composer.js +10 -1
  36. package/dist/chat-dock/chat-dock.js +1 -0
  37. package/dist/chat-message/chat-message.js +2 -1
  38. package/dist/chat-typing/chat-typing.js +1 -0
  39. package/dist/checkbox/checkbox.d.ts +1 -1
  40. package/dist/checkbox/checkbox.js +9 -2
  41. package/dist/checkbox/llms.txt +3 -0
  42. package/dist/chip/chip.js +1 -0
  43. package/dist/chips-input/chips-input.d.ts +3 -1
  44. package/dist/chips-input/chips-input.js +14 -5
  45. package/dist/chips-input/llms.txt +3 -0
  46. package/dist/clock/clock.d.ts +9 -4
  47. package/dist/clock/clock.js +35 -5
  48. package/dist/clock/llms.txt +6 -2
  49. package/dist/color-picker/color-picker.d.ts +5 -1
  50. package/dist/color-picker/color-picker.js +36 -17
  51. package/dist/color-picker/llms.txt +3 -0
  52. package/dist/confirm/confirm-container.js +1 -0
  53. package/dist/confirm/confirm-store.js +6 -0
  54. package/dist/confirm/llms.txt +2 -1
  55. package/dist/context-menu/context-menu.js +35 -12
  56. package/dist/context-menu/llms.txt +3 -0
  57. package/dist/data-table/data-table.d.ts +12 -1
  58. package/dist/data-table/data-table.js +113 -51
  59. package/dist/data-table/export-csv.d.ts +2 -2
  60. package/dist/data-table/export-csv.js +7 -9
  61. package/dist/data-table/llms.txt +19 -4
  62. package/dist/data-view/data-view.js +1 -0
  63. package/dist/date-range/date-range.d.ts +16 -4
  64. package/dist/date-range/date-range.js +69 -46
  65. package/dist/date-range/llms.txt +17 -3
  66. package/dist/datepicker/datepicker.d.ts +17 -3
  67. package/dist/datepicker/datepicker.js +86 -54
  68. package/dist/datepicker/llms.txt +17 -3
  69. package/dist/dialog/dialog.js +29 -8
  70. package/dist/dialog/llms.txt +3 -0
  71. package/dist/divider/divider.d.ts +7 -1
  72. package/dist/divider/divider.js +3 -2
  73. package/dist/divider/llms.txt +1 -0
  74. package/dist/drawer/drawer.d.ts +6 -1
  75. package/dist/drawer/drawer.js +3 -2
  76. package/dist/drawer/llms.txt +3 -0
  77. package/dist/fieldset/fieldset.js +4 -1
  78. package/dist/file-upload/file-upload.d.ts +7 -1
  79. package/dist/file-upload/file-upload.js +18 -7
  80. package/dist/file-upload/llms.txt +6 -3
  81. package/dist/form-field/form-field.js +1 -0
  82. package/dist/galleria/galleria.js +4 -1
  83. package/dist/i18n/i18n-context.d.ts +3 -3
  84. package/dist/i18n/i18n-context.js +25 -3
  85. package/dist/icon/icon.js +4 -1
  86. package/dist/image/image.js +4 -4
  87. package/dist/input/input.js +1 -0
  88. package/dist/input-mask/input-mask.d.ts +7 -2
  89. package/dist/input-mask/input-mask.js +15 -41
  90. package/dist/input-mask/llms.txt +3 -0
  91. package/dist/input-number/input-number.d.ts +3 -1
  92. package/dist/input-number/input-number.js +15 -11
  93. package/dist/input-number/llms.txt +5 -2
  94. package/dist/input-otp/input-otp.d.ts +16 -3
  95. package/dist/input-otp/input-otp.js +43 -19
  96. package/dist/input-otp/llms.txt +11 -0
  97. package/dist/internal/mask.d.ts +14 -0
  98. package/dist/internal/mask.js +62 -0
  99. package/dist/internal/number-group.d.ts +22 -0
  100. package/dist/internal/number-group.js +53 -0
  101. package/dist/internal/option-key.d.ts +6 -0
  102. package/dist/internal/option-key.js +8 -0
  103. package/dist/internal/overlay.js +37 -7
  104. package/dist/internal/password-strength.d.ts +6 -0
  105. package/dist/internal/password-strength.js +74 -0
  106. package/dist/internal/scroll-lock.d.ts +6 -0
  107. package/dist/internal/scroll-lock.js +35 -0
  108. package/dist/internal/slot-children.d.ts +13 -0
  109. package/dist/internal/slot-children.js +31 -0
  110. package/dist/internal/step.d.ts +12 -0
  111. package/dist/internal/step.js +31 -0
  112. package/dist/internal/tween.js +2 -3
  113. package/dist/internal/use-controllable-state.d.ts +5 -0
  114. package/dist/internal/use-controllable-state.js +25 -4
  115. package/dist/internal/use-modal-focus.d.ts +1 -0
  116. package/dist/internal/use-modal-focus.js +12 -1
  117. package/dist/knob/knob.d.ts +5 -2
  118. package/dist/knob/knob.js +15 -14
  119. package/dist/knob/llms.txt +4 -2
  120. package/dist/listbox/listbox.d.ts +3 -1
  121. package/dist/listbox/listbox.js +30 -10
  122. package/dist/listbox/llms.txt +3 -0
  123. package/dist/map/llms.txt +5 -1
  124. package/dist/map/map-create-dialog.js +30 -22
  125. package/dist/map/map-hours-field.js +1 -0
  126. package/dist/map/map-image-field.js +2 -1
  127. package/dist/map/map-legend.js +1 -0
  128. package/dist/map/map-poi-card.js +2 -1
  129. package/dist/map/map-search.js +54 -30
  130. package/dist/map/map.d.ts +6 -1
  131. package/dist/map/map.js +62 -34
  132. package/dist/megamenu/llms.txt +11 -0
  133. package/dist/megamenu/megamenu.d.ts +7 -1
  134. package/dist/megamenu/megamenu.js +15 -6
  135. package/dist/menu/menu-rows.js +1 -0
  136. package/dist/menu/menu.js +10 -3
  137. package/dist/menubar/llms.txt +11 -0
  138. package/dist/menubar/menubar.d.ts +7 -1
  139. package/dist/menubar/menubar.js +15 -3
  140. package/dist/meter-group/meter-group.js +1 -0
  141. package/dist/order-list/llms.txt +3 -0
  142. package/dist/order-list/order-list.js +21 -6
  143. package/dist/paginator/paginator.js +7 -4
  144. package/dist/panel/panel.js +5 -3
  145. package/dist/password/llms.txt +6 -2
  146. package/dist/password/password.js +4 -14
  147. package/dist/peek/llms.txt +1 -1
  148. package/dist/peek/peek.js +37 -15
  149. package/dist/pick-list/llms.txt +3 -0
  150. package/dist/pick-list/pick-list.js +19 -3
  151. package/dist/popover/llms.txt +3 -0
  152. package/dist/popover/popover.d.ts +3 -1
  153. package/dist/popover/popover.js +10 -3
  154. package/dist/progress/progress.js +1 -0
  155. package/dist/public-api.d.ts +1 -1
  156. package/dist/radio-group/llms.txt +3 -0
  157. package/dist/radio-group/radio-group.d.ts +3 -1
  158. package/dist/radio-group/radio-group.js +13 -2
  159. package/dist/rating/rating.js +1 -0
  160. package/dist/rich-text/llms.txt +14 -0
  161. package/dist/rich-text/rich-text.d.ts +3 -9
  162. package/dist/rich-text/rich-text.js +241 -96
  163. package/dist/rich-text/sanitize.d.ts +9 -0
  164. package/dist/rich-text/sanitize.js +77 -29
  165. package/dist/roadmap/llms.txt +9 -3
  166. package/dist/roadmap/roadmap.js +88 -11
  167. package/dist/scroll-top/scroll-top.js +1 -0
  168. package/dist/select/llms.txt +3 -0
  169. package/dist/select/select.d.ts +3 -1
  170. package/dist/select/select.js +46 -32
  171. package/dist/shared/types.d.ts +21 -0
  172. package/dist/shared/types.js +9 -1
  173. package/dist/sidenav/sidenav.d.ts +1 -2
  174. package/dist/sidenav/sidenav.js +4 -3
  175. package/dist/slider/llms.txt +3 -0
  176. package/dist/slider/slider.d.ts +3 -1
  177. package/dist/slider/slider.js +15 -4
  178. package/dist/speed-dial/speed-dial.js +2 -1
  179. package/dist/spinner/spinner.js +1 -0
  180. package/dist/split-button/split-button.js +10 -3
  181. package/dist/splitter/splitter.js +23 -8
  182. package/dist/stepper/llms.txt +3 -0
  183. package/dist/stepper/stepper.js +9 -5
  184. package/dist/switch/llms.txt +3 -0
  185. package/dist/switch/switch.d.ts +1 -1
  186. package/dist/switch/switch.js +9 -2
  187. package/dist/switch-group/llms.txt +3 -0
  188. package/dist/switch-group/switch-group.d.ts +3 -1
  189. package/dist/switch-group/switch-group.js +5 -3
  190. package/dist/tabs/llms.txt +3 -0
  191. package/dist/tabs/tabs.js +30 -14
  192. package/dist/textarea/textarea.js +1 -0
  193. package/dist/toast/llms.txt +3 -0
  194. package/dist/toast/toast-container.js +1 -0
  195. package/dist/toast/toast-store.d.ts +5 -1
  196. package/dist/toast/toast-store.js +1 -1
  197. package/dist/tooltip/llms.txt +5 -3
  198. package/dist/tooltip/tooltip.d.ts +3 -2
  199. package/dist/tooltip/tooltip.js +148 -55
  200. package/dist/tree/llms.txt +1 -0
  201. package/dist/tree/tree.d.ts +6 -1
  202. package/dist/tree/tree.js +14 -7
  203. package/package.json +15 -14
@@ -1,5 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useId, useRef, useState } from 'react';
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
- const needle = term.trim();
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
- setLoading(true);
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
- geocoder
67
- .search(needle, {
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
- }, [term, geocoder, minLength, debounce, limit, viewbox, countryCodes, locale, applyResults]);
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}-panel` : undefined, "aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined, onChange: (event) => {
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, boundsOf, clusterLocations, countByCategory, haversine, isCluster, isSplittable, renderClusterHtml, renderMarkerHtml, renderUserMarkerHtml, resolveCountryView, } from '@konce-pt/map';
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
- const [engineKind, setEngineKind] = useState(null);
57
- const [ready, setReady] = useState(false);
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
- latest.current = { entries, allowCreate, popup, contextTarget, onLocationClick, onClusterClick, onViewChange };
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
- setReady(true);
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
- setReady(false);
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 || !engineRef.current || !ready)
374
+ if (!popup || !liveEngine)
347
375
  return null;
348
- return engineRef.current.project(popup.location.at);
349
- }, [viewportTick, popup, ready]);
376
+ return liveEngine.project(popup.location.at);
377
+ }, [viewportTick, popup, liveEngine]);
350
378
  const contextPosition = useMemo(() => {
351
379
  void viewportTick;
352
- if (!contextTarget || !engineRef.current || !ready)
380
+ if (!contextTarget || !liveEngine)
353
381
  return null;
354
- return engineRef.current.project(contextTarget.at);
355
- }, [viewportTick, contextTarget, ready]);
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": engineKind ?? undefined, 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: () => {
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);
@@ -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
- const barRef = useRef(null);
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
- 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: barRef, children: items.map((item, index) => {
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: barRef.current, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-megamenu__panel", role: "menu", id: panelId, "aria-label": openIndex === null ? undefined : items[openIndex]?.label, onKeyDown: onPanelKeyDown, children: columns.map((column, columnIndex) => {
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
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { KptAvatar } from "../avatar/avatar.js";
3
4
  import { KptBadge } from "../badge/badge.js";
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: triggerRef, onClick: () => {
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: triggerRef.current, onClose: close, children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-menu__panel", role: "menu", onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-menu", onPick: pick }) }) })] }));
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
  }
@@ -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;
@@ -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: buttonsRef.current[index] ?? null, onClose: closeAll, children: _jsx("div", { ref: panelRef, className: "kpt-menubar__dropdown", role: "menu", id: panelId(index), "aria-label": item.label, onKeyDown: (event) => onPanelKeyDown(event, index), children: (item.items ?? []).map((sub, subIndex) => sub.separator ? (_jsx("div", { className: "kpt-menubar__separator", role: "separator" }, subIndex)) : isLink(sub) ? (_jsxs("a", { className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, href: sub.url, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex)) : (_jsxs("button", { type: "button", className: cn('kpt-menubar__item', sub.danger && 'kpt-menubar__item--danger'), role: "menuitem", tabIndex: -1, disabled: sub.disabled, onClick: () => pick(sub, index), children: [sub.icon ? _jsx(KptIcon, { name: sub.icon }) : null, _jsx("span", { children: sub.label })] }, subIndex))) }) })] }, index));
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
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -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.