@arbor-education/design-system.components 0.23.1 → 0.24.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 (122) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/sidebarNav/SidebarNav.d.ts +46 -0
  3. package/dist/components/sidebarNav/SidebarNav.d.ts.map +1 -0
  4. package/dist/components/sidebarNav/SidebarNav.js +102 -0
  5. package/dist/components/sidebarNav/SidebarNav.js.map +1 -0
  6. package/dist/components/sidebarNav/SidebarNav.stories.d.ts +61 -0
  7. package/dist/components/sidebarNav/SidebarNav.stories.d.ts.map +1 -0
  8. package/dist/components/sidebarNav/SidebarNav.stories.js +253 -0
  9. package/dist/components/sidebarNav/SidebarNav.stories.js.map +1 -0
  10. package/dist/components/sidebarNav/SidebarNav.test.d.ts +2 -0
  11. package/dist/components/sidebarNav/SidebarNav.test.d.ts.map +1 -0
  12. package/dist/components/sidebarNav/SidebarNav.test.js +240 -0
  13. package/dist/components/sidebarNav/SidebarNav.test.js.map +1 -0
  14. package/dist/components/sidebarNav/SidebarNavContext.d.ts +13 -0
  15. package/dist/components/sidebarNav/SidebarNavContext.d.ts.map +1 -0
  16. package/dist/components/sidebarNav/SidebarNavContext.js +15 -0
  17. package/dist/components/sidebarNav/SidebarNavContext.js.map +1 -0
  18. package/dist/components/sidebarNav/SidebarNavGroup.d.ts +10 -0
  19. package/dist/components/sidebarNav/SidebarNavGroup.d.ts.map +1 -0
  20. package/dist/components/sidebarNav/SidebarNavGroup.js +16 -0
  21. package/dist/components/sidebarNav/SidebarNavGroup.js.map +1 -0
  22. package/dist/components/sidebarNav/SidebarNavItem.d.ts +32 -0
  23. package/dist/components/sidebarNav/SidebarNavItem.d.ts.map +1 -0
  24. package/dist/components/sidebarNav/SidebarNavItem.js +43 -0
  25. package/dist/components/sidebarNav/SidebarNavItem.js.map +1 -0
  26. package/dist/components/sidebarNav/SidebarNavItemFavourite.d.ts +8 -0
  27. package/dist/components/sidebarNav/SidebarNavItemFavourite.d.ts.map +1 -0
  28. package/dist/components/sidebarNav/SidebarNavItemFavourite.js +14 -0
  29. package/dist/components/sidebarNav/SidebarNavItemFavourite.js.map +1 -0
  30. package/dist/components/sidebarNav/SidebarNavPanel.d.ts +4 -0
  31. package/dist/components/sidebarNav/SidebarNavPanel.d.ts.map +1 -0
  32. package/dist/components/sidebarNav/SidebarNavPanel.js +9 -0
  33. package/dist/components/sidebarNav/SidebarNavPanel.js.map +1 -0
  34. package/dist/components/sidebarNav/SidebarNavPanelNav.d.ts +10 -0
  35. package/dist/components/sidebarNav/SidebarNavPanelNav.d.ts.map +1 -0
  36. package/dist/components/sidebarNav/SidebarNavPanelNav.js +21 -0
  37. package/dist/components/sidebarNav/SidebarNavPanelNav.js.map +1 -0
  38. package/dist/components/sidebarNav/SidebarNavRail.d.ts +6 -0
  39. package/dist/components/sidebarNav/SidebarNavRail.d.ts.map +1 -0
  40. package/dist/components/sidebarNav/SidebarNavRail.js +7 -0
  41. package/dist/components/sidebarNav/SidebarNavRail.js.map +1 -0
  42. package/dist/components/sidebarNav/SidebarNavRailItem.d.ts +10 -0
  43. package/dist/components/sidebarNav/SidebarNavRailItem.d.ts.map +1 -0
  44. package/dist/components/sidebarNav/SidebarNavRailItem.js +24 -0
  45. package/dist/components/sidebarNav/SidebarNavRailItem.js.map +1 -0
  46. package/dist/components/sidebarNav/SidebarNavRailList.d.ts +4 -0
  47. package/dist/components/sidebarNav/SidebarNavRailList.d.ts.map +1 -0
  48. package/dist/components/sidebarNav/SidebarNavRailList.js +7 -0
  49. package/dist/components/sidebarNav/SidebarNavRailList.js.map +1 -0
  50. package/dist/components/sidebarNav/SidebarNavRailSlot.d.ts +6 -0
  51. package/dist/components/sidebarNav/SidebarNavRailSlot.d.ts.map +1 -0
  52. package/dist/components/sidebarNav/SidebarNavRailSlot.js +7 -0
  53. package/dist/components/sidebarNav/SidebarNavRailSlot.js.map +1 -0
  54. package/dist/components/sidebarNav/SidebarNavSeparator.d.ts +6 -0
  55. package/dist/components/sidebarNav/SidebarNavSeparator.d.ts.map +1 -0
  56. package/dist/components/sidebarNav/SidebarNavSeparator.js +8 -0
  57. package/dist/components/sidebarNav/SidebarNavSeparator.js.map +1 -0
  58. package/dist/components/sidebarNav/SidebarNavTitle.d.ts +4 -0
  59. package/dist/components/sidebarNav/SidebarNavTitle.d.ts.map +1 -0
  60. package/dist/components/sidebarNav/SidebarNavTitle.js +7 -0
  61. package/dist/components/sidebarNav/SidebarNavTitle.js.map +1 -0
  62. package/dist/components/sidebarNav/SidebarNavTooltip.d.ts +7 -0
  63. package/dist/components/sidebarNav/SidebarNavTooltip.d.ts.map +1 -0
  64. package/dist/components/sidebarNav/SidebarNavTooltip.js +9 -0
  65. package/dist/components/sidebarNav/SidebarNavTooltip.js.map +1 -0
  66. package/dist/components/sidebarNav/SidebarNavTrigger.d.ts +8 -0
  67. package/dist/components/sidebarNav/SidebarNavTrigger.d.ts.map +1 -0
  68. package/dist/components/sidebarNav/SidebarNavTrigger.js +15 -0
  69. package/dist/components/sidebarNav/SidebarNavTrigger.js.map +1 -0
  70. package/dist/components/sidebarNav/index.d.ts +4 -0
  71. package/dist/components/sidebarNav/index.d.ts.map +1 -0
  72. package/dist/components/sidebarNav/index.js +3 -0
  73. package/dist/components/sidebarNav/index.js.map +1 -0
  74. package/dist/components/sidebarNav/resolvePanelItemProps.d.ts +4 -0
  75. package/dist/components/sidebarNav/resolvePanelItemProps.d.ts.map +1 -0
  76. package/dist/components/sidebarNav/resolvePanelItemProps.js +43 -0
  77. package/dist/components/sidebarNav/resolvePanelItemProps.js.map +1 -0
  78. package/dist/components/sidebarNav/resolvePanelItemProps.test.d.ts +2 -0
  79. package/dist/components/sidebarNav/resolvePanelItemProps.test.d.ts.map +1 -0
  80. package/dist/components/sidebarNav/resolvePanelItemProps.test.js +52 -0
  81. package/dist/components/sidebarNav/resolvePanelItemProps.test.js.map +1 -0
  82. package/dist/components/sidebarNav/types.d.ts +100 -0
  83. package/dist/components/sidebarNav/types.d.ts.map +1 -0
  84. package/dist/components/sidebarNav/types.js +4 -0
  85. package/dist/components/sidebarNav/types.js.map +1 -0
  86. package/dist/components/sidebarNav/useControllableBoolean.d.ts +9 -0
  87. package/dist/components/sidebarNav/useControllableBoolean.d.ts.map +1 -0
  88. package/dist/components/sidebarNav/useControllableBoolean.js +14 -0
  89. package/dist/components/sidebarNav/useControllableBoolean.js.map +1 -0
  90. package/dist/index.css +287 -10
  91. package/dist/index.css.map +1 -1
  92. package/dist/index.d.ts +3 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +2 -0
  95. package/dist/index.js.map +1 -1
  96. package/package.json +7 -4
  97. package/src/components/sidebarNav/SidebarNav.stories.tsx +484 -0
  98. package/src/components/sidebarNav/SidebarNav.test.tsx +611 -0
  99. package/src/components/sidebarNav/SidebarNav.tsx +230 -0
  100. package/src/components/sidebarNav/SidebarNavContext.tsx +28 -0
  101. package/src/components/sidebarNav/SidebarNavGroup.tsx +59 -0
  102. package/src/components/sidebarNav/SidebarNavItem.tsx +160 -0
  103. package/src/components/sidebarNav/SidebarNavItemFavourite.tsx +49 -0
  104. package/src/components/sidebarNav/SidebarNavPanel.tsx +20 -0
  105. package/src/components/sidebarNav/SidebarNavPanelNav.tsx +55 -0
  106. package/src/components/sidebarNav/SidebarNavRail.tsx +20 -0
  107. package/src/components/sidebarNav/SidebarNavRailItem.tsx +84 -0
  108. package/src/components/sidebarNav/SidebarNavRailList.tsx +11 -0
  109. package/src/components/sidebarNav/SidebarNavRailSlot.tsx +15 -0
  110. package/src/components/sidebarNav/SidebarNavSeparator.tsx +19 -0
  111. package/src/components/sidebarNav/SidebarNavTitle.tsx +13 -0
  112. package/src/components/sidebarNav/SidebarNavTooltip.tsx +24 -0
  113. package/src/components/sidebarNav/SidebarNavTrigger.tsx +52 -0
  114. package/src/components/sidebarNav/index.ts +6 -0
  115. package/src/components/sidebarNav/resolvePanelItemProps.test.ts +57 -0
  116. package/src/components/sidebarNav/resolvePanelItemProps.ts +50 -0
  117. package/src/components/sidebarNav/sidebarNav.scss +283 -0
  118. package/src/components/sidebarNav/types.ts +126 -0
  119. package/src/components/sidebarNav/useControllableBoolean.ts +20 -0
  120. package/src/index.scss +1 -0
  121. package/src/index.ts +12 -0
  122. 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
+ };