@wordpress/boot 0.1.1-next.2f1c7c01b.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 (130) hide show
  1. package/LICENSE.md +788 -0
  2. package/build-module/components/app/index.js +32 -0
  3. package/build-module/components/app/index.js.map +7 -0
  4. package/build-module/components/app/router.js +119 -0
  5. package/build-module/components/app/router.js.map +7 -0
  6. package/build-module/components/navigation/drilldown-item/index.js +49 -0
  7. package/build-module/components/navigation/drilldown-item/index.js.map +7 -0
  8. package/build-module/components/navigation/dropdown-item/index.js +162 -0
  9. package/build-module/components/navigation/dropdown-item/index.js.map +7 -0
  10. package/build-module/components/navigation/index.js +101 -0
  11. package/build-module/components/navigation/index.js.map +7 -0
  12. package/build-module/components/navigation/items.js +60 -0
  13. package/build-module/components/navigation/items.js.map +7 -0
  14. package/build-module/components/navigation/navigation-item/index.js +180 -0
  15. package/build-module/components/navigation/navigation-item/index.js.map +7 -0
  16. package/build-module/components/navigation/navigation-screen/index.js +196 -0
  17. package/build-module/components/navigation/navigation-screen/index.js.map +7 -0
  18. package/build-module/components/navigation/path-matching.js +78 -0
  19. package/build-module/components/navigation/path-matching.js.map +7 -0
  20. package/build-module/components/navigation/router-link-item.js +14 -0
  21. package/build-module/components/navigation/router-link-item.js.map +7 -0
  22. package/build-module/components/navigation/use-sidebar-parent.js +52 -0
  23. package/build-module/components/navigation/use-sidebar-parent.js.map +7 -0
  24. package/build-module/components/root/index.js +115 -0
  25. package/build-module/components/root/index.js.map +7 -0
  26. package/build-module/components/sidebar/index.js +78 -0
  27. package/build-module/components/sidebar/index.js.map +7 -0
  28. package/build-module/components/site-hub/index.js +153 -0
  29. package/build-module/components/site-hub/index.js.map +7 -0
  30. package/build-module/components/site-icon/index.js +115 -0
  31. package/build-module/components/site-icon/index.js.map +7 -0
  32. package/build-module/components/site-icon-link/index.js +101 -0
  33. package/build-module/components/site-icon-link/index.js.map +7 -0
  34. package/build-module/index.js +622 -0
  35. package/build-module/index.js.map +7 -0
  36. package/build-module/lock-unlock.js +11 -0
  37. package/build-module/lock-unlock.js.map +7 -0
  38. package/build-module/store/actions.js +19 -0
  39. package/build-module/store/actions.js.map +7 -0
  40. package/build-module/store/index.js +17 -0
  41. package/build-module/store/index.js.map +7 -0
  42. package/build-module/store/reducer.js +27 -0
  43. package/build-module/store/reducer.js.map +7 -0
  44. package/build-module/store/selectors.js +12 -0
  45. package/build-module/store/selectors.js.map +7 -0
  46. package/build-module/store/types.js +1 -0
  47. package/build-module/store/types.js.map +7 -0
  48. package/build-style/style-rtl.css +612 -0
  49. package/build-style/style.css +612 -0
  50. package/build-types/components/app/index.d.ts +6 -0
  51. package/build-types/components/app/index.d.ts.map +1 -0
  52. package/build-types/components/app/router.d.ts +7 -0
  53. package/build-types/components/app/router.d.ts.map +1 -0
  54. package/build-types/components/navigation/drilldown-item/index.d.ts +34 -0
  55. package/build-types/components/navigation/drilldown-item/index.d.ts.map +1 -0
  56. package/build-types/components/navigation/dropdown-item/index.d.ts +36 -0
  57. package/build-types/components/navigation/dropdown-item/index.d.ts.map +1 -0
  58. package/build-types/components/navigation/index.d.ts +3 -0
  59. package/build-types/components/navigation/index.d.ts.map +1 -0
  60. package/build-types/components/navigation/items.d.ts +16 -0
  61. package/build-types/components/navigation/items.d.ts.map +1 -0
  62. package/build-types/components/navigation/navigation-item/index.d.ts +28 -0
  63. package/build-types/components/navigation/navigation-item/index.d.ts.map +1 -0
  64. package/build-types/components/navigation/navigation-screen/index.d.ts +24 -0
  65. package/build-types/components/navigation/navigation-screen/index.d.ts.map +1 -0
  66. package/build-types/components/navigation/path-matching.d.ts +30 -0
  67. package/build-types/components/navigation/path-matching.d.ts.map +1 -0
  68. package/build-types/components/navigation/router-link-item.d.ts +5 -0
  69. package/build-types/components/navigation/router-link-item.d.ts.map +1 -0
  70. package/build-types/components/navigation/use-sidebar-parent.d.ts +12 -0
  71. package/build-types/components/navigation/use-sidebar-parent.d.ts.map +1 -0
  72. package/build-types/components/root/index.d.ts +3 -0
  73. package/build-types/components/root/index.d.ts.map +1 -0
  74. package/build-types/components/sidebar/index.d.ts +3 -0
  75. package/build-types/components/sidebar/index.d.ts.map +1 -0
  76. package/build-types/components/site-hub/index.d.ts +4 -0
  77. package/build-types/components/site-hub/index.d.ts.map +1 -0
  78. package/build-types/components/site-icon/index.d.ts +9 -0
  79. package/build-types/components/site-icon/index.d.ts.map +1 -0
  80. package/build-types/components/site-icon-link/index.d.ts +8 -0
  81. package/build-types/components/site-icon-link/index.d.ts.map +1 -0
  82. package/build-types/index.d.ts +6 -0
  83. package/build-types/index.d.ts.map +1 -0
  84. package/build-types/lock-unlock.d.ts +2 -0
  85. package/build-types/lock-unlock.d.ts.map +1 -0
  86. package/build-types/store/actions.d.ts +15 -0
  87. package/build-types/store/actions.d.ts.map +1 -0
  88. package/build-types/store/index.d.ts +6 -0
  89. package/build-types/store/index.d.ts.map +1 -0
  90. package/build-types/store/reducer.d.ts +7 -0
  91. package/build-types/store/reducer.d.ts.map +1 -0
  92. package/build-types/store/selectors.d.ts +7 -0
  93. package/build-types/store/selectors.d.ts.map +1 -0
  94. package/build-types/store/types.d.ts +63 -0
  95. package/build-types/store/types.d.ts.map +1 -0
  96. package/package.json +64 -0
  97. package/src/components/app/index.tsx +45 -0
  98. package/src/components/app/router.tsx +198 -0
  99. package/src/components/navigation/drilldown-item/index.tsx +88 -0
  100. package/src/components/navigation/dropdown-item/index.tsx +134 -0
  101. package/src/components/navigation/dropdown-item/style.scss +23 -0
  102. package/src/components/navigation/index.tsx +126 -0
  103. package/src/components/navigation/items.tsx +93 -0
  104. package/src/components/navigation/navigation-item/index.tsx +88 -0
  105. package/src/components/navigation/navigation-item/style.scss +52 -0
  106. package/src/components/navigation/navigation-screen/index.tsx +147 -0
  107. package/src/components/navigation/navigation-screen/style.scss +34 -0
  108. package/src/components/navigation/path-matching.ts +149 -0
  109. package/src/components/navigation/router-link-item.tsx +22 -0
  110. package/src/components/navigation/use-sidebar-parent.ts +77 -0
  111. package/src/components/root/index.tsx +42 -0
  112. package/src/components/root/style.scss +41 -0
  113. package/src/components/sidebar/index.tsx +17 -0
  114. package/src/components/sidebar/style.scss +15 -0
  115. package/src/components/site-hub/index.tsx +67 -0
  116. package/src/components/site-hub/style.scss +54 -0
  117. package/src/components/site-icon/index.tsx +60 -0
  118. package/src/components/site-icon/style.scss +19 -0
  119. package/src/components/site-icon-link/index.tsx +43 -0
  120. package/src/components/site-icon-link/style.scss +24 -0
  121. package/src/index.tsx +5 -0
  122. package/src/lock-unlock.ts +9 -0
  123. package/src/store/actions.ts +23 -0
  124. package/src/store/index.ts +23 -0
  125. package/src/store/reducer.ts +31 -0
  126. package/src/store/selectors.ts +12 -0
  127. package/src/store/types.ts +70 -0
  128. package/src/style.scss +2 -0
  129. package/tsconfig.json +23 -0
  130. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ createRouter,
