@dnanpm/ui-components 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/components/Accordion/Accordion.css +6 -3
  2. package/dist/components/AccordionItem/AccordionItem.css +6 -3
  3. package/dist/components/Breadcrumb/Breadcrumb.css +6 -3
  4. package/dist/components/Button/Button.css +2 -3
  5. package/dist/components/ButtonCard/ButtonCard.css +2 -3
  6. package/dist/components/ContentCard/ContentCard.cjs +96 -0
  7. package/dist/components/ContentCard/ContentCard.cjs.map +1 -0
  8. package/dist/components/ContentCard/ContentCard.css +249 -0
  9. package/dist/components/ContentCard/ContentCard.d.ts +3 -0
  10. package/dist/components/ContentCard/ContentCard.d.ts.map +1 -0
  11. package/dist/components/ContentCard/ContentCard.js +96 -0
  12. package/dist/components/ContentCard/ContentCard.js.map +1 -0
  13. package/dist/components/ContentCard/ContentCard.module.css.cjs +24 -0
  14. package/dist/components/ContentCard/ContentCard.module.css.cjs.map +1 -0
  15. package/dist/components/ContentCard/ContentCard.module.css.js +25 -0
  16. package/dist/components/ContentCard/ContentCard.module.css.js.map +1 -0
  17. package/dist/components/ContentCard/ContentCard.types.d.ts +99 -0
  18. package/dist/components/ContentCard/ContentCard.types.d.ts.map +1 -0
  19. package/dist/components/ContentCard/index.d.ts +3 -0
  20. package/dist/components/ContentCard/index.d.ts.map +1 -0
  21. package/dist/components/Drawer/Drawer.css +2 -3
  22. package/dist/components/Footer/Footer.css +6 -3
  23. package/dist/components/FunctionButton/FunctionButton.css +2 -3
  24. package/dist/components/Hero/Hero.cjs +24 -26
  25. package/dist/components/Hero/Hero.cjs.map +1 -1
  26. package/dist/components/Hero/Hero.css +81 -74
  27. package/dist/components/Hero/Hero.js +24 -26
  28. package/dist/components/Hero/Hero.js.map +1 -1
  29. package/dist/components/Hero/Hero.module.css.cjs +20 -17
  30. package/dist/components/Hero/Hero.module.css.cjs.map +1 -1
  31. package/dist/components/Hero/Hero.module.css.js +20 -17
  32. package/dist/components/Hero/Hero.module.css.js.map +1 -1
  33. package/dist/components/IconButton/IconButton.css +2 -3
  34. package/dist/components/Label/Label.css +2 -3
  35. package/dist/components/MainNavigation/MainNavigation.cjs +407 -96
  36. package/dist/components/MainNavigation/MainNavigation.cjs.map +1 -1
  37. package/dist/components/MainNavigation/MainNavigation.css +255 -151
  38. package/dist/components/MainNavigation/MainNavigation.d.ts +1 -1
  39. package/dist/components/MainNavigation/MainNavigation.d.ts.map +1 -1
  40. package/dist/components/MainNavigation/MainNavigation.js +409 -98
  41. package/dist/components/MainNavigation/MainNavigation.js.map +1 -1
  42. package/dist/components/MainNavigation/MainNavigation.module.css.cjs +78 -68
  43. package/dist/components/MainNavigation/MainNavigation.module.css.cjs.map +1 -1
  44. package/dist/components/MainNavigation/MainNavigation.module.css.js +78 -68
  45. package/dist/components/MainNavigation/MainNavigation.module.css.js.map +1 -1
  46. package/dist/components/MainNavigation/MainNavigation.types.d.ts +64 -17
  47. package/dist/components/MainNavigation/MainNavigation.types.d.ts.map +1 -1
  48. package/dist/components/MainNavigation/MainNavigation.utils.cjs +20 -0
  49. package/dist/components/MainNavigation/MainNavigation.utils.cjs.map +1 -1
  50. package/dist/components/MainNavigation/MainNavigation.utils.d.ts +10 -1
  51. package/dist/components/MainNavigation/MainNavigation.utils.d.ts.map +1 -1
  52. package/dist/components/MainNavigation/MainNavigation.utils.js +20 -0
  53. package/dist/components/MainNavigation/MainNavigation.utils.js.map +1 -1
  54. package/dist/components/MainNavigation/index.d.ts +1 -1
  55. package/dist/components/MainNavigation/index.d.ts.map +1 -1
  56. package/dist/components/Modal/Modal.css +2 -3
  57. package/dist/components/Notification/Notification.css +6 -3
  58. package/dist/components/Pictograph/Pictograph.css +6 -3
  59. package/dist/components/Pictograph/PictographItem.css +6 -3
  60. package/dist/components/Quote/Quote.css +2 -3
  61. package/dist/components/Spinner/Spinner.css +6 -3
  62. package/dist/index.cjs +2 -0
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.ts +3 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/styles/utils/elevation.css +2 -3
  69. package/dist/styles/utils/focus.css +25 -22
  70. package/dist/styles/utils/focus.module.css.cjs +3 -3
  71. package/dist/styles/utils/focus.module.css.js +3 -3
  72. package/dist/styles/utils/overlay.css +2 -3
  73. package/dist/styles/utils/text-decoration.css +2 -3
  74. package/dist/styles.css +17 -7
  75. package/package.json +1 -1
