@telefonica/mistica 17.3.0 → 17.4.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 (334) hide show
  1. package/css/blau.css +24 -16
  2. package/css/esimflag.css +35 -18
  3. package/css/mistica.css +1 -1
  4. package/css/movistar.css +29 -18
  5. package/css/o2.css +26 -18
  6. package/css/telefonica.css +38 -18
  7. package/css/vivo-evolution.css +66 -20
  8. package/css/vivo.css +26 -18
  9. package/dist/accordion.css-mistica.js +10 -10
  10. package/dist/align.css-mistica.js +1 -1
  11. package/dist/autocomplete.css-mistica.js +4 -4
  12. package/dist/avatar.css-mistica.js +1 -1
  13. package/dist/badge.css-mistica.js +1 -1
  14. package/dist/box.css-mistica.js +13 -13
  15. package/dist/boxed.css-mistica.js +24 -24
  16. package/dist/boxed.d.ts +9 -0
  17. package/dist/boxed.js +18 -15
  18. package/dist/button-group.css-mistica.js +1 -1
  19. package/dist/button-layout.css-mistica.js +19 -19
  20. package/dist/button.css-mistica.js +50 -50
  21. package/dist/callout.css-mistica.js +13 -13
  22. package/dist/card-internal.css-mistica.js +22 -22
  23. package/dist/carousel.css-mistica.js +30 -30
  24. package/dist/checkbox.css-mistica.js +15 -15
  25. package/dist/chip.css-mistica.js +18 -18
  26. package/dist/circle.css-mistica.js +2 -2
  27. package/dist/community/advanced-data-card.css-mistica.js +19 -19
  28. package/dist/community/ai-card.css-mistica.js +12 -12
  29. package/dist/community/blocks.css-mistica.js +1 -1
  30. package/dist/community/cyber/example-component.css-mistica.js +1 -1
  31. package/dist/community/example-component.css-mistica.js +2 -2
  32. package/dist/community/skins/cyber-skin.js +19 -17
  33. package/dist/counter.css-mistica.js +6 -6
  34. package/dist/cover-hero.css-mistica.js +7 -7
  35. package/dist/credit-card-number-field.css-mistica.js +3 -3
  36. package/dist/date-field.css-mistica.js +1 -1
  37. package/dist/date-time-picker.css-mistica.js +1 -1
  38. package/dist/dialog.css-mistica.js +10 -10
  39. package/dist/divider.css-mistica.js +5 -5
  40. package/dist/double-field.css-mistica.js +2 -2
  41. package/dist/drawer.css-mistica.js +4 -4
  42. package/dist/empty-state-card.css-mistica.js +1 -1
  43. package/dist/empty-state.css-mistica.js +8 -8
  44. package/dist/fade-in.css-mistica.js +1 -1
  45. package/dist/feedback.css-mistica.js +5 -5
  46. package/dist/file-upload.css-mistica.js +10 -10
  47. package/dist/fixed-footer-layout.css-mistica.js +6 -6
  48. package/dist/form.css-mistica.js +1 -1
  49. package/dist/generated/mistica-icons/icon-panel-collapse-regular.js +165 -0
  50. package/dist/generated/mistica-icons/icon-panel-expand-regular.js +159 -0
  51. package/dist/grid-layout.css-mistica.js +3 -3
  52. package/dist/grid.css-mistica.js +120 -120
  53. package/dist/header.css-mistica.js +1 -1
  54. package/dist/hero.css-mistica.js +4 -4
  55. package/dist/horizontal-scroll.css-mistica.js +1 -1
  56. package/dist/icon-button.css-mistica.js +53 -53
  57. package/dist/icons/icon-chevron.css-mistica.js +2 -2
  58. package/dist/icons/icon-error.css-mistica.js +1 -1
  59. package/dist/image.css-mistica.js +8 -8
  60. package/dist/index.d.ts +5 -0
  61. package/dist/index.js +10 -1
  62. package/dist/inline.css-mistica.js +11 -11
  63. package/dist/list.css-mistica.js +40 -28
  64. package/dist/list.css.d.ts +2 -0
  65. package/dist/list.d.ts +3 -1
  66. package/dist/list.js +176 -165
  67. package/dist/loading-bar.css-mistica.js +1 -1
  68. package/dist/loading-screen.css-mistica.js +12 -12
  69. package/dist/logo.css-mistica.js +5 -5
  70. package/dist/menu.css-mistica.js +13 -13
  71. package/dist/mobile-navigation-menu.d.ts +26 -0
  72. package/dist/mobile-navigation-menu.js +145 -0
  73. package/dist/mosaic.css-mistica.js +1 -1
  74. package/dist/navigation-bar.css-mistica.js +31 -97
  75. package/dist/navigation-bar.css.d.ts +0 -24
  76. package/dist/navigation-bar.d.ts +3 -38
  77. package/dist/navigation-bar.js +399 -586
  78. package/dist/navigation-breadcrumbs.css-mistica.js +1 -1
  79. package/dist/navigation-shared.css-mistica.js +79 -0
  80. package/dist/navigation-shared.css.d.ts +26 -0
  81. package/dist/navigation-shared.d.ts +66 -0
  82. package/dist/navigation-shared.js +264 -0
  83. package/dist/package-version.js +1 -1
  84. package/dist/pagination.css-mistica.js +22 -22
  85. package/dist/pin-field.css-mistica.js +4 -4
  86. package/dist/popover.css-mistica.js +1 -1
  87. package/dist/portal.d.ts +1 -1
  88. package/dist/progress-bar.css-mistica.js +7 -7
  89. package/dist/radio-button.css-mistica.js +27 -27
  90. package/dist/rating.css-mistica.js +4 -4
  91. package/dist/responsive-layout.css-mistica.js +6 -6
  92. package/dist/screen-reader-only.css-mistica.js +1 -1
  93. package/dist/select.css-mistica.js +21 -21
  94. package/dist/sheet-action-row.css-mistica.js +1 -1
  95. package/dist/sheet-common.css-mistica.js +7 -7
  96. package/dist/sheet-info.css-mistica.js +1 -1
  97. package/dist/sidenav-bar-context.d.ts +25 -0
  98. package/dist/sidenav-bar-context.js +72 -0
  99. package/dist/sidenav-bar-data.d.ts +15 -0
  100. package/dist/sidenav-bar-data.js +38 -0
  101. package/dist/sidenav-bar-first-level-item.d.ts +42 -0
  102. package/dist/sidenav-bar-first-level-item.js +174 -0
  103. package/dist/sidenav-bar-keyboard.d.ts +13 -0
  104. package/dist/sidenav-bar-keyboard.js +314 -0
  105. package/dist/sidenav-bar-layout.css-mistica.js +28 -0
  106. package/dist/sidenav-bar-layout.css.d.ts +5 -0
  107. package/dist/sidenav-bar-layout.d.ts +22 -0
  108. package/dist/sidenav-bar-layout.js +103 -0
  109. package/dist/sidenav-bar-mobile.d.ts +24 -0
  110. package/dist/sidenav-bar-mobile.js +332 -0
  111. package/dist/sidenav-bar-motion.d.ts +32 -0
  112. package/dist/sidenav-bar-motion.js +84 -0
  113. package/dist/sidenav-bar-panel.d.ts +31 -0
  114. package/dist/sidenav-bar-panel.js +208 -0
  115. package/dist/sidenav-bar-row.d.ts +82 -0
  116. package/dist/sidenav-bar-row.js +230 -0
  117. package/dist/sidenav-bar-second-level-item.d.ts +43 -0
  118. package/dist/sidenav-bar-second-level-item.js +89 -0
  119. package/dist/sidenav-bar-section.d.ts +23 -0
  120. package/dist/sidenav-bar-section.js +167 -0
  121. package/dist/sidenav-bar-types.d.ts +192 -0
  122. package/dist/sidenav-bar-types.js +11 -0
  123. package/dist/sidenav-bar.css-mistica.js +325 -0
  124. package/dist/sidenav-bar.css.d.ts +93 -0
  125. package/dist/sidenav-bar.d.ts +102 -0
  126. package/dist/sidenav-bar.js +492 -0
  127. package/dist/skeletons.css-mistica.js +8 -8
  128. package/dist/skins/blau.js +12 -8
  129. package/dist/skins/esimflag.js +13 -9
  130. package/dist/skins/movistar.js +13 -9
  131. package/dist/skins/o2.js +13 -9
  132. package/dist/skins/skin-contract.css-mistica.js +735 -731
  133. package/dist/skins/skin-contract.css.d.ts +4 -0
  134. package/dist/skins/telefonica.js +13 -9
  135. package/dist/skins/types/colors.d.ts +2 -0
  136. package/dist/skins/vivo-evolution.js +14 -10
  137. package/dist/skins/vivo.js +13 -9
  138. package/dist/skip-link.css-mistica.js +2 -2
  139. package/dist/slider.css-mistica.js +24 -24
  140. package/dist/snackbar.css-mistica.js +14 -14
  141. package/dist/spinner.css-mistica.js +1 -1
  142. package/dist/square.css-mistica.js +1 -1
  143. package/dist/stack.css-mistica.js +5 -5
  144. package/dist/stacking-group.css-mistica.js +1 -1
  145. package/dist/stepper.css-mistica.js +9 -9
  146. package/dist/switch-component.css-mistica.js +48 -42
  147. package/dist/switch-component.css.d.ts +2 -0
  148. package/dist/switch-component.js +18 -18
  149. package/dist/table.css-mistica.js +9 -9
  150. package/dist/tabs.css-mistica.js +22 -22
  151. package/dist/tag.css-mistica.js +3 -3
  152. package/dist/text-field-base.css-mistica.js +19 -19
  153. package/dist/text-field-components.css-mistica.js +10 -10
  154. package/dist/text-link.css-mistica.js +6 -6
  155. package/dist/text-tokens.d.ts +6 -0
  156. package/dist/text-tokens.js +157 -133
  157. package/dist/text.css-mistica.js +6 -6
  158. package/dist/timeline.css-mistica.js +11 -11
  159. package/dist/timer.css-mistica.js +6 -6
  160. package/dist/tooltip.css-mistica.js +2 -2
  161. package/dist/touchable.css-mistica.js +4 -4
  162. package/dist/utils/aspect-ratio-support.css-mistica.js +2 -2
  163. package/dist/video.css-mistica.js +1 -1
  164. package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  165. package/dist-es/accordion.css-mistica.js +7 -7
  166. package/dist-es/align.css-mistica.js +1 -1
  167. package/dist-es/autocomplete.css-mistica.js +2 -2
  168. package/dist-es/avatar.css-mistica.js +1 -1
  169. package/dist-es/badge.css-mistica.js +1 -1
  170. package/dist-es/box.css-mistica.js +13 -13
  171. package/dist-es/boxed.css-mistica.js +23 -23
  172. package/dist-es/boxed.js +25 -25
  173. package/dist-es/button-group.css-mistica.js +1 -1
  174. package/dist-es/button-layout.css-mistica.js +16 -16
  175. package/dist-es/button.css-mistica.js +38 -38
  176. package/dist-es/callout.css-mistica.js +12 -12
  177. package/dist-es/card-internal.css-mistica.js +17 -17
  178. package/dist-es/carousel.css-mistica.js +10 -10
  179. package/dist-es/checkbox.css-mistica.js +13 -13
  180. package/dist-es/chip.css-mistica.js +17 -17
  181. package/dist-es/circle.css-mistica.js +2 -2
  182. package/dist-es/community/advanced-data-card.css-mistica.js +7 -7
  183. package/dist-es/community/ai-card.css-mistica.js +4 -4
  184. package/dist-es/community/blocks.css-mistica.js +1 -1
  185. package/dist-es/community/cyber/example-component.css-mistica.js +1 -1
  186. package/dist-es/community/example-component.css-mistica.js +2 -2
  187. package/dist-es/community/skins/cyber-skin.js +19 -17
  188. package/dist-es/counter.css-mistica.js +2 -2
  189. package/dist-es/cover-hero.css-mistica.js +4 -4
  190. package/dist-es/credit-card-number-field.css-mistica.js +3 -3
  191. package/dist-es/date-field.css-mistica.js +1 -1
  192. package/dist-es/date-time-picker.css-mistica.js +1 -1
  193. package/dist-es/dialog.css-mistica.js +5 -5
  194. package/dist-es/divider.css-mistica.js +5 -5
  195. package/dist-es/double-field.css-mistica.js +2 -2
  196. package/dist-es/drawer.css-mistica.js +2 -2
  197. package/dist-es/empty-state-card.css-mistica.js +1 -1
  198. package/dist-es/empty-state.css-mistica.js +7 -7
  199. package/dist-es/fade-in.css-mistica.js +1 -1
  200. package/dist-es/feedback.css-mistica.js +2 -2
  201. package/dist-es/file-upload.css-mistica.js +8 -8
  202. package/dist-es/fixed-footer-layout.css-mistica.js +3 -3
  203. package/dist-es/form.css-mistica.js +1 -1
  204. package/dist-es/generated/mistica-icons/icon-panel-collapse-regular.js +115 -0
  205. package/dist-es/generated/mistica-icons/icon-panel-expand-regular.js +109 -0
  206. package/dist-es/grid-layout.css-mistica.js +3 -3
  207. package/dist-es/grid.css-mistica.js +120 -120
  208. package/dist-es/header.css-mistica.js +1 -1
  209. package/dist-es/hero.css-mistica.js +3 -3
  210. package/dist-es/horizontal-scroll.css-mistica.js +1 -1
  211. package/dist-es/icon-button.css-mistica.js +53 -53
  212. package/dist-es/icons/icon-chevron.css-mistica.js +2 -2
  213. package/dist-es/icons/icon-error.css-mistica.js +1 -1
  214. package/dist-es/image.css-mistica.js +4 -4
  215. package/dist-es/index.js +2279 -2276
  216. package/dist-es/inline.css-mistica.js +11 -11
  217. package/dist-es/list.css-mistica.js +8 -2
  218. package/dist-es/list.js +275 -264
  219. package/dist-es/loading-bar.css-mistica.js +1 -1
  220. package/dist-es/loading-screen.css-mistica.js +5 -5
  221. package/dist-es/logo.css-mistica.js +5 -5
  222. package/dist-es/menu.css-mistica.js +13 -13
  223. package/dist-es/mobile-navigation-menu.js +90 -0
  224. package/dist-es/mosaic.css-mistica.js +1 -1
  225. package/dist-es/navigation-bar.css-mistica.js +11 -20
  226. package/dist-es/navigation-bar.js +469 -656
  227. package/dist-es/navigation-breadcrumbs.css-mistica.js +1 -1
  228. package/dist-es/navigation-shared.css-mistica.js +11 -0
  229. package/dist-es/navigation-shared.js +192 -0
  230. package/dist-es/package-version.js +1 -1
  231. package/dist-es/pagination.css-mistica.js +19 -19
  232. package/dist-es/pin-field.css-mistica.js +2 -2
  233. package/dist-es/popover.css-mistica.js +1 -1
  234. package/dist-es/progress-bar.css-mistica.js +7 -7
  235. package/dist-es/radio-button.css-mistica.js +22 -22
  236. package/dist-es/rating.css-mistica.js +3 -3
  237. package/dist-es/responsive-layout.css-mistica.js +6 -6
  238. package/dist-es/screen-reader-only.css-mistica.js +1 -1
  239. package/dist-es/select.css-mistica.js +18 -18
  240. package/dist-es/sheet-action-row.css-mistica.js +1 -1
  241. package/dist-es/sheet-common.css-mistica.js +2 -2
  242. package/dist-es/sheet-info.css-mistica.js +1 -1
  243. package/dist-es/sidenav-bar-context.js +14 -0
  244. package/dist-es/sidenav-bar-data.js +12 -0
  245. package/dist-es/sidenav-bar-first-level-item.js +124 -0
  246. package/dist-es/sidenav-bar-keyboard.js +253 -0
  247. package/dist-es/sidenav-bar-layout.css-mistica.js +2 -0
  248. package/dist-es/sidenav-bar-layout.js +86 -0
  249. package/dist-es/sidenav-bar-mobile.js +277 -0
  250. package/dist-es/sidenav-bar-motion.js +26 -0
  251. package/dist-es/sidenav-bar-panel.js +146 -0
  252. package/dist-es/sidenav-bar-row.js +208 -0
  253. package/dist-es/sidenav-bar-second-level-item.js +72 -0
  254. package/dist-es/sidenav-bar-section.js +112 -0
  255. package/dist-es/sidenav-bar-types.js +2 -0
  256. package/dist-es/sidenav-bar.css-mistica.js +74 -0
  257. package/dist-es/sidenav-bar.js +429 -0
  258. package/dist-es/skeletons.css-mistica.js +7 -7
  259. package/dist-es/skins/blau.js +12 -8
  260. package/dist-es/skins/esimflag.js +13 -9
  261. package/dist-es/skins/movistar.js +13 -9
  262. package/dist-es/skins/o2.js +13 -9
  263. package/dist-es/skins/skin-contract.css-mistica.js +735 -731
  264. package/dist-es/skins/telefonica.js +13 -9
  265. package/dist-es/skins/vivo-evolution.js +14 -10
  266. package/dist-es/skins/vivo.js +13 -9
  267. package/dist-es/skip-link.css-mistica.js +2 -2
  268. package/dist-es/slider.css-mistica.js +20 -20
  269. package/dist-es/snackbar.css-mistica.js +5 -5
  270. package/dist-es/spinner.css-mistica.js +1 -1
  271. package/dist-es/square.css-mistica.js +1 -1
  272. package/dist-es/stack.css-mistica.js +5 -5
  273. package/dist-es/stacking-group.css-mistica.js +1 -1
  274. package/dist-es/stepper.css-mistica.js +3 -3
  275. package/dist-es/style.css +1 -1
  276. package/dist-es/switch-component.css-mistica.js +37 -37
  277. package/dist-es/switch-component.js +26 -26
  278. package/dist-es/table.css-mistica.js +9 -9
  279. package/dist-es/tabs.css-mistica.js +19 -19
  280. package/dist-es/tag.css-mistica.js +2 -2
  281. package/dist-es/text-field-base.css-mistica.js +17 -17
  282. package/dist-es/text-field-components.css-mistica.js +4 -4
  283. package/dist-es/text-link.css-mistica.js +6 -6
  284. package/dist-es/text-tokens.js +82 -67
  285. package/dist-es/text.css-mistica.js +6 -6
  286. package/dist-es/timeline.css-mistica.js +10 -10
  287. package/dist-es/timer.css-mistica.js +6 -6
  288. package/dist-es/tooltip.css-mistica.js +2 -2
  289. package/dist-es/touchable.css-mistica.js +2 -2
  290. package/dist-es/utils/aspect-ratio-support.css-mistica.js +2 -2
  291. package/dist-es/video.css-mistica.js +1 -1
  292. package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  293. package/package.json +2 -1
  294. package/src/boxed.css.ts +1 -0
  295. package/src/boxed.tsx +18 -5
  296. package/src/callout.css.ts +4 -2
  297. package/src/community/skins/cyber-skin.tsx +19 -16
  298. package/src/index.tsx +14 -0
  299. package/src/list.css.ts +36 -1
  300. package/src/list.tsx +370 -344
  301. package/src/mobile-navigation-menu.tsx +168 -0
  302. package/src/navigation-bar.css.ts +2 -267
  303. package/src/navigation-bar.tsx +77 -370
  304. package/src/navigation-shared.css.ts +289 -0
  305. package/src/navigation-shared.tsx +262 -0
  306. package/src/package-version.tsx +1 -1
  307. package/src/portal.tsx +1 -1
  308. package/src/sidenav-bar-context.tsx +41 -0
  309. package/src/sidenav-bar-data.ts +49 -0
  310. package/src/sidenav-bar-first-level-item.tsx +203 -0
  311. package/src/sidenav-bar-keyboard.tsx +393 -0
  312. package/src/sidenav-bar-layout.css.ts +89 -0
  313. package/src/sidenav-bar-layout.tsx +61 -0
  314. package/src/sidenav-bar-mobile.tsx +302 -0
  315. package/src/sidenav-bar-motion.tsx +70 -0
  316. package/src/sidenav-bar-panel.tsx +269 -0
  317. package/src/sidenav-bar-row.tsx +272 -0
  318. package/src/sidenav-bar-second-level-item.tsx +99 -0
  319. package/src/sidenav-bar-section.tsx +98 -0
  320. package/src/sidenav-bar-types.ts +224 -0
  321. package/src/sidenav-bar.css.ts +1018 -0
  322. package/src/sidenav-bar.tsx +821 -0
  323. package/src/skins/blau.tsx +12 -8
  324. package/src/skins/esimflag.tsx +13 -9
  325. package/src/skins/movistar.tsx +13 -9
  326. package/src/skins/o2.tsx +13 -9
  327. package/src/skins/skin-contract.css.ts +2 -0
  328. package/src/skins/telefonica.tsx +13 -9
  329. package/src/skins/types/colors.tsx +2 -0
  330. package/src/skins/vivo-evolution.tsx +14 -10
  331. package/src/skins/vivo.tsx +13 -9
  332. package/src/switch-component.css.ts +20 -0
  333. package/src/switch-component.tsx +9 -1
  334. package/src/text-tokens.tsx +24 -0