6
+ createRootRoute,
7
+ createRoute,
8
+ RouterProvider,
9
+ createBrowserHistory,
10
+ type AnyRoute,
11
+ } from '@tanstack/react-router';
12
+ import { parseHref } from '@tanstack/history';
13
+ import type { ComponentType } from 'react';
14
+
15
+ /**
16
+ * WordPress dependencies
17
+ */
18
+ import { __ } from '@wordpress/i18n';
19
+ import { lazy, useState, useEffect } from '@wordpress/element';
20
+ import { Page } from '@wordpress/admin-ui';
21
+
22
+ /**
23
+ * Internal dependencies
24
+ */
25
+ import Root from '../root';
26
+ import type { Route, RouteLoaderContext } from '../../store/types';
27
+
28
+ // Not found component
29
+ function NotFoundComponent() {
30
+ return (
31
+ <div className="boot-layout__stage">
32
+ <Page title={ __( 'Route not found' ) } hasPadding>
33
+ { __( "The page you're looking for does not exist" ) }
34
+ </Page>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ function RouteComponent( {
40
+ stage: Stage,
41
+ inspector: Inspector,
42
+ }: {
43
+ stage?: ComponentType;
44
+ inspector?: ComponentType;
45
+ } ) {
46
+ return (
47
+ <>
48
+ { Stage && (
49
+ <div className="boot-layout__stage">
50
+ <Stage />
51
+ </div>
52
+ ) }
53
+ { Inspector && (
54
+ <div className="boot-layout__inspector">
55
+ <Inspector />
56
+ </div>
57
+ ) }
58
+ </>
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Creates a TanStack route from a Route definition.
64
+ *
65
+ * @param route Route configuration
66
+ * @param parentRoute Parent route.
67
+ * @return Tanstack Route.
68
+ */
69
+ async function createRouteFromDefinition(
70
+ route: Route,
71
+ parentRoute: AnyRoute
72
+ ) {
73
+ // Create lazy components for stage and inspector surfaces
74
+ const SurfacesModule = route.content_module
75
+ ? lazy( async () => {
76
+ const module = await import( route.content_module! );
77
+ // Return a component that renders the surfaces
78
+ return {
79
+ default: () => (
80
+ <RouteComponent
81
+ stage={ module.stage }
82
+ inspector={ module.inspector }
83
+ />
84
+ ),
85
+ };
86
+ } )
87
+ : () => null;
88
+
89
+ // Load route module for lifecycle functions if specified
90
+ let routeConfig: {
91
+ beforeLoad?: ( context: RouteLoaderContext ) => void | Promise< void >;
92
+ loader?: ( context: RouteLoaderContext ) => Promise< unknown >;
93
+ } = {};
94
+
95
+ if ( route.route_module ) {
96
+ const module = await import( route.route_module );
97
+ routeConfig = module.route || {};
98
+ }
99
+
100
+ return createRoute( {
101
+ getParentRoute: () => parentRoute,
102
+ path: route.path,
103
+ beforeLoad: routeConfig.beforeLoad
104
+ ? async ( opts: any ) => {
105
+ const context: RouteLoaderContext = {
106
+ params: opts.params || {},
107
+ search: opts.search || {},
108
+ };
109
+ await routeConfig.beforeLoad!( context );
110
+ }
111
+ : undefined,
112
+ loader: routeConfig.loader
113
+ ? async ( opts: any ) => {
114
+ const context: RouteLoaderContext = {
115
+ params: opts.params || {},
116
+ search: opts.search || {},
117
+ };
118
+ return await routeConfig.loader!( context );
119
+ }
120
+ : undefined,
121
+ component: SurfacesModule,
122
+ } );
123
+ }
124
+
125
+ /**
126
+ * Creates a route tree from route definitions.
127
+ *
128
+ * @param routes Routes definition.
129
+ * @return Router tree.
130
+ */
131
+ async function createRouteTree( routes: Route[] ) {
132
+ const rootRoute = createRootRoute( {
133
+ component: Root,
134
+ context: () => ( {} ),
135
+ } );
136
+
137
+ // Create routes from definitions
138
+ const dynamicRoutes = await Promise.all(
139
+ routes.map( ( route ) => createRouteFromDefinition( route, rootRoute ) )
140
+ );
141
+
142
+ return rootRoute.addChildren( dynamicRoutes );
143
+ }
144
+
145
+ // Create custom history that parses ?p= query parameter
146
+ function createPathHistory() {
147
+ return createBrowserHistory( {
148
+ parseLocation: () => {
149
+ const url = new URL( window.location.href );
150
+ const path = url.searchParams.get( 'p' ) || '/';
151
+ const pathHref = `${ path }${ url.hash }`;
152
+ return parseHref( pathHref, window.history.state );
153
+ },
154
+ createHref: ( href ) => {
155
+ const searchParams = new URLSearchParams( window.location.search );
156
+ searchParams.set( 'p', href );
157
+ return `${ window.location.pathname }?${ searchParams }`;
158
+ },
159
+ } );
160
+ }
161
+
162
+ interface RouterProps {
163
+ routes: Route[];
164
+ }
165
+
166
+ export default function Router( { routes }: RouterProps ) {
167
+ const [ router, setRouter ] = useState< any >( null );
168
+
169
+ useEffect( () => {
170
+ let cancelled = false;
171
+
172
+ async function initializeRouter() {
173
+ const history = createPathHistory();
174
+ const routeTree = await createRouteTree( routes );
175
+
176
+ if ( ! cancelled ) {
177
+ const newRouter = createRouter( {
178
+ history,
179
+ routeTree,
180
+ defaultNotFoundComponent: NotFoundComponent,
181
+ } );
182
+ setRouter( newRouter );
183
+ }
184
+ }
185
+
186
+ initializeRouter();
187
+
188
+ return () => {
189
+ cancelled = true;
190
+ };
191
+ }, [ routes ] );
192
+
193
+ if ( ! router ) {
194
+ return <div>Loading routes...</div>;
195
+ }
196
+
197
+ return <RouterProvider router={ router } />;
198
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+ import type { ReactNode } from 'react';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import {
11
+ FlexBlock,
12
+ __experimentalItem as Item,
13
+ // @ts-ignore
14
+ __experimentalHStack as HStack,
15
+ Icon,
16
+ } from '@wordpress/components';
17
+ import { isRTL } from '@wordpress/i18n';
18
+ import { chevronRightSmall, chevronLeftSmall } from '@wordpress/icons';
19
+
20
+ /**
21
+ * Internal dependencies
22
+ */
23
+ import { wrapIcon } from '../items';
24
+ import type { IconType } from '../../../store/types';
25
+
26
+ interface DrilldownItemProps {
27
+ /**
28
+ * Optional CSS class name.
29
+ */
30
+ className?: string;
31
+ /**
32
+ * Identifier of the navigation item.
33
+ */
34
+ id: string;
35
+ /**
36
+ * Icon to display with the navigation item.
37
+ */
38
+ icon?: IconType;
39
+ /**
40
+ * Whether to show placeholder icons for alignment.
41
+ */
42
+ shouldShowPlaceholder?: boolean;
43
+ /**
44
+ * Content to display inside the navigation item.
45
+ */
46
+ children: ReactNode;
47
+ /**
48
+ * Function to handle sidebar navigation when the item is clicked.
49
+ */
50
+ onNavigate: ( {
51
+ id,
52
+ direction,
53
+ }: {
54
+ id?: string;
55
+ direction: 'forward' | 'backward';
56
+ } ) => void;
57
+ }
58
+
59
+ export default function DrilldownItem( {
60
+ className,
61
+ id,
62
+ icon,
63
+ shouldShowPlaceholder = true,
64
+ children,
65
+ onNavigate,
66
+ }: DrilldownItemProps ) {
67
+ const handleClick = ( e: React.MouseEvent ) => {
68
+ e.preventDefault();
69
+ onNavigate( { id, direction: 'forward' } );
70
+ };
71
+
72
+ return (
73
+ <Item
74
+ className={ clsx( 'boot-navigation-item', className ) }
75
+ onClick={ handleClick }
76
+ >
77
+ <HStack
78
+ justify="flex-start"
79
+ spacing={ 2 }
80
+ style={ { flexGrow: '1' } }
81
+ >
82
+ { wrapIcon( icon, shouldShowPlaceholder ) }
83
+ <FlexBlock>{ children }</FlexBlock>
84
+ <Icon icon={ isRTL() ? chevronLeftSmall : chevronRightSmall } />
85
+ </HStack>
86
+ </Item>
87
+ );
88
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+ import type { ReactNode } from 'react';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import {
11
+ FlexBlock,
12
+ __experimentalItem as Item,
13
+ // @ts-ignore
14
+ __experimentalHStack as HStack,
15
+ Icon,
16
+ __unstableMotion as motion,
17
+ __unstableAnimatePresence as AnimatePresence,
18
+ } from '@wordpress/components';
19
+ import { chevronDownSmall } from '@wordpress/icons';
20
+ import { useReducedMotion } from '@wordpress/compose';
21
+ import { useSelect } from '@wordpress/data';
22
+
23
+ /**
24
+ * Internal dependencies
25
+ */
26
+ import { STORE_NAME } from '../../../store';
27
+ import NavigationItem from '../navigation-item';
28
+ import { wrapIcon } from '../items';
29
+ import type { IconType, MenuItem } from '../../../store/types';
30
+ import './style.scss';
31
+
32
+ const ANIMATION_DURATION = 0.2;
33
+
34
+ interface DropdownItemProps {
35
+ /**
36
+ * Optional CSS class name.
37
+ */
38
+ className?: string;
39
+ /**
40
+ * Identifier of the parent menu item.
41
+ */
42
+ id: string;
43
+ /**
44
+ * Icon to display with the dropdown item.
45
+ */
46
+ icon?: IconType;
47
+ /**
48
+ * Whether to show placeholder icons for alignment.
49
+ */
50
+ shouldShowPlaceholder?: boolean;
51
+ /**
52
+ * Content to display inside the dropdown item.
53
+ */
54
+ children: ReactNode;
55
+ /**
56
+ * Whether this dropdown is currently expanded.
57
+ */
58
+ isExpanded: boolean;
59
+ /**
60
+ * Function to toggle this dropdown's expanded state.
61
+ */
62
+ onToggle: () => void;
63
+ }
64
+
65
+ export default function DropdownItem( {
66
+ className,
67
+ id,
68
+ icon,
69
+ children,
70
+ isExpanded,
71
+ onToggle,
72
+ }: DropdownItemProps ) {
73
+ const menuItems: MenuItem[] = useSelect(
74
+ ( select ) =>
75
+ // @ts-ignore
76
+ select( STORE_NAME ).getMenuItems(),
77
+ []
78
+ );
79
+ const items = menuItems.filter( ( item ) => item.parent === id );
80
+ const disableMotion = useReducedMotion();
81
+ return (
82
+ <div className="boot-dropdown-item">
83
+ <Item
84
+ className={ clsx( 'boot-navigation-item', className ) }
85
+ onClick={ ( e ) => {
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ onToggle();
89
+ } }
90
+ onMouseDown={ ( e ) => e.preventDefault() }
91
+ >
92
+ <HStack
93
+ justify="flex-start"
94
+ spacing={ 2 }
95
+ style={ { flexGrow: '1' } }
96
+ >
97
+ { wrapIcon( icon, false ) }
98
+ <FlexBlock>{ children }</FlexBlock>
99
+ <Icon
100
+ icon={ chevronDownSmall }
101
+ className={ clsx( 'boot-dropdown-item__chevron', {
102
+ 'is-up': isExpanded,
103
+ } ) }
104
+ />
105
+ </HStack>
106
+ </Item>
107
+ <AnimatePresence initial={ false }>
108
+ { isExpanded && (
109
+ <motion.div
110
+ initial={ { height: 0 } }
111
+ animate={ { height: 'auto' } }
112
+ exit={ { height: 0 } }
113
+ transition={ {
114
+ type: 'tween',
115
+ duration: disableMotion ? 0 : ANIMATION_DURATION,
116
+ ease: 'easeOut',
117
+ } }
118
+ className="boot-dropdown-item__children"
119
+ >
120
+ { items.map( ( item, index ) => (
121
+ <NavigationItem
122
+ key={ index }
123
+ to={ item.to }
124
+ shouldShowPlaceholder={ false }
125
+ >
126
+ { item.label }
127
+ </NavigationItem>
128
+ ) ) }
129
+ </motion.div>
130
+ ) }
131
+ </AnimatePresence>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,23 @@
1
+ @use "@wordpress/base-styles/variables";
2
+
3
+ .boot-dropdown-item__children {
4
+ display: flex;
5
+ flex-direction: column;
6
+
7
+ // In order to avoid the focus ring of each list item from being cut off,
8
+ // we add padding around the menu items.
9
+ // At the same time, we use the same value to tweak margins so that
10
+ // the items still retain the same position and footprint.
11
+ $padding-to-avoid-cutting-focus-ring: 2px;
12
+ padding: $padding-to-avoid-cutting-focus-ring;
13
+ margin-block-start: -$padding-to-avoid-cutting-focus-ring;
14
+ margin-block-end: $padding-to-avoid-cutting-focus-ring;
15
+ margin-inline-start:
16
+ variables.$grid-unit-40 -
17
+ $padding-to-avoid-cutting-focus-ring;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .boot-dropdown-item__chevron.is-up {
22
+ transform: rotate(180deg);
23
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useMemo, useRef } from '@wordpress/element';
5
+ import { useSelect } from '@wordpress/data';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { STORE_NAME } from '../../store';
11
+ import NavigationItem from './navigation-item';
12
+ import DrilldownItem from './drilldown-item';
13
+ import DropdownItem from './dropdown-item';
14
+ import NavigationScreen from './navigation-screen';
15
+ import { useSidebarParent } from './use-sidebar-parent';
16
+ import type { MenuItem } from '../../store/types';
17
+
18
+ function Navigation() {
19
+ const backButtonRef = useRef< HTMLButtonElement >( null );
20
+ const [ animationDirection, setAnimationDirection ] = useState<
21
+ 'forward' | 'backward' | null
22
+ >( null );
23
+ const [ parentId, setParentId, parentDropdownId, setParentDropdownId ] =
24
+ useSidebarParent();
25
+ const menuItems = useSelect(
26
+ ( select ) =>
27
+ // @ts-ignore
28
+ select( STORE_NAME ).getMenuItems() as MenuItem[],
29
+ []
30
+ );
31
+ const parent = useMemo(
32
+ () => menuItems.find( ( item ) => item.id === parentId ),
33
+ [ menuItems, parentId ]
34
+ );
35
+ // Create a unique key for the current navigation state
36
+ // The sidebar will animate when the key changes.
37
+ const navigationKey = parent ? `drilldown-${ parent.id }` : 'root';
38
+
39
+ // We use transitions to handle navigation clicks
40
+ // This allows smooth animations and non blocking navigation.
41
+ const handleNavigate = ( {
42
+ id,
43
+ direction,
44
+ }: {
45
+ id?: string;
46
+ direction: 'forward' | 'backward';
47
+ } ) => {
48
+ setAnimationDirection( direction );
49
+ setParentId( id );
50
+ };
51
+
52
+ const handleDropdownToggle = ( dropdownId: string ) => {
53
+ setParentDropdownId(
54
+ parentDropdownId === dropdownId ? undefined : dropdownId
55
+ );
56
+ };
57
+
58
+ const items = useMemo(
59
+ () => menuItems.filter( ( item ) => item.parent === parentId ),
60
+ [ menuItems, parentId ]
61
+ );
62
+
63
+ const hasRealIcons = items.some( ( item ) => !! item.icon );
64
+
65
+ return (
66
+ <NavigationScreen
67
+ isRoot={ ! parent }
68
+ title={ parent ? parent.label : '' }
69
+ backMenuItem={ parent?.parent }
70
+ backButtonRef={ backButtonRef }
71
+ animationDirection={ animationDirection || undefined }
72
+ navigationKey={ navigationKey }
73
+ onNavigate={ handleNavigate }
74
+ content={
75
+ <>
76
+ { items.map( ( item: MenuItem ) => {
77
+ if ( item.parent_type === 'dropdown' ) {
78
+ return (
79
+ <DropdownItem
80
+ key={ item.id }
81
+ id={ item.id }
82
+ className="boot-navigation-item"
83
+ icon={ item.icon }
84
+ shouldShowPlaceholder={ hasRealIcons }
85
+ isExpanded={ parentDropdownId === item.id }
86
+ onToggle={ () =>
87
+ handleDropdownToggle( item.id )
88
+ }
89
+ >
90
+ { item.label }
91
+ </DropdownItem>
92
+ );
93
+ }
94
+
95
+ if ( item.parent_type === 'drilldown' ) {
96
+ return (
97
+ <DrilldownItem
98
+ key={ item.id }
99
+ id={ item.id }
100
+ icon={ item.icon }
101
+ shouldShowPlaceholder={ hasRealIcons }
102
+ onNavigate={ handleNavigate }
103
+ >
104
+ { item.label }
105
+ </DrilldownItem>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <NavigationItem
111
+ key={ item.id }
112
+ to={ item.to }
113
+ icon={ item.icon }
114
+ shouldShowPlaceholder={ hasRealIcons }
115
+ >
116
+ { item.label }
117
+ </NavigationItem>
118
+ );
119
+ } ) }
120
+ </>
121
+ }
122
+ />
123
+ );
124
+ }
125
+
126
+ export default Navigation;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { isValidElement } from '@wordpress/element';
5
+ import { Dashicon, Icon } from '@wordpress/components';
6
+ import { SVG } from '@wordpress/primitives';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import type { IconType } from '../../store/types';
12
+
13
+ /**
14
+ * Type guard for verifying whether a given element
15
+ * is a valid SVG element for the Icon component.
16
+ *
17
+ * @param element - The element to check
18
+ */
19
+ function isSvg( element: unknown ): element is JSX.Element {
20
+ return (
21
+ isValidElement( element ) &&
22
+ ( element.type === SVG || element.type === 'svg' )
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Converts the given IconType into a renderable component:
28
+ * - Dashicon string into a Dashicon component
29
+ * - JSX SVG element into an Icon component
30
+ * - Data URL into an img element
31
+ *
32
+ * @param icon - The icon to convert
33
+ * @param shouldShowPlaceholder - Whether to show placeholder when no icon is provided
34
+ * @return The converted icon as a JSX element
35
+ */
36
+ export function wrapIcon(
37
+ icon?: IconType,
38
+ shouldShowPlaceholder: boolean = true
39
+ ) {
40
+ if ( isSvg( icon ) ) {
41
+ return <Icon icon={ icon } />;
42
+ }
43
+
44
+ if ( typeof icon === 'string' && icon.startsWith( 'dashicons-' ) ) {
45
+ const iconKey = icon.replace(
46
+ /^dashicons-/,
47
+ ''
48
+ ) as React.ComponentProps< typeof Dashicon >[ 'icon' ];
49
+
50
+ return (
51
+ <Dashicon
52
+ style={ { padding: '2px' } }
53
+ icon={ iconKey }
54
+ aria-hidden="true"
55
+ />
56
+ );
57
+ }
58
+
59
+ // Handle data URLs (SVG images)
60
+ if ( typeof icon === 'string' && icon.startsWith( 'data:' ) ) {
61
+ return (
62
+ <img
63
+ src={ icon }
64
+ alt=""
65
+ aria-hidden="true"
66
+ style={ {
67
+ width: '20px',
68
+ height: '20px',
69
+ display: 'block',
70
+ padding: '2px',
71
+ } }
72
+ />
73
+ );
74
+ }
75
+
76
+ // If icon is provided and valid, return it as-is
77
+ if ( icon ) {
78
+ return icon;
79
+ }
80
+
81
+ // Return empty 24px placeholder for alignment when no icon is provided
82
+ // Only if shouldShowPlaceholder is true
83
+ if ( shouldShowPlaceholder ) {
84
+ return (
85
+ <div
86
+ style={ { width: '24px', height: '24px' } }
87
+ aria-hidden="true"
88
+ />
89
+ );
90
+ }
91
+
92
+ return null;
93
+ }