@telefonica/mistica 17.2.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 (347) hide show
  1. package/css/blau.css +30 -16
  2. package/css/community/cyber.css +1491 -0
  3. package/css/esimflag.css +41 -18
  4. package/css/mistica-common.css +6 -0
  5. package/css/mistica.css +1 -1
  6. package/css/movistar.css +35 -18
  7. package/css/o2.css +32 -18
  8. package/css/telefonica.css +44 -18
  9. package/css/vivo-evolution.css +72 -20
  10. package/css/vivo.css +32 -18
  11. package/dist/accordion.css-mistica.js +10 -10
  12. package/dist/align.css-mistica.js +1 -1
  13. package/dist/autocomplete.css-mistica.js +4 -4
  14. package/dist/avatar.css-mistica.js +1 -1
  15. package/dist/badge.css-mistica.js +1 -1
  16. package/dist/box.css-mistica.js +13 -13
  17. package/dist/boxed.css-mistica.js +24 -24
  18. package/dist/boxed.d.ts +9 -0
  19. package/dist/boxed.js +18 -15
  20. package/dist/button-group.css-mistica.js +1 -1
  21. package/dist/button-layout.css-mistica.js +19 -19
  22. package/dist/button.css-mistica.js +50 -50
  23. package/dist/callout.css-mistica.js +13 -13
  24. package/dist/card-internal.css-mistica.js +22 -22
  25. package/dist/carousel.css-mistica.js +30 -30
  26. package/dist/checkbox.css-mistica.js +15 -15
  27. package/dist/chip.css-mistica.js +18 -18
  28. package/dist/circle.css-mistica.js +2 -2
  29. package/dist/community/advanced-data-card.css-mistica.js +19 -19
  30. package/dist/community/ai-card.css-mistica.js +9 -9
  31. package/dist/community/blocks.css-mistica.js +3 -3
  32. package/dist/community/cyber/example-component.css-mistica.js +1 -1
  33. package/dist/community/example-component.css-mistica.js +1 -1
  34. package/dist/community/skins/cyber-skin.d.ts +5 -0
  35. package/dist/community/skins/cyber-skin.js +289 -256
  36. package/dist/counter.css-mistica.js +6 -6
  37. package/dist/cover-hero.css-mistica.js +7 -7
  38. package/dist/credit-card-number-field.css-mistica.js +3 -3
  39. package/dist/date-field.css-mistica.js +1 -1
  40. package/dist/date-time-picker.css-mistica.js +1 -1
  41. package/dist/dialog.css-mistica.js +10 -10
  42. package/dist/divider.css-mistica.js +5 -5
  43. package/dist/double-field.css-mistica.js +2 -2
  44. package/dist/drawer.css-mistica.js +4 -4
  45. package/dist/empty-state-card.css-mistica.js +1 -1
  46. package/dist/empty-state.css-mistica.js +8 -8
  47. package/dist/fade-in.css-mistica.js +1 -1
  48. package/dist/feedback.css-mistica.js +5 -5
  49. package/dist/file-upload.css-mistica.js +10 -10
  50. package/dist/fixed-footer-layout.css-mistica.js +6 -6
  51. package/dist/form.css-mistica.js +1 -1
  52. package/dist/generated/mistica-icons/icon-panel-collapse-regular.js +165 -0
  53. package/dist/generated/mistica-icons/icon-panel-expand-regular.js +159 -0
  54. package/dist/grid-layout.css-mistica.js +3 -3
  55. package/dist/grid.css-mistica.js +120 -120
  56. package/dist/header.css-mistica.js +1 -1
  57. package/dist/hero.css-mistica.js +4 -4
  58. package/dist/horizontal-scroll.css-mistica.js +1 -1
  59. package/dist/icon-button.css-mistica.js +53 -53
  60. package/dist/icons/icon-chevron.css-mistica.js +2 -2
  61. package/dist/icons/icon-error.css-mistica.js +1 -1
  62. package/dist/image.css-mistica.js +8 -8
  63. package/dist/index.d.ts +5 -0
  64. package/dist/index.js +10 -1
  65. package/dist/inline.css-mistica.js +11 -11
  66. package/dist/list.css-mistica.js +40 -28
  67. package/dist/list.css.d.ts +2 -0
  68. package/dist/list.d.ts +3 -1
  69. package/dist/list.js +176 -165
  70. package/dist/loading-bar.css-mistica.js +1 -1
  71. package/dist/loading-screen.css-mistica.js +12 -12
  72. package/dist/logo-movistar.js +1 -1
  73. package/dist/logo.css-mistica.js +5 -5
  74. package/dist/menu.css-mistica.js +13 -13
  75. package/dist/mobile-navigation-menu.d.ts +26 -0
  76. package/dist/mobile-navigation-menu.js +145 -0
  77. package/dist/mosaic.css-mistica.js +1 -1
  78. package/dist/navigation-bar.css-mistica.js +31 -97
  79. package/dist/navigation-bar.css.d.ts +0 -24
  80. package/dist/navigation-bar.d.ts +3 -38
  81. package/dist/navigation-bar.js +399 -586
  82. package/dist/navigation-breadcrumbs.css-mistica.js +1 -1
  83. package/dist/navigation-shared.css-mistica.js +79 -0
  84. package/dist/navigation-shared.css.d.ts +26 -0
  85. package/dist/navigation-shared.d.ts +66 -0
  86. package/dist/navigation-shared.js +264 -0
  87. package/dist/package-version.js +1 -1
  88. package/dist/pagination.css-mistica.js +22 -22
  89. package/dist/pin-field.css-mistica.js +4 -4
  90. package/dist/popover.css-mistica.js +1 -1
  91. package/dist/portal.d.ts +1 -1
  92. package/dist/progress-bar.css-mistica.js +7 -7
  93. package/dist/radio-button.css-mistica.js +27 -27
  94. package/dist/rating.css-mistica.js +4 -4
  95. package/dist/responsive-layout.css-mistica.js +6 -6
  96. package/dist/screen-reader-only.css-mistica.js +1 -1
  97. package/dist/select.css-mistica.js +21 -21
  98. package/dist/sheet-action-row.css-mistica.js +1 -1
  99. package/dist/sheet-common.css-mistica.js +7 -7
  100. package/dist/sheet-info.css-mistica.js +1 -1
  101. package/dist/sidenav-bar-context.d.ts +25 -0
  102. package/dist/sidenav-bar-context.js +72 -0
  103. package/dist/sidenav-bar-data.d.ts +15 -0
  104. package/dist/sidenav-bar-data.js +38 -0
  105. package/dist/sidenav-bar-first-level-item.d.ts +42 -0
  106. package/dist/sidenav-bar-first-level-item.js +174 -0
  107. package/dist/sidenav-bar-keyboard.d.ts +13 -0
  108. package/dist/sidenav-bar-keyboard.js +314 -0
  109. package/dist/sidenav-bar-layout.css-mistica.js +28 -0
  110. package/dist/sidenav-bar-layout.css.d.ts +5 -0
  111. package/dist/sidenav-bar-layout.d.ts +22 -0
  112. package/dist/sidenav-bar-layout.js +103 -0
  113. package/dist/sidenav-bar-mobile.d.ts +24 -0
  114. package/dist/sidenav-bar-mobile.js +332 -0
  115. package/dist/sidenav-bar-motion.d.ts +32 -0
  116. package/dist/sidenav-bar-motion.js +84 -0
  117. package/dist/sidenav-bar-panel.d.ts +31 -0
  118. package/dist/sidenav-bar-panel.js +208 -0
  119. package/dist/sidenav-bar-row.d.ts +82 -0
  120. package/dist/sidenav-bar-row.js +230 -0
  121. package/dist/sidenav-bar-second-level-item.d.ts +43 -0
  122. package/dist/sidenav-bar-second-level-item.js +89 -0
  123. package/dist/sidenav-bar-section.d.ts +23 -0
  124. package/dist/sidenav-bar-section.js +167 -0
  125. package/dist/sidenav-bar-types.d.ts +192 -0
  126. package/dist/sidenav-bar-types.js +11 -0
  127. package/dist/sidenav-bar.css-mistica.js +325 -0
  128. package/dist/sidenav-bar.css.d.ts +93 -0
  129. package/dist/sidenav-bar.d.ts +102 -0
  130. package/dist/sidenav-bar.js +492 -0
  131. package/dist/skeletons.css-mistica.js +8 -8
  132. package/dist/skins/blau.d.ts +5 -0
  133. package/dist/skins/blau.js +12 -8
  134. package/dist/skins/esimflag.d.ts +5 -0
  135. package/dist/skins/esimflag.js +13 -9
  136. package/dist/skins/movistar.d.ts +5 -0
  137. package/dist/skins/movistar.js +13 -9
  138. package/dist/skins/o2.d.ts +5 -0
  139. package/dist/skins/o2.js +13 -9
  140. package/dist/skins/skin-contract.css-mistica.js +735 -731
  141. package/dist/skins/skin-contract.css.d.ts +16 -12
  142. package/dist/skins/telefonica.d.ts +5 -0
  143. package/dist/skins/telefonica.js +13 -9
  144. package/dist/skins/types/colors.d.ts +13 -6
  145. package/dist/skins/vivo-evolution.d.ts +5 -0
  146. package/dist/skins/vivo-evolution.js +14 -10
  147. package/dist/skins/vivo.d.ts +5 -0
  148. package/dist/skins/vivo.js +13 -9
  149. package/dist/skip-link.css-mistica.js +2 -2
  150. package/dist/slider.css-mistica.js +24 -24
  151. package/dist/snackbar.css-mistica.js +14 -14
  152. package/dist/spinner.css-mistica.js +1 -1
  153. package/dist/square.css-mistica.js +1 -1
  154. package/dist/stack.css-mistica.js +5 -5
  155. package/dist/stacking-group.css-mistica.js +1 -1
  156. package/dist/stepper.css-mistica.js +9 -9
  157. package/dist/switch-component.css-mistica.js +48 -42
  158. package/dist/switch-component.css.d.ts +2 -0
  159. package/dist/switch-component.js +18 -18
  160. package/dist/table.css-mistica.js +9 -9
  161. package/dist/tabs.css-mistica.js +22 -22
  162. package/dist/tag.css-mistica.js +3 -3
  163. package/dist/text-field-base.css-mistica.js +19 -19
  164. package/dist/text-field-components.css-mistica.js +10 -10
  165. package/dist/text-link.css-mistica.js +6 -6
  166. package/dist/text-tokens.d.ts +6 -0
  167. package/dist/text-tokens.js +157 -133
  168. package/dist/text.css-mistica.js +6 -6
  169. package/dist/timeline.css-mistica.js +11 -11
  170. package/dist/timer.css-mistica.js +6 -6
  171. package/dist/tooltip.css-mistica.js +2 -2
  172. package/dist/touchable.css-mistica.js +4 -4
  173. package/dist/utils/aspect-ratio-support.css-mistica.js +2 -2
  174. package/dist/video.css-mistica.js +1 -1
  175. package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  176. package/dist-es/accordion.css-mistica.js +7 -7
  177. package/dist-es/align.css-mistica.js +1 -1
  178. package/dist-es/autocomplete.css-mistica.js +2 -2
  179. package/dist-es/avatar.css-mistica.js +1 -1
  180. package/dist-es/badge.css-mistica.js +1 -1
  181. package/dist-es/box.css-mistica.js +13 -13
  182. package/dist-es/boxed.css-mistica.js +23 -23
  183. package/dist-es/boxed.js +25 -25
  184. package/dist-es/button-group.css-mistica.js +1 -1
  185. package/dist-es/button-layout.css-mistica.js +16 -16
  186. package/dist-es/button.css-mistica.js +38 -38
  187. package/dist-es/callout.css-mistica.js +12 -12
  188. package/dist-es/card-internal.css-mistica.js +17 -17
  189. package/dist-es/carousel.css-mistica.js +10 -10
  190. package/dist-es/checkbox.css-mistica.js +13 -13
  191. package/dist-es/chip.css-mistica.js +17 -17
  192. package/dist-es/circle.css-mistica.js +2 -2
  193. package/dist-es/community/advanced-data-card.css-mistica.js +7 -7
  194. package/dist-es/community/ai-card.css-mistica.js +3 -3
  195. package/dist-es/community/blocks.css-mistica.js +2 -2
  196. package/dist-es/community/cyber/example-component.css-mistica.js +1 -1
  197. package/dist-es/community/example-component.css-mistica.js +1 -1
  198. package/dist-es/community/skins/cyber-skin.js +289 -256
  199. package/dist-es/counter.css-mistica.js +2 -2
  200. package/dist-es/cover-hero.css-mistica.js +4 -4
  201. package/dist-es/credit-card-number-field.css-mistica.js +3 -3
  202. package/dist-es/date-field.css-mistica.js +1 -1
  203. package/dist-es/date-time-picker.css-mistica.js +1 -1
  204. package/dist-es/dialog.css-mistica.js +5 -5
  205. package/dist-es/divider.css-mistica.js +5 -5
  206. package/dist-es/double-field.css-mistica.js +2 -2
  207. package/dist-es/drawer.css-mistica.js +2 -2
  208. package/dist-es/empty-state-card.css-mistica.js +1 -1
  209. package/dist-es/empty-state.css-mistica.js +7 -7
  210. package/dist-es/fade-in.css-mistica.js +1 -1
  211. package/dist-es/feedback.css-mistica.js +2 -2
  212. package/dist-es/file-upload.css-mistica.js +8 -8
  213. package/dist-es/fixed-footer-layout.css-mistica.js +3 -3
  214. package/dist-es/form.css-mistica.js +1 -1
  215. package/dist-es/generated/mistica-icons/icon-panel-collapse-regular.js +115 -0
  216. package/dist-es/generated/mistica-icons/icon-panel-expand-regular.js +109 -0
  217. package/dist-es/grid-layout.css-mistica.js +3 -3
  218. package/dist-es/grid.css-mistica.js +120 -120
  219. package/dist-es/header.css-mistica.js +1 -1
  220. package/dist-es/hero.css-mistica.js +3 -3
  221. package/dist-es/horizontal-scroll.css-mistica.js +1 -1
  222. package/dist-es/icon-button.css-mistica.js +53 -53
  223. package/dist-es/icons/icon-chevron.css-mistica.js +2 -2
  224. package/dist-es/icons/icon-error.css-mistica.js +1 -1
  225. package/dist-es/image.css-mistica.js +4 -4
  226. package/dist-es/index.js +2279 -2276
  227. package/dist-es/inline.css-mistica.js +11 -11
  228. package/dist-es/list.css-mistica.js +8 -2
  229. package/dist-es/list.js +275 -264
  230. package/dist-es/loading-bar.css-mistica.js +1 -1
  231. package/dist-es/loading-screen.css-mistica.js +5 -5
  232. package/dist-es/logo-movistar.js +1 -1
  233. package/dist-es/logo.css-mistica.js +5 -5
  234. package/dist-es/menu.css-mistica.js +13 -13
  235. package/dist-es/mobile-navigation-menu.js +90 -0
  236. package/dist-es/mosaic.css-mistica.js +1 -1
  237. package/dist-es/navigation-bar.css-mistica.js +11 -20
  238. package/dist-es/navigation-bar.js +469 -656
  239. package/dist-es/navigation-breadcrumbs.css-mistica.js +1 -1
  240. package/dist-es/navigation-shared.css-mistica.js +11 -0
  241. package/dist-es/navigation-shared.js +192 -0
  242. package/dist-es/package-version.js +1 -1
  243. package/dist-es/pagination.css-mistica.js +19 -19
  244. package/dist-es/pin-field.css-mistica.js +2 -2
  245. package/dist-es/popover.css-mistica.js +1 -1
  246. package/dist-es/progress-bar.css-mistica.js +7 -7
  247. package/dist-es/radio-button.css-mistica.js +22 -22
  248. package/dist-es/rating.css-mistica.js +3 -3
  249. package/dist-es/responsive-layout.css-mistica.js +6 -6
  250. package/dist-es/screen-reader-only.css-mistica.js +1 -1
  251. package/dist-es/select.css-mistica.js +18 -18
  252. package/dist-es/sheet-action-row.css-mistica.js +1 -1
  253. package/dist-es/sheet-common.css-mistica.js +2 -2
  254. package/dist-es/sheet-info.css-mistica.js +1 -1
  255. package/dist-es/sidenav-bar-context.js +14 -0
  256. package/dist-es/sidenav-bar-data.js +12 -0
  257. package/dist-es/sidenav-bar-first-level-item.js +124 -0
  258. package/dist-es/sidenav-bar-keyboard.js +253 -0
  259. package/dist-es/sidenav-bar-layout.css-mistica.js +2 -0
  260. package/dist-es/sidenav-bar-layout.js +86 -0
  261. package/dist-es/sidenav-bar-mobile.js +277 -0
  262. package/dist-es/sidenav-bar-motion.js +26 -0
  263. package/dist-es/sidenav-bar-panel.js +146 -0
  264. package/dist-es/sidenav-bar-row.js +208 -0
  265. package/dist-es/sidenav-bar-second-level-item.js +72 -0
  266. package/dist-es/sidenav-bar-section.js +112 -0
  267. package/dist-es/sidenav-bar-types.js +2 -0
  268. package/dist-es/sidenav-bar.css-mistica.js +74 -0
  269. package/dist-es/sidenav-bar.js +429 -0
  270. package/dist-es/skeletons.css-mistica.js +7 -7
  271. package/dist-es/skins/blau.js +12 -8
  272. package/dist-es/skins/esimflag.js +13 -9
  273. package/dist-es/skins/movistar.js +13 -9
  274. package/dist-es/skins/o2.js +13 -9
  275. package/dist-es/skins/skin-contract.css-mistica.js +735 -731
  276. package/dist-es/skins/telefonica.js +13 -9
  277. package/dist-es/skins/vivo-evolution.js +14 -10
  278. package/dist-es/skins/vivo.js +13 -9
  279. package/dist-es/skip-link.css-mistica.js +2 -2
  280. package/dist-es/slider.css-mistica.js +20 -20
  281. package/dist-es/snackbar.css-mistica.js +5 -5
  282. package/dist-es/spinner.css-mistica.js +1 -1
  283. package/dist-es/square.css-mistica.js +1 -1
  284. package/dist-es/stack.css-mistica.js +5 -5
  285. package/dist-es/stacking-group.css-mistica.js +1 -1
  286. package/dist-es/stepper.css-mistica.js +3 -3
  287. package/dist-es/style.css +1 -1
  288. package/dist-es/switch-component.css-mistica.js +37 -37
  289. package/dist-es/switch-component.js +26 -26
  290. package/dist-es/table.css-mistica.js +9 -9
  291. package/dist-es/tabs.css-mistica.js +19 -19
  292. package/dist-es/tag.css-mistica.js +2 -2
  293. package/dist-es/text-field-base.css-mistica.js +17 -17
  294. package/dist-es/text-field-components.css-mistica.js +4 -4
  295. package/dist-es/text-link.css-mistica.js +6 -6
  296. package/dist-es/text-tokens.js +82 -67
  297. package/dist-es/text.css-mistica.js +6 -6
  298. package/dist-es/timeline.css-mistica.js +10 -10
  299. package/dist-es/timer.css-mistica.js +6 -6
  300. package/dist-es/tooltip.css-mistica.js +2 -2
  301. package/dist-es/touchable.css-mistica.js +2 -2
  302. package/dist-es/utils/aspect-ratio-support.css-mistica.js +2 -2
  303. package/dist-es/video.css-mistica.js +1 -1
  304. package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
  305. package/package.json +3 -2
  306. package/src/boxed.css.ts +1 -0
  307. package/src/boxed.tsx +18 -5
  308. package/src/callout.css.ts +4 -2
  309. package/src/community/skins/cyber-skin.tsx +127 -439
  310. package/src/index.tsx +14 -0
  311. package/src/list.css.ts +36 -1
  312. package/src/list.tsx +370 -344
  313. package/src/logo-movistar.tsx +1 -1
  314. package/src/mobile-navigation-menu.tsx +168 -0
  315. package/src/navigation-bar.css.ts +2 -267
  316. package/src/navigation-bar.tsx +77 -370
  317. package/src/navigation-shared.css.ts +289 -0
  318. package/src/navigation-shared.tsx +262 -0
  319. package/src/package-version.tsx +1 -1
  320. package/src/portal.tsx +1 -1
  321. package/src/sidenav-bar-context.tsx +41 -0
  322. package/src/sidenav-bar-data.ts +49 -0
  323. package/src/sidenav-bar-first-level-item.tsx +203 -0
  324. package/src/sidenav-bar-keyboard.tsx +393 -0
  325. package/src/sidenav-bar-layout.css.ts +89 -0
  326. package/src/sidenav-bar-layout.tsx +61 -0
  327. package/src/sidenav-bar-mobile.tsx +302 -0
  328. package/src/sidenav-bar-motion.tsx +70 -0
  329. package/src/sidenav-bar-panel.tsx +269 -0
  330. package/src/sidenav-bar-row.tsx +272 -0
  331. package/src/sidenav-bar-second-level-item.tsx +99 -0
  332. package/src/sidenav-bar-section.tsx +98 -0
  333. package/src/sidenav-bar-types.ts +224 -0
  334. package/src/sidenav-bar.css.ts +1018 -0
  335. package/src/sidenav-bar.tsx +821 -0
  336. package/src/skins/blau.tsx +18 -8
  337. package/src/skins/esimflag.tsx +19 -9
  338. package/src/skins/movistar.tsx +19 -9
  339. package/src/skins/o2.tsx +19 -9
  340. package/src/skins/skin-contract.css.ts +2 -0
  341. package/src/skins/telefonica.tsx +19 -9
  342. package/src/skins/types/colors.tsx +14 -6
  343. package/src/skins/vivo-evolution.tsx +20 -10
  344. package/src/skins/vivo.tsx +19 -9
  345. package/src/switch-component.css.ts +20 -0
  346. package/src/switch-component.tsx +9 -1
  347. package/src/text-tokens.tsx +24 -0
