@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.
- package/LICENSE.md +788 -0
- package/build-module/components/app/index.js +32 -0
- package/build-module/components/app/index.js.map +7 -0
- package/build-module/components/app/router.js +119 -0
- package/build-module/components/app/router.js.map +7 -0
- package/build-module/components/navigation/drilldown-item/index.js +49 -0
- package/build-module/components/navigation/drilldown-item/index.js.map +7 -0
- package/build-module/components/navigation/dropdown-item/index.js +162 -0
- package/build-module/components/navigation/dropdown-item/index.js.map +7 -0
- package/build-module/components/navigation/index.js +101 -0
- package/build-module/components/navigation/index.js.map +7 -0
- package/build-module/components/navigation/items.js +60 -0
- package/build-module/components/navigation/items.js.map +7 -0
- package/build-module/components/navigation/navigation-item/index.js +180 -0
- package/build-module/components/navigation/navigation-item/index.js.map +7 -0
- package/build-module/components/navigation/navigation-screen/index.js +196 -0
- package/build-module/components/navigation/navigation-screen/index.js.map +7 -0
- package/build-module/components/navigation/path-matching.js +78 -0
- package/build-module/components/navigation/path-matching.js.map +7 -0
- package/build-module/components/navigation/router-link-item.js +14 -0
- package/build-module/components/navigation/router-link-item.js.map +7 -0
- package/build-module/components/navigation/use-sidebar-parent.js +52 -0
- package/build-module/components/navigation/use-sidebar-parent.js.map +7 -0
- package/build-module/components/root/index.js +115 -0
- package/build-module/components/root/index.js.map +7 -0
- package/build-module/components/sidebar/index.js +78 -0
- package/build-module/components/sidebar/index.js.map +7 -0
- package/build-module/components/site-hub/index.js +153 -0
- package/build-module/components/site-hub/index.js.map +7 -0
- package/build-module/components/site-icon/index.js +115 -0
- package/build-module/components/site-icon/index.js.map +7 -0
- package/build-module/components/site-icon-link/index.js +101 -0
- package/build-module/components/site-icon-link/index.js.map +7 -0
- package/build-module/index.js +622 -0
- package/build-module/index.js.map +7 -0
- package/build-module/lock-unlock.js +11 -0
- package/build-module/lock-unlock.js.map +7 -0
- package/build-module/store/actions.js +19 -0
- package/build-module/store/actions.js.map +7 -0
- package/build-module/store/index.js +17 -0
- package/build-module/store/index.js.map +7 -0
- package/build-module/store/reducer.js +27 -0
- package/build-module/store/reducer.js.map +7 -0
- package/build-module/store/selectors.js +12 -0
- package/build-module/store/selectors.js.map +7 -0
- package/build-module/store/types.js +1 -0
- package/build-module/store/types.js.map +7 -0
- package/build-style/style-rtl.css +612 -0
- package/build-style/style.css +612 -0
- package/build-types/components/app/index.d.ts +6 -0
- package/build-types/components/app/index.d.ts.map +1 -0
- package/build-types/components/app/router.d.ts +7 -0
- package/build-types/components/app/router.d.ts.map +1 -0
- package/build-types/components/navigation/drilldown-item/index.d.ts +34 -0
- package/build-types/components/navigation/drilldown-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/dropdown-item/index.d.ts +36 -0
- package/build-types/components/navigation/dropdown-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/index.d.ts +3 -0
- package/build-types/components/navigation/index.d.ts.map +1 -0
- package/build-types/components/navigation/items.d.ts +16 -0
- package/build-types/components/navigation/items.d.ts.map +1 -0
- package/build-types/components/navigation/navigation-item/index.d.ts +28 -0
- package/build-types/components/navigation/navigation-item/index.d.ts.map +1 -0
- package/build-types/components/navigation/navigation-screen/index.d.ts +24 -0
- package/build-types/components/navigation/navigation-screen/index.d.ts.map +1 -0
- package/build-types/components/navigation/path-matching.d.ts +30 -0
- package/build-types/components/navigation/path-matching.d.ts.map +1 -0
- package/build-types/components/navigation/router-link-item.d.ts +5 -0
- package/build-types/components/navigation/router-link-item.d.ts.map +1 -0
- package/build-types/components/navigation/use-sidebar-parent.d.ts +12 -0
- package/build-types/components/navigation/use-sidebar-parent.d.ts.map +1 -0
- package/build-types/components/root/index.d.ts +3 -0
- package/build-types/components/root/index.d.ts.map +1 -0
- package/build-types/components/sidebar/index.d.ts +3 -0
- package/build-types/components/sidebar/index.d.ts.map +1 -0
- package/build-types/components/site-hub/index.d.ts +4 -0
- package/build-types/components/site-hub/index.d.ts.map +1 -0
- package/build-types/components/site-icon/index.d.ts +9 -0
- package/build-types/components/site-icon/index.d.ts.map +1 -0
- package/build-types/components/site-icon-link/index.d.ts +8 -0
- package/build-types/components/site-icon-link/index.d.ts.map +1 -0
- package/build-types/index.d.ts +6 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/lock-unlock.d.ts +2 -0
- package/build-types/lock-unlock.d.ts.map +1 -0
- package/build-types/store/actions.d.ts +15 -0
- package/build-types/store/actions.d.ts.map +1 -0
- package/build-types/store/index.d.ts +6 -0
- package/build-types/store/index.d.ts.map +1 -0
- package/build-types/store/reducer.d.ts +7 -0
- package/build-types/store/reducer.d.ts.map +1 -0
- package/build-types/store/selectors.d.ts +7 -0
- package/build-types/store/selectors.d.ts.map +1 -0
- package/build-types/store/types.d.ts +63 -0
- package/build-types/store/types.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/components/app/index.tsx +45 -0
- package/src/components/app/router.tsx +198 -0
- package/src/components/navigation/drilldown-item/index.tsx +88 -0
- package/src/components/navigation/dropdown-item/index.tsx +134 -0
- package/src/components/navigation/dropdown-item/style.scss +23 -0
- package/src/components/navigation/index.tsx +126 -0
- package/src/components/navigation/items.tsx +93 -0
- package/src/components/navigation/navigation-item/index.tsx +88 -0
- package/src/components/navigation/navigation-item/style.scss +52 -0
- package/src/components/navigation/navigation-screen/index.tsx +147 -0
- package/src/components/navigation/navigation-screen/style.scss +34 -0
- package/src/components/navigation/path-matching.ts +149 -0
- package/src/components/navigation/router-link-item.tsx +22 -0
- package/src/components/navigation/use-sidebar-parent.ts +77 -0
- package/src/components/root/index.tsx +42 -0
- package/src/components/root/style.scss +41 -0
- package/src/components/sidebar/index.tsx +17 -0
- package/src/components/sidebar/style.scss +15 -0
- package/src/components/site-hub/index.tsx +67 -0
- package/src/components/site-hub/style.scss +54 -0
- package/src/components/site-icon/index.tsx +60 -0
- package/src/components/site-icon/style.scss +19 -0
- package/src/components/site-icon-link/index.tsx +43 -0
- package/src/components/site-icon-link/style.scss +24 -0
- package/src/index.tsx +5 -0
- package/src/lock-unlock.ts +9 -0
- package/src/store/actions.ts +23 -0
- package/src/store/index.ts +23 -0
- package/src/store/reducer.ts +31 -0
- package/src/store/selectors.ts +12 -0
- package/src/store/types.ts +70 -0
- package/src/style.scss +2 -0
- package/tsconfig.json +23 -0
- 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
|
+
}
|