@servicetitan/navigation 14.2.0 → 14.3.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/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts +2 -0
- package/dist/components/titan-layout/__tests__/layout-sidebar-links-internal.test.d.ts.map +1 -0
- package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts +2 -0
- package/dist/components/titan-layout/__tests__/navigation-catalog-reset.test.d.ts.map +1 -0
- package/dist/components/titan-layout/interface.d.ts +66 -0
- package/dist/components/titan-layout/interface.d.ts.map +1 -1
- package/dist/components/titan-layout/interface.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +3 -0
- package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar-links-internal.js +58 -13
- package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.d.ts +14 -0
- package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.js +169 -97
- package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.module.less +230 -144
- package/dist/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
- package/dist/components/titan-layout/navigation-catalog/index.d.ts +3 -0
- package/dist/components/titan-layout/navigation-catalog/index.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/index.js +4 -0
- package/dist/components/titan-layout/navigation-catalog/index.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts +8 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js +88 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-footer.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts +21 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js +138 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts +14 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js +382 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js.map +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less +436 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +24 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.d.ts +12 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.d.ts.map +1 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.js +48 -0
- package/dist/components/titan-layout/navigation-scroll-indicators.js.map +1 -0
- package/dist/components/titan-layout/titan-layout.d.ts +29 -1
- package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
- package/dist/components/titan-layout/titan-layout.js +46 -6
- package/dist/components/titan-layout/titan-layout.js.map +1 -1
- package/dist/components/titan-layout/titan-layout.module.less +2 -1
- package/dist/components/titan-layout/use-navigation-scroll.d.ts +20 -0
- package/dist/components/titan-layout/use-navigation-scroll.d.ts.map +1 -0
- package/dist/components/titan-layout/use-navigation-scroll.js +140 -0
- package/dist/components/titan-layout/use-navigation-scroll.js.map +1 -0
- package/dist/utils/__tests__/navigation-notifications.test.d.ts +2 -0
- package/dist/utils/__tests__/navigation-notifications.test.d.ts.map +1 -0
- package/dist/utils/__tests__/submenu-state.test.d.ts +2 -0
- package/dist/utils/__tests__/submenu-state.test.d.ts.map +1 -0
- package/dist/utils/navigation-notifications.d.ts +21 -0
- package/dist/utils/navigation-notifications.d.ts.map +1 -0
- package/dist/utils/navigation-notifications.js +39 -0
- package/dist/utils/navigation-notifications.js.map +1 -0
- package/dist/utils/side-nav.js +3 -1
- package/dist/utils/side-nav.js.map +1 -1
- package/dist/utils/submenu-state.d.ts +3 -0
- package/dist/utils/submenu-state.d.ts.map +1 -0
- package/dist/utils/submenu-state.js +15 -0
- package/dist/utils/submenu-state.js.map +1 -0
- package/package.json +2 -2
- package/src/components/titan-layout/__tests__/layout-sidebar-links-internal.test.tsx +225 -0
- package/src/components/titan-layout/__tests__/navigation-catalog-reset.test.tsx +186 -0
- package/src/components/titan-layout/interface.ts +73 -0
- package/src/components/titan-layout/layout-sidebar-links-internal.tsx +86 -14
- package/src/components/titan-layout/layout-sidebar.module.less +230 -144
- package/src/components/titan-layout/layout-sidebar.module.less.d.ts +16 -5
- package/src/components/titan-layout/layout-sidebar.tsx +193 -117
- package/src/components/titan-layout/navigation-catalog/index.ts +2 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-footer.tsx +88 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less +89 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts +9 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx +170 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less +436 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.module.less.d.ts +24 -0
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.tsx +458 -0
- package/src/components/titan-layout/navigation-scroll-indicators.tsx +80 -0
- package/src/components/titan-layout/titan-layout.module.less +2 -1
- package/src/components/titan-layout/titan-layout.tsx +81 -5
- package/src/components/titan-layout/use-navigation-scroll.ts +145 -0
- package/src/utils/__tests__/navigation-notifications.test.ts +70 -0
- package/src/utils/__tests__/submenu-state.test.ts +39 -0
- package/src/utils/navigation-notifications.ts +59 -0
- package/src/utils/side-nav.ts +1 -1
- package/src/utils/submenu-state.ts +16 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { Divider, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
1
|
+
import { Button, Divider, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
2
2
|
import SvgClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
3
|
+
import SvgCollapseAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_less.svg';
|
|
4
|
+
import SvgExpandAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_more.svg';
|
|
3
5
|
import SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';
|
|
4
6
|
import SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';
|
|
5
7
|
import classNames from 'classnames';
|
|
6
8
|
import { Children, FC, Fragment, ReactElement, isValidElement } from 'react';
|
|
7
9
|
import { NavigationItemData, NavigationSubmenuData } from '../../utils/navigation';
|
|
8
10
|
import { NavLinkComponentProps } from '../../utils/navigation-context';
|
|
11
|
+
import { getModuleNotificationCount } from '../../utils/navigation-notifications';
|
|
9
12
|
import { getCounterTag, getSubmenuGroupTag } from '../../utils/side-nav';
|
|
13
|
+
import { PinnedNavigationConfig } from './interface';
|
|
10
14
|
import { NavigationComponentProps } from './interface-internal';
|
|
11
15
|
import { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';
|
|
12
16
|
import {
|
|
@@ -15,13 +19,30 @@ import {
|
|
|
15
19
|
InternalSideNavigationLink,
|
|
16
20
|
} from './layout-sidebar-links-internal';
|
|
17
21
|
import * as Styles from './layout-sidebar.module.less';
|
|
22
|
+
import { NavigationCatalogTrigger } from './navigation-catalog';
|
|
23
|
+
import { NavigationScrollIndicators } from './navigation-scroll-indicators';
|
|
24
|
+
import { useNavigationScroll } from './use-navigation-scroll';
|
|
18
25
|
import { withTooltip } from './with-tooltip';
|
|
19
26
|
|
|
20
27
|
export interface LayoutSidebarProps {
|
|
21
28
|
className?: string;
|
|
22
29
|
top?: ReactElement[];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Destinations rendered above the divider, ahead of `top`.
|
|
33
|
+
*
|
|
34
|
+
* Separate from `top` rather than widening it: that one takes elements a host builds itself
|
|
35
|
+
* and renders them as-is, which is what a bespoke control needs. This takes navigation items
|
|
36
|
+
* and puts them through the same renderer as `mainItems`, so one with pages beneath it is an
|
|
37
|
+
* expandable group instead of being flattened to a bare link.
|
|
38
|
+
*/
|
|
39
|
+
topItems?: NavigationItemData[];
|
|
40
|
+
|
|
41
|
+
/** Whether the rail shows the scroll and overflow-notification indicators. */
|
|
42
|
+
scrollIndicators?: boolean;
|
|
23
43
|
bottom?: ReactElement;
|
|
24
44
|
mainItems?: NavigationItemData[];
|
|
45
|
+
pinnedNavigation?: PinnedNavigationConfig;
|
|
25
46
|
barExpanded: boolean;
|
|
26
47
|
submenusOpened: string[] | undefined;
|
|
27
48
|
drawerOpened: boolean;
|
|
@@ -31,6 +52,7 @@ export interface LayoutSidebarProps {
|
|
|
31
52
|
onBarExpandChange(expanded: boolean): void;
|
|
32
53
|
onDrawerOpenChange(expanded: boolean): void;
|
|
33
54
|
onSubmenuOpenChange(id: string, opened: boolean): void;
|
|
55
|
+
onSubmenusOpenChange?(ids: string[], opened: boolean): void;
|
|
34
56
|
onNavigationLinkClick?(itemId: string, submenuItemId?: string): void;
|
|
35
57
|
}
|
|
36
58
|
|
|
@@ -43,13 +65,19 @@ export const LayoutSidebar: FC<LayoutSidebarProps> = ({
|
|
|
43
65
|
drawerOpened,
|
|
44
66
|
onBarExpandChange,
|
|
45
67
|
onSubmenuOpenChange,
|
|
68
|
+
onSubmenusOpenChange,
|
|
46
69
|
onDrawerOpenChange,
|
|
47
70
|
onNavigationLinkClick,
|
|
48
71
|
mainItems,
|
|
72
|
+
pinnedNavigation,
|
|
49
73
|
top,
|
|
74
|
+
topItems,
|
|
75
|
+
scrollIndicators,
|
|
50
76
|
bottom,
|
|
51
77
|
navigationComponent,
|
|
52
78
|
}) => {
|
|
79
|
+
const { regionRef, scrollRef, revealNotification, ...scrollOverflow } =
|
|
80
|
+
useNavigationScroll(scrollIndicators);
|
|
53
81
|
const closeDrawer = () => {
|
|
54
82
|
if (mobile) {
|
|
55
83
|
onDrawerOpenChange?.(false);
|
|
@@ -57,6 +85,90 @@ export const LayoutSidebar: FC<LayoutSidebarProps> = ({
|
|
|
57
85
|
};
|
|
58
86
|
|
|
59
87
|
const effectiveExpanded = mobile ? drawerOpened : barExpanded;
|
|
88
|
+
/*
|
|
89
|
+
* Both regions, because a destination above the divider expands exactly like one below it.
|
|
90
|
+
* Counting only `mainItems` would let Expand All skip those groups — and hide the control
|
|
91
|
+
* entirely on a rail whose only expandable modules sit up there.
|
|
92
|
+
*/
|
|
93
|
+
const expandableIds = [...(topItems ?? []), ...(mainItems ?? [])]
|
|
94
|
+
.filter(item => item.submenu?.groups.some(group => group.links.length > 0))
|
|
95
|
+
.map(item => item.id);
|
|
96
|
+
const hasOpenSubmenus = expandableIds.some(id => submenusOpened?.includes(id));
|
|
97
|
+
const toggleAllLabel = hasOpenSubmenus ? 'Collapse All' : 'Expand All';
|
|
98
|
+
const toggleAll =
|
|
99
|
+
effectiveExpanded && expandableIds.length > 0 && onSubmenusOpenChange
|
|
100
|
+
? withTooltip(
|
|
101
|
+
<Button
|
|
102
|
+
appearance="secondary"
|
|
103
|
+
size="small"
|
|
104
|
+
icon={hasOpenSubmenus ? SvgCollapseAll : SvgExpandAll}
|
|
105
|
+
aria-label={toggleAllLabel}
|
|
106
|
+
aria-expanded={hasOpenSubmenus}
|
|
107
|
+
data-cy="navigation-toggle-all"
|
|
108
|
+
data-pendo="navigation-toggle-all"
|
|
109
|
+
onClick={event => {
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
onSubmenusOpenChange(expandableIds, !hasOpenSubmenus);
|
|
112
|
+
}}
|
|
113
|
+
/>,
|
|
114
|
+
toggleAllLabel,
|
|
115
|
+
{ placement: 'top' }
|
|
116
|
+
)
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
const scrollHeader = !mobile && !barExpanded;
|
|
120
|
+
const navigationHeader = (
|
|
121
|
+
<div className={Styles.navHeader}>
|
|
122
|
+
{mobile && (
|
|
123
|
+
<div className={Styles.navCloseWrapper}>
|
|
124
|
+
<div className={Styles.navClose} onClick={() => onBarExpandChange(false)}>
|
|
125
|
+
<Icon svg={SvgClose} size="large" />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
{pinnedNavigation && (
|
|
130
|
+
<NavigationCatalogTrigger
|
|
131
|
+
expanded={effectiveExpanded}
|
|
132
|
+
{...pinnedNavigation}
|
|
133
|
+
trailingAction={toggleAll}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
{!pinnedNavigation && toggleAll && <div className={Styles.navTools}>{toggleAll}</div>}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* Shared by the region above the divider and the list below it, so a destination renders the
|
|
142
|
+
* same way wherever the host puts it. Two copies of this branch would drift.
|
|
143
|
+
*/
|
|
144
|
+
const renderItems = (items: NavigationItemData[] | undefined) =>
|
|
145
|
+
items?.map(item =>
|
|
146
|
+
item.submenu ? (
|
|
147
|
+
<SideNavigationGroupItem
|
|
148
|
+
key={item.id}
|
|
149
|
+
item={item}
|
|
150
|
+
barExpanded={effectiveExpanded}
|
|
151
|
+
submenuOpened={!!item.id && !!submenusOpened?.includes(item.id)}
|
|
152
|
+
onSubmenuOpen={onSubmenuOpenChange}
|
|
153
|
+
navigationComponent={navigationComponent}
|
|
154
|
+
onNavigationLinkClick={onNavigationLinkClick}
|
|
155
|
+
touchDevice={touchDevice}
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
<InternalSideNavigationLink
|
|
159
|
+
key={item.id}
|
|
160
|
+
id={item.id}
|
|
161
|
+
to={item.to}
|
|
162
|
+
title={effectiveExpanded ? item.title : (item.collapsedTitle ?? item.title)}
|
|
163
|
+
isActive={item.isActive}
|
|
164
|
+
icon={item.icon}
|
|
165
|
+
iconActive={item.iconActive}
|
|
166
|
+
className={item.className}
|
|
167
|
+
tag={getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag))}
|
|
168
|
+
navigationComponent={navigationComponent}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
);
|
|
60
172
|
|
|
61
173
|
return (
|
|
62
174
|
<LayoutPlacementContext.Provider value="side">
|
|
@@ -74,69 +186,46 @@ export const LayoutSidebar: FC<LayoutSidebarProps> = ({
|
|
|
74
186
|
data-cy="side-navigation"
|
|
75
187
|
onClick={closeDrawer}
|
|
76
188
|
>
|
|
77
|
-
<ThemeProvider mode="dark" className={Styles.
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
189
|
+
<ThemeProvider mode="dark" className={Styles.navContent}>
|
|
190
|
+
{!scrollHeader && navigationHeader}
|
|
191
|
+
<div ref={regionRef} className={Styles.navScrollRegion}>
|
|
192
|
+
<div ref={scrollRef} className={Styles.navMain} data-cy="navigation-scroll">
|
|
193
|
+
<div>
|
|
194
|
+
{scrollHeader && navigationHeader}
|
|
195
|
+
{(!!top?.length || !!topItems?.length) && (
|
|
196
|
+
<SidebarTop>
|
|
197
|
+
{renderItems(topItems)}
|
|
198
|
+
{top}
|
|
199
|
+
</SidebarTop>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
<div className={Styles.navItems} data-cy="navigation-items">
|
|
203
|
+
{renderItems(mainItems)}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{!!bottom && <SidebarBottom>{bottom}</SidebarBottom>}
|
|
85
207
|
</div>
|
|
86
208
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<SideNavigationGroupItem
|
|
94
|
-
key={item.id}
|
|
95
|
-
item={item}
|
|
96
|
-
barExpanded={effectiveExpanded}
|
|
97
|
-
submenuOpened={!!item.id && !!submenusOpened?.includes(item.id)}
|
|
98
|
-
onSubmenuOpen={onSubmenuOpenChange}
|
|
99
|
-
navigationComponent={navigationComponent}
|
|
100
|
-
onNavigationLinkClick={onNavigationLinkClick}
|
|
101
|
-
touchDevice={touchDevice}
|
|
102
|
-
/>
|
|
103
|
-
) : (
|
|
104
|
-
<InternalSideNavigationLink
|
|
105
|
-
key={item.id}
|
|
106
|
-
id={item.id}
|
|
107
|
-
to={item.to}
|
|
108
|
-
title={
|
|
109
|
-
effectiveExpanded
|
|
110
|
-
? item.title
|
|
111
|
-
: (item.collapsedTitle ?? item.title)
|
|
112
|
-
}
|
|
113
|
-
isActive={item.isActive}
|
|
114
|
-
icon={item.icon}
|
|
115
|
-
iconActive={item.iconActive}
|
|
116
|
-
className={item.className}
|
|
117
|
-
tag={getSubmenuGroupTag(
|
|
118
|
-
item.submenu,
|
|
119
|
-
getCounterTag(item.counter, item.tag)
|
|
120
|
-
)}
|
|
121
|
-
navigationComponent={navigationComponent}
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
209
|
+
{scrollIndicators && (
|
|
210
|
+
<NavigationScrollIndicators
|
|
211
|
+
{...scrollOverflow}
|
|
212
|
+
expanded={effectiveExpanded}
|
|
213
|
+
onReveal={revealNotification}
|
|
214
|
+
/>
|
|
124
215
|
)}
|
|
125
216
|
</div>
|
|
126
|
-
|
|
127
|
-
{!!bottom && <SidebarBottom>{bottom}</SidebarBottom>}
|
|
128
217
|
</ThemeProvider>
|
|
129
218
|
|
|
130
219
|
{!mobile && (
|
|
131
|
-
<
|
|
220
|
+
<ThemeProvider mode="dark" className={Styles.navFooter}>
|
|
132
221
|
<div className={Styles.divider} />
|
|
133
222
|
<div className={Styles.toggleWrapper}>
|
|
134
223
|
<SideNavigationOptionsToggle
|
|
135
|
-
|
|
224
|
+
expanded={barExpanded}
|
|
136
225
|
onExpandedChange={() => onBarExpandChange(!barExpanded)}
|
|
137
226
|
/>
|
|
138
227
|
</div>
|
|
139
|
-
</
|
|
228
|
+
</ThemeProvider>
|
|
140
229
|
)}
|
|
141
230
|
</div>
|
|
142
231
|
</LayoutPlacementContext.Provider>
|
|
@@ -146,43 +235,22 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
146
235
|
|
|
147
236
|
/** Side Navigation options toggle */
|
|
148
237
|
const SideNavigationOptionsToggle: FC<{
|
|
149
|
-
|
|
238
|
+
expanded: boolean;
|
|
150
239
|
onExpandedChange?(expanded: boolean): void;
|
|
151
|
-
}> = ({
|
|
240
|
+
}> = ({ expanded, onExpandedChange }) =>
|
|
152
241
|
withTooltip(
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
<Button
|
|
243
|
+
appearance="ghost"
|
|
244
|
+
size="small"
|
|
245
|
+
icon={expanded ? SvgCollapse : SvgExpand}
|
|
156
246
|
data-cy="navigation-left-options"
|
|
157
247
|
data-pendo="navigation-left-options"
|
|
158
|
-
aria-label={
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
onExpandedChange?.(appearance === 'expand');
|
|
165
|
-
}
|
|
166
|
-
}}
|
|
167
|
-
>
|
|
168
|
-
<div className={Styles.toggleContent}>
|
|
169
|
-
<div className={Styles.toggleIconWrapper}>
|
|
170
|
-
<Icon
|
|
171
|
-
className={Styles.toggleIcon}
|
|
172
|
-
svg={appearance === 'expand' ? SvgExpand : SvgCollapse}
|
|
173
|
-
/>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{appearance === 'collapse-button' && (
|
|
177
|
-
<span className={Styles.toggleText}>Collapse Menu</span>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
</div>,
|
|
181
|
-
appearance === 'expand'
|
|
182
|
-
? 'Expand Menu'
|
|
183
|
-
: appearance === 'collapse'
|
|
184
|
-
? 'Collapse Menu'
|
|
185
|
-
: undefined,
|
|
248
|
+
aria-label={expanded ? 'Collapse Menu' : 'Expand Menu'}
|
|
249
|
+
aria-expanded={expanded}
|
|
250
|
+
className={Styles.toggle}
|
|
251
|
+
onClick={() => onExpandedChange?.(!expanded)}
|
|
252
|
+
/>,
|
|
253
|
+
expanded ? 'Collapse Menu' : 'Expand Menu',
|
|
186
254
|
{ placement: 'right' }
|
|
187
255
|
);
|
|
188
256
|
|
|
@@ -222,6 +290,7 @@ const SideNavigationGroupItem: FC<{
|
|
|
222
290
|
iconActive={item.iconActive}
|
|
223
291
|
className={item.className}
|
|
224
292
|
tag={tag}
|
|
293
|
+
notificationCount={getModuleNotificationCount(item, submenuOpened)}
|
|
225
294
|
submenuOpened={submenuOpened}
|
|
226
295
|
onSubmenuOpen={onSubmenuOpen}
|
|
227
296
|
navigationComponent={navigationComponent}
|
|
@@ -234,7 +303,13 @@ const SideNavigationGroupItem: FC<{
|
|
|
234
303
|
/>
|
|
235
304
|
</InternalSideNavigationGroup>
|
|
236
305
|
) : (
|
|
237
|
-
<Popover
|
|
306
|
+
<Popover
|
|
307
|
+
placement="right-start"
|
|
308
|
+
openOnHover={!touchDevice}
|
|
309
|
+
delay={200}
|
|
310
|
+
noPadding
|
|
311
|
+
disableCaret
|
|
312
|
+
>
|
|
238
313
|
<Popover.Trigger>
|
|
239
314
|
{(triggerProps: any) => (
|
|
240
315
|
<div {...triggerProps}>
|
|
@@ -250,6 +325,7 @@ const SideNavigationGroupItem: FC<{
|
|
|
250
325
|
touchDevice && Styles.pointerEventsNone
|
|
251
326
|
)}
|
|
252
327
|
tag={tag}
|
|
328
|
+
notificationCount={getModuleNotificationCount(item, false)}
|
|
253
329
|
navigationComponent={navigationComponent}
|
|
254
330
|
/>
|
|
255
331
|
</div>
|
|
@@ -291,43 +367,42 @@ const SideNavigationGroupContent: FC<
|
|
|
291
367
|
> = ({ groups, parentId, navigationComponent, onNavigationLinkClick }) => {
|
|
292
368
|
return (
|
|
293
369
|
<Fragment>
|
|
294
|
-
{groups.
|
|
370
|
+
{groups.map((group, index) => {
|
|
295
371
|
if (!group.links.length) {
|
|
296
|
-
return
|
|
372
|
+
return null;
|
|
297
373
|
}
|
|
298
374
|
|
|
299
375
|
const title = group.title?.trim() ?? '';
|
|
300
376
|
/* eslint-disable react/no-array-index-key */
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
377
|
+
return (
|
|
378
|
+
<div key={`:group:${parentId}:${index}`} className={Styles.submenuGroup}>
|
|
379
|
+
{title && (
|
|
380
|
+
<Text
|
|
381
|
+
variant="eyebrow"
|
|
382
|
+
size="small"
|
|
383
|
+
className={Styles.submenuGroupHeader}
|
|
384
|
+
>
|
|
385
|
+
{title}
|
|
386
|
+
</Text>
|
|
387
|
+
)}
|
|
388
|
+
{group.links.map((link, index) => (
|
|
389
|
+
<InternalSideNavigationGroupLink
|
|
390
|
+
key={`:${parentId}:${link.id}:${index}`}
|
|
391
|
+
id={link.id}
|
|
392
|
+
to={link.to}
|
|
393
|
+
title={link.title}
|
|
394
|
+
isActive={link.isActive}
|
|
395
|
+
className={undefined}
|
|
396
|
+
tag={getCounterTag(link.counter, link.tag)}
|
|
397
|
+
parentId={parentId}
|
|
398
|
+
navigationComponent={navigationComponent}
|
|
399
|
+
onLinkClick={onNavigationLinkClick}
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
326
403
|
);
|
|
327
404
|
/* eslint-enable react/no-array-index-key */
|
|
328
|
-
|
|
329
|
-
return out;
|
|
330
|
-
}, [] as ReactElement[])}
|
|
405
|
+
})}
|
|
331
406
|
</Fragment>
|
|
332
407
|
);
|
|
333
408
|
};
|
|
@@ -338,8 +413,9 @@ function SidebarTop({ children }: any) {
|
|
|
338
413
|
});
|
|
339
414
|
return list?.length ? (
|
|
340
415
|
<ThemeProvider mode="dark" className={Styles.navTop} data-cy="navigation-items-top">
|
|
341
|
-
{list}
|
|
342
|
-
|
|
416
|
+
<div className={Styles.navTopItems}>{list}</div>
|
|
417
|
+
{/* Anvil 2 spacing 3 resolves to the design's 12px above and below. */}
|
|
418
|
+
<Divider spacing={3} className={Styles.topDivider} />
|
|
343
419
|
</ThemeProvider>
|
|
344
420
|
) : null;
|
|
345
421
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Button, Dialog, Text } from '@servicetitan/anvil2';
|
|
2
|
+
import IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';
|
|
3
|
+
import { FC, Fragment, useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import * as styles from './navigation-catalog.module.less';
|
|
5
|
+
|
|
6
|
+
interface NavigationCatalogFooterProps {
|
|
7
|
+
/** Present only when the current favorites differ from the host's defaults. */
|
|
8
|
+
onRestore?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const NavigationCatalogFooter: FC<NavigationCatalogFooterProps> = ({ onRestore }) => {
|
|
12
|
+
const [confirming, setConfirming] = useState(false);
|
|
13
|
+
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
14
|
+
const restoreRef = useRef<HTMLButtonElement>(null);
|
|
15
|
+
const returnFocus = useRef(false);
|
|
16
|
+
const confirmationId = useId();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (confirming) {
|
|
20
|
+
cancelRef.current?.focus();
|
|
21
|
+
} else if (returnFocus.current) {
|
|
22
|
+
restoreRef.current?.focus();
|
|
23
|
+
returnFocus.current = false;
|
|
24
|
+
}
|
|
25
|
+
}, [confirming]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!onRestore) {
|
|
29
|
+
setConfirming(false);
|
|
30
|
+
}
|
|
31
|
+
}, [onRestore]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Dialog.Footer className={styles.footer} data-cy="navigation-catalog-footer">
|
|
35
|
+
{confirming && onRestore ? (
|
|
36
|
+
<Fragment>
|
|
37
|
+
<Text size="small" id={confirmationId}>
|
|
38
|
+
This action replaces your current favorites. You can favorite items again
|
|
39
|
+
anytime.
|
|
40
|
+
</Text>
|
|
41
|
+
<div className={styles.footerActions}>
|
|
42
|
+
<Button
|
|
43
|
+
ref={cancelRef}
|
|
44
|
+
appearance="secondary"
|
|
45
|
+
size="xsmall"
|
|
46
|
+
aria-describedby={confirmationId}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
returnFocus.current = true;
|
|
49
|
+
setConfirming(false);
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
Cancel
|
|
53
|
+
</Button>
|
|
54
|
+
<Button
|
|
55
|
+
appearance="primary"
|
|
56
|
+
size="xsmall"
|
|
57
|
+
aria-describedby={confirmationId}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
setConfirming(false);
|
|
60
|
+
onRestore();
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
Restore
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
</Fragment>
|
|
67
|
+
) : (
|
|
68
|
+
<Fragment>
|
|
69
|
+
<Text size="small" subdued>
|
|
70
|
+
Add or remove items to your navigation by clicking the star icon to the
|
|
71
|
+
right
|
|
72
|
+
</Text>
|
|
73
|
+
{onRestore && (
|
|
74
|
+
<Button
|
|
75
|
+
ref={restoreRef}
|
|
76
|
+
appearance="secondary"
|
|
77
|
+
size="xsmall"
|
|
78
|
+
icon={IconUndo}
|
|
79
|
+
onClick={() => setConfirming(true)}
|
|
80
|
+
>
|
|
81
|
+
Restore Default Favorites
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</Fragment>
|
|
85
|
+
)}
|
|
86
|
+
</Dialog.Footer>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
@import (reference) '@servicetitan/tokens/core/tokens.less';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Scoped to `.anvil2.anvil2` so these rules outrank anvil2's own.
|
|
5
|
+
*
|
|
6
|
+
* anvil2 nests every component rule under `.anvil2`, giving it two classes of specificity,
|
|
7
|
+
* which a bare class here loses to. Doubling the scope wins outright and leaves the selectors
|
|
8
|
+
* inside it plain, rather than each one having to defend itself with `!important`.
|
|
9
|
+
*/
|
|
10
|
+
:global(.anvil2.anvil2) {
|
|
11
|
+
.container {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 12px;
|
|
15
|
+
padding: @spacing-1 @spacing-1 @spacing-2;
|
|
16
|
+
|
|
17
|
+
> :last-child:not(:first-child) {
|
|
18
|
+
flex-shrink: 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* The rail stretches its children to its own width, so the button follows rather than sizing to
|
|
24
|
+
* its label. Everything else about the control — surface, border, radius, focus ring, hover — is
|
|
25
|
+
* the anvil2 secondary button's own.
|
|
26
|
+
*/
|
|
27
|
+
.trigger {
|
|
28
|
+
width: 100%;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
flex: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The reference uses the core subdued swatch (#bcbcbd).
|
|
34
|
+
.triggerLabel {
|
|
35
|
+
color: @color-neutral-70;
|
|
36
|
+
line-height: @spacing-2;
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Held back from the label so the chord reads as an aside rather than as part of the name, and
|
|
42
|
+
* kept from wrapping when the rail is at its narrowest expanded width.
|
|
43
|
+
*/
|
|
44
|
+
.shortcut {
|
|
45
|
+
--a2-mod-chip-min-height: @spacing-2;
|
|
46
|
+
--a2-mod-chip-inner-padding-block: 1px;
|
|
47
|
+
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
flex-shrink: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.containerCollapsed {
|
|
53
|
+
// Together with the button's 6px bottom inset, keep 12px before Dashboard's icon box.
|
|
54
|
+
padding: @spacing-0 @spacing-0 6px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.triggerCollapsed {
|
|
58
|
+
width: 60px;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
min-height: 48px;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
padding: 5px @spacing-half;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 2px;
|
|
65
|
+
color: @color-white;
|
|
66
|
+
|
|
67
|
+
[data-anv='icon'] {
|
|
68
|
+
width: 24px;
|
|
69
|
+
height: 24px;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
svg {
|
|
77
|
+
width: 20px;
|
|
78
|
+
height: 20px;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.collapsedLabel {
|
|
83
|
+
font-family: @base-font-family;
|
|
84
|
+
font-size: @typescale-0;
|
|
85
|
+
font-weight: @font-weight-regular;
|
|
86
|
+
line-height: 10px;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.module.less.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const __esModule: true;
|
|
2
|
+
export const collapsedLabel: string;
|
|
3
|
+
export const container: string;
|
|
4
|
+
export const containerCollapsed: string;
|
|
5
|
+
export const shortcut: string;
|
|
6
|
+
export const trigger: string;
|
|
7
|
+
export const triggerCollapsed: string;
|
|
8
|
+
export const triggerLabel: string;
|
|
9
|
+
|