@arbor-education/design-system.components 0.23.2 → 0.24.1
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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +24 -22
- package/dist/components/sidebarNav/SidebarNav.d.ts +46 -0
- package/dist/components/sidebarNav/SidebarNav.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNav.js +102 -0
- package/dist/components/sidebarNav/SidebarNav.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNav.stories.d.ts +61 -0
- package/dist/components/sidebarNav/SidebarNav.stories.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNav.stories.js +253 -0
- package/dist/components/sidebarNav/SidebarNav.stories.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNav.test.d.ts +2 -0
- package/dist/components/sidebarNav/SidebarNav.test.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNav.test.js +240 -0
- package/dist/components/sidebarNav/SidebarNav.test.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavContext.d.ts +13 -0
- package/dist/components/sidebarNav/SidebarNavContext.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavContext.js +15 -0
- package/dist/components/sidebarNav/SidebarNavContext.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavGroup.d.ts +10 -0
- package/dist/components/sidebarNav/SidebarNavGroup.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavGroup.js +16 -0
- package/dist/components/sidebarNav/SidebarNavGroup.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavItem.d.ts +32 -0
- package/dist/components/sidebarNav/SidebarNavItem.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavItem.js +43 -0
- package/dist/components/sidebarNav/SidebarNavItem.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavItemFavourite.d.ts +8 -0
- package/dist/components/sidebarNav/SidebarNavItemFavourite.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavItemFavourite.js +14 -0
- package/dist/components/sidebarNav/SidebarNavItemFavourite.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavPanel.d.ts +4 -0
- package/dist/components/sidebarNav/SidebarNavPanel.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavPanel.js +9 -0
- package/dist/components/sidebarNav/SidebarNavPanel.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavPanelNav.d.ts +10 -0
- package/dist/components/sidebarNav/SidebarNavPanelNav.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavPanelNav.js +21 -0
- package/dist/components/sidebarNav/SidebarNavPanelNav.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRail.d.ts +6 -0
- package/dist/components/sidebarNav/SidebarNavRail.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRail.js +7 -0
- package/dist/components/sidebarNav/SidebarNavRail.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailItem.d.ts +10 -0
- package/dist/components/sidebarNav/SidebarNavRailItem.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailItem.js +24 -0
- package/dist/components/sidebarNav/SidebarNavRailItem.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailList.d.ts +4 -0
- package/dist/components/sidebarNav/SidebarNavRailList.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailList.js +7 -0
- package/dist/components/sidebarNav/SidebarNavRailList.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailSlot.d.ts +6 -0
- package/dist/components/sidebarNav/SidebarNavRailSlot.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavRailSlot.js +7 -0
- package/dist/components/sidebarNav/SidebarNavRailSlot.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavSeparator.d.ts +6 -0
- package/dist/components/sidebarNav/SidebarNavSeparator.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavSeparator.js +8 -0
- package/dist/components/sidebarNav/SidebarNavSeparator.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTitle.d.ts +4 -0
- package/dist/components/sidebarNav/SidebarNavTitle.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTitle.js +7 -0
- package/dist/components/sidebarNav/SidebarNavTitle.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTooltip.d.ts +7 -0
- package/dist/components/sidebarNav/SidebarNavTooltip.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTooltip.js +9 -0
- package/dist/components/sidebarNav/SidebarNavTooltip.js.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTrigger.d.ts +8 -0
- package/dist/components/sidebarNav/SidebarNavTrigger.d.ts.map +1 -0
- package/dist/components/sidebarNav/SidebarNavTrigger.js +15 -0
- package/dist/components/sidebarNav/SidebarNavTrigger.js.map +1 -0
- package/dist/components/sidebarNav/index.d.ts +4 -0
- package/dist/components/sidebarNav/index.d.ts.map +1 -0
- package/dist/components/sidebarNav/index.js +3 -0
- package/dist/components/sidebarNav/index.js.map +1 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.d.ts +4 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.d.ts.map +1 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.js +43 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.js.map +1 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.test.d.ts +2 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.test.d.ts.map +1 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.test.js +52 -0
- package/dist/components/sidebarNav/resolvePanelItemProps.test.js.map +1 -0
- package/dist/components/sidebarNav/types.d.ts +100 -0
- package/dist/components/sidebarNav/types.d.ts.map +1 -0
- package/dist/components/sidebarNav/types.js +4 -0
- package/dist/components/sidebarNav/types.js.map +1 -0
- package/dist/components/sidebarNav/useControllableBoolean.d.ts +9 -0
- package/dist/components/sidebarNav/useControllableBoolean.d.ts.map +1 -0
- package/dist/components/sidebarNav/useControllableBoolean.js +14 -0
- package/dist/components/sidebarNav/useControllableBoolean.js.map +1 -0
- package/dist/index.css +275 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/sidebarNav/SidebarNav.stories.tsx +484 -0
- package/src/components/sidebarNav/SidebarNav.test.tsx +611 -0
- package/src/components/sidebarNav/SidebarNav.tsx +230 -0
- package/src/components/sidebarNav/SidebarNavContext.tsx +28 -0
- package/src/components/sidebarNav/SidebarNavGroup.tsx +59 -0
- package/src/components/sidebarNav/SidebarNavItem.tsx +160 -0
- package/src/components/sidebarNav/SidebarNavItemFavourite.tsx +49 -0
- package/src/components/sidebarNav/SidebarNavPanel.tsx +20 -0
- package/src/components/sidebarNav/SidebarNavPanelNav.tsx +55 -0
- package/src/components/sidebarNav/SidebarNavRail.tsx +20 -0
- package/src/components/sidebarNav/SidebarNavRailItem.tsx +84 -0
- package/src/components/sidebarNav/SidebarNavRailList.tsx +11 -0
- package/src/components/sidebarNav/SidebarNavRailSlot.tsx +15 -0
- package/src/components/sidebarNav/SidebarNavSeparator.tsx +19 -0
- package/src/components/sidebarNav/SidebarNavTitle.tsx +13 -0
- package/src/components/sidebarNav/SidebarNavTooltip.tsx +24 -0
- package/src/components/sidebarNav/SidebarNavTrigger.tsx +52 -0
- package/src/components/sidebarNav/index.ts +6 -0
- package/src/components/sidebarNav/resolvePanelItemProps.test.ts +57 -0
- package/src/components/sidebarNav/resolvePanelItemProps.ts +50 -0
- package/src/components/sidebarNav/sidebarNav.scss +283 -0
- package/src/components/sidebarNav/types.ts +126 -0
- package/src/components/sidebarNav/useControllableBoolean.ts +20 -0
- package/src/docs/Contributing.mdx +7 -0
- package/src/index.scss +1 -0
- package/src/index.ts +12 -0
- package/src/tokens.scss +14 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Icon } from 'Components/icon/Icon';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from 'radix-ui';
|
|
4
|
+
import React, { useId, useMemo } from 'react';
|
|
5
|
+
import { SidebarNavProvider } from './SidebarNavContext.js';
|
|
6
|
+
import { SidebarNavGroup } from './SidebarNavGroup.js';
|
|
7
|
+
import type { SidebarNavGroupProps } from './SidebarNavGroup.js';
|
|
8
|
+
import { SidebarNavItem } from './SidebarNavItem.js';
|
|
9
|
+
import type { SidebarNavItemProps } from './SidebarNavItem.js';
|
|
10
|
+
import { SidebarNavPanelNav } from './SidebarNavPanelNav.js';
|
|
11
|
+
import type { SidebarNavPanelNavProps } from './SidebarNavPanelNav.js';
|
|
12
|
+
import { SidebarNavPanel } from './SidebarNavPanel.js';
|
|
13
|
+
import type { SidebarNavPanelProps } from './SidebarNavPanel.js';
|
|
14
|
+
import { SidebarNavRail } from './SidebarNavRail.js';
|
|
15
|
+
import type { SidebarNavRailProps } from './SidebarNavRail.js';
|
|
16
|
+
import { SidebarNavRailItem } from './SidebarNavRailItem.js';
|
|
17
|
+
import type { SidebarNavRailItemProps } from './SidebarNavRailItem.js';
|
|
18
|
+
import { SidebarNavRailList } from './SidebarNavRailList.js';
|
|
19
|
+
import { SidebarNavRailSlot } from './SidebarNavRailSlot.js';
|
|
20
|
+
import { SidebarNavSeparator } from './SidebarNavSeparator.js';
|
|
21
|
+
import { SidebarNavTitle } from './SidebarNavTitle.js';
|
|
22
|
+
import { SidebarNavTrigger } from './SidebarNavTrigger.js';
|
|
23
|
+
import type { SidebarNavTriggerProps } from './SidebarNavTrigger.js';
|
|
24
|
+
import type {
|
|
25
|
+
SidebarNavProps,
|
|
26
|
+
SidebarNavRailEntry,
|
|
27
|
+
SidebarNavRailItemConfig,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
import { isSidebarNavRailSeparator } from './types.js';
|
|
30
|
+
import { useControllableBoolean } from './useControllableBoolean.js';
|
|
31
|
+
|
|
32
|
+
function renderRailListItem(item: SidebarNavRailItemConfig) {
|
|
33
|
+
if (item.renderItem) {
|
|
34
|
+
return (
|
|
35
|
+
<React.Fragment key={item.id}>
|
|
36
|
+
{item.renderItem(item)}
|
|
37
|
+
</React.Fragment>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<SidebarNavRailItem
|
|
43
|
+
key={item.id}
|
|
44
|
+
data-testid={`sidebar-nav-rail-item-${item.id}`}
|
|
45
|
+
href={item.href}
|
|
46
|
+
onClick={item.onClick}
|
|
47
|
+
current={item.current}
|
|
48
|
+
aria-label={item.label}
|
|
49
|
+
>
|
|
50
|
+
<Icon name={item.iconName} size={24} color={item.iconColor} />
|
|
51
|
+
{item.badgeContent}
|
|
52
|
+
</SidebarNavRailItem>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function SidebarNavRailFromData(props: { items: SidebarNavRailEntry[] }) {
|
|
57
|
+
const { items } = props;
|
|
58
|
+
const nodes: React.ReactNode[] = [];
|
|
59
|
+
let currentGroup: SidebarNavRailItemConfig[] = [];
|
|
60
|
+
let groupKey = 'rail-list-leading';
|
|
61
|
+
|
|
62
|
+
const flushGroup = () => {
|
|
63
|
+
if (currentGroup.length === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
nodes.push(
|
|
68
|
+
<SidebarNavRailList key={groupKey}>
|
|
69
|
+
{currentGroup.map(renderRailListItem)}
|
|
70
|
+
</SidebarNavRailList>,
|
|
71
|
+
);
|
|
72
|
+
currentGroup = [];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const entry of items) {
|
|
76
|
+
if (isSidebarNavRailSeparator(entry)) {
|
|
77
|
+
flushGroup();
|
|
78
|
+
nodes.push(<SidebarNavSeparator key={entry.id} id={entry.id} />);
|
|
79
|
+
groupKey = `rail-list-after-${entry.id}`;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (entry.opensPanel) {
|
|
84
|
+
flushGroup();
|
|
85
|
+
nodes.push(
|
|
86
|
+
<SidebarNavTrigger
|
|
87
|
+
key={entry.id}
|
|
88
|
+
data-testid={`sidebar-nav-panel-trigger-${entry.id}`}
|
|
89
|
+
tooltip={entry.label}
|
|
90
|
+
onClick={(e) => {
|
|
91
|
+
entry.onClick?.(e);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<Icon name={entry.iconName} size={24} color={entry.iconColor} />
|
|
95
|
+
</SidebarNavTrigger>,
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
currentGroup.push(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
flushGroup();
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<SidebarNavRail>
|
|
107
|
+
{nodes}
|
|
108
|
+
</SidebarNavRail>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SidebarNavImpl = (props: SidebarNavProps) => {
|
|
113
|
+
const {
|
|
114
|
+
expanded,
|
|
115
|
+
defaultExpanded = true,
|
|
116
|
+
onExpandedChange,
|
|
117
|
+
renderLink,
|
|
118
|
+
railItems,
|
|
119
|
+
panelTitle,
|
|
120
|
+
panelNavItems,
|
|
121
|
+
className,
|
|
122
|
+
children,
|
|
123
|
+
...rest
|
|
124
|
+
} = props;
|
|
125
|
+
|
|
126
|
+
const panelId = useId();
|
|
127
|
+
const { value: isExpanded, set: setExpanded } = useControllableBoolean({
|
|
128
|
+
value: expanded,
|
|
129
|
+
defaultValue: defaultExpanded,
|
|
130
|
+
onChange: onExpandedChange,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const useDataRail = railItems !== undefined;
|
|
134
|
+
const useDataPanel = panelTitle !== undefined || panelNavItems !== undefined;
|
|
135
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
136
|
+
const railChild = childArray.find(c => c.type === SidebarNavRail);
|
|
137
|
+
const panelChild = childArray.find(c => c.type === SidebarNavPanel);
|
|
138
|
+
|
|
139
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
140
|
+
if (useDataPanel && !useDataRail && railChild == null && childArray.length > 0) {
|
|
141
|
+
console.warn(
|
|
142
|
+
'SidebarNav: composable SidebarNav.Rail was not found. In hybrid mode, SidebarNav.Rail must be a direct child of SidebarNav (not wrapped in Fragment or other wrappers).',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const content
|
|
148
|
+
= useDataRail || useDataPanel
|
|
149
|
+
? (
|
|
150
|
+
<>
|
|
151
|
+
{useDataRail
|
|
152
|
+
? <SidebarNavRailFromData items={railItems} />
|
|
153
|
+
: railChild}
|
|
154
|
+
{useDataPanel
|
|
155
|
+
? (
|
|
156
|
+
<SidebarNavPanel>
|
|
157
|
+
{panelTitle !== undefined && (
|
|
158
|
+
<SidebarNavTitle>{panelTitle}</SidebarNavTitle>
|
|
159
|
+
)}
|
|
160
|
+
<SidebarNavPanelNav items={panelNavItems ?? []} />
|
|
161
|
+
</SidebarNavPanel>
|
|
162
|
+
)
|
|
163
|
+
: panelChild}
|
|
164
|
+
</>
|
|
165
|
+
)
|
|
166
|
+
: children;
|
|
167
|
+
|
|
168
|
+
const contextValue = useMemo(
|
|
169
|
+
() => ({
|
|
170
|
+
expanded: isExpanded,
|
|
171
|
+
setExpanded,
|
|
172
|
+
panelId,
|
|
173
|
+
renderLink,
|
|
174
|
+
}),
|
|
175
|
+
[isExpanded, setExpanded, panelId, renderLink],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<TooltipPrimitive.Provider delayDuration={400}>
|
|
180
|
+
<SidebarNavProvider value={contextValue}>
|
|
181
|
+
<div
|
|
182
|
+
className={classNames('ds-sidebar-nav', className)}
|
|
183
|
+
data-testid="sidebar-nav"
|
|
184
|
+
{...rest}
|
|
185
|
+
>
|
|
186
|
+
{content}
|
|
187
|
+
</div>
|
|
188
|
+
</SidebarNavProvider>
|
|
189
|
+
</TooltipPrimitive.Provider>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
type SidebarNavComponent = typeof SidebarNavImpl & {
|
|
194
|
+
Rail: typeof SidebarNavRail;
|
|
195
|
+
Separator: typeof SidebarNavSeparator;
|
|
196
|
+
RailList: typeof SidebarNavRailList;
|
|
197
|
+
RailItem: typeof SidebarNavRailItem;
|
|
198
|
+
RailSlot: typeof SidebarNavRailSlot;
|
|
199
|
+
Trigger: typeof SidebarNavTrigger;
|
|
200
|
+
Panel: typeof SidebarNavPanel;
|
|
201
|
+
Title: typeof SidebarNavTitle;
|
|
202
|
+
PanelNav: typeof SidebarNavPanelNav;
|
|
203
|
+
Group: typeof SidebarNavGroup;
|
|
204
|
+
Item: typeof SidebarNavItem;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const SidebarNav = SidebarNavImpl as SidebarNavComponent;
|
|
208
|
+
|
|
209
|
+
SidebarNav.Rail = SidebarNavRail;
|
|
210
|
+
SidebarNav.Separator = SidebarNavSeparator;
|
|
211
|
+
SidebarNav.RailList = SidebarNavRailList;
|
|
212
|
+
SidebarNav.RailItem = SidebarNavRailItem;
|
|
213
|
+
SidebarNav.RailSlot = SidebarNavRailSlot;
|
|
214
|
+
SidebarNav.Trigger = SidebarNavTrigger;
|
|
215
|
+
SidebarNav.Panel = SidebarNavPanel;
|
|
216
|
+
SidebarNav.Title = SidebarNavTitle;
|
|
217
|
+
SidebarNav.PanelNav = SidebarNavPanelNav;
|
|
218
|
+
SidebarNav.Group = SidebarNavGroup;
|
|
219
|
+
SidebarNav.Item = SidebarNavItem;
|
|
220
|
+
|
|
221
|
+
export namespace SidebarNav {
|
|
222
|
+
export type Props = SidebarNavProps;
|
|
223
|
+
export type RailProps = SidebarNavRailProps;
|
|
224
|
+
export type RailItemProps = SidebarNavRailItemProps;
|
|
225
|
+
export type TriggerProps = SidebarNavTriggerProps;
|
|
226
|
+
export type PanelProps = SidebarNavPanelProps;
|
|
227
|
+
export type PanelNavProps = SidebarNavPanelNavProps;
|
|
228
|
+
export type GroupProps = SidebarNavGroupProps;
|
|
229
|
+
export type ItemProps = SidebarNavItemProps;
|
|
230
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { SidebarNavRenderLinkArgs } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type SidebarNavContextValue = {
|
|
5
|
+
expanded: boolean;
|
|
6
|
+
setExpanded: (expanded: boolean) => void;
|
|
7
|
+
panelId: string;
|
|
8
|
+
renderLink?: (args: SidebarNavRenderLinkArgs) => React.ReactElement;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SidebarNavContext = createContext<SidebarNavContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export function SidebarNavProvider(props: { value: SidebarNavContextValue; children: React.ReactNode }) {
|
|
14
|
+
const { value, children } = props;
|
|
15
|
+
return (
|
|
16
|
+
<SidebarNavContext.Provider value={value}>
|
|
17
|
+
{children}
|
|
18
|
+
</SidebarNavContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useSidebarNavContext() {
|
|
23
|
+
const ctx = useContext(SidebarNavContext);
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
throw new Error('SidebarNav components must be used within <SidebarNav>.');
|
|
26
|
+
}
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Icon } from 'Components/icon/Icon';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import type { HTMLAttributes } from 'react';
|
|
4
|
+
import { useId } from 'react';
|
|
5
|
+
import { useControllableBoolean } from './useControllableBoolean.js';
|
|
6
|
+
|
|
7
|
+
export type SidebarNavGroupProps = {
|
|
8
|
+
label: string;
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
defaultExpanded?: boolean;
|
|
11
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
} & Omit<HTMLAttributes<HTMLLIElement>, 'children'>;
|
|
14
|
+
|
|
15
|
+
export const SidebarNavGroup = (props: SidebarNavGroupProps) => {
|
|
16
|
+
const {
|
|
17
|
+
label,
|
|
18
|
+
expanded,
|
|
19
|
+
defaultExpanded = false,
|
|
20
|
+
onExpandedChange,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
} = props;
|
|
25
|
+
|
|
26
|
+
const ulId = useId();
|
|
27
|
+
const { value: isExpanded, set: setExpanded } = useControllableBoolean({
|
|
28
|
+
value: expanded,
|
|
29
|
+
defaultValue: defaultExpanded,
|
|
30
|
+
onChange: onExpandedChange,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<li className={classNames('ds-sidebar-nav__list-item', 'ds-sidebar-nav__group', className)} {...rest}>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className="ds-sidebar-nav__group-link"
|
|
38
|
+
aria-expanded={isExpanded}
|
|
39
|
+
aria-controls={ulId}
|
|
40
|
+
onClick={() => setExpanded(!isExpanded)}
|
|
41
|
+
>
|
|
42
|
+
<Icon
|
|
43
|
+
name="chevron-right"
|
|
44
|
+
size={16}
|
|
45
|
+
className="ds-sidebar-nav__group-chevron"
|
|
46
|
+
/>
|
|
47
|
+
<span className="ds-sidebar-nav__group-label">{label}</span>
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
<ul
|
|
51
|
+
id={ulId}
|
|
52
|
+
aria-hidden={!isExpanded}
|
|
53
|
+
className="ds-sidebar-nav__group-children"
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</ul>
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type {
|
|
3
|
+
AnchorHTMLAttributes,
|
|
4
|
+
ButtonHTMLAttributes,
|
|
5
|
+
HTMLAttributes,
|
|
6
|
+
MouseEventHandler,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useSidebarNavContext } from './SidebarNavContext.js';
|
|
9
|
+
import { SidebarNavItemFavourite } from './SidebarNavItemFavourite.js';
|
|
10
|
+
import type { SidebarNavLinkProps } from './types.js';
|
|
11
|
+
|
|
12
|
+
type SidebarNavItemFavouriteVariant
|
|
13
|
+
= | { canFavourite?: false; favouriteTooltip?: never }
|
|
14
|
+
| { canFavourite: true; favouriteTooltip: string };
|
|
15
|
+
|
|
16
|
+
type SidebarNavItemLinkVariant = {
|
|
17
|
+
linkElement?: 'link';
|
|
18
|
+
href: string;
|
|
19
|
+
linkElementProps?: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'className' | 'children'>;
|
|
20
|
+
linkProps?: SidebarNavLinkProps;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SidebarNavItemButtonVariant = {
|
|
24
|
+
linkElement: 'button';
|
|
25
|
+
href?: never;
|
|
26
|
+
linkElementProps?: ButtonHTMLAttributes<HTMLButtonElement>;
|
|
27
|
+
linkProps?: never;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type SidebarNavItemProps = {
|
|
31
|
+
current?: boolean;
|
|
32
|
+
itemId: string;
|
|
33
|
+
isPressed?: boolean;
|
|
34
|
+
onFavouriteClick?: MouseEventHandler<HTMLButtonElement>;
|
|
35
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
} & Omit<HTMLAttributes<HTMLLIElement>, 'children' | 'onClick'>
|
|
38
|
+
& SidebarNavItemFavouriteVariant
|
|
39
|
+
& (SidebarNavItemLinkVariant | SidebarNavItemButtonVariant);
|
|
40
|
+
|
|
41
|
+
function omitOnClick<T extends { onClick?: MouseEventHandler<HTMLElement> }>(
|
|
42
|
+
props: T | undefined,
|
|
43
|
+
): Omit<T, 'onClick'> | undefined {
|
|
44
|
+
if (props == null) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rest = { ...props };
|
|
49
|
+
delete rest.onClick;
|
|
50
|
+
return rest;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SidebarNavItem = (props: SidebarNavItemProps) => {
|
|
54
|
+
const {
|
|
55
|
+
current,
|
|
56
|
+
itemId,
|
|
57
|
+
canFavourite,
|
|
58
|
+
favouriteTooltip,
|
|
59
|
+
isPressed = false,
|
|
60
|
+
onFavouriteClick,
|
|
61
|
+
children,
|
|
62
|
+
className,
|
|
63
|
+
linkElement = 'link',
|
|
64
|
+
href,
|
|
65
|
+
linkElementProps,
|
|
66
|
+
linkProps,
|
|
67
|
+
onClick,
|
|
68
|
+
...rest
|
|
69
|
+
} = props;
|
|
70
|
+
|
|
71
|
+
const { renderLink } = useSidebarNavContext();
|
|
72
|
+
|
|
73
|
+
const linkClassName = classNames(
|
|
74
|
+
'ds-sidebar-nav__item-link',
|
|
75
|
+
{ 'ds-sidebar-nav__item-link--current': Boolean(current) },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const linkChildren = <span className="ds-sidebar-nav__item-label">{children}</span>;
|
|
79
|
+
|
|
80
|
+
const linkElementOnClick = linkElementProps?.onClick;
|
|
81
|
+
|
|
82
|
+
const handleClick: MouseEventHandler<HTMLElement> = (event) => {
|
|
83
|
+
if (onClick) {
|
|
84
|
+
onClick(event);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
linkElementOnClick?.(
|
|
89
|
+
event as React.MouseEvent<HTMLButtonElement> & React.MouseEvent<HTMLAnchorElement>,
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const buttonLinkElementProps = omitOnClick(
|
|
94
|
+
linkElementProps as ButtonHTMLAttributes<HTMLButtonElement> | undefined,
|
|
95
|
+
);
|
|
96
|
+
const anchorLinkElementProps = omitOnClick(
|
|
97
|
+
linkElementProps as AnchorHTMLAttributes<HTMLAnchorElement> | undefined,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const linkElementNode = linkElement === 'button'
|
|
101
|
+
? (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={linkClassName}
|
|
105
|
+
aria-current={current ? 'page' : undefined}
|
|
106
|
+
{...buttonLinkElementProps}
|
|
107
|
+
onClick={handleClick as MouseEventHandler<HTMLButtonElement>}
|
|
108
|
+
>
|
|
109
|
+
{linkChildren}
|
|
110
|
+
</button>
|
|
111
|
+
)
|
|
112
|
+
: renderLink
|
|
113
|
+
? renderLink({
|
|
114
|
+
href: href!,
|
|
115
|
+
className: linkClassName,
|
|
116
|
+
ariaCurrent: current ? 'page' : undefined,
|
|
117
|
+
linkProps,
|
|
118
|
+
linkElementProps: anchorLinkElementProps as Omit<
|
|
119
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
120
|
+
'href' | 'className' | 'children'
|
|
121
|
+
> | undefined,
|
|
122
|
+
onClick: handleClick,
|
|
123
|
+
children: linkChildren,
|
|
124
|
+
})
|
|
125
|
+
: (
|
|
126
|
+
<a
|
|
127
|
+
href={href}
|
|
128
|
+
className={linkClassName}
|
|
129
|
+
aria-current={current ? 'page' : undefined}
|
|
130
|
+
{...linkProps}
|
|
131
|
+
{...anchorLinkElementProps}
|
|
132
|
+
onClick={handleClick as MouseEventHandler<HTMLAnchorElement>}
|
|
133
|
+
>
|
|
134
|
+
{linkChildren}
|
|
135
|
+
</a>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<li
|
|
140
|
+
className={classNames(
|
|
141
|
+
'ds-sidebar-nav__list-item',
|
|
142
|
+
'ds-sidebar-nav__item',
|
|
143
|
+
{ 'ds-sidebar-nav__item--has-favourite': canFavourite },
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
data-testid={`sidebar-nav-panel-item-${itemId}`}
|
|
147
|
+
{...rest}
|
|
148
|
+
>
|
|
149
|
+
{linkElementNode}
|
|
150
|
+
{canFavourite === true && (
|
|
151
|
+
<SidebarNavItemFavourite
|
|
152
|
+
isPressed={isPressed}
|
|
153
|
+
itemId={itemId}
|
|
154
|
+
favouriteTooltip={favouriteTooltip}
|
|
155
|
+
onClick={onFavouriteClick}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
</li>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Icon } from 'Components/icon/Icon';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import type { ButtonHTMLAttributes } from 'react';
|
|
4
|
+
import { SidebarNavTooltip } from './SidebarNavTooltip.js';
|
|
5
|
+
|
|
6
|
+
export type SidebarNavItemFavouriteProps = {
|
|
7
|
+
isPressed: boolean;
|
|
8
|
+
itemId: string;
|
|
9
|
+
favouriteTooltip: string;
|
|
10
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
|
11
|
+
|
|
12
|
+
export const SidebarNavItemFavourite = (props: SidebarNavItemFavouriteProps) => {
|
|
13
|
+
const {
|
|
14
|
+
isPressed,
|
|
15
|
+
itemId,
|
|
16
|
+
favouriteTooltip,
|
|
17
|
+
className,
|
|
18
|
+
onClick,
|
|
19
|
+
...rest
|
|
20
|
+
} = props;
|
|
21
|
+
|
|
22
|
+
const button = (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className={classNames('ds-sidebar-nav__item-favourite', className)}
|
|
26
|
+
aria-pressed={isPressed}
|
|
27
|
+
aria-label={favouriteTooltip}
|
|
28
|
+
data-testid={`sidebar-nav-panel-item-favourite-${itemId}`}
|
|
29
|
+
onClick={(e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
onClick?.(e);
|
|
33
|
+
}}
|
|
34
|
+
{...rest}
|
|
35
|
+
>
|
|
36
|
+
<Icon
|
|
37
|
+
name={isPressed ? 'favourite-filled' : 'favourite-outline'}
|
|
38
|
+
size={16}
|
|
39
|
+
color={isPressed ? 'var(--color-brand-600)' : 'currentColor'}
|
|
40
|
+
/>
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SidebarNavTooltip label={favouriteTooltip} side="left">
|
|
46
|
+
{button}
|
|
47
|
+
</SidebarNavTooltip>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
import { useSidebarNavContext } from './SidebarNavContext.js';
|
|
4
|
+
|
|
5
|
+
export type SidebarNavPanelProps = HTMLAttributes<HTMLDivElement>;
|
|
6
|
+
|
|
7
|
+
export const SidebarNavPanel = (props: SidebarNavPanelProps) => {
|
|
8
|
+
const { className, ...rest } = props;
|
|
9
|
+
const { expanded, panelId } = useSidebarNavContext();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
id={panelId}
|
|
14
|
+
aria-hidden={!expanded}
|
|
15
|
+
className={classNames('ds-sidebar-nav__panel', className)}
|
|
16
|
+
data-testid="sidebar-nav-panel"
|
|
17
|
+
{...rest}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
import { SidebarNavGroup } from './SidebarNavGroup.js';
|
|
4
|
+
import { SidebarNavItem } from './SidebarNavItem.js';
|
|
5
|
+
import { resolvePanelItemProps } from './resolvePanelItemProps.js';
|
|
6
|
+
import type { SidebarNavItemNode, SidebarNavNode } from './types.js';
|
|
7
|
+
|
|
8
|
+
export type SidebarNavPanelNavProps = {
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
items?: SidebarNavNode[];
|
|
11
|
+
} & Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function renderItemNode(node: SidebarNavItemNode) {
|
|
16
|
+
return <SidebarNavItem key={node.id} {...resolvePanelItemProps(node)} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderNodes(nodes: SidebarNavNode[]) {
|
|
20
|
+
return nodes.map((node) => {
|
|
21
|
+
if (node.type === 'group') {
|
|
22
|
+
return (
|
|
23
|
+
<SidebarNavGroup
|
|
24
|
+
key={node.id}
|
|
25
|
+
label={node.label}
|
|
26
|
+
defaultExpanded={node.defaultExpanded}
|
|
27
|
+
expanded={node.expanded}
|
|
28
|
+
onExpandedChange={node.onExpandedChange}
|
|
29
|
+
>
|
|
30
|
+
{renderNodes(node.children ?? [])}
|
|
31
|
+
</SidebarNavGroup>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return renderItemNode(node);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SidebarNavPanelNav = (props: SidebarNavPanelNavProps) => {
|
|
40
|
+
const {
|
|
41
|
+
ariaLabel = 'Sidebar links',
|
|
42
|
+
items,
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...rest
|
|
46
|
+
} = props;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<nav aria-label={ariaLabel} className={classNames('ds-sidebar-nav__nav', className)} {...rest}>
|
|
50
|
+
<ul className="ds-sidebar-nav__list">
|
|
51
|
+
{items ? renderNodes(items) : children}
|
|
52
|
+
</ul>
|
|
53
|
+
</nav>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
export type SidebarNavRailProps = {
|
|
5
|
+
ariaLabel?: string;
|
|
6
|
+
} & HTMLAttributes<HTMLElement>;
|
|
7
|
+
|
|
8
|
+
export const SidebarNavRail = (props: SidebarNavRailProps) => {
|
|
9
|
+
const { className, children, ariaLabel = 'Sidebar rail', ...rest } = props;
|
|
10
|
+
return (
|
|
11
|
+
<nav
|
|
12
|
+
aria-label={ariaLabel}
|
|
13
|
+
className={classNames('ds-sidebar-nav__rail', className)}
|
|
14
|
+
data-testid="sidebar-nav-rail"
|
|
15
|
+
{...rest}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</nav>
|
|
19
|
+
);
|
|
20
|
+
};
|