@@ -11,6 +11,7 @@ const MainNavigation_module = require("./MainNavigation.module.css.cjs");
11
11
  const MainNavigation_utils = require("./MainNavigation.utils.cjs");
12
12
  const cn = require("../../utils/cn/cn.cjs");
13
13
  const Button = require("../Button/Button.cjs");
14
+ const Drawer = require("../Drawer/Drawer.cjs");
14
15
  const IconButton = require("../IconButton/IconButton.cjs");
15
16
  const defaultOrganizations = [];
16
17
  const defaultItems = [];
@@ -21,25 +22,30 @@ const mobileBodyScrollLockClass = "dds-un-scrollable";
21
22
  const defaultOverflowCategoryLabel = "Muut kategoriat";
22
23
  const desktopMinWidthMediaQuery = "(min-width: 1024px)";
23
24
  const defaultRenderLink = (props) => /* @__PURE__ */ jsxRuntime.jsx("a", { ...props });
25
+ const defaultRenderLanguageLink = (props) => /* @__PURE__ */ jsxRuntime.jsx("a", { ...props });
24
26
  const MainNavigation = ({
25
27
  organizations = defaultOrganizations,
26
28
  items = defaultItems,
27
- loginHref = "#",
28
29
  loginLabel = "Kirjaudu",
29
30
  logoHref = "/",
30
- languageSelectorLabel = "FI",
31
+ languages,
32
+ renderLanguageLink = defaultRenderLanguageLink,
31
33
  searchLabel = "Haku",
32
34
  cartLabel = "Ostoskori",
33
- showLanguageSelector = false,
34
- showSearch = false,
35
- showCart = false,
36
35
  cartCounter,
37
36
  cartCounterLabel = "tuotetta ostoskorissa",
38
- onLanguageSelectorClick,
39
- onSearchClick,
40
- onCartClick,
41
- mobileMenuOpenAriaLabel = "Avaa valikko",
42
- mobileMenuCloseAriaLabel = "Sulje valikko",
37
+ defaultCartOpen = false,
38
+ cartOpen,
39
+ onCartOpenChange,
40
+ defaultOpenDropdown,
41
+ onSearchOpenChange,
42
+ onLanguageOpenChange,
43
+ onLoginOpenChange,
44
+ searchDropdownContent,
45
+ loginDropdownContent,
46
+ cartContent,
47
+ mobileMenuOpenAriaLabel = "Avaa päävalikko",
48
+ mobileMenuCloseAriaLabel = "Sulje päävalikko",
43
49
  organizationStripAriaLabel = "Sivustot",
44
50
  logoAriaLabel = "Siirry etusivulle",
45
51
  mobilePrimaryNavigationAriaLabel = "Päävalikko",
@@ -61,8 +67,17 @@ const MainNavigation = ({
61
67
  const [openTopLevelItemIndex, setOpenTopLevelItemIndex] = react.useState(
62
68
  defaultOpenItemIndex
63
69
  );
64
- const [isDesktopHeaderOpen, setIsDesktopHeaderOpen] = react.useState(true);
70
+ const [isHeaderOpen, setIsHeaderOpen] = react.useState(true);
65
71
  const [mobileActiveCategoryIndex, setMobileActiveCategoryIndex] = react.useState(null);
72
+ const [internalCartOpen, setInternalCartOpen] = react.useState(defaultCartOpen);
73
+ const isCartControlled = cartOpen !== void 0;
74
+ const isCartOpen = cartOpen ?? internalCartOpen;
75
+ const hasInitialNavigationSurface = (cartOpen ?? defaultCartOpen) || defaultMobileMenuOpen || defaultOpenItemIndex != null && items[defaultOpenItemIndex]?.panel != null;
76
+ const [openUtilityDropdown, setOpenUtilityDropdown] = react.useState(hasInitialNavigationSurface ? null : defaultOpenDropdown ?? null);
77
+ const openUtilityDropdownRef = react.useRef(openUtilityDropdown);
78
+ const searchDropdownId = react.useId();
79
+ const languageDropdownId = react.useId();
80
+ const loginDropdownId = react.useId();
66
81
  const [isMobileSecondPanelClosing, setIsMobileSecondPanelClosing] = react.useState(false);
67
82
  const [isMobileThirdPanelClosing, setIsMobileThirdPanelClosing] = react.useState(false);
68
83
  const mobileSecondPanelCloseTimeoutRef = react.useRef(null);
@@ -78,6 +93,9 @@ const MainNavigation = ({
78
93
  );
79
94
  const headerElementRef = react.useRef(null);
80
95
  const desktopPanelTriggerButtonRefs = react.useRef([]);
96
+ const searchTriggerRef = react.useRef(null);
97
+ const languageTriggerRef = react.useRef(null);
98
+ const loginTriggerRef = react.useRef(null);
81
99
  const mobilePrimaryNavRef = react.useRef(null);
82
100
  const mobileMenuToggleButtonRef = react.useRef(null);
83
101
  const mobileModalPanelRef = react.useRef(null);
@@ -87,9 +105,14 @@ const MainNavigation = ({
87
105
  const mobileLevelTwoCategoryButtonRefs = react.useRef([]);
88
106
  const focusMobilePanelBackOnOpenRef = react.useRef(false);
89
107
  const focusMobileThirdPanelBackOnOpenRef = react.useRef(false);
108
+ const lastInteractionWasKeyboardRef = react.useRef(false);
90
109
  const focusReturnToMobileCategoryTimeoutRef = react.useRef(
91
110
  null
92
111
  );
112
+ const mobileMenuOpenRef = react.useRef(mobileMenuOpen);
113
+ const openTopLevelItemIndexRef = react.useRef(openTopLevelItemIndex);
114
+ const lastAppliedViewportModeRef = react.useRef(null);
115
+ const internalCartOpenRef = react.useRef(internalCartOpen);
93
116
  const setHeaderRef = react.useCallback(
94
117
  (node) => {
95
118
  headerElementRef.current = node;
@@ -132,19 +155,18 @@ const MainNavigation = ({
132
155
  let previousScrollY = window.pageYOffset;
133
156
  let headerHeight = parseFloat(window.getComputedStyle(headerElement).height.replace("px", "")) || headerElement.offsetHeight;
134
157
  const openHeader = () => {
135
- setIsDesktopHeaderOpen(true);
158
+ setIsHeaderOpen(true);
136
159
  };
137
160
  const closeHeader = () => {
138
- setIsDesktopHeaderOpen(false);
161
+ setIsHeaderOpen(false);
162
+ };
163
+ const headerHasVisibleFocus = () => {
164
+ const active = document.activeElement;
165
+ return lastInteractionWasKeyboardRef.current && active instanceof HTMLElement && headerElement.contains(active);
139
166
  };
140
167
  const handleScroll = () => {
141
168
  const currentScrollY = window.pageYOffset;
142
- if (!desktopQuery.matches) {
143
- openHeader();
144
- previousScrollY = currentScrollY;
145
- return;
146
- }
147
- if (mobileMenuOpen || openTopLevelItemIndex != null) {
169
+ if (mobileMenuOpen || openTopLevelItemIndex != null || openUtilityDropdown != null || headerHasVisibleFocus()) {
148
170
  openHeader();
149
171
  previousScrollY = currentScrollY;
150
172
  return;
@@ -158,6 +180,11 @@ const MainNavigation = ({
158
180
  }
159
181
  previousScrollY = currentScrollY;
160
182
  };
183
+ const handleFocusIn = () => {
184
+ if (headerHasVisibleFocus()) {
185
+ openHeader();
186
+ }
187
+ };
161
188
  const handleViewportChange = () => {
162
189
  openHeader();
163
190
  previousScrollY = window.pageYOffset;
@@ -165,14 +192,33 @@ const MainNavigation = ({
165
192
  };
166
193
  window.addEventListener("scroll", handleScroll, { passive: true });
167
194
  window.addEventListener("resize", handleViewportChange);
195
+ document.addEventListener("focusin", handleFocusIn);
168
196
  desktopQuery.addEventListener("change", handleViewportChange);
169
197
  handleScroll();
170
198
  return () => {
171
199
  window.removeEventListener("scroll", handleScroll);
172
200
  window.removeEventListener("resize", handleViewportChange);
201
+ document.removeEventListener("focusin", handleFocusIn);
173
202
  desktopQuery.removeEventListener("change", handleViewportChange);
174
203
  };
175
- }, [mobileMenuOpen, openTopLevelItemIndex]);
204
+ }, [mobileMenuOpen, openTopLevelItemIndex, openUtilityDropdown]);
205
+ react.useEffect(() => {
206
+ if (typeof document === "undefined") {
207
+ return;
208
+ }
209
+ const handleKeyDown = () => {
210
+ lastInteractionWasKeyboardRef.current = true;
211
+ };
212
+ const handlePointerDown = () => {
213
+ lastInteractionWasKeyboardRef.current = false;
214
+ };
215
+ document.addEventListener("keydown", handleKeyDown, true);
216
+ document.addEventListener("pointerdown", handlePointerDown, true);
217
+ return () => {
218
+ document.removeEventListener("keydown", handleKeyDown, true);
219
+ document.removeEventListener("pointerdown", handlePointerDown, true);
220
+ };
221
+ }, []);
176
222
  react.useEffect(() => {
177
223
  const browserWindow = globalThis.window;
178
224
  const browserDocument = globalThis.document;
@@ -210,6 +256,11 @@ const MainNavigation = ({
210
256
  }
211
257
  const desktopQuery = window.matchMedia(desktopMinWidthMediaQuery);
212
258
  const applyViewportModeChange = ({ matches: isDesktopViewport2 }) => {
259
+ const viewportMode = isDesktopViewport2 ? "desktop" : "mobile";
260
+ if (lastAppliedViewportModeRef.current === viewportMode) {
261
+ return;
262
+ }
263
+ lastAppliedViewportModeRef.current = viewportMode;
213
264
  if (isDesktopViewport2) {
214
265
  clearTimeout(mobileSecondPanelCloseTimeoutRef.current ?? void 0);
215
266
  mobileSecondPanelCloseTimeoutRef.current = null;
@@ -217,23 +268,23 @@ const MainNavigation = ({
217
268
  mobileThirdPanelCloseTimeoutRef.current = null;
218
269
  clearTimeout(resetMobileThirdPanelScrollTimeoutRef.current ?? void 0);
219
270
  resetMobileThirdPanelScrollTimeoutRef.current = null;
220
- setMobileMenuOpen((previous) => {
221
- if (previous) {
222
- onMobileMenuToggle?.(false);
223
- }
224
- return false;
225
- });
271
+ const wasMobileMenuOpen = mobileMenuOpenRef.current;
272
+ mobileMenuOpenRef.current = false;
273
+ if (wasMobileMenuOpen) {
274
+ onMobileMenuToggle?.(false);
275
+ }
276
+ setMobileMenuOpen(false);
226
277
  setMobileActiveCategoryIndex(null);
227
278
  setIsMobileSecondPanelClosing(false);
228
279
  setIsMobileThirdPanelClosing(false);
229
280
  return;
230
281
  }
231
- setOpenTopLevelItemIndex((previous) => {
232
- if (previous !== null) {
233
- onDesktopItemToggle?.(null);
234
- }
235
- return null;
236
- });
282
+ const hadOpenDesktopItem = openTopLevelItemIndexRef.current !== null;
283
+ openTopLevelItemIndexRef.current = null;
284
+ if (hadOpenDesktopItem) {
285
+ onDesktopItemToggle?.(null);
286
+ }
287
+ setOpenTopLevelItemIndex(null);
237
288
  };
238
289
  applyViewportModeChange(desktopQuery);
239
290
  desktopQuery.addEventListener("change", applyViewportModeChange);
@@ -242,6 +293,7 @@ const MainNavigation = ({
242
293
  };
243
294
  }, [onDesktopItemToggle, onMobileMenuToggle]);
244
295
  const resetMobilePanelState = react.useCallback(() => {
296
+ openTopLevelItemIndexRef.current = null;
245
297
  setOpenTopLevelItemIndex(null);
246
298
  setMobileActiveCategoryIndex(null);
247
299
  setIsMobileSecondPanelClosing(false);
@@ -258,20 +310,20 @@ const MainNavigation = ({
258
310
  return;
259
311
  }
260
312
  const shouldAnimateMobileClose = mobileMenuOpen;
313
+ const wasMobileMenuOpen = mobileMenuOpenRef.current;
314
+ const openDesktopItemIndex = openTopLevelItemIndexRef.current;
315
+ mobileMenuOpenRef.current = false;
316
+ openTopLevelItemIndexRef.current = null;
317
+ if (wasMobileMenuOpen) {
318
+ onMobileMenuToggle?.(false);
319
+ }
320
+ if (openDesktopItemIndex !== null) {
321
+ onDesktopItemToggle?.(null);
322
+ }
261
323
  clearTimeout(mobileMenuCloseResetTimeoutRef.current ?? void 0);
262
324
  setIsMobileMenuClosing(shouldAnimateMobileClose);
263
- setMobileMenuOpen((previous) => {
264
- if (previous) {
265
- onMobileMenuToggle?.(false);
266
- }
267
- return false;
268
- });
269
- setOpenTopLevelItemIndex((previous) => {
270
- if (previous !== null) {
271
- onDesktopItemToggle?.(null);
272
- }
273
- return null;
274
- });
325
+ setMobileMenuOpen(false);
326
+ setOpenTopLevelItemIndex(null);
275
327
  if (shouldAnimateMobileClose) {
276
328
  mobileMenuCloseResetTimeoutRef.current = setTimeout(() => {
277
329
  resetMobilePanelState();
@@ -294,19 +346,21 @@ const MainNavigation = ({
294
346
  closeMobileMenu();
295
347
  return;
296
348
  }
349
+ closeUtilityDropdown();
350
+ mobileMenuOpenRef.current = true;
297
351
  setMobileMenuOpen(true);
298
352
  setIsMobileMenuClosing(false);
299
353
  onMobileMenuToggle?.(true);
300
354
  };
301
355
  const toggleTopLevelItem = (index) => {
302
- setOpenTopLevelItemIndex((previous) => {
303
- const next = previous === index ? null : index;
304
- setMobileActiveCategoryIndex(null);
305
- setIsMobileSecondPanelClosing(false);
306
- setIsMobileThirdPanelClosing(false);
307
- onDesktopItemToggle?.(next);
308
- return next;
309
- });
356
+ closeUtilityDropdown();
357
+ const next = openTopLevelItemIndex === index ? null : index;
358
+ openTopLevelItemIndexRef.current = next;
359
+ setOpenTopLevelItemIndex(next);
360
+ setMobileActiveCategoryIndex(null);
361
+ setIsMobileSecondPanelClosing(false);
362
+ setIsMobileThirdPanelClosing(false);
363
+ onDesktopItemToggle?.(next);
310
364
  };
311
365
  const isDesktopViewport = react.useCallback(() => {
312
366
  return MainNavigation_utils.isDesktopViewportByEnvironment(globalThis.window);
@@ -358,6 +412,23 @@ const MainNavigation = ({
358
412
  },
359
413
  [getDesktopPanelInteractiveElements, isFocusable]
360
414
  );
415
+ const getUtilityDropdownFocusLoopElements = react.useCallback(
416
+ (kind) => {
417
+ const focusLoopElements = /* @__PURE__ */ new Set();
418
+ const triggerButton = kind === "search" ? searchTriggerRef.current : kind === "language" ? languageTriggerRef.current : loginTriggerRef.current;
419
+ if (triggerButton != null && isFocusable(triggerButton)) {
420
+ focusLoopElements.add(triggerButton);
421
+ }
422
+ getFocusableElements(
423
+ headerElementRef.current,
424
+ `.${MainNavigation_module["main-navigation__dropdown"]} a[href], .${MainNavigation_module["main-navigation__dropdown"]} button:not([disabled]), .${MainNavigation_module["main-navigation__dropdown"]} input:not([disabled]), .${MainNavigation_module["main-navigation__dropdown"]} select:not([disabled]), .${MainNavigation_module["main-navigation__dropdown"]} textarea:not([disabled]), .${MainNavigation_module["main-navigation__dropdown"]} [tabindex]:not([tabindex="-1"])`
425
+ ).forEach((element) => {
426
+ focusLoopElements.add(element);
427
+ });
428
+ return Array.from(focusLoopElements);
429
+ },
430
+ [getFocusableElements, isFocusable]
431
+ );
361
432
  const getMobileFocusableElements = react.useCallback(() => {
362
433
  const focusableElements = [];
363
434
  const addFocusableElements = (elements) => {
@@ -401,6 +472,26 @@ const MainNavigation = ({
401
472
  resetMobileThirdPanelScrollTimeoutRef.current = null;
402
473
  }, 0);
403
474
  }, []);
475
+ const notifyUtilityOpenChange = react.useCallback(
476
+ (kind, open) => {
477
+ if (kind === "search") {
478
+ onSearchOpenChange?.(open);
479
+ } else if (kind === "language") {
480
+ onLanguageOpenChange?.(open);
481
+ } else {
482
+ onLoginOpenChange?.(open);
483
+ }
484
+ },
485
+ [onLanguageOpenChange, onLoginOpenChange, onSearchOpenChange]
486
+ );
487
+ const closeUtilityDropdown = react.useCallback(() => {
488
+ const openDropdown = openUtilityDropdownRef.current;
489
+ if (openDropdown != null) {
490
+ openUtilityDropdownRef.current = null;
491
+ notifyUtilityOpenChange(openDropdown, false);
492
+ }
493
+ setOpenUtilityDropdown(null);
494
+ }, [notifyUtilityOpenChange]);
404
495
  const closeMenus = react.useCallback(() => {
405
496
  clearTimeout(mobileSecondPanelCloseTimeoutRef.current ?? void 0);
406
497
  mobileSecondPanelCloseTimeoutRef.current = null;
@@ -416,8 +507,14 @@ const MainNavigation = ({
416
507
  focusReturnToMobileTopLevelItemTimeoutRef.current = null;
417
508
  clearTimeout(focusReturnToMobileCategoryTimeoutRef.current ?? void 0);
418
509
  focusReturnToMobileCategoryTimeoutRef.current = null;
510
+ const openDropdown = openUtilityDropdownRef.current;
511
+ if (openDropdown != null) {
512
+ openUtilityDropdownRef.current = null;
513
+ notifyUtilityOpenChange(openDropdown, false);
514
+ }
515
+ setOpenUtilityDropdown(null);
419
516
  closeMobileMenu();
420
- }, [closeMobileMenu]);
517
+ }, [closeMobileMenu, notifyUtilityOpenChange]);
421
518
  const closeMobilePanel = react.useCallback(
422
519
  ({
423
520
  isClosing,
@@ -466,9 +563,87 @@ const MainNavigation = ({
466
563
  const openItem = openTopLevelItemIndex != null ? items[openTopLevelItemIndex] : void 0;
467
564
  const openPanel = openItem?.panel;
468
565
  const isMobilePanelClosingOut = isMobileMenuClosing && !mobileMenuOpen;
469
- const showPageOverlay = mobileMenuOpen || openPanel != null || isMobilePanelClosingOut;
470
566
  const isOverlayClosingOut = isMobilePanelClosingOut && openPanel == null;
567
+ const showSearch = searchDropdownContent != null && typeof searchDropdownContent !== "boolean";
568
+ const showLogin = loginDropdownContent != null && typeof loginDropdownContent !== "boolean";
569
+ const showCart = cartContent != null && typeof cartContent !== "boolean";
471
570
  const cartAccessibleLabel = cartCounter != null ? `${cartLabel} ${cartCounter} ${cartCounterLabel}` : cartLabel;
571
+ const currentLanguage = languages?.find((language) => language.current) ?? languages?.[0];
572
+ const alternativeLanguages = currentLanguage != null ? (languages ?? []).filter((language) => language.code !== currentLanguage.code) : [];
573
+ const showLanguage = currentLanguage != null && alternativeLanguages.length > 0;
574
+ const languageAnnouncements = currentLanguage != null ? MainNavigation_utils.getLanguageToggleAnnouncements(
575
+ (languages ?? []).map((language) => language.code),
576
+ currentLanguage.code
577
+ ) : [];
578
+ const activeUtilityDropdown = openUtilityDropdown === "search" && showSearch ? "search" : openUtilityDropdown === "language" && showLanguage ? "language" : openUtilityDropdown === "login" && showLogin ? "login" : null;
579
+ const isSearchOpen = activeUtilityDropdown === "search";
580
+ const isLanguageOpen = activeUtilityDropdown === "language";
581
+ const isLoginOpen = activeUtilityDropdown === "login";
582
+ const showPageOverlay = mobileMenuOpen || openPanel != null || isMobilePanelClosingOut || activeUtilityDropdown != null;
583
+ const toggleUtilityDropdown = (kind) => {
584
+ const previous = openUtilityDropdown;
585
+ const next = previous === kind ? null : kind;
586
+ if (previous != null && previous !== next) {
587
+ notifyUtilityOpenChange(previous, false);
588
+ }
589
+ if (next != null) {
590
+ notifyUtilityOpenChange(next, true);
591
+ closeMobileMenu();
592
+ if (isCartOpen) {
593
+ if (!isCartControlled) {
594
+ internalCartOpenRef.current = false;
595
+ setInternalCartOpen(false);
596
+ }
597
+ onCartOpenChange?.(false);
598
+ }
599
+ }
600
+ openUtilityDropdownRef.current = next;
601
+ setOpenUtilityDropdown(next);
602
+ };
603
+ const handleLanguageClick = () => {
604
+ toggleUtilityDropdown("language");
605
+ };
606
+ const handleSearchClick = () => {
607
+ toggleUtilityDropdown("search");
608
+ };
609
+ const handleLoginClick = () => {
610
+ toggleUtilityDropdown("login");
611
+ };
612
+ const handleCartClick = () => {
613
+ const next = !isCartOpen;
614
+ if (next) {
615
+ closeMenus();
616
+ }
617
+ if (!isCartControlled) {
618
+ internalCartOpenRef.current = next;
619
+ setInternalCartOpen(next);
620
+ }
621
+ onCartOpenChange?.(next);
622
+ };
623
+ const handleCartOpenChange = (open) => {
624
+ if (!isCartControlled) {
625
+ internalCartOpenRef.current = open;
626
+ setInternalCartOpen(open);
627
+ }
628
+ onCartOpenChange?.(open);
629
+ };
630
+ react.useEffect(() => {
631
+ if (isCartOpen) {
632
+ closeMenus();
633
+ }
634
+ }, [closeMenus, isCartOpen]);
635
+ react.useEffect(() => {
636
+ if (openUtilityDropdown != null && activeUtilityDropdown == null) {
637
+ closeUtilityDropdown();
638
+ }
639
+ }, [activeUtilityDropdown, closeUtilityDropdown, openUtilityDropdown]);
640
+ react.useEffect(() => {
641
+ if (!showCart && !isCartControlled && internalCartOpen && internalCartOpenRef.current) {
642
+ internalCartOpenRef.current = false;
643
+ setInternalCartOpen(false);
644
+ onCartOpenChange?.(false);
645
+ }
646
+ }, [internalCartOpen, isCartControlled, onCartOpenChange, showCart]);
472
647
  react.useEffect(() => {
473
648
  if (!showPageOverlay) {
474
649
  return;
@@ -576,6 +751,7 @@ const MainNavigation = ({
576
751
  closeTimeoutRef: mobileSecondPanelCloseTimeoutRef,
577
752
  onClosed: () => {
578
753
  const closedTopLevelItemIndex = openTopLevelItemIndex;
754
+ openTopLevelItemIndexRef.current = null;
579
755
  setOpenTopLevelItemIndex(null);
580
756
  setMobileActiveCategoryIndex(null);
581
757
  onDesktopItemToggle?.(null);
@@ -911,7 +1087,7 @@ const MainNavigation = ({
911
1087
  ref: setHeaderRef,
912
1088
  className: cn.cn(
913
1089
  MainNavigation_module["main-navigation"],
914
- isDesktopHeaderOpen && MainNavigation_module["main-navigation--open"],
1090
+ isHeaderOpen && MainNavigation_module["main-navigation--open"],
915
1091
  mobileMenuOpen && MainNavigation_module["main-navigation--menu-open"],
916
1092
  isMobileMenuClosing && MainNavigation_module["main-navigation--menu-closing"],
917
1093
  showPageOverlay && MainNavigation_module["main-navigation--overlay"],
@@ -921,6 +1097,21 @@ const MainNavigation = ({
921
1097
  ),
922
1098
  ...props,
923
1099
  onKeyDown: (event) => {
1100
+ if (event.key === "Tab" && activeUtilityDropdown != null) {
1101
+ const focusLoopElements = getUtilityDropdownFocusLoopElements(activeUtilityDropdown);
1102
+ if (focusLoopElements.length > 1) {
1103
+ event.preventDefault();
1104
+ const currentIndex = focusLoopElements.indexOf(
1105
+ document.activeElement
1106
+ );
1107
+ const direction = event.shiftKey ? -1 : 1;
1108
+ const nextIndex = currentIndex === -1 ? event.shiftKey ? focusLoopElements.length - 1 : 0 : (currentIndex + direction + focusLoopElements.length) % focusLoopElements.length;
1109
+ focusLoopElements[nextIndex]?.focus();
1110
+ } else {
1111
+ closeUtilityDropdown();
1112
+ }
1113
+ return;
1114
+ }
924
1115
  if (event.key === "Tab" && mobileMenuOpen && !isDesktopViewport()) {
925
1116
  const focusableElements = getMobileFocusableElements();
926
1117
  if (focusableElements.length > 0) {
@@ -948,7 +1139,12 @@ const MainNavigation = ({
948
1139
  }
949
1140
  }
950
1141
  if (event.key === "Escape") {
1142
+ const utilityDropdownToRestore = activeUtilityDropdown;
951
1143
  closeMenus();
1144
+ if (utilityDropdownToRestore != null) {
1145
+ const triggerButton = utilityDropdownToRestore === "search" ? searchTriggerRef.current : utilityDropdownToRestore === "language" ? languageTriggerRef.current : loginTriggerRef.current;
1146
+ triggerButton?.focus();
1147
+ }
952
1148
  }
953
1149
  props.onKeyDown?.(event);
954
1150
  },
@@ -1000,48 +1196,116 @@ const MainNavigation = ({
1000
1196
  }
1001
1197
  ),
1002
1198
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MainNavigation_module["main-navigation__icon-nav"], children: [
1003
- showLanguageSelector && /* @__PURE__ */ jsxRuntime.jsxs(
1004
- "button",
1005
- {
1006
- type: "button",
1007
- className: cn.cn(
1008
- MainNavigation_module["main-navigation__icon-nav-item"],
1009
- focus_module.default.focusableInset
1010
- ),
1011
- "aria-label": languageSelectorLabel,
1012
- onClick: onLanguageSelectorClick,
1013
- children: [
1014
- /* @__PURE__ */ jsxRuntime.jsx(icons.Globe, {}),
1015
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: MainNavigation_module["main-navigation__icon-nav-label"], children: languageSelectorLabel })
1016
- ]
1017
- }
1018
- ),
1019
- showSearch && /* @__PURE__ */ jsxRuntime.jsxs(
1020
- "button",
1021
- {
1022
- type: "button",
1023
- className: cn.cn(
1024
- MainNavigation_module["main-navigation__icon-nav-item"],
1025
- focus_module.default.focusableInset
1199
+ showLanguage && currentLanguage != null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MainNavigation_module["main-navigation__utility"], children: [
1200
+ /* @__PURE__ */ jsxRuntime.jsxs(
1201
+ "button",
1202
+ {
1203
+ ref: languageTriggerRef,
1204
+ type: "button",
1205
+ className: cn.cn(
1206
+ MainNavigation_module["main-navigation__icon-nav-item"],
1207
+ isLanguageOpen && MainNavigation_module["main-navigation__icon-nav-item--active"],
1208
+ focus_module.default.focusableInset
1209
+ ),
1210
+ "aria-expanded": isLanguageOpen,
1211
+ "aria-controls": isLanguageOpen ? languageDropdownId : void 0,
1212
+ onClick: handleLanguageClick,
1213
+ children: [
1214
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Globe, { "aria-hidden": "true" }),
1215
+ /* @__PURE__ */ jsxRuntime.jsx(
1216
+ "span",
1217
+ {
1218
+ "aria-hidden": "true",
1219
+ className: MainNavigation_module["main-navigation__icon-nav-label"],
1220
+ children: currentLanguage.code.toUpperCase()
1221
+ }
1222
+ ),
1223
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MainNavigation_module["main-navigation__sr-only"], children: languageAnnouncements.map((announcement, index) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1224
+ index > 0 ? " " : null,
1225
+ /* @__PURE__ */ jsxRuntime.jsx("span", { lang: announcement.lang, children: announcement.text })
1226
+ ] }, announcement.lang)) })
1227
+ ]
1228
+ }
1229
+ ),
1230
+ isLanguageOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1231
+ /* @__PURE__ */ jsxRuntime.jsx(
1232
+ "span",
1233
+ {
1234
+ "aria-hidden": "true",
1235
+ className: MainNavigation_module["main-navigation__dropdown-caret"]
1236
+ }
1026
1237
  ),
1027
- "aria-label": searchLabel,
1028
- onClick: onSearchClick,
1029
- children: [
1030
- /* @__PURE__ */ jsxRuntime.jsx(icons.Search, {}),
1031
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: MainNavigation_module["main-navigation__icon-nav-label"], children: searchLabel })
1032
- ]
1033
- }
1034
- ),
1238
+ /* @__PURE__ */ jsxRuntime.jsx(
1239
+ "div",
1240
+ {
1241
+ id: languageDropdownId,
1242
+ className: cn.cn(
1243
+ MainNavigation_module["main-navigation__dropdown"],
1244
+ MainNavigation_module["main-navigation__dropdown--language"],
1245
+ elevation_module.default.elevationMedium
1246
+ ),
1247
+ children: /* @__PURE__ */ jsxRuntime.jsx("ul", { className: MainNavigation_module["main-navigation__language-list"], children: alternativeLanguages.map((language) => /* @__PURE__ */ jsxRuntime.jsx(
1248
+ "li",
1249
+ {
1250
+ className: MainNavigation_module["main-navigation__language-list-item"],
1251
+ children: renderLanguageLink({
1252
+ href: MainNavigation_utils.toNavigableHref(language.href),
1253
+ lang: language.code,
1254
+ "aria-label": language.ariaLabel ?? MainNavigation_utils.languageLinkAriaLabels[language.code],
1255
+ className: cn.cn(
1256
+ MainNavigation_module["main-navigation__language-link"],
1257
+ focus_module.default.focusable
1258
+ ),
1259
+ children: language.code.toUpperCase()
1260
+ })
1261
+ },
1262
+ language.code
1263
+ )) })
1264
+ }
1265
+ )
1266
+ ] })
1267
+ ] }),
1268
+ showSearch && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MainNavigation_module["main-navigation__utility"], children: [
1269
+ /* @__PURE__ */ jsxRuntime.jsxs(
1270
+ "button",
1271
+ {
1272
+ ref: searchTriggerRef,
1273
+ type: "button",
1274
+ className: cn.cn(
1275
+ MainNavigation_module["main-navigation__icon-nav-item"],
1276
+ isSearchOpen && MainNavigation_module["main-navigation__icon-nav-item--active"],
1277
+ focus_module.default.focusableInset
1278
+ ),
1279
+ "aria-label": searchLabel,
1280
+ "aria-expanded": isSearchOpen,
1281
+ "aria-controls": isSearchOpen ? searchDropdownId : void 0,
1282
+ onClick: handleSearchClick,
1283
+ children: [
1284
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Search, {}),
1285
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: MainNavigation_module["main-navigation__icon-nav-label"], children: searchLabel })
1286
+ ]
1287
+ }
1288
+ ),
1289
+ isSearchOpen && /* @__PURE__ */ jsxRuntime.jsx(
1290
+ "span",
1291
+ {
1292
+ "aria-hidden": "true",
1293
+ className: MainNavigation_module["main-navigation__dropdown-caret"]
1294
+ }
1295
+ )
1296
+ ] }),
1035
1297
  showCart && /* @__PURE__ */ jsxRuntime.jsxs(
1036
1298
  "button",
1037
1299
  {
1038
1300
  type: "button",
1039
1301
  className: cn.cn(
1040
1302
  MainNavigation_module["main-navigation__icon-nav-item"],
1303
+ isCartOpen && MainNavigation_module["main-navigation__icon-nav-item--active"],
1041
1304
  focus_module.default.focusableInset
1042
1305
  ),
1043
1306
  "aria-label": cartAccessibleLabel,
1044
- onClick: onCartClick,
1307
+ "aria-expanded": isCartOpen,
1308
+ onClick: handleCartClick,
1045
1309
  children: [
1046
1310
  /* @__PURE__ */ jsxRuntime.jsx(icons.Cart, {}),
1047
1311
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: MainNavigation_module["main-navigation__icon-nav-label"], children: cartLabel }),
@@ -1056,16 +1320,53 @@ const MainNavigation = ({
1056
1320
  ]
1057
1321
  }
1058
1322
  ),
1059
- /* @__PURE__ */ jsxRuntime.jsx(
1060
- Button.Button,
1061
- {
1062
- href: MainNavigation_utils.toNavigableHref(loginHref),
1063
- size: "small",
1064
- renderLink: renderUtilityLink,
1065
- children: loginLabel
1066
- }
1067
- )
1323
+ showLogin && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: MainNavigation_module["main-navigation__utility"], children: [
1324
+ /* @__PURE__ */ jsxRuntime.jsx(
1325
+ Button.Button,
1326
+ {
1327
+ ref: loginTriggerRef,
1328
+ size: "small",
1329
+ "aria-expanded": isLoginOpen,
1330
+ "aria-controls": isLoginOpen ? loginDropdownId : void 0,
1331
+ onClick: handleLoginClick,
1332
+ children: loginLabel
1333
+ }
1334
+ ),
1335
+ isLoginOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1336
+ /* @__PURE__ */ jsxRuntime.jsx(
1337
+ "span",
1338
+ {
1339
+ "aria-hidden": "true",
1340
+ className: MainNavigation_module["main-navigation__dropdown-caret"]
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ jsxRuntime.jsx(
1344
+ "div",
1345
+ {
1346
+ id: loginDropdownId,
1347
+ className: cn.cn(
1348
+ MainNavigation_module["main-navigation__dropdown"],
1349
+ MainNavigation_module["main-navigation__dropdown--login"],
1350
+ elevation_module.default.elevationMedium
1351
+ ),
1352
+ children: loginDropdownContent
1353
+ }
1354
+ )
1355
+ ] })
1356
+ ] })
1068
1357
  ] }),
1358
+ showSearch && isSearchOpen && /* @__PURE__ */ jsxRuntime.jsx(
1359
+ "div",
1360
+ {
1361
+ id: searchDropdownId,
1362
+ className: cn.cn(
1363
+ MainNavigation_module["main-navigation__dropdown"],
1364
+ MainNavigation_module["main-navigation__dropdown--search"],
1365
+ elevation_module.default.elevationMedium
1366
+ ),
1367
+ children: searchDropdownContent
1368
+ }
1369
+ ),
1069
1370
  /* @__PURE__ */ jsxRuntime.jsx(
1070
1371
  "button",
1071
1372
  {
@@ -1085,6 +1386,16 @@ const MainNavigation = ({
1085
1386
  ] }),
1086
1387
  !mobileMenuOpen && renderPanel(
1087
1388
  cn.cn(MainNavigation_module["main-navigation__panel"], MainNavigation_module["main-navigation__panel--desktop"])
1389
+ ),
1390
+ showCart && /* @__PURE__ */ jsxRuntime.jsx(
1391
+ Drawer.Drawer,
1392
+ {
1393
+ open: isCartOpen,
1394
+ onOpenChange: handleCartOpenChange,
1395
+ title: cartLabel,
1396
+ size: "small",
1397
+ children: cartContent
1398
+ }
1088
1399
  )
1089
1400
  ]
1090
1401
  }