@@ -0,0 +1,269 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import classnames from 'classnames';
4
+ import * as styles from './sidenav-bar.css';
5
+ import {useSidenavBarContext} from './sidenav-bar-context';
6
+ import {useDialogPanelKeyboard, getItemFocusables} from './sidenav-bar-keyboard';
7
+ import {Portal} from './portal';
8
+ import {Text2} from './text';
9
+ import {listenResize} from './utils/dom';
10
+ import {combineRefs} from './utils/common';
11
+ import {ThemeVariant} from './theme-variant-context';
12
+
13
+ import type {NonDeprecatedVariant} from './theme-variant-context';
14
+
15
+ /*
16
+ * A panel holds the children of a parent item away from its row: `SidenavDialogPanel` floats over the
17
+ * collapsed rail, and `SidenavDoublePanel` is the second column. The expanded rail opens the children in
18
+ * place instead, so that form is not a panel.
19
+ */
20
+
21
+ /** Minimum distance kept between the panel and the viewport edges. */
22
+ const VIEWPORT_MARGIN = 8;
23
+
24
+ /**
25
+ * Closes the dialog panel when the user presses outside of it, or presses the Escape key. A press on a
26
+ * parent item does not close the panel here: that item toggles the panel itself, or replaces its content.
27
+ *
28
+ * The double panel does not use this hook: it is a column of the sidenav, not a floating dialog, so only
29
+ * a press outside of the whole bar dismisses it. `SidenavBar` owns that rule.
30
+ */
31
+ const useClosePanelOnOutsideInteraction = (panelElement: HTMLElement | null): void => {
32
+ const {setPanelOpenForItemId} = useSidenavBarContext();
33
+
34
+ React.useEffect(() => {
35
+ const handleClickOutside = (event: MouseEvent) => {
36
+ if (!panelElement) {
37
+ return;
38
+ }
39
+ // See the same call in `sidenav-bar.tsx`: the path holds the nodes that the press travelled
40
+ // through, and not the node that survived until this listener ran.
41
+ const path = event.composedPath();
42
+ if (path.includes(panelElement)) {
43
+ return;
44
+ }
45
+ // A press on a parent item lands outside of this panel, but that item owns the panel and
46
+ // toggles it on its own, so this listener leaves it alone. The marker travels in the path
47
+ // above, which is why it still reads a row that React already replaced.
48
+ const isParentItem = path.some(
49
+ (node) => node instanceof Element && node.matches('[data-parent-item="true"]')
50
+ );
51
+ if (!isParentItem) {
52
+ setPanelOpenForItemId(null);
53
+ }
54
+ };
55
+
56
+ const handleEscape = (event: KeyboardEvent) => {
57
+ if (event.key === 'Escape') {
58
+ setPanelOpenForItemId(null);
59
+ }
60
+ };
61
+
62
+ document.addEventListener('click', handleClickOutside);
63
+ document.addEventListener('keydown', handleEscape);
64
+
65
+ return () => {
66
+ document.removeEventListener('click', handleClickOutside);
67
+ document.removeEventListener('keydown', handleEscape);
68
+ };
69
+ }, [panelElement, setPanelOpenForItemId]);
70
+ };
71
+
72
+ type SidenavDialogPanelProps = {
73
+ id: string;
74
+ itemId: string;
75
+ label: string;
76
+ containerRef: React.RefObject<HTMLElement | null>;
77
+ children: React.ReactNode;
78
+ };
79
+
80
+ /**
81
+ * A floating panel with the children of a parent item. It is used when the sidenav is collapsed and the
82
+ * double panel mode is off. The panel is anchored to the top of its trigger whenever it fits.
83
+ */
84
+ const SidenavDialogPanel = ({
85
+ id,
86
+ itemId,
87
+ label,
88
+ containerRef,
89
+ children,
90
+ }: SidenavDialogPanelProps): JSX.Element => {
91
+ const {setPanelOpenForItemId} = useSidenavBarContext();
92
+ // `Portal` creates its host element in an effect, so the panel node appears one render after the
93
+ // panel mounts. Keeping the node in state (instead of in a ref) re-runs the effects that measure
94
+ // it as soon as it exists.
95
+ const [panelElement, setPanelElement] = React.useState<HTMLDivElement | null>(null);
96
+ const [panelPosition, setPanelPosition] = React.useState<{top: number; left: number} | null>(null);
97
+ // The visible title names the list, so the name a screen reader speaks is always the text the user
98
+ // sees. The panel itself carries no role and no name: the named list is the whole structure.
99
+ const titleId = React.useId();
100
+
101
+ useClosePanelOnOutsideInteraction(panelElement);
102
+ // The first item of the panel takes the focus only once the panel stands where it belongs: it stays
103
+ // hidden until the effect below measures it, and a hidden element takes no focus.
104
+ useDialogPanelKeyboard({
105
+ panelElement,
106
+ containerRef,
107
+ itemId,
108
+ isPositioned: panelPosition !== null,
109
+ onClose: () => setPanelOpenForItemId(null),
110
+ });
111
+
112
+ // The panel opens aligned with its trigger, but a trigger close to the bottom edge pushes the
113
+ // panel out of the viewport. The top is therefore clamped to the lowest position where the whole
114
+ // panel still fits, so every item stays readable and clickable. A panel taller than the viewport
115
+ // starts at the top margin and scrolls inside its own `maxHeight`.
116
+ React.useEffect(() => {
117
+ const updatePosition = () => {
118
+ const container = containerRef.current;
119
+ if (!container) return;
120
+
121
+ const triggerElement = container.querySelector(`[data-sidenav-item-id="${itemId}"]`);
122
+ if (!triggerElement) return;
123
+
124
+ const containerRect = container.getBoundingClientRect();
125
+ const triggerRect = triggerElement.getBoundingClientRect();
126
+ const panelHeight = panelElement?.offsetHeight ?? 0;
127
+ const lowestFittingTop = window.innerHeight - panelHeight - VIEWPORT_MARGIN;
128
+
129
+ setPanelPosition({
130
+ top: Math.max(VIEWPORT_MARGIN, Math.min(triggerRect.top, lowestFittingTop)),
131
+ left: containerRect.right + 8,
132
+ });
133
+ };
134
+
135
+ updatePosition();
136
+
137
+ const stopListeningResize = panelElement ? listenResize(panelElement, updatePosition) : undefined;
138
+ window.addEventListener('resize', updatePosition);
139
+ return () => {
140
+ stopListeningResize?.();
141
+ window.removeEventListener('resize', updatePosition);
142
+ };
143
+ }, [itemId, containerRef, panelElement]);
144
+
145
+ return (
146
+ <Portal>
147
+ <div
148
+ ref={setPanelElement}
149
+ id={id}
150
+ className={styles.dialogPanel}
151
+ data-sidenav-dialog-panel={itemId}
152
+ style={
153
+ panelPosition
154
+ ? {
155
+ position: 'fixed' as const,
156
+ top: panelPosition.top,
157
+ left: panelPosition.left,
158
+ }
159
+ : // The panel needs its own height to know whether it fits below the
160
+ // trigger, so it stays hidden for the render that precedes the
161
+ // measurement instead of flashing at a placeholder position.
162
+ {
163
+ position: 'fixed' as const,
164
+ top: 0,
165
+ left: 0,
166
+ visibility: 'hidden' as const,
167
+ }
168
+ }
169
+ >
170
+ <ThemeVariant variant="default">
171
+ <div
172
+ id={titleId}
173
+ className={classnames(styles.dialogPanelTitle, styles.sectionTitleVariant.default)}
174
+ // The list already speaks this text as its name, so the title steps out of the
175
+ // reading order. The name survives: `aria-labelledby` reads a hidden element that
176
+ // it references directly.
177
+ aria-hidden="true"
178
+ >
179
+ <Text2 medium color="inherit">
180
+ {label}
181
+ </Text2>
182
+ </div>
183
+ {/* The title names the list, and the list gives the count of its items. */}
184
+ <div className={styles.panelRows} role="list" aria-labelledby={titleId}>
185
+ {children}
186
+ </div>
187
+ </ThemeVariant>
188
+ </div>
189
+ </Portal>
190
+ );
191
+ };
192
+
193
+ type SidenavDoublePanelProps = {
194
+ itemId: string;
195
+ label: string;
196
+ variant: NonDeprecatedVariant;
197
+ backgroundColor?: string;
198
+ children: React.ReactNode;
199
+ };
200
+
201
+ /**
202
+ * The second column of the sidenav, with the children of the open parent item. It is used in double
203
+ * panel mode, both when the sidenav is expanded and when it is collapsed.
204
+ *
205
+ * It forwards its ref because `SidenavBar` slides it out and back in with a `CSSTransition`, which needs
206
+ * the node of the column. The column itself carries no padding: an inner box of the width of the open
207
+ * column holds it instead, so the column can narrow to zero without reflowing its content.
208
+ */
209
+ const SidenavDoublePanel = React.forwardRef<HTMLDivElement, SidenavDoublePanelProps>(
210
+ ({itemId, label, variant, backgroundColor, children}, ref) => {
211
+ const {containerRef} = useSidenavBarContext();
212
+ // The bar owns the forwarded ref for its transition; this one lets the column read its own node.
213
+ const columnRef = React.useRef<HTMLDivElement>(null);
214
+ // The visible title names the list, so the name a screen reader speaks is always the text the
215
+ // user sees. The column itself carries no role and no name: the named list is the whole structure.
216
+ const titleId = React.useId();
217
+
218
+ // The first item takes the focus when the column opens (the bar mounts it then), and again when
219
+ // the column moves to another parent, so a screen reader announces the named list that the user
220
+ // entered. It only takes a focus that already belongs to the sidenav, or one that fell to the
221
+ // body when the collapsed rail replaced the trigger row. An app that moves the selection from
222
+ // elsewhere on the page opens this column too, and it must not drag the user out of the place
223
+ // they were reading.
224
+ React.useEffect(() => {
225
+ const column = columnRef.current;
226
+ const container = containerRef.current;
227
+ if (!column || !container) {
228
+ return;
229
+ }
230
+ const active = document.activeElement;
231
+ if (active && active !== document.body && !container.contains(active)) {
232
+ return;
233
+ }
234
+ getItemFocusables(column)[0]?.focus();
235
+ }, [itemId, containerRef]);
236
+
237
+ return (
238
+ <div
239
+ ref={combineRefs(ref, columnRef)}
240
+ className={classnames(styles.doublePanelColumn, styles.regionBackground[variant])}
241
+ style={backgroundColor ? {backgroundColor} : undefined}
242
+ // Marks the column with the id of the item that owns it, so the keyboard of the rail finds
243
+ // the list of a trigger, and the trigger of a list.
244
+ data-sidenav-double-panel={itemId}
245
+ >
246
+ <div className={styles.doublePanelContent}>
247
+ <div
248
+ id={titleId}
249
+ className={classnames(styles.doublePanelTitle, styles.sectionTitleVariant[variant])}
250
+ // The list already speaks this text as its name, so the title steps out of the
251
+ // reading order. The name survives: `aria-labelledby` reads a hidden element that
252
+ // it references directly.
253
+ aria-hidden="true"
254
+ >
255
+ <Text2 medium color="inherit">
256
+ {label}
257
+ </Text2>
258
+ </div>
259
+ {/* The title names the list, and the list gives the count of its items. */}
260
+ <div className={styles.panelRows} role="list" aria-labelledby={titleId}>
261
+ {children}
262
+ </div>
263
+ </div>
264
+ </div>
265
+ );
266
+ }
267
+ );
268
+
269
+ export {SidenavDialogPanel, SidenavDoublePanel};
@@ -0,0 +1,272 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import classnames from 'classnames';
4
+ import * as styles from './sidenav-bar.css';
5
+ import {useSidenavBarContext} from './sidenav-bar-context';
6
+ import {useRestWidth} from './sidenav-bar-motion';
7
+ import {getPrefixedDataAttributes} from './utils/dom';
8
+ import {useThemeVariant} from './theme-variant-context';
9
+ import Touchable from './touchable';
10
+ import Tooltip from './tooltip';
11
+ import {Text2} from './text';
12
+ import IconChevron from './icons/icon-chevron';
13
+
14
+ import type {DataAttributes} from './utils/types';
15
+ import type {SidenavAsset} from './sidenav-bar-types';
16
+
17
+ /**
18
+ * Where a row stands. The rail (a first-level row) and the accordion (a second-level row under its parent)
19
+ * move with the bar when it collapses. A panel (the dialog panel, or the second column) never moves with
20
+ * it, so a row of a panel always sees a bar at rest expanded.
21
+ */
22
+ type SidenavRowPlacement = 'rail' | 'accordion' | 'panel';
23
+
24
+ /** Props that make the row navigate. The item builds them with `useSidenavNavigation`. */
25
+ type SidenavRowNavigationProps =
26
+ | {onPress: () => Promise<void>}
27
+ | {href: string; newTab?: boolean; onNavigate: () => Promise<void>}
28
+ | {to: string; newTab?: boolean; onNavigate: () => Promise<void>};
29
+
30
+ /** Props that make the row open and close its children instead of navigating. */
31
+ type SidenavRowToggleProps = {
32
+ onPress: () => void;
33
+ expanded: boolean;
34
+ /** Id of the dialog panel that the row opens, when it opens one. */
35
+ controls?: string;
36
+ };
37
+
38
+ type SidenavRowProps = {
39
+ /** Id of the item, written to the DOM so the keyboard code and the tests find the row. */
40
+ id?: string;
41
+ label: string;
42
+ asset?: SidenavAsset;
43
+ /** Hide the asset without removing it from the data. The collapsed rail always shows it. */
44
+ showAsset?: boolean;
45
+ rightSlot?: React.ReactNode;
46
+ /** Paint the selected background. A parent takes it for a hidden selected descendant. */
47
+ showBackground?: boolean;
48
+ /** Paint the accent bar. Only the selected item itself takes it, never a parent of it. */
49
+ showAccent?: boolean;
50
+ /** Id written on the label, so the accordion list can take its name from it. */
51
+ labelId?: string;
52
+ /** Render the chevron of a parent item, rotated when its children are open. */
53
+ chevron?: {rotated: boolean; direction: 'down' | 'right'};
54
+ /**
55
+ * Where the row stands, which tells it whether the collapse motion of the bar reaches it. The context
56
+ * gives the phase of that motion, and it cannot tell the place.
57
+ */
58
+ placement: SidenavRowPlacement;
59
+ /** Show the label as a tooltip on the right, for the collapsed rail. */
60
+ tooltip?: boolean;
61
+ /** Present when the row navigates. Mutually exclusive with `toggle`. */
62
+ navigation?: SidenavRowNavigationProps;
63
+ /** Present when the row opens and closes its children. Mutually exclusive with `navigation`. */
64
+ toggle?: SidenavRowToggleProps;
65
+ /** True when the row is the current page, or holds it among its children. */
66
+ current?: boolean;
67
+ dataAttributes?: DataAttributes;
68
+ };
69
+
70
+ /**
71
+ * The visible row of an item, at both levels: the accent, the asset, the label, the right slot, the
72
+ * chevron, and the tooltip of the collapsed rail. It holds no state: `SidenavFirstLevelItem` and
73
+ * `SidenavSecondLevelItem` decide what the row shows, and this component paints it.
74
+ */
75
+ const SidenavRow = ({
76
+ id,
77
+ label,
78
+ asset,
79
+ showAsset = true,
80
+ rightSlot,
81
+ showBackground = false,
82
+ showAccent = false,
83
+ labelId,
84
+ chevron,
85
+ placement,
86
+ tooltip = false,
87
+ navigation,
88
+ toggle,
89
+ current = false,
90
+ dataAttributes,
91
+ }: SidenavRowProps): JSX.Element => {
92
+ // Read from context, not from `SidenavBar`: the floating panel restores the default variant.
93
+ const variant = useThemeVariant();
94
+
95
+ // The row paints the collapsed rail (the asset alone, and the label faded but in the DOM) when the
96
+ // bar is collapsed or collapsing, and it holds the width of its label whenever the bar does not rest
97
+ // expanded. A row of a panel never moves with the bar, so it sees a bar at rest expanded.
98
+ const {collapseState: barCollapseState} = useSidenavBarContext();
99
+ const collapseState = placement === 'panel' ? 'expanded' : barCollapseState;
100
+ const collapsed = collapseState === 'collapsed' || collapseState === 'collapsing';
101
+ // The label keeps the width of its text while the bar moves in either direction, and wraps only at
102
+ // rest expanded. See `itemLabelKeepsWidth`.
103
+ const labelWidthFixed = collapseState !== 'expanded';
104
+
105
+ let assetContent: React.ReactNode = null;
106
+ if (typeof asset === 'function') {
107
+ const Asset = asset;
108
+ assetContent = <Asset size={20} color="currentColor" />;
109
+ } else if (asset) {
110
+ assetContent = asset;
111
+ }
112
+ const assetElement =
113
+ showAsset && assetContent ? (
114
+ <span className={classnames(styles.itemAsset, styles.itemAssetVariant[variant])}>
115
+ {assetContent}
116
+ </span>
117
+ ) : null;
118
+
119
+ // The collapsed label stays in the DOM: a screen reader still reads it, and the fade needs it.
120
+ const {ref: labelRef, frozenWidth: labelWidth} = useRestWidth(labelWidthFixed);
121
+ const labelNode = (
122
+ <div
123
+ ref={labelRef}
124
+ id={labelId}
125
+ className={classnames(styles.itemLabel, {
126
+ [styles.itemLabelCollapsed]: collapsed,
127
+ [styles.itemLabelFrozenWidth]: labelWidth !== undefined,
128
+ [styles.itemLabelKeepsWidth]: labelWidthFixed && labelWidth === undefined,
129
+ })}
130
+ style={labelWidth !== undefined ? {width: labelWidth} : undefined}
131
+ >
132
+ <div className={styles.itemLabelContent}>
133
+ <Text2 regular color="inherit">
134
+ {label}
135
+ </Text2>
136
+ </div>
137
+ </div>
138
+ );
139
+
140
+ const touchableClassName = classnames(styles.itemTouchable, styles.itemTouchableVariant[variant], {
141
+ [styles.itemTouchableSelected[variant]]: showBackground,
142
+ [styles.itemTouchableCollapsed]: collapsed,
143
+ });
144
+
145
+ const rowContent = (
146
+ <>
147
+ {assetElement}
148
+ {labelNode}
149
+ {rightSlot && (
150
+ <span
151
+ className={classnames(styles.itemRightSlot, {
152
+ [styles.itemRightSlotCollapsed]: collapsed,
153
+ })}
154
+ >
155
+ {rightSlot}
156
+ </span>
157
+ )}
158
+ {chevron && (
159
+ <span
160
+ className={classnames(styles.itemChevron, styles.itemChevronVariant[variant], {
161
+ [styles.itemChevronRotated]: chevron.rotated,
162
+ [styles.itemChevronCollapsed]: collapsed,
163
+ })}
164
+ aria-hidden="true"
165
+ >
166
+ <IconChevron size={16} color="currentColor" direction={chevron.direction} />
167
+ </span>
168
+ )}
169
+ </>
170
+ );
171
+
172
+ const ariaCurrent = current ? ('page' as const) : undefined;
173
+
174
+ const interactiveRow = (() => {
175
+ if (toggle) {
176
+ return (
177
+ <Touchable
178
+ className={touchableClassName}
179
+ onPress={toggle.onPress}
180
+ aria-expanded={toggle.expanded}
181
+ aria-controls={toggle.controls}
182
+ aria-label={label}
183
+ dataAttributes={{'parent-item': 'true'}}
184
+ >
185
+ {rowContent}
186
+ </Touchable>
187
+ );
188
+ }
189
+
190
+ if (navigation) {
191
+ return (
192
+ <Touchable
193
+ {...navigation}
194
+ className={touchableClassName}
195
+ aria-current={ariaCurrent}
196
+ aria-label={collapsed ? label : undefined}
197
+ >
198
+ {rowContent}
199
+ </Touchable>
200
+ );
201
+ }
202
+
203
+ return (
204
+ <div className={touchableClassName} aria-current={ariaCurrent}>
205
+ {rowContent}
206
+ </div>
207
+ );
208
+ })();
209
+
210
+ const rowDataAttributes: DataAttributes = {testid: 'SidenavItem', ...dataAttributes};
211
+ if (id) {
212
+ rowDataAttributes['sidenav-item-id'] = id;
213
+ }
214
+
215
+ return (
216
+ <div className={styles.itemRow} {...getPrefixedDataAttributes(rowDataAttributes)}>
217
+ {showAccent && (
218
+ <div className={classnames(styles.itemAccent, styles.itemAccentVariant[variant])} />
219
+ )}
220
+ {tooltip ? (
221
+ <Tooltip
222
+ position="right"
223
+ description={label}
224
+ target={interactiveRow}
225
+ // Without `minWidth: 0` the wrapper never shrinks below its content, past the rail.
226
+ targetStyle={{flex: 1, minWidth: 0}}
227
+ />
228
+ ) : (
229
+ interactiveRow
230
+ )}
231
+ </div>
232
+ );
233
+ };
234
+
235
+ /**
236
+ * Builds the props that make a row navigate. Every navigation of the sidenav reports the new selection
237
+ * and closes the open panel in one call, so the selection does not reopen the column that the press
238
+ * closed.
239
+ */
240
+ const useSidenavNavigation = (
241
+ itemId: string,
242
+ props: {
243
+ onPress?: () => void;
244
+ href?: string;
245
+ to?: string;
246
+ newTab?: boolean;
247
+ onNavigate?: () => void | Promise<void>;
248
+ }
249
+ ): SidenavRowNavigationProps | undefined => {
250
+ const {selectItemAndClosePanel} = useSidenavBarContext();
251
+
252
+ const wrapNavCallback = (callback?: () => void | Promise<void>): (() => Promise<void>) => {
253
+ return async () => {
254
+ selectItemAndClosePanel(itemId ?? null);
255
+ await callback?.();
256
+ };
257
+ };
258
+
259
+ if (props.onPress) {
260
+ return {onPress: wrapNavCallback(props.onPress)};
261
+ }
262
+ if (props.href !== undefined) {
263
+ return {href: props.href, newTab: props.newTab, onNavigate: wrapNavCallback(props.onNavigate)};
264
+ }
265
+ if (props.to !== undefined) {
266
+ return {to: props.to, newTab: props.newTab, onNavigate: wrapNavCallback(props.onNavigate)};
267
+ }
268
+ return undefined;
269
+ };
270
+
271
+ export {SidenavRow, useSidenavNavigation};
272
+ export type {SidenavRowPlacement};
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import {useSidenavBarContext} from './sidenav-bar-context';
4
+ import {SidenavRow, useSidenavNavigation} from './sidenav-bar-row';
5
+
6
+ import type {SidenavRowPlacement} from './sidenav-bar-row';
7
+ import type {DataAttributes} from './utils/types';
8
+ import type {SidenavAsset, SidenavSecondLevelItem as SidenavSecondLevelItemData} from './sidenav-bar-types';
9
+
10
+ /** A second-level row never stands on the rail: it stands in the accordion of its parent, or in a panel. */
11
+ type SidenavSecondLevelPlacement = Exclude<SidenavRowPlacement, 'rail'>;
12
+
13
+ type SidenavSecondLevelItemProps = {
14
+ /** Unique identifier for selection tracking. */
15
+ id: string;
16
+ /** Display text. A text longer than the row wraps over several lines, and it never truncates. */
17
+ label: string;
18
+ /** Icon component or element. The collapsed rail never shows a second-level row, so it is optional. */
19
+ asset?: SidenavAsset;
20
+ /** Custom content on the right side (e.g., Badge). */
21
+ rightSlot?: React.ReactNode;
22
+ /**
23
+ * Where the row stands. The accordion of its parent opens in place and moves with the bar; a panel
24
+ * (the dialog panel, or the second column) never moves with it. The context alone cannot tell the two
25
+ * apart, so the parent names the place.
26
+ */
27
+ placement: SidenavSecondLevelPlacement;
28
+ onPress?: () => void;
29
+ href?: string;
30
+ to?: string;
31
+ /** Open link in new tab. @default false */
32
+ newTab?: boolean;
33
+ /** Called after navigation. */
34
+ onNavigate?: () => void | Promise<void>;
35
+ dataAttributes?: DataAttributes;
36
+ };
37
+
38
+ /**
39
+ * A second-level item. It always navigates, and it never has children of its own, so it holds no state:
40
+ * it reads the current selection and renders one row.
41
+ */
42
+ const SidenavSecondLevelItem = ({
43
+ id,
44
+ label,
45
+ asset,
46
+ rightSlot,
47
+ placement,
48
+ dataAttributes,
49
+ ...navigationProps
50
+ }: SidenavSecondLevelItemProps): JSX.Element => {
51
+ const {selectedItemId} = useSidenavBarContext();
52
+ const navigation = useSidenavNavigation(id, navigationProps);
53
+ const selected = selectedItemId === id;
54
+
55
+ return (
56
+ <div role="listitem">
57
+ <SidenavRow
58
+ id={id}
59
+ label={label}
60
+ asset={asset}
61
+ rightSlot={rightSlot}
62
+ showBackground={selected}
63
+ showAccent={selected}
64
+ current={selected}
65
+ // A row of the accordion moves with the bar like the row of its parent: it folds and fades,
66
+ // and its label keeps its width. Without this, the labels re-wrap as the rail narrows, the
67
+ // rows grow, and the accordion bounces while it folds. A row of a panel never moves.
68
+ placement={placement}
69
+ navigation={navigation}
70
+ dataAttributes={dataAttributes}
71
+ />
72
+ </div>
73
+ );
74
+ };
75
+
76
+ /**
77
+ * Renders a second-level item from its data. The accordion of a parent item, the dialog panel and the second
78
+ * column list the same items, so this is the one place that maps the data to the props of the row.
79
+ */
80
+ const renderSidenavSecondLevelItem = (
81
+ item: SidenavSecondLevelItemData,
82
+ {placement}: {placement: SidenavSecondLevelPlacement}
83
+ ): React.ReactElement => (
84
+ <SidenavSecondLevelItem
85
+ key={item.id}
86
+ id={item.id}
87
+ label={item.label}
88
+ asset={item.asset}
89
+ rightSlot={item.rightSlot}
90
+ placement={placement}
91
+ href={item.href}
92
+ to={item.to}
93
+ onPress={item.onPress}
94
+ newTab={item.newTab}
95
+ onNavigate={item.onNavigate}
96
+ />
97
+ );
98
+
99
+ export {SidenavSecondLevelItem, renderSidenavSecondLevelItem};