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