@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.
- package/css/blau.css +30 -16
- package/css/community/cyber.css +1491 -0
- package/css/esimflag.css +41 -18
- package/css/mistica-common.css +6 -0
- package/css/mistica.css +1 -1
- package/css/movistar.css +35 -18
- package/css/o2.css +32 -18
- package/css/telefonica.css +44 -18
- package/css/vivo-evolution.css +72 -20
- package/css/vivo.css +32 -18
- package/dist/accordion.css-mistica.js +10 -10
- package/dist/align.css-mistica.js +1 -1
- package/dist/autocomplete.css-mistica.js +4 -4
- package/dist/avatar.css-mistica.js +1 -1
- package/dist/badge.css-mistica.js +1 -1
- package/dist/box.css-mistica.js +13 -13
- package/dist/boxed.css-mistica.js +24 -24
- package/dist/boxed.d.ts +9 -0
- package/dist/boxed.js +18 -15
- package/dist/button-group.css-mistica.js +1 -1
- package/dist/button-layout.css-mistica.js +19 -19
- package/dist/button.css-mistica.js +50 -50
- package/dist/callout.css-mistica.js +13 -13
- package/dist/card-internal.css-mistica.js +22 -22
- package/dist/carousel.css-mistica.js +30 -30
- package/dist/checkbox.css-mistica.js +15 -15
- package/dist/chip.css-mistica.js +18 -18
- package/dist/circle.css-mistica.js +2 -2
- package/dist/community/advanced-data-card.css-mistica.js +19 -19
- package/dist/community/ai-card.css-mistica.js +9 -9
- package/dist/community/blocks.css-mistica.js +3 -3
- package/dist/community/cyber/example-component.css-mistica.js +1 -1
- package/dist/community/example-component.css-mistica.js +1 -1
- package/dist/community/skins/cyber-skin.d.ts +5 -0
- package/dist/community/skins/cyber-skin.js +289 -256
- package/dist/counter.css-mistica.js +6 -6
- package/dist/cover-hero.css-mistica.js +7 -7
- package/dist/credit-card-number-field.css-mistica.js +3 -3
- package/dist/date-field.css-mistica.js +1 -1
- package/dist/date-time-picker.css-mistica.js +1 -1
- package/dist/dialog.css-mistica.js +10 -10
- package/dist/divider.css-mistica.js +5 -5
- package/dist/double-field.css-mistica.js +2 -2
- package/dist/drawer.css-mistica.js +4 -4
- package/dist/empty-state-card.css-mistica.js +1 -1
- package/dist/empty-state.css-mistica.js +8 -8
- package/dist/fade-in.css-mistica.js +1 -1
- package/dist/feedback.css-mistica.js +5 -5
- package/dist/file-upload.css-mistica.js +10 -10
- package/dist/fixed-footer-layout.css-mistica.js +6 -6
- package/dist/form.css-mistica.js +1 -1
- package/dist/generated/mistica-icons/icon-panel-collapse-regular.js +165 -0
- package/dist/generated/mistica-icons/icon-panel-expand-regular.js +159 -0
- package/dist/grid-layout.css-mistica.js +3 -3
- package/dist/grid.css-mistica.js +120 -120
- package/dist/header.css-mistica.js +1 -1
- package/dist/hero.css-mistica.js +4 -4
- package/dist/horizontal-scroll.css-mistica.js +1 -1
- package/dist/icon-button.css-mistica.js +53 -53
- package/dist/icons/icon-chevron.css-mistica.js +2 -2
- package/dist/icons/icon-error.css-mistica.js +1 -1
- package/dist/image.css-mistica.js +8 -8
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -1
- package/dist/inline.css-mistica.js +11 -11
- package/dist/list.css-mistica.js +40 -28
- package/dist/list.css.d.ts +2 -0
- package/dist/list.d.ts +3 -1
- package/dist/list.js +176 -165
- package/dist/loading-bar.css-mistica.js +1 -1
- package/dist/loading-screen.css-mistica.js +12 -12
- package/dist/logo-movistar.js +1 -1
- package/dist/logo.css-mistica.js +5 -5
- package/dist/menu.css-mistica.js +13 -13
- package/dist/mobile-navigation-menu.d.ts +26 -0
- package/dist/mobile-navigation-menu.js +145 -0
- package/dist/mosaic.css-mistica.js +1 -1
- package/dist/navigation-bar.css-mistica.js +31 -97
- package/dist/navigation-bar.css.d.ts +0 -24
- package/dist/navigation-bar.d.ts +3 -38
- package/dist/navigation-bar.js +399 -586
- package/dist/navigation-breadcrumbs.css-mistica.js +1 -1
- package/dist/navigation-shared.css-mistica.js +79 -0
- package/dist/navigation-shared.css.d.ts +26 -0
- package/dist/navigation-shared.d.ts +66 -0
- package/dist/navigation-shared.js +264 -0
- package/dist/package-version.js +1 -1
- package/dist/pagination.css-mistica.js +22 -22
- package/dist/pin-field.css-mistica.js +4 -4
- package/dist/popover.css-mistica.js +1 -1
- package/dist/portal.d.ts +1 -1
- package/dist/progress-bar.css-mistica.js +7 -7
- package/dist/radio-button.css-mistica.js +27 -27
- package/dist/rating.css-mistica.js +4 -4
- package/dist/responsive-layout.css-mistica.js +6 -6
- package/dist/screen-reader-only.css-mistica.js +1 -1
- package/dist/select.css-mistica.js +21 -21
- package/dist/sheet-action-row.css-mistica.js +1 -1
- package/dist/sheet-common.css-mistica.js +7 -7
- package/dist/sheet-info.css-mistica.js +1 -1
- package/dist/sidenav-bar-context.d.ts +25 -0
- package/dist/sidenav-bar-context.js +72 -0
- package/dist/sidenav-bar-data.d.ts +15 -0
- package/dist/sidenav-bar-data.js +38 -0
- package/dist/sidenav-bar-first-level-item.d.ts +42 -0
- package/dist/sidenav-bar-first-level-item.js +174 -0
- package/dist/sidenav-bar-keyboard.d.ts +13 -0
- package/dist/sidenav-bar-keyboard.js +314 -0
- package/dist/sidenav-bar-layout.css-mistica.js +28 -0
- package/dist/sidenav-bar-layout.css.d.ts +5 -0
- package/dist/sidenav-bar-layout.d.ts +22 -0
- package/dist/sidenav-bar-layout.js +103 -0
- package/dist/sidenav-bar-mobile.d.ts +24 -0
- package/dist/sidenav-bar-mobile.js +332 -0
- package/dist/sidenav-bar-motion.d.ts +32 -0
- package/dist/sidenav-bar-motion.js +84 -0
- package/dist/sidenav-bar-panel.d.ts +31 -0
- package/dist/sidenav-bar-panel.js +208 -0
- package/dist/sidenav-bar-row.d.ts +82 -0
- package/dist/sidenav-bar-row.js +230 -0
- package/dist/sidenav-bar-second-level-item.d.ts +43 -0
- package/dist/sidenav-bar-second-level-item.js +89 -0
- package/dist/sidenav-bar-section.d.ts +23 -0
- package/dist/sidenav-bar-section.js +167 -0
- package/dist/sidenav-bar-types.d.ts +192 -0
- package/dist/sidenav-bar-types.js +11 -0
- package/dist/sidenav-bar.css-mistica.js +325 -0
- package/dist/sidenav-bar.css.d.ts +93 -0
- package/dist/sidenav-bar.d.ts +102 -0
- package/dist/sidenav-bar.js +492 -0
- package/dist/skeletons.css-mistica.js +8 -8
- package/dist/skins/blau.d.ts +5 -0
- package/dist/skins/blau.js +12 -8
- package/dist/skins/esimflag.d.ts +5 -0
- package/dist/skins/esimflag.js +13 -9
- package/dist/skins/movistar.d.ts +5 -0
- package/dist/skins/movistar.js +13 -9
- package/dist/skins/o2.d.ts +5 -0
- package/dist/skins/o2.js +13 -9
- package/dist/skins/skin-contract.css-mistica.js +735 -731
- package/dist/skins/skin-contract.css.d.ts +16 -12
- package/dist/skins/telefonica.d.ts +5 -0
- package/dist/skins/telefonica.js +13 -9
- package/dist/skins/types/colors.d.ts +13 -6
- package/dist/skins/vivo-evolution.d.ts +5 -0
- package/dist/skins/vivo-evolution.js +14 -10
- package/dist/skins/vivo.d.ts +5 -0
- package/dist/skins/vivo.js +13 -9
- package/dist/skip-link.css-mistica.js +2 -2
- package/dist/slider.css-mistica.js +24 -24
- package/dist/snackbar.css-mistica.js +14 -14
- package/dist/spinner.css-mistica.js +1 -1
- package/dist/square.css-mistica.js +1 -1
- package/dist/stack.css-mistica.js +5 -5
- package/dist/stacking-group.css-mistica.js +1 -1
- package/dist/stepper.css-mistica.js +9 -9
- package/dist/switch-component.css-mistica.js +48 -42
- package/dist/switch-component.css.d.ts +2 -0
- package/dist/switch-component.js +18 -18
- package/dist/table.css-mistica.js +9 -9
- package/dist/tabs.css-mistica.js +22 -22
- package/dist/tag.css-mistica.js +3 -3
- package/dist/text-field-base.css-mistica.js +19 -19
- package/dist/text-field-components.css-mistica.js +10 -10
- package/dist/text-link.css-mistica.js +6 -6
- package/dist/text-tokens.d.ts +6 -0
- package/dist/text-tokens.js +157 -133
- package/dist/text.css-mistica.js +6 -6
- package/dist/timeline.css-mistica.js +11 -11
- package/dist/timer.css-mistica.js +6 -6
- package/dist/tooltip.css-mistica.js +2 -2
- package/dist/touchable.css-mistica.js +4 -4
- package/dist/utils/aspect-ratio-support.css-mistica.js +2 -2
- package/dist/video.css-mistica.js +1 -1
- package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
- package/dist-es/accordion.css-mistica.js +7 -7
- package/dist-es/align.css-mistica.js +1 -1
- package/dist-es/autocomplete.css-mistica.js +2 -2
- package/dist-es/avatar.css-mistica.js +1 -1
- package/dist-es/badge.css-mistica.js +1 -1
- package/dist-es/box.css-mistica.js +13 -13
- package/dist-es/boxed.css-mistica.js +23 -23
- package/dist-es/boxed.js +25 -25
- package/dist-es/button-group.css-mistica.js +1 -1
- package/dist-es/button-layout.css-mistica.js +16 -16
- package/dist-es/button.css-mistica.js +38 -38
- package/dist-es/callout.css-mistica.js +12 -12
- package/dist-es/card-internal.css-mistica.js +17 -17
- package/dist-es/carousel.css-mistica.js +10 -10
- package/dist-es/checkbox.css-mistica.js +13 -13
- package/dist-es/chip.css-mistica.js +17 -17
- package/dist-es/circle.css-mistica.js +2 -2
- package/dist-es/community/advanced-data-card.css-mistica.js +7 -7
- package/dist-es/community/ai-card.css-mistica.js +3 -3
- package/dist-es/community/blocks.css-mistica.js +2 -2
- package/dist-es/community/cyber/example-component.css-mistica.js +1 -1
- package/dist-es/community/example-component.css-mistica.js +1 -1
- package/dist-es/community/skins/cyber-skin.js +289 -256
- package/dist-es/counter.css-mistica.js +2 -2
- package/dist-es/cover-hero.css-mistica.js +4 -4
- package/dist-es/credit-card-number-field.css-mistica.js +3 -3
- package/dist-es/date-field.css-mistica.js +1 -1
- package/dist-es/date-time-picker.css-mistica.js +1 -1
- package/dist-es/dialog.css-mistica.js +5 -5
- package/dist-es/divider.css-mistica.js +5 -5
- package/dist-es/double-field.css-mistica.js +2 -2
- package/dist-es/drawer.css-mistica.js +2 -2
- package/dist-es/empty-state-card.css-mistica.js +1 -1
- package/dist-es/empty-state.css-mistica.js +7 -7
- package/dist-es/fade-in.css-mistica.js +1 -1
- package/dist-es/feedback.css-mistica.js +2 -2
- package/dist-es/file-upload.css-mistica.js +8 -8
- package/dist-es/fixed-footer-layout.css-mistica.js +3 -3
- package/dist-es/form.css-mistica.js +1 -1
- package/dist-es/generated/mistica-icons/icon-panel-collapse-regular.js +115 -0
- package/dist-es/generated/mistica-icons/icon-panel-expand-regular.js +109 -0
- package/dist-es/grid-layout.css-mistica.js +3 -3
- package/dist-es/grid.css-mistica.js +120 -120
- package/dist-es/header.css-mistica.js +1 -1
- package/dist-es/hero.css-mistica.js +3 -3
- package/dist-es/horizontal-scroll.css-mistica.js +1 -1
- package/dist-es/icon-button.css-mistica.js +53 -53
- package/dist-es/icons/icon-chevron.css-mistica.js +2 -2
- package/dist-es/icons/icon-error.css-mistica.js +1 -1
- package/dist-es/image.css-mistica.js +4 -4
- package/dist-es/index.js +2279 -2276
- package/dist-es/inline.css-mistica.js +11 -11
- package/dist-es/list.css-mistica.js +8 -2
- package/dist-es/list.js +275 -264
- package/dist-es/loading-bar.css-mistica.js +1 -1
- package/dist-es/loading-screen.css-mistica.js +5 -5
- package/dist-es/logo-movistar.js +1 -1
- package/dist-es/logo.css-mistica.js +5 -5
- package/dist-es/menu.css-mistica.js +13 -13
- package/dist-es/mobile-navigation-menu.js +90 -0
- package/dist-es/mosaic.css-mistica.js +1 -1
- package/dist-es/navigation-bar.css-mistica.js +11 -20
- package/dist-es/navigation-bar.js +469 -656
- package/dist-es/navigation-breadcrumbs.css-mistica.js +1 -1
- package/dist-es/navigation-shared.css-mistica.js +11 -0
- package/dist-es/navigation-shared.js +192 -0
- package/dist-es/package-version.js +1 -1
- package/dist-es/pagination.css-mistica.js +19 -19
- package/dist-es/pin-field.css-mistica.js +2 -2
- package/dist-es/popover.css-mistica.js +1 -1
- package/dist-es/progress-bar.css-mistica.js +7 -7
- package/dist-es/radio-button.css-mistica.js +22 -22
- package/dist-es/rating.css-mistica.js +3 -3
- package/dist-es/responsive-layout.css-mistica.js +6 -6
- package/dist-es/screen-reader-only.css-mistica.js +1 -1
- package/dist-es/select.css-mistica.js +18 -18
- package/dist-es/sheet-action-row.css-mistica.js +1 -1
- package/dist-es/sheet-common.css-mistica.js +2 -2
- package/dist-es/sheet-info.css-mistica.js +1 -1
- package/dist-es/sidenav-bar-context.js +14 -0
- package/dist-es/sidenav-bar-data.js +12 -0
- package/dist-es/sidenav-bar-first-level-item.js +124 -0
- package/dist-es/sidenav-bar-keyboard.js +253 -0
- package/dist-es/sidenav-bar-layout.css-mistica.js +2 -0
- package/dist-es/sidenav-bar-layout.js +86 -0
- package/dist-es/sidenav-bar-mobile.js +277 -0
- package/dist-es/sidenav-bar-motion.js +26 -0
- package/dist-es/sidenav-bar-panel.js +146 -0
- package/dist-es/sidenav-bar-row.js +208 -0
- package/dist-es/sidenav-bar-second-level-item.js +72 -0
- package/dist-es/sidenav-bar-section.js +112 -0
- package/dist-es/sidenav-bar-types.js +2 -0
- package/dist-es/sidenav-bar.css-mistica.js +74 -0
- package/dist-es/sidenav-bar.js +429 -0
- package/dist-es/skeletons.css-mistica.js +7 -7
- package/dist-es/skins/blau.js +12 -8
- package/dist-es/skins/esimflag.js +13 -9
- package/dist-es/skins/movistar.js +13 -9
- package/dist-es/skins/o2.js +13 -9
- package/dist-es/skins/skin-contract.css-mistica.js +735 -731
- package/dist-es/skins/telefonica.js +13 -9
- package/dist-es/skins/vivo-evolution.js +14 -10
- package/dist-es/skins/vivo.js +13 -9
- package/dist-es/skip-link.css-mistica.js +2 -2
- package/dist-es/slider.css-mistica.js +20 -20
- package/dist-es/snackbar.css-mistica.js +5 -5
- package/dist-es/spinner.css-mistica.js +1 -1
- package/dist-es/square.css-mistica.js +1 -1
- package/dist-es/stack.css-mistica.js +5 -5
- package/dist-es/stacking-group.css-mistica.js +1 -1
- package/dist-es/stepper.css-mistica.js +3 -3
- package/dist-es/style.css +1 -1
- package/dist-es/switch-component.css-mistica.js +37 -37
- package/dist-es/switch-component.js +26 -26
- package/dist-es/table.css-mistica.js +9 -9
- package/dist-es/tabs.css-mistica.js +19 -19
- package/dist-es/tag.css-mistica.js +2 -2
- package/dist-es/text-field-base.css-mistica.js +17 -17
- package/dist-es/text-field-components.css-mistica.js +4 -4
- package/dist-es/text-link.css-mistica.js +6 -6
- package/dist-es/text-tokens.js +82 -67
- package/dist-es/text.css-mistica.js +6 -6
- package/dist-es/timeline.css-mistica.js +10 -10
- package/dist-es/timer.css-mistica.js +6 -6
- package/dist-es/tooltip.css-mistica.js +2 -2
- package/dist-es/touchable.css-mistica.js +2 -2
- package/dist-es/utils/aspect-ratio-support.css-mistica.js +2 -2
- package/dist-es/video.css-mistica.js +1 -1
- package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +1 -1
- package/package.json +3 -2
- package/src/boxed.css.ts +1 -0
- package/src/boxed.tsx +18 -5
- package/src/callout.css.ts +4 -2
- package/src/community/skins/cyber-skin.tsx +127 -439
- package/src/index.tsx +14 -0
- package/src/list.css.ts +36 -1
- package/src/list.tsx +370 -344
- package/src/logo-movistar.tsx +1 -1
- package/src/mobile-navigation-menu.tsx +168 -0
- package/src/navigation-bar.css.ts +2 -267
- package/src/navigation-bar.tsx +77 -370
- package/src/navigation-shared.css.ts +289 -0
- package/src/navigation-shared.tsx +262 -0
- package/src/package-version.tsx +1 -1
- package/src/portal.tsx +1 -1
- package/src/sidenav-bar-context.tsx +41 -0
- package/src/sidenav-bar-data.ts +49 -0
- package/src/sidenav-bar-first-level-item.tsx +203 -0
- package/src/sidenav-bar-keyboard.tsx +393 -0
- package/src/sidenav-bar-layout.css.ts +89 -0
- package/src/sidenav-bar-layout.tsx +61 -0
- package/src/sidenav-bar-mobile.tsx +302 -0
- package/src/sidenav-bar-motion.tsx +70 -0
- package/src/sidenav-bar-panel.tsx +269 -0
- package/src/sidenav-bar-row.tsx +272 -0
- package/src/sidenav-bar-second-level-item.tsx +99 -0
- package/src/sidenav-bar-section.tsx +98 -0
- package/src/sidenav-bar-types.ts +224 -0
- package/src/sidenav-bar.css.ts +1018 -0
- package/src/sidenav-bar.tsx +821 -0
- package/src/skins/blau.tsx +18 -8
- package/src/skins/esimflag.tsx +19 -9
- package/src/skins/movistar.tsx +19 -9
- package/src/skins/o2.tsx +19 -9
- package/src/skins/skin-contract.css.ts +2 -0
- package/src/skins/telefonica.tsx +19 -9
- package/src/skins/types/colors.tsx +14 -6
- package/src/skins/vivo-evolution.tsx +20 -10
- package/src/skins/vivo.tsx +19 -9
- package/src/switch-component.css.ts +20 -0
- package/src/switch-component.tsx +9 -1
- 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};
|