@@ -0,0 +1,821 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import classnames from 'classnames';
4
+ import {CSSTransition} from 'react-transition-group';
5
+ import * as styles from './sidenav-bar.css';
6
+ import {
7
+ DEFAULT_WIDTH,
8
+ COLLAPSED_WIDTH,
9
+ LOGO_SIZE,
10
+ COLLAPSE_DURATION_MS,
11
+ CONTENT_DURATION_MS,
12
+ } from './sidenav-bar.css';
13
+ import {ThemeVariant, normalizeVariant, useThemeVariant} from './theme-variant-context';
14
+ import {getPrefixedDataAttributes} from './utils/dom';
15
+ import {applyCssVars} from './utils/css';
16
+ import {useScreenSize, useTheme} from './hooks';
17
+ import {IconButton} from './icon-button';
18
+ import {Logo} from './logo';
19
+ import IconPanelExpandRegular from './generated/mistica-icons/icon-panel-expand-regular';
20
+ import IconPanelCollapseRegular from './generated/mistica-icons/icon-panel-collapse-regular';
21
+ import {SidenavFirstLevelItem} from './sidenav-bar-first-level-item';
22
+ import {renderSidenavSecondLevelItem} from './sidenav-bar-second-level-item';
23
+ import {SidenavSection} from './sidenav-bar-section';
24
+ import {SidenavDoublePanel} from './sidenav-bar-panel';
25
+ import {SidenavBarMobile} from './sidenav-bar-mobile';
26
+ import {useIsMotionOff} from './sidenav-bar-motion';
27
+ import {useSidenavRailKeyboard} from './sidenav-bar-keyboard';
28
+ import {SidenavBarContext} from './sidenav-bar-context';
29
+ import {shouldShowBoxedBorder} from './boxed';
30
+ import {
31
+ isSidenavSection,
32
+ getFirstLevelItems,
33
+ getSidenavSectionTitle,
34
+ renderSidenavSlot,
35
+ renderSidenavLogo,
36
+ } from './sidenav-bar-data';
37
+ import * as tokens from './text-tokens';
38
+ import {isSidenavMoving} from './sidenav-bar-types';
39
+
40
+ import type {Variant} from './theme-variant-context';
41
+ import type {ExclusifyUnion} from './utils/utility-types';
42
+ import type {DataAttributes} from './utils/types';
43
+ import type {
44
+ SidenavEntry,
45
+ SidenavFirstLevelItem as SidenavFirstLevelItemData,
46
+ SidenavSecondLevelItem as SidenavSecondLevelItemData,
47
+ SidenavLogo,
48
+ SidenavSlot,
49
+ SidenavSlotRenderProps,
50
+ SidenavCollapseState,
51
+ } from './sidenav-bar-types';
52
+
53
+ type SidenavBarBaseProps = {
54
+ /**
55
+ * First-level entries of the body. Each entry is either a section with items, or a stand-alone item
56
+ * that needs no section.
57
+ * @see SidenavEntry
58
+ * @see SidenavSection
59
+ * @see SidenavFirstLevelItem
60
+ */
61
+ entries?: ReadonlyArray<SidenavEntry>;
62
+ /** Accessible name of the navigation landmark. Defaults to a localized "Main navigation". */
63
+ 'aria-label'?: string;
64
+ /** Color variant (default, brand, alternative, negative, media). @default 'default' */
65
+ variant?: Variant;
66
+ /** Width of expanded sidenav in pixels. The second column takes the same width. @default 240 */
67
+ width?: number;
68
+ /** Opens the children of a parent item in a second column, to the right of the sidenav.
69
+ * @default false */
70
+ doublePanel?: boolean;
71
+ /** Logo of the header. Defaults to the isotype of the skin at 32px, in both the expanded and the
72
+ * collapsed state. It takes true for that same default, false to hide the logo, an element of your
73
+ * own, or a function that receives the collapsed state and returns one logo for each state. The
74
+ * collapsed rail clamps the width of the logo to 32px, and the spot is at least 32px tall: a taller
75
+ * logo of your own makes the spot grow. The mobile top bar matches the main navigation
76
+ * bar: it shows the isotype at 40px, and it reports a collapsed state of false.
77
+ * @see SidenavLogoRenderProps */
78
+ logo?: SidenavLogo;
79
+ /** Custom content below logo/collapse in header. It takes an element, or a function that receives the
80
+ * collapse state and returns the content for that state.
81
+ * @see SidenavSlotRenderProps */
82
+ headerSlot?: SidenavSlot;
83
+ /** Custom background color of the whole sidenav: header, body, footer and second column. Use an
84
+ * opaque color: the header and the footer are sticky over the scrolling body, so a translucent color
85
+ * lets the body content show through them. */
86
+ background?: string;
87
+ /** ID of currently selected item (controlled selection). */
88
+ selectedItemId?: string | null;
89
+ /** Called when selection changes. */
90
+ onSelectedItemIdChange?: (id: string | null) => void;
91
+ dataAttributes?: DataAttributes;
92
+ };
93
+
94
+ /**
95
+ * Constraints enforced by the type system:
96
+ *
97
+ * - A boxed sidenav has its own edge, so the vertical right divider does not apply to it:
98
+ * `divider` is only accepted when `boxed` is false.
99
+ * - The collapsed state is either controlled through `collapsed` (requires `onCollapse`)
100
+ * or uncontrolled through `defaultCollapsed` (optional `onCollapse`), never both.
101
+ * - `showCollapseButton: false` removes the built-in collapse button, which is the only control that
102
+ * toggles the state from inside. Without it, the sidenav never changes the state by itself, so
103
+ * `onCollapse` has no event to report and `defaultCollapsed` has no state to seed: both are not
104
+ * allowed. That sidenav takes `collapsed` alone, and mirrors it on every render, so a control of your
105
+ * own (for example in a slot) still collapses it through that prop.
106
+ * - `fixedFooter` is only allowed when `footerSlot` is provided.
107
+ */
108
+ type SidenavBarProps = SidenavBarBaseProps &
109
+ ExclusifyUnion<
110
+ | {
111
+ /** Renders as a floating box (with own edge). Divider not applicable. */
112
+ boxed: true;
113
+ }
114
+ | {
115
+ /** Renders as full-width. @default false */
116
+ boxed?: false;
117
+ /** Shows vertical right divider (only when boxed=false). @default true */
118
+ divider?: boolean;
119
+ }
120
+ > &
121
+ ExclusifyUnion<
122
+ | {
123
+ /** Controlled collapsed state. */
124
+ collapsed: boolean;
125
+ /** Handler for collapsed state changes (required for controlled mode). */
126
+ onCollapse: (collapsed: boolean) => void;
127
+ /** Renders the built-in collapse button, which lets the user toggle the state. @default true */
128
+ showCollapseButton?: true;
129
+ }
130
+ | {
131
+ /** Initial collapsed state (uncontrolled). @default false */
132
+ defaultCollapsed?: boolean;
133
+ /** Optional handler for collapsed state changes (for logging/effects). */
134
+ onCollapse?: (collapsed: boolean) => void;
135
+ /** Renders the built-in collapse button, which lets the user toggle the state. @default true */
136
+ showCollapseButton?: true;
137
+ }
138
+ | {
139
+ /** Collapsed state. There is no built-in button to toggle it, so the sidenav mirrors this
140
+ * prop on every render. Unlike `defaultCollapsed`, it is not a seed: a later change of it
141
+ * moves the sidenav. @default false */
142
+ collapsed?: boolean;
143
+ /** Hides the built-in collapse button. Only a change of `collapsed` moves the sidenav. */
144
+ showCollapseButton: false;
145
+ }
146
+ > &
147
+ ExclusifyUnion<
148
+ | {
149
+ /** Custom content in footer region (at bottom of sidenav). It takes an element, or a function
150
+ * that receives the collapse state and returns the content for that state.
151
+ * @see SidenavSlotRenderProps */
152
+ footerSlot: SidenavSlot;
153
+ /** Keep footer fixed when scrolling. @default false */
154
+ fixedFooter?: boolean;
155
+ }
156
+ | {
157
+ /** No footer slot. */
158
+ footerSlot?: undefined;
159
+ }
160
+ >;
161
+
162
+ const renderFirstLevelItem = (
163
+ item: SidenavFirstLevelItemData,
164
+ {standalone}: {standalone?: boolean} = {}
165
+ ): React.ReactElement => (
166
+ <SidenavFirstLevelItem
167
+ key={item.id}
168
+ id={item.id}
169
+ label={item.label}
170
+ asset={item.asset}
171
+ showAssetWhenExpanded={item.showAssetWhenExpanded}
172
+ rightSlot={item.rightSlot}
173
+ standalone={standalone}
174
+ defaultOpen={item.defaultOpen}
175
+ secondLevelItems={item.children}
176
+ href={item.href}
177
+ to={item.to}
178
+ onPress={item.onPress}
179
+ newTab={item.newTab}
180
+ onNavigate={item.onNavigate}
181
+ />
182
+ );
183
+
184
+ /**
185
+ * Finds the first-level item that owns the given child id. The sidenav supports a single nesting
186
+ * level, so the parent of an item is always a first-level item.
187
+ */
188
+ const findParentOfItem = (
189
+ entries: ReadonlyArray<SidenavEntry>,
190
+ childId: string
191
+ ): SidenavFirstLevelItemData | undefined =>
192
+ getFirstLevelItems(entries).find((item) => item.children?.some((child) => child.id === childId));
193
+
194
+ /** Finds a first-level item by id. Only these items can open a panel. */
195
+ const findFirstLevelItem = (
196
+ entries: ReadonlyArray<SidenavEntry>,
197
+ itemId: string
198
+ ): SidenavFirstLevelItemData | undefined => getFirstLevelItems(entries).find((item) => item.id === itemId);
199
+
200
+ /**
201
+ * Development-only validation of the entries. It walks the data instead of checking inside each
202
+ * `SidenavItem` render: every item of the sidenav comes from this data, so one walk covers all of
203
+ * them, it reports each problem once, and it does not see the re-renders of the panel.
204
+ */
205
+ const validateSidenavEntries = (entries: ReadonlyArray<SidenavEntry>): void => {
206
+ const seenIds = new Set<string>();
207
+ const duplicateIds = new Set<string>();
208
+
209
+ const visitItem = (item: SidenavFirstLevelItemData | SidenavSecondLevelItemData, level: number): void => {
210
+ if (seenIds.has(item.id)) {
211
+ duplicateIds.add(item.id);
212
+ } else {
213
+ seenIds.add(item.id);
214
+ }
215
+ if (level > 0 && item.children?.length) {
216
+ console.error(
217
+ `SidenavItem "${item.label}" at level ${level} cannot have children. ` +
218
+ `SidenavItem supports maximum 2 levels of nesting. ` +
219
+ `Only level 0 items can have children.`
220
+ );
221
+ }
222
+ item.children?.forEach((child) => visitItem(child, level + 1));
223
+ };
224
+
225
+ getFirstLevelItems(entries).forEach((item) => visitItem(item, 0));
226
+
227
+ if (duplicateIds.size > 0) {
228
+ console.error(
229
+ `SidenavBar: duplicate item IDs found: ${Array.from(duplicateIds).join(', ')}. ` +
230
+ `All SidenavItem ids must be unique within a SidenavBar.`
231
+ );
232
+ }
233
+ };
234
+
235
+ const renderSidenavEntries = (entries: ReadonlyArray<SidenavEntry>): Array<React.ReactElement> => {
236
+ // Every entry of the first level is one item of the body list, a section as much as a stand-alone
237
+ // item. A section holds a list of its own, and a stand-alone item holds a single row.
238
+ return entries.map((entry, entryIndex) => {
239
+ if (isSidenavSection(entry)) {
240
+ const previousEntry = entries[entryIndex - 1];
241
+ const sharesDividerWithPrevious =
242
+ !!entry.dividerTop &&
243
+ !!previousEntry &&
244
+ isSidenavSection(previousEntry) &&
245
+ !!previousEntry.dividerBottom;
246
+ const isFirstEntry = entryIndex === 0;
247
+ const isLastEntry = entryIndex === entries.length - 1;
248
+ return (
249
+ <div key={`${getSidenavSectionTitle(entry.title).text}-${entryIndex}`} role="listitem">
250
+ <SidenavSection
251
+ title={entry.title}
252
+ dividerTop={entry.dividerTop && !isFirstEntry && !sharesDividerWithPrevious}
253
+ dividerBottom={entry.dividerBottom && !isLastEntry}
254
+ >
255
+ {entry.items.map((item) => renderFirstLevelItem(item))}
256
+ </SidenavSection>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ return (
262
+ <div key={entry.id} className={styles.standaloneItem} role="listitem">
263
+ {renderFirstLevelItem(entry as SidenavFirstLevelItemData, {standalone: true})}
264
+ </div>
265
+ );
266
+ });
267
+ };
268
+
269
+ const SidenavBar = ({
270
+ entries,
271
+ 'aria-label': ariaLabelProp,
272
+ variant = 'default',
273
+ boxed = false,
274
+ divider = true,
275
+ showCollapseButton = true,
276
+ collapsed: collapsedProp,
277
+ defaultCollapsed = false,
278
+ onCollapse,
279
+ doublePanel = false,
280
+ width = DEFAULT_WIDTH,
281
+ logo,
282
+ headerSlot,
283
+ footerSlot,
284
+ fixedFooter = false,
285
+ background,
286
+ selectedItemId,
287
+ onSelectedItemIdChange,
288
+ dataAttributes,
289
+ }: SidenavBarProps): JSX.Element => {
290
+ const {isTabletOrSmaller} = useScreenSize();
291
+ const {componentProperties, texts, t} = useTheme();
292
+ const ariaLabel = ariaLabelProp ?? (texts.sidenavLandmark || t(tokens.sidenavLandmark));
293
+ const isMotionOff = useIsMotionOff();
294
+ // Read before the `ThemeVariant` of the returned tree, so this is the variant of the page that holds the
295
+ // sidenav, and not the variant of the sidenav itself.
296
+ const pageVariant = normalizeVariant(useThemeVariant());
297
+ const [panelOpenForItemId, setPanelOpenForItemId] = React.useState<string | null>(() =>
298
+ doublePanel && entries && selectedItemId
299
+ ? findParentOfItem(entries, selectedItemId)?.id ?? null
300
+ : null
301
+ );
302
+
303
+ const isCollapsedControlled = collapsedProp !== undefined;
304
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = React.useState(defaultCollapsed);
305
+ // A controlled sidenav (`collapsed` set) reads the prop on every render. An uncontrolled one owns its
306
+ // state, which `defaultCollapsed` seeds once. Without the built-in button nothing toggles that state,
307
+ // so a sidenav that hides the button and omits `collapsed` keeps the seed, which the types pin to false.
308
+ const collapsed = isCollapsedControlled ? Boolean(collapsedProp) : uncontrolledCollapsed;
309
+ const containerRef = React.useRef<HTMLElement>(null);
310
+
311
+ // The second column slides away instead of disappearing, so it still renders while it closes, when the
312
+ // item that opened it is already gone. It therefore keeps the title and the children of that item
313
+ // until a new item replaces them.
314
+ const doublePanelRef = React.useRef<HTMLDivElement>(null);
315
+ const [doublePanelContent, setDoublePanelContent] = React.useState<{
316
+ itemId: string;
317
+ label: string;
318
+ children: ReadonlyArray<SidenavSecondLevelItemData>;
319
+ } | null>(null);
320
+
321
+ // The settled state follows `collapsed` once the rail rests. A user who turned motion down sees no
322
+ // movement, so it follows at once there.
323
+ const [collapsedSettled, setCollapsedSettled] = React.useState(collapsed);
324
+ React.useEffect(() => {
325
+ if (isMotionOff) {
326
+ setCollapsedSettled(collapsed);
327
+ return;
328
+ }
329
+ const timeoutId = setTimeout(() => setCollapsedSettled(collapsed), COLLAPSE_DURATION_MS);
330
+ return () => clearTimeout(timeoutId);
331
+ }, [collapsed, isMotionOff]);
332
+ const collapseState: SidenavCollapseState = collapsed
333
+ ? collapsedSettled
334
+ ? 'collapsed'
335
+ : 'collapsing'
336
+ : collapsedSettled
337
+ ? 'expanding'
338
+ : 'expanded';
339
+
340
+ // A press on an item of the sidenav closes the panel and moves the selection at the same time.
341
+ // The press records its selection here, so the adjustment below knows the user already dismissed
342
+ // the second column for that selection, and does not reopen it.
343
+ const [dismissedSelection, setDismissedSelection] = React.useState<string | null>(null);
344
+
345
+ const selectItemAndClosePanel = React.useCallback(
346
+ (itemId: string | null) => {
347
+ if (doublePanel) {
348
+ setDismissedSelection(itemId);
349
+ }
350
+ setPanelOpenForItemId(null);
351
+ if (itemId) {
352
+ onSelectedItemIdChange?.(itemId);
353
+ }
354
+ },
355
+ [doublePanel, onSelectedItemIdChange]
356
+ );
357
+
358
+ // The second column follows the selection, which can also move from outside of the sidenav (a
359
+ // breadcrumb, a card, a button of the app):
360
+ // - a second-level item opens the column on its parent, so that the new selection stays visible;
361
+ // - a first-level item without children closes the column, because it has nothing to show there;
362
+ // - a press inside the sidenav closes the column through `selectItemAndClosePanel`, and that press
363
+ // wins over the selection it carries.
364
+ // The adjustment runs during the render, where the entries and the previous selection are both in
365
+ // scope, so it needs no effect and no refs. React applies the state it sets before it paints.
366
+ const [previousSelection, setPreviousSelection] = React.useState(selectedItemId);
367
+ if (selectedItemId !== previousSelection) {
368
+ setPreviousSelection(selectedItemId);
369
+ if (dismissedSelection !== null) {
370
+ setDismissedSelection(null);
371
+ }
372
+ if (doublePanel && entries && selectedItemId && dismissedSelection !== selectedItemId) {
373
+ const parent = findParentOfItem(entries, selectedItemId);
374
+ if (parent) {
375
+ setPanelOpenForItemId(parent.id);
376
+ } else {
377
+ const firstLevelItem = findFirstLevelItem(entries, selectedItemId);
378
+ if (firstLevelItem && !firstLevelItem.children?.length) {
379
+ setPanelOpenForItemId(null);
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ // The column closes as soon as the double panel mode goes off. Only the change of the mode closes it:
386
+ // the collapsed rail opens its dialog panel through this same state with the mode off.
387
+ const [previousDoublePanel, setPreviousDoublePanel] = React.useState(doublePanel);
388
+ if (doublePanel !== previousDoublePanel) {
389
+ setPreviousDoublePanel(doublePanel);
390
+ if (!doublePanel) {
391
+ setPanelOpenForItemId(null);
392
+ }
393
+ }
394
+
395
+ // A change of the entries invalidates the open column, whose parent item may not exist anymore.
396
+ const [previousEntriesLength, setPreviousEntriesLength] = React.useState(entries?.length ?? 0);
397
+ if ((entries?.length ?? 0) !== previousEntriesLength) {
398
+ setPreviousEntriesLength(entries?.length ?? 0);
399
+ setPanelOpenForItemId(null);
400
+ }
401
+
402
+ // Only a press outside of the whole bar, or Escape, dismisses the second column: a press inside the
403
+ // bar that lands on no item keeps it open, and a press on an item closes it through
404
+ // `selectItemAndClosePanel`. A press that also carries a new selection does not race the close: the
405
+ // adjustment above reopens the column for that selection in the same batch of updates.
406
+ React.useEffect(() => {
407
+ if (!doublePanel || !panelOpenForItemId) {
408
+ return;
409
+ }
410
+
411
+ const handlePressOutside = (event: MouseEvent) => {
412
+ const container = containerRef.current;
413
+ if (!container) return;
414
+ // The browser builds this path when it dispatches the press, so the path still holds the node
415
+ // that the user pressed and all of its ancestors, even when React replaced them before this
416
+ // listener ran. Reading `event.target` instead counts a press on a control that swaps its own
417
+ // node as a press outside of the bar: the collapse action swaps its icon, and a parent item
418
+ // swaps its whole row, so both of them closed the column that they should have left alone.
419
+ if (event.composedPath().includes(container)) return;
420
+ setPanelOpenForItemId(null);
421
+ };
422
+
423
+ const handleEscape = (event: KeyboardEvent) => {
424
+ if (event.key === 'Escape') {
425
+ setPanelOpenForItemId(null);
426
+ }
427
+ };
428
+
429
+ document.addEventListener('click', handlePressOutside);
430
+ document.addEventListener('keydown', handleEscape);
431
+ return () => {
432
+ document.removeEventListener('click', handlePressOutside);
433
+ document.removeEventListener('keydown', handleEscape);
434
+ };
435
+ }, [doublePanel, panelOpenForItemId]);
436
+
437
+ const backgroundStyle = background ? {backgroundColor: background} : undefined;
438
+
439
+ const [showHeaderDivider, setShowHeaderDivider] = React.useState(false);
440
+ const [showFooterDivider, setShowFooterDivider] = React.useState(false);
441
+ // The panel keeps its node while it slides back into the main column, so the column separator stays with
442
+ // it until the movement ends. `isDoublePanelOpen` alone drops the separator when the panel starts to
443
+ // close, and the boundary disappears while the column is still visible.
444
+ const [isDoublePanelMounted, setIsDoublePanelMounted] = React.useState(false);
445
+ const headerDividerSentinelRef = React.useRef<HTMLDivElement>(null);
446
+ const footerDividerSentinelRef = React.useRef<HTMLDivElement>(null);
447
+ const bodyRef = React.useRef<HTMLDivElement>(null);
448
+
449
+ React.useEffect(() => {
450
+ if (!bodyRef.current) return;
451
+
452
+ // The sentinels have no height, and at rest they sit exactly on the edge of the scrollport. A
453
+ // fractional layout (a boxed sidenav measures 100vh minus its margins) can leave them half a
454
+ // pixel outside, and the observer would then paint the divider before any scroll. The 1px root
455
+ // margin counts an edge-adjacent sentinel as inside.
456
+ const headerObserver = new IntersectionObserver(
457
+ ([entry]) => {
458
+ setShowHeaderDivider(!entry.isIntersecting);
459
+ },
460
+ {root: bodyRef.current, threshold: 0, rootMargin: '1px 0px 1px 0px'}
461
+ );
462
+
463
+ const footerObserver = new IntersectionObserver(
464
+ ([entry]) => {
465
+ setShowFooterDivider(!entry.isIntersecting);
466
+ },
467
+ {root: bodyRef.current, threshold: 0, rootMargin: '1px 0px 1px 0px'}
468
+ );
469
+
470
+ if (headerDividerSentinelRef.current) {
471
+ headerObserver.observe(headerDividerSentinelRef.current);
472
+ }
473
+ if (footerDividerSentinelRef.current) {
474
+ footerObserver.observe(footerDividerSentinelRef.current);
475
+ }
476
+
477
+ return () => {
478
+ headerObserver.disconnect();
479
+ footerObserver.disconnect();
480
+ };
481
+ }, []);
482
+
483
+ // The rail is travelling between its two widths. See `columnsWhileMoving`.
484
+ const isMoving = isSidenavMoving(collapseState);
485
+
486
+ const toggleCollapsed = React.useCallback(() => {
487
+ // The collapse action keeps the focus while the rail moves, so the pointer rule of
488
+ // `columnsWhileMoving` does not reach a press made with the keyboard. This does.
489
+ if (isMoving) {
490
+ return;
491
+ }
492
+ const next = !collapsed;
493
+ if (!isCollapsedControlled) {
494
+ setUncontrolledCollapsed(next);
495
+ onCollapse?.(next);
496
+ return;
497
+ }
498
+ onCollapse?.(next);
499
+ }, [collapsed, isMoving, isCollapsedControlled, onCollapse]);
500
+
501
+ const contextValue = React.useMemo(
502
+ () => ({
503
+ collapsed,
504
+ collapseState,
505
+ isMotionOff,
506
+ doublePanel,
507
+ panelOpenForItemId,
508
+ setPanelOpenForItemId,
509
+ selectItemAndClosePanel,
510
+ containerRef,
511
+ selectedItemId: selectedItemId ?? null,
512
+ }),
513
+ [
514
+ collapsed,
515
+ collapseState,
516
+ isMotionOff,
517
+ doublePanel,
518
+ panelOpenForItemId,
519
+ selectItemAndClosePanel,
520
+ containerRef,
521
+ selectedItemId,
522
+ ]
523
+ );
524
+
525
+ const handleRailKeyDown = useSidenavRailKeyboard(containerRef);
526
+
527
+ const currentWidth = collapsed ? COLLAPSED_WIDTH : width;
528
+
529
+ if (process.env.NODE_ENV !== 'production' && entries) {
530
+ validateSidenavEntries(entries);
531
+ }
532
+
533
+ const normalizedVariant = normalizeVariant(variant);
534
+
535
+ // A tablet has no room for the rail either, so both breakpoints take the mobile treatment.
536
+ if (isTabletOrSmaller) {
537
+ return (
538
+ <SidenavBarMobile
539
+ entries={entries}
540
+ aria-label={ariaLabel}
541
+ variant={normalizedVariant}
542
+ logo={logo}
543
+ headerSlot={headerSlot}
544
+ footerSlot={footerSlot}
545
+ selectedItemId={selectedItemId ?? null}
546
+ onSelectedItemIdChange={onSelectedItemIdChange}
547
+ dataAttributes={dataAttributes}
548
+ />
549
+ );
550
+ }
551
+
552
+ const slotRenderProps: SidenavSlotRenderProps = {collapsed, state: collapseState};
553
+ const isDefaultLogo = logo === undefined || logo === true;
554
+ const logoElement = renderSidenavLogo(logo, slotRenderProps, <Logo size={LOGO_SIZE} type="isotype" />);
555
+
556
+ const collapseActionElement = (() => {
557
+ if (!showCollapseButton) {
558
+ return null;
559
+ }
560
+
561
+ return (
562
+ <IconButton
563
+ Icon={collapsed ? IconPanelExpandRegular : IconPanelCollapseRegular}
564
+ type="neutral"
565
+ backgroundType="transparent"
566
+ small
567
+ onPress={toggleCollapsed}
568
+ aria-label={
569
+ collapsed
570
+ ? texts.sidenavExpand || t(tokens.sidenavExpand)
571
+ : texts.sidenavCollapse || t(tokens.sidenavCollapse)
572
+ }
573
+ aria-expanded={!collapsed}
574
+ />
575
+ );
576
+ })();
577
+
578
+ const headerSlotElement = renderSidenavSlot(headerSlot, slotRenderProps);
579
+ const footerSlotElement = renderSidenavSlot(footerSlot, slotRenderProps);
580
+ const hasHeader = Boolean(logoElement || collapseActionElement || headerSlotElement);
581
+ // todo https://github.com/Telefonica/mistica-design/issues/2827 review Boxed border rendering logic
582
+ const hasBoxedBorder =
583
+ boxed && shouldShowBoxedBorder(normalizedVariant, pageVariant, componentProperties.showBoxedBorder);
584
+
585
+ // The second column belongs to the sidenav, not to the item that opens it, so that it can span the
586
+ // whole height of the sidenav and push the content of the layout.
587
+ const doublePanelItem =
588
+ doublePanel && panelOpenForItemId && entries
589
+ ? findFirstLevelItem(entries, panelOpenForItemId)
590
+ : undefined;
591
+ const doublePanelChildren = doublePanelItem?.children;
592
+ const isDoublePanelOpen = Boolean(doublePanelChildren?.length);
593
+
594
+ // This sets state during the render, as the block above does. React then paints once, with the new
595
+ // content. The comparison skips the update when the content did not change, which would loop.
596
+ if (
597
+ doublePanelItem &&
598
+ doublePanelChildren?.length &&
599
+ (doublePanelContent?.label !== doublePanelItem.label ||
600
+ doublePanelContent?.children !== doublePanelChildren)
601
+ ) {
602
+ setDoublePanelContent({
603
+ itemId: doublePanelItem.id,
604
+ label: doublePanelItem.label,
605
+ children: doublePanelChildren,
606
+ });
607
+ }
608
+
609
+ return (
610
+ <ThemeVariant variant={normalizedVariant}>
611
+ <SidenavBarContext.Provider value={contextValue}>
612
+ {/* The rail moves the focus between its items with the arrow keys, so the landmark holds the
613
+ key handler that owns that movement. */}
614
+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
615
+ <nav
616
+ ref={containerRef}
617
+ aria-label={ariaLabel}
618
+ onKeyDown={handleRailKeyDown}
619
+ className={classnames(styles.container, {
620
+ [styles.withRightDivider[normalizedVariant]]: divider && !boxed,
621
+ [styles.boxed]: boxed,
622
+ [styles.boxedBorder]: hasBoxedBorder,
623
+ [styles.columnsWhileMoving]: isMoving,
624
+ })}
625
+ style={applyCssVars({
626
+ [styles.sidenavWidthVar]: `${currentWidth}px`,
627
+ [styles.sidenavPanelWidthVar]: `${width}px`,
628
+ [styles.collapseDurationVar]: `${isMotionOff ? 0 : COLLAPSE_DURATION_MS}ms`,
629
+ [styles.contentDurationVar]: `${isMotionOff ? 0 : CONTENT_DURATION_MS}ms`,
630
+ })}
631
+ {...getPrefixedDataAttributes({testid: 'SidenavBar', ...dataAttributes})}
632
+ >
633
+ <div
634
+ className={classnames(styles.mainColumn, {
635
+ [styles.columnSeparator[normalizedVariant]]:
636
+ isDoublePanelOpen || isDoublePanelMounted,
637
+ })}
638
+ >
639
+ {hasHeader && (
640
+ <div
641
+ className={classnames(
642
+ styles.headerBase,
643
+ styles.regionBackground[normalizedVariant],
644
+ {
645
+ [styles.headerNegativeBoxedBackground]:
646
+ boxed && normalizedVariant === 'negative',
647
+ }
648
+ )}
649
+ style={backgroundStyle}
650
+ >
651
+ <div
652
+ className={classnames(styles.headerControls, {
653
+ [styles.headerControlsCollapsed]: collapsed,
654
+ })}
655
+ >
656
+ {logoElement && (
657
+ <div
658
+ className={classnames(styles.logo, {
659
+ [styles.logoDefault]: isDefaultLogo,
660
+ [styles.logoCollapsed]: collapsed,
661
+ })}
662
+ // Only the default brand mark leaves the reading order. A logo of
663
+ // the consumer keeps its own accessibility.
664
+ aria-hidden={isDefaultLogo || undefined}
665
+ >
666
+ {logoElement}
667
+ </div>
668
+ )}
669
+ {collapseActionElement}
670
+ </div>
671
+ {headerSlotElement && (
672
+ <div
673
+ className={classnames(styles.headerSlot, {
674
+ [styles.headerSlotCollapsed]: collapsed,
675
+ })}
676
+ >
677
+ {headerSlotElement}
678
+ </div>
679
+ )}
680
+ </div>
681
+ )}
682
+ <div
683
+ ref={bodyRef}
684
+ className={classnames(
685
+ styles.bodyBase,
686
+ styles.regionBackground[normalizedVariant],
687
+ {[styles.bodyWithFixedFooter]: !!footerSlot && fixedFooter}
688
+ )}
689
+ style={backgroundStyle}
690
+ onFocus={(event) => {
691
+ // A row can take the focus while its ring crosses a seam: the row itself
692
+ // is visible, so the browser scrolls nothing, and the scroll padding of
693
+ // the body never engages. The nudge applies it. `nearest` keeps an
694
+ // already-clear row untouched, and the check leaves a pointer focus
695
+ // alone, which draws no ring.
696
+ const target = event.target as HTMLElement;
697
+ try {
698
+ if (target.matches(':focus-visible')) {
699
+ target.scrollIntoView({block: 'nearest'});
700
+ }
701
+ } catch {
702
+ // jsdom implements neither `:focus-visible` nor `scrollIntoView`.
703
+ }
704
+ }}
705
+ >
706
+ <div ref={headerDividerSentinelRef} />
707
+ <div
708
+ className={classnames(styles.headerScrollSpacer, {
709
+ [styles.regionBackground[normalizedVariant]]:
710
+ hasHeader && showHeaderDivider,
711
+ })}
712
+ style={hasHeader && showHeaderDivider ? backgroundStyle : undefined}
713
+ >
714
+ {hasHeader && showHeaderDivider && (
715
+ <div
716
+ className={classnames(
717
+ styles.scrollSpacerDivider,
718
+ styles.scrollDividerVariant[normalizedVariant]
719
+ )}
720
+ />
721
+ )}
722
+ </div>
723
+ {entries && (
724
+ // The body is the list of the first level. Each entry is one of its items.
725
+ <div className={styles.bodyContent} role="list">
726
+ {renderSidenavEntries(entries)}
727
+ </div>
728
+ )}
729
+ {footerSlot && !fixedFooter && (
730
+ <>
731
+ <div ref={footerDividerSentinelRef} />
732
+ {showFooterDivider && (
733
+ <div
734
+ className={classnames(
735
+ styles.scrollDivider,
736
+ styles.footerScrollDivider,
737
+ styles.scrollDividerVariant[normalizedVariant]
738
+ )}
739
+ />
740
+ )}
741
+ <div
742
+ className={classnames(
743
+ styles.footerBase,
744
+ styles.regionBackground[normalizedVariant],
745
+ {[styles.footerCollapsed]: collapsed}
746
+ )}
747
+ style={backgroundStyle}
748
+ >
749
+ {footerSlotElement}
750
+ </div>
751
+ </>
752
+ )}
753
+ {footerSlot && fixedFooter && (
754
+ <>
755
+ <div ref={footerDividerSentinelRef} />
756
+ <div
757
+ className={classnames(styles.footerScrollSpacer, {
758
+ [styles.regionBackground[normalizedVariant]]: showFooterDivider,
759
+ })}
760
+ style={showFooterDivider ? backgroundStyle : undefined}
761
+ >
762
+ {showFooterDivider && (
763
+ <div
764
+ className={classnames(
765
+ styles.scrollSpacerDivider,
766
+ styles.scrollDividerVariant[normalizedVariant]
767
+ )}
768
+ />
769
+ )}
770
+ </div>
771
+ </>
772
+ )}
773
+ </div>
774
+ {footerSlot && fixedFooter && (
775
+ <div
776
+ className={classnames(
777
+ styles.footerBase,
778
+ styles.footerFixed,
779
+ styles.regionBackground[normalizedVariant],
780
+ {[styles.footerCollapsed]: collapsed}
781
+ )}
782
+ style={backgroundStyle}
783
+ >
784
+ {footerSlotElement}
785
+ </div>
786
+ )}
787
+ </div>
788
+ {doublePanelContent && (
789
+ <CSSTransition
790
+ in={isDoublePanelOpen}
791
+ timeout={isMotionOff ? 0 : COLLAPSE_DURATION_MS}
792
+ nodeRef={doublePanelRef}
793
+ classNames={styles.doublePanelTransitionClasses}
794
+ appear
795
+ mountOnEnter
796
+ unmountOnExit
797
+ onEnter={() => setIsDoublePanelMounted(true)}
798
+ onExited={() => setIsDoublePanelMounted(false)}
799
+ >
800
+ <SidenavDoublePanel
801
+ ref={doublePanelRef}
802
+ itemId={doublePanelContent.itemId}
803
+ label={doublePanelContent.label}
804
+ variant={normalizedVariant}
805
+ backgroundColor={background}
806
+ >
807
+ {doublePanelContent.children.map((child) =>
808
+ renderSidenavSecondLevelItem(child, {placement: 'panel'})
809
+ )}
810
+ </SidenavDoublePanel>
811
+ </CSSTransition>
812
+ )}
813
+ </nav>
814
+ </SidenavBarContext.Provider>
815
+ </ThemeVariant>
816
+ );
817
+ };
818
+
819
+ export default SidenavBar;
820
+ export {SidenavBar};
821
+ export type {SidenavBarProps};