@servicetitan/navigation 14.4.0 → 14.5.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/layout-sidebar.d.ts.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.js +30 -11
- package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
- package/dist/components/titan-layout/layout-sidebar.module.less +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog-trigger.js.map +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.d.ts.map +1 -1
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js +1 -0
- package/dist/components/titan-layout/navigation-catalog/navigation-catalog.js.map +1 -1
- package/dist/utils/__tests__/side-nav.test.d.ts +2 -0
- package/dist/utils/__tests__/side-nav.test.d.ts.map +1 -0
- package/dist/utils/side-nav.d.ts +17 -0
- package/dist/utils/side-nav.d.ts.map +1 -1
- package/dist/utils/side-nav.js +32 -0
- package/dist/utils/side-nav.js.map +1 -1
- package/package.json +2 -2
- package/src/components/titan-layout/layout-sidebar.module.less +1 -1
- package/src/components/titan-layout/layout-sidebar.tsx +34 -10
- package/src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx +1 -1
- package/src/components/titan-layout/navigation-catalog/navigation-catalog.tsx +1 -0
- package/src/utils/__tests__/side-nav.test.ts +77 -0
- package/src/utils/side-nav.ts +42 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout-sidebar.d.ts","sourceRoot":"","sources":["../../../src/components/titan-layout/layout-sidebar.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAY,EAAE,EAAY,YAAY,
|
|
1
|
+
{"version":3,"file":"layout-sidebar.d.ts","sourceRoot":"","sources":["../../../src/components/titan-layout/layout-sidebar.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAY,EAAE,EAAY,YAAY,EAAuC,MAAM,OAAO,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAAyB,MAAM,wBAAwB,CAAC;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAQvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAcrD,MAAM,WAAW,kBAAkB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,YAAY,EAAE,CAAC;IAErB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAEhC,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACjC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACrC,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,mBAAmB,EAAE,EAAE,CAAC,qBAAqB,CAAC,CAAC;IAC/C,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5C,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvD,oBAAoB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5D,qBAAqB,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxE;AAED,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA8KhD,CAAC"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Divider, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
2
|
+
import { Button, Divider, Flex, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
3
3
|
import SvgClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
4
4
|
import SvgCollapseAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_less.svg';
|
|
5
5
|
import SvgExpandAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_more.svg';
|
|
6
6
|
import SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';
|
|
7
7
|
import SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';
|
|
8
8
|
import classNames from 'classnames';
|
|
9
|
-
import { Children, Fragment, isValidElement } from 'react';
|
|
9
|
+
import { Children, Fragment, isValidElement, useCallback, useRef } from 'react';
|
|
10
10
|
import { getModuleNotificationCount } from '../../utils/navigation-notifications';
|
|
11
|
-
import { getCounterTag, getSubmenuGroupTag } from '../../utils/side-nav';
|
|
11
|
+
import { getCounterTag, getModuleActiveMatcher, getModuleDestination, getSubmenuGroupTag } from '../../utils/side-nav';
|
|
12
12
|
import { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';
|
|
13
13
|
import { InternalSideNavigationGroup, InternalSideNavigationGroupLink, InternalSideNavigationLink } from './layout-sidebar-links-internal';
|
|
14
14
|
import * as Styles from './layout-sidebar.module.less';
|
|
@@ -164,7 +164,8 @@ export const LayoutSidebar = ({ className, mobile, touchDevice, barExpanded, sub
|
|
|
164
164
|
/*#__PURE__*/ _jsx("div", {
|
|
165
165
|
className: Styles.divider
|
|
166
166
|
}),
|
|
167
|
-
/*#__PURE__*/ _jsx(
|
|
167
|
+
/*#__PURE__*/ _jsx(Flex, {
|
|
168
|
+
direction: "column",
|
|
168
169
|
className: Styles.toggleWrapper,
|
|
169
170
|
children: /*#__PURE__*/ _jsx(SideNavigationOptionsToggle, {
|
|
170
171
|
expanded: barExpanded,
|
|
@@ -182,6 +183,8 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
182
183
|
/** Side Navigation options toggle */ const SideNavigationOptionsToggle = ({ expanded, onExpandedChange })=>withTooltip(/*#__PURE__*/ _jsx(Button, {
|
|
183
184
|
appearance: "ghost",
|
|
184
185
|
size: "small",
|
|
186
|
+
flexGrow: 1,
|
|
187
|
+
justifyContent: "flex-start",
|
|
185
188
|
icon: expanded ? SvgCollapse : SvgExpand,
|
|
186
189
|
"data-cy": "navigation-left-options",
|
|
187
190
|
"data-pendo": "navigation-left-options",
|
|
@@ -190,18 +193,32 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
190
193
|
className: Styles.toggle,
|
|
191
194
|
onClick: ()=>onExpandedChange === null || onExpandedChange === void 0 ? void 0 : onExpandedChange(!expanded)
|
|
192
195
|
}), expanded ? 'Collapse Menu' : 'Expand Menu', {
|
|
193
|
-
placement: 'right'
|
|
196
|
+
placement: 'right',
|
|
197
|
+
pointerEventsNone: true
|
|
194
198
|
});
|
|
195
199
|
/** Side Navigation menu item */ const SideNavigationGroupItem = ({ item, onSubmenuOpen, onNavigationLinkClick, barExpanded, submenuOpened, navigationComponent, touchDevice })=>{
|
|
196
|
-
var _ref, _ref1;
|
|
200
|
+
var _item_isActive, _ref, _ref1;
|
|
197
201
|
var _item_submenu, _item_submenu1;
|
|
198
202
|
const { sidebar: { styles: { popoverContent } } } = useTitanLayoutContext();
|
|
199
203
|
const tag = getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag));
|
|
204
|
+
const isActive = (_item_isActive = item.isActive) !== null && _item_isActive !== void 0 ? _item_isActive : getModuleActiveMatcher(item.to, item.submenu);
|
|
205
|
+
/*
|
|
206
|
+
* Some destinations (legacy monolith pages) never fire the route-change signal the popover
|
|
207
|
+
* would otherwise rely on to dismiss itself, leaving it stranded open over the loaded page.
|
|
208
|
+
* Closing explicitly on click, rather than waiting on navigation to settle, works everywhere.
|
|
209
|
+
*/ const popoverRef = useRef(null);
|
|
210
|
+
const handleSubmenuLinkClick = useCallback((itemId, submenuItemId)=>{
|
|
211
|
+
var _popoverRef_current;
|
|
212
|
+
(_popoverRef_current = popoverRef.current) === null || _popoverRef_current === void 0 ? void 0 : _popoverRef_current.closePopover();
|
|
213
|
+
onNavigationLinkClick === null || onNavigationLinkClick === void 0 ? void 0 : onNavigationLinkClick(itemId, submenuItemId);
|
|
214
|
+
}, [
|
|
215
|
+
onNavigationLinkClick
|
|
216
|
+
]);
|
|
200
217
|
return barExpanded ? /*#__PURE__*/ _jsx(InternalSideNavigationGroup, {
|
|
201
218
|
id: item.id,
|
|
202
|
-
to: item.to,
|
|
219
|
+
to: getModuleDestination(item.to, item.submenu),
|
|
203
220
|
title: item.title,
|
|
204
|
-
isActive:
|
|
221
|
+
isActive: isActive,
|
|
205
222
|
icon: item.icon,
|
|
206
223
|
iconActive: item.iconActive,
|
|
207
224
|
className: item.className,
|
|
@@ -217,6 +234,7 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
217
234
|
onNavigationLinkClick: onNavigationLinkClick
|
|
218
235
|
})
|
|
219
236
|
}) : /*#__PURE__*/ _jsxs(Popover, {
|
|
237
|
+
ref: popoverRef,
|
|
220
238
|
placement: "right-start",
|
|
221
239
|
openOnHover: !touchDevice,
|
|
222
240
|
delay: 200,
|
|
@@ -230,9 +248,9 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
230
248
|
...triggerProps,
|
|
231
249
|
children: /*#__PURE__*/ _jsx(InternalSideNavigationLink, {
|
|
232
250
|
id: item.id,
|
|
233
|
-
to: item.to,
|
|
251
|
+
to: getModuleDestination(item.to, item.submenu),
|
|
234
252
|
title: (_item_collapsedTitle = item.collapsedTitle) !== null && _item_collapsedTitle !== void 0 ? _item_collapsedTitle : item.title,
|
|
235
|
-
isActive:
|
|
253
|
+
isActive: isActive,
|
|
236
254
|
icon: item.icon,
|
|
237
255
|
iconActive: item.iconActive,
|
|
238
256
|
className: classNames(item.className, touchDevice && Styles.pointerEventsNone),
|
|
@@ -265,7 +283,8 @@ LayoutSidebar.displayName = 'LayoutSidebar';
|
|
|
265
283
|
children: /*#__PURE__*/ _jsx(SideNavigationGroupContent, {
|
|
266
284
|
parentId: item.id,
|
|
267
285
|
groups: (_ref1 = (_item_submenu1 = item.submenu) === null || _item_submenu1 === void 0 ? void 0 : _item_submenu1.groups) !== null && _ref1 !== void 0 ? _ref1 : [],
|
|
268
|
-
navigationComponent: navigationComponent
|
|
286
|
+
navigationComponent: navigationComponent,
|
|
287
|
+
onNavigationLinkClick: handleSubmenuLinkClick
|
|
269
288
|
})
|
|
270
289
|
})
|
|
271
290
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/titan-layout/layout-sidebar.tsx"],"sourcesContent":["import { Button, Divider, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';\nimport SvgClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';\nimport SvgCollapseAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_less.svg';\nimport SvgExpandAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_more.svg';\nimport SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';\nimport SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';\nimport classNames from 'classnames';\nimport { Children, FC, Fragment, ReactElement, isValidElement } from 'react';\nimport { NavigationItemData, NavigationSubmenuData } from '../../utils/navigation';\nimport { NavLinkComponentProps } from '../../utils/navigation-context';\nimport { getModuleNotificationCount } from '../../utils/navigation-notifications';\nimport { getCounterTag, getSubmenuGroupTag } from '../../utils/side-nav';\nimport { PinnedNavigationConfig } from './interface';\nimport { NavigationComponentProps } from './interface-internal';\nimport { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';\nimport {\n InternalSideNavigationGroup,\n InternalSideNavigationGroupLink,\n InternalSideNavigationLink,\n} from './layout-sidebar-links-internal';\nimport * as Styles from './layout-sidebar.module.less';\nimport { NavigationCatalogTrigger } from './navigation-catalog';\nimport { NavigationScrollIndicators } from './navigation-scroll-indicators';\nimport { useNavigationScroll } from './use-navigation-scroll';\nimport { withTooltip } from './with-tooltip';\n\nexport interface LayoutSidebarProps {\n className?: string;\n top?: ReactElement[];\n\n /**\n * Destinations rendered above the divider, ahead of `top`.\n *\n * Separate from `top` rather than widening it: that one takes elements a host builds itself\n * and renders them as-is, which is what a bespoke control needs. This takes navigation items\n * and puts them through the same renderer as `mainItems`, so one with pages beneath it is an\n * expandable group instead of being flattened to a bare link.\n */\n topItems?: NavigationItemData[];\n\n /** Whether the rail shows the scroll and overflow-notification indicators. */\n scrollIndicators?: boolean;\n bottom?: ReactElement;\n mainItems?: NavigationItemData[];\n pinnedNavigation?: PinnedNavigationConfig;\n barExpanded: boolean;\n submenusOpened: string[] | undefined;\n drawerOpened: boolean;\n mobile: boolean;\n touchDevice: boolean;\n navigationComponent: FC<NavLinkComponentProps>;\n onBarExpandChange(expanded: boolean): void;\n onDrawerOpenChange(expanded: boolean): void;\n onSubmenuOpenChange(id: string, opened: boolean): void;\n onSubmenusOpenChange?(ids: string[], opened: boolean): void;\n onNavigationLinkClick?(itemId: string, submenuItemId?: string): void;\n}\n\nexport const LayoutSidebar: FC<LayoutSidebarProps> = ({\n className,\n mobile,\n touchDevice,\n barExpanded,\n submenusOpened,\n drawerOpened,\n onBarExpandChange,\n onSubmenuOpenChange,\n onSubmenusOpenChange,\n onDrawerOpenChange,\n onNavigationLinkClick,\n mainItems,\n pinnedNavigation,\n top,\n topItems,\n scrollIndicators,\n bottom,\n navigationComponent,\n}) => {\n const { regionRef, scrollRef, revealNotification, ...scrollOverflow } =\n useNavigationScroll(scrollIndicators);\n const closeDrawer = () => {\n if (mobile) {\n onDrawerOpenChange?.(false);\n }\n };\n\n const effectiveExpanded = mobile ? drawerOpened : barExpanded;\n /*\n * Both regions, because a destination above the divider expands exactly like one below it.\n * Counting only `mainItems` would let Expand All skip those groups — and hide the control\n * entirely on a rail whose only expandable modules sit up there.\n */\n const expandableIds = [...(topItems ?? []), ...(mainItems ?? [])]\n .filter(item => item.submenu?.groups.some(group => group.links.length > 0))\n .map(item => item.id);\n const hasOpenSubmenus = expandableIds.some(id => submenusOpened?.includes(id));\n const toggleAllLabel = hasOpenSubmenus ? 'Collapse All' : 'Expand All';\n const toggleAll =\n effectiveExpanded && expandableIds.length > 0 && onSubmenusOpenChange\n ? withTooltip(\n <Button\n appearance=\"secondary\"\n size=\"small\"\n icon={hasOpenSubmenus ? SvgCollapseAll : SvgExpandAll}\n aria-label={toggleAllLabel}\n aria-expanded={hasOpenSubmenus}\n data-cy=\"navigation-toggle-all\"\n data-pendo=\"navigation-toggle-all\"\n onClick={event => {\n event.stopPropagation();\n onSubmenusOpenChange(expandableIds, !hasOpenSubmenus);\n }}\n />,\n toggleAllLabel,\n { placement: 'top' }\n )\n : undefined;\n\n const scrollHeader = !mobile && !barExpanded;\n const navigationHeader = (\n <div className={Styles.navHeader}>\n {mobile && (\n <div className={Styles.navCloseWrapper}>\n <div className={Styles.navClose} onClick={() => onBarExpandChange(false)}>\n <Icon svg={SvgClose} size=\"large\" />\n </div>\n </div>\n )}\n {pinnedNavigation && (\n <NavigationCatalogTrigger\n expanded={effectiveExpanded}\n {...pinnedNavigation}\n trailingAction={toggleAll}\n />\n )}\n {!pinnedNavigation && toggleAll && <div className={Styles.navTools}>{toggleAll}</div>}\n </div>\n );\n\n /*\n * Shared by the region above the divider and the list below it, so a destination renders the\n * same way wherever the host puts it. Two copies of this branch would drift.\n */\n const renderItems = (items: NavigationItemData[] | undefined) =>\n items?.map(item =>\n item.submenu ? (\n <SideNavigationGroupItem\n key={item.id}\n item={item}\n barExpanded={effectiveExpanded}\n submenuOpened={!!item.id && !!submenusOpened?.includes(item.id)}\n onSubmenuOpen={onSubmenuOpenChange}\n navigationComponent={navigationComponent}\n onNavigationLinkClick={onNavigationLinkClick}\n touchDevice={touchDevice}\n />\n ) : (\n <InternalSideNavigationLink\n key={item.id}\n id={item.id}\n to={item.to}\n title={effectiveExpanded ? item.title : (item.collapsedTitle ?? item.title)}\n isActive={item.isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={item.className}\n tag={getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag))}\n navigationComponent={navigationComponent}\n />\n )\n );\n\n return (\n <LayoutPlacementContext.Provider value=\"side\">\n {mobile && drawerOpened && (\n <div className={Styles.navDrawerBackdrop} onClick={closeDrawer} />\n )}\n <div\n className={classNames(\n Styles.nav,\n mobile && Styles.navDrawer,\n mobile && drawerOpened && Styles.navDrawerOpened,\n !mobile && (barExpanded ? Styles.navWide : Styles.navSlim),\n className\n )}\n data-cy=\"side-navigation\"\n onClick={closeDrawer}\n >\n <ThemeProvider mode=\"dark\" className={Styles.navContent}>\n {!scrollHeader && navigationHeader}\n <div ref={regionRef} className={Styles.navScrollRegion}>\n <div ref={scrollRef} className={Styles.navMain} data-cy=\"navigation-scroll\">\n <div>\n {scrollHeader && navigationHeader}\n {(!!top?.length || !!topItems?.length) && (\n <SidebarTop>\n {renderItems(topItems)}\n {top}\n </SidebarTop>\n )}\n\n <div className={Styles.navItems} data-cy=\"navigation-items\">\n {renderItems(mainItems)}\n </div>\n\n {!!bottom && <SidebarBottom>{bottom}</SidebarBottom>}\n </div>\n </div>\n {scrollIndicators && (\n <NavigationScrollIndicators\n {...scrollOverflow}\n expanded={effectiveExpanded}\n onReveal={revealNotification}\n />\n )}\n </div>\n </ThemeProvider>\n\n {!mobile && (\n <ThemeProvider mode=\"dark\" className={Styles.navFooter}>\n <div className={Styles.divider} />\n <div className={Styles.toggleWrapper}>\n <SideNavigationOptionsToggle\n expanded={barExpanded}\n onExpandedChange={() => onBarExpandChange(!barExpanded)}\n />\n </div>\n </ThemeProvider>\n )}\n </div>\n </LayoutPlacementContext.Provider>\n );\n};\nLayoutSidebar.displayName = 'LayoutSidebar';\n\n/** Side Navigation options toggle */\nconst SideNavigationOptionsToggle: FC<{\n expanded: boolean;\n onExpandedChange?(expanded: boolean): void;\n}> = ({ expanded, onExpandedChange }) =>\n withTooltip(\n <Button\n appearance=\"ghost\"\n size=\"small\"\n icon={expanded ? SvgCollapse : SvgExpand}\n data-cy=\"navigation-left-options\"\n data-pendo=\"navigation-left-options\"\n aria-label={expanded ? 'Collapse Menu' : 'Expand Menu'}\n aria-expanded={expanded}\n className={Styles.toggle}\n onClick={() => onExpandedChange?.(!expanded)}\n />,\n expanded ? 'Collapse Menu' : 'Expand Menu',\n { placement: 'right' }\n );\n\n/** Side Navigation menu item */\nconst SideNavigationGroupItem: FC<{\n item: NavigationItemData;\n navigationComponent: FC<NavLinkComponentProps>;\n touchDevice: boolean;\n barExpanded: boolean;\n submenuOpened: boolean;\n onSubmenuOpen: (id: string, opened: boolean) => void;\n onNavigationLinkClick?: (itemId: string, submenuItemId?: string) => void;\n}> = ({\n item,\n onSubmenuOpen,\n onNavigationLinkClick,\n barExpanded,\n submenuOpened,\n navigationComponent,\n touchDevice,\n}) => {\n const {\n sidebar: {\n styles: { popoverContent },\n },\n } = useTitanLayoutContext();\n\n const tag = getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag));\n\n return barExpanded ? (\n <InternalSideNavigationGroup\n id={item.id}\n to={item.to}\n title={item.title}\n isActive={item.isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={item.className}\n tag={tag}\n notificationCount={getModuleNotificationCount(item, submenuOpened)}\n submenuOpened={submenuOpened}\n onSubmenuOpen={onSubmenuOpen}\n navigationComponent={navigationComponent}\n >\n <SideNavigationGroupContent\n parentId={item.id}\n groups={item.submenu?.groups ?? []}\n navigationComponent={navigationComponent}\n onNavigationLinkClick={onNavigationLinkClick}\n />\n </InternalSideNavigationGroup>\n ) : (\n <Popover\n placement=\"right-start\"\n openOnHover={!touchDevice}\n delay={200}\n noPadding\n disableCaret\n >\n <Popover.Trigger>\n {(triggerProps: any) => (\n <div {...triggerProps}>\n <InternalSideNavigationLink\n id={item.id}\n to={item.to}\n title={item.collapsedTitle ?? item.title}\n isActive={item.isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={classNames(\n item.className,\n touchDevice && Styles.pointerEventsNone\n )}\n tag={tag}\n notificationCount={getModuleNotificationCount(item, false)}\n navigationComponent={navigationComponent}\n />\n </div>\n )}\n </Popover.Trigger>\n <Popover.Content\n style={popoverContent}\n className={classNames('z-global-nav-i', Styles.submenuPopoverContent)}\n data-cy=\"navigation-item-popover-content\"\n >\n <div className={Styles.submenuPopover}>\n <Text\n variant=\"headline\"\n el=\"h4\"\n size=\"small\"\n className={Styles.submenuPopoverTitle}\n >\n {item.title}\n </Text>\n <Divider className={Styles.submenuPopoverDivider} />\n <div className={Styles.submenuPopoverBody}>\n <SideNavigationGroupContent\n parentId={item.id}\n groups={item.submenu?.groups ?? []}\n navigationComponent={navigationComponent}\n />\n </div>\n </div>\n </Popover.Content>\n </Popover>\n );\n};\nconst SideNavigationGroupContent: FC<\n NavigationSubmenuData &\n NavigationComponentProps & {\n parentId: string;\n onNavigationLinkClick?: (itemId: string, submenuItemId?: string) => void;\n }\n> = ({ groups, parentId, navigationComponent, onNavigationLinkClick }) => {\n return (\n <Fragment>\n {groups.map((group, index) => {\n if (!group.links.length) {\n return null;\n }\n\n const title = group.title?.trim() ?? '';\n /* eslint-disable react/no-array-index-key */\n return (\n <div key={`:group:${parentId}:${index}`} className={Styles.submenuGroup}>\n {title && (\n <Text\n variant=\"eyebrow\"\n size=\"small\"\n className={Styles.submenuGroupHeader}\n >\n {title}\n </Text>\n )}\n {group.links.map((link, index) => (\n <InternalSideNavigationGroupLink\n key={`:${parentId}:${link.id}:${index}`}\n id={link.id}\n to={link.to}\n title={link.title}\n isActive={link.isActive}\n className={undefined}\n tag={getCounterTag(link.counter, link.tag)}\n parentId={parentId}\n navigationComponent={navigationComponent}\n onLinkClick={onNavigationLinkClick}\n />\n ))}\n </div>\n );\n /* eslint-enable react/no-array-index-key */\n })}\n </Fragment>\n );\n};\n\nfunction SidebarTop({ children }: any) {\n const list = Children.map(children, child => {\n return child && isValidElement(child) ? child : null;\n });\n return list?.length ? (\n <ThemeProvider mode=\"dark\" className={Styles.navTop} data-cy=\"navigation-items-top\">\n <div className={Styles.navTopItems}>{list}</div>\n {/* Anvil 2 spacing 3 resolves to the design's 12px above and below. */}\n <Divider spacing={3} className={Styles.topDivider} />\n </ThemeProvider>\n ) : null;\n}\n\nfunction SidebarBottom({ children }: any) {\n return (\n <ThemeProvider mode=\"dark\" className={Styles.navBottom} data-cy=\"navigation-items-bottom\">\n <div className={Styles.divider} />\n {children}\n </ThemeProvider>\n );\n}\n"],"names":["Button","Divider","Icon","Popover","Text","ThemeProvider","SvgClose","SvgCollapseAll","SvgExpandAll","SvgCollapse","SvgExpand","classNames","Children","Fragment","isValidElement","getModuleNotificationCount","getCounterTag","getSubmenuGroupTag","LayoutPlacementContext","useTitanLayoutContext","InternalSideNavigationGroup","InternalSideNavigationGroupLink","InternalSideNavigationLink","Styles","NavigationCatalogTrigger","NavigationScrollIndicators","useNavigationScroll","withTooltip","LayoutSidebar","className","mobile","touchDevice","barExpanded","submenusOpened","drawerOpened","onBarExpandChange","onSubmenuOpenChange","onSubmenusOpenChange","onDrawerOpenChange","onNavigationLinkClick","mainItems","pinnedNavigation","top","topItems","scrollIndicators","bottom","navigationComponent","regionRef","scrollRef","revealNotification","scrollOverflow","closeDrawer","effectiveExpanded","expandableIds","filter","item","submenu","groups","some","group","links","length","map","id","hasOpenSubmenus","includes","toggleAllLabel","toggleAll","appearance","size","icon","aria-label","aria-expanded","data-cy","data-pendo","onClick","event","stopPropagation","placement","undefined","scrollHeader","navigationHeader","div","navHeader","navCloseWrapper","navClose","svg","expanded","trailingAction","navTools","renderItems","items","SideNavigationGroupItem","submenuOpened","onSubmenuOpen","to","title","collapsedTitle","isActive","iconActive","tag","counter","Provider","value","navDrawerBackdrop","nav","navDrawer","navDrawerOpened","navWide","navSlim","mode","navContent","ref","navScrollRegion","navMain","SidebarTop","navItems","SidebarBottom","onReveal","navFooter","divider","toggleWrapper","SideNavigationOptionsToggle","onExpandedChange","displayName","toggle","sidebar","styles","popoverContent","notificationCount","SideNavigationGroupContent","parentId","openOnHover","delay","noPadding","disableCaret","Trigger","triggerProps","pointerEventsNone","Content","style","submenuPopoverContent","submenuPopover","variant","el","submenuPopoverTitle","submenuPopoverDivider","submenuPopoverBody","index","trim","submenuGroup","submenuGroupHeader","link","onLinkClick","children","list","child","navTop","navTopItems","spacing","topDivider","navBottom"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/titan-layout/layout-sidebar.tsx"],"sourcesContent":["import { Button, Divider, Flex, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';\nimport SvgClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';\nimport SvgCollapseAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_less.svg';\nimport SvgExpandAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_more.svg';\nimport SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';\nimport SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';\nimport classNames from 'classnames';\nimport { Children, FC, Fragment, ReactElement, isValidElement, useCallback, useRef } from 'react';\nimport { NavigationItemData, NavigationSubmenuData } from '../../utils/navigation';\nimport { NavLinkComponentProps } from '../../utils/navigation-context';\nimport { getModuleNotificationCount } from '../../utils/navigation-notifications';\nimport {\n getCounterTag,\n getModuleActiveMatcher,\n getModuleDestination,\n getSubmenuGroupTag,\n} from '../../utils/side-nav';\nimport { PinnedNavigationConfig } from './interface';\nimport { NavigationComponentProps } from './interface-internal';\nimport { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';\nimport {\n InternalSideNavigationGroup,\n InternalSideNavigationGroupLink,\n InternalSideNavigationLink,\n} from './layout-sidebar-links-internal';\nimport * as Styles from './layout-sidebar.module.less';\nimport { NavigationCatalogTrigger } from './navigation-catalog';\nimport { NavigationScrollIndicators } from './navigation-scroll-indicators';\nimport { useNavigationScroll } from './use-navigation-scroll';\nimport { withTooltip } from './with-tooltip';\n\nexport interface LayoutSidebarProps {\n className?: string;\n top?: ReactElement[];\n\n /**\n * Destinations rendered above the divider, ahead of `top`.\n *\n * Separate from `top` rather than widening it: that one takes elements a host builds itself\n * and renders them as-is, which is what a bespoke control needs. This takes navigation items\n * and puts them through the same renderer as `mainItems`, so one with pages beneath it is an\n * expandable group instead of being flattened to a bare link.\n */\n topItems?: NavigationItemData[];\n\n /** Whether the rail shows the scroll and overflow-notification indicators. */\n scrollIndicators?: boolean;\n bottom?: ReactElement;\n mainItems?: NavigationItemData[];\n pinnedNavigation?: PinnedNavigationConfig;\n barExpanded: boolean;\n submenusOpened: string[] | undefined;\n drawerOpened: boolean;\n mobile: boolean;\n touchDevice: boolean;\n navigationComponent: FC<NavLinkComponentProps>;\n onBarExpandChange(expanded: boolean): void;\n onDrawerOpenChange(expanded: boolean): void;\n onSubmenuOpenChange(id: string, opened: boolean): void;\n onSubmenusOpenChange?(ids: string[], opened: boolean): void;\n onNavigationLinkClick?(itemId: string, submenuItemId?: string): void;\n}\n\nexport const LayoutSidebar: FC<LayoutSidebarProps> = ({\n className,\n mobile,\n touchDevice,\n barExpanded,\n submenusOpened,\n drawerOpened,\n onBarExpandChange,\n onSubmenuOpenChange,\n onSubmenusOpenChange,\n onDrawerOpenChange,\n onNavigationLinkClick,\n mainItems,\n pinnedNavigation,\n top,\n topItems,\n scrollIndicators,\n bottom,\n navigationComponent,\n}) => {\n const { regionRef, scrollRef, revealNotification, ...scrollOverflow } =\n useNavigationScroll(scrollIndicators);\n const closeDrawer = () => {\n if (mobile) {\n onDrawerOpenChange?.(false);\n }\n };\n\n const effectiveExpanded = mobile ? drawerOpened : barExpanded;\n /*\n * Both regions, because a destination above the divider expands exactly like one below it.\n * Counting only `mainItems` would let Expand All skip those groups — and hide the control\n * entirely on a rail whose only expandable modules sit up there.\n */\n const expandableIds = [...(topItems ?? []), ...(mainItems ?? [])]\n .filter(item => item.submenu?.groups.some(group => group.links.length > 0))\n .map(item => item.id);\n const hasOpenSubmenus = expandableIds.some(id => submenusOpened?.includes(id));\n const toggleAllLabel = hasOpenSubmenus ? 'Collapse All' : 'Expand All';\n const toggleAll =\n effectiveExpanded && expandableIds.length > 0 && onSubmenusOpenChange\n ? withTooltip(\n <Button\n appearance=\"secondary\"\n size=\"small\"\n icon={hasOpenSubmenus ? SvgCollapseAll : SvgExpandAll}\n aria-label={toggleAllLabel}\n aria-expanded={hasOpenSubmenus}\n data-cy=\"navigation-toggle-all\"\n data-pendo=\"navigation-toggle-all\"\n onClick={event => {\n event.stopPropagation();\n onSubmenusOpenChange(expandableIds, !hasOpenSubmenus);\n }}\n />,\n toggleAllLabel,\n { placement: 'top' }\n )\n : undefined;\n\n const scrollHeader = !mobile && !barExpanded;\n const navigationHeader = (\n <div className={Styles.navHeader}>\n {mobile && (\n <div className={Styles.navCloseWrapper}>\n <div className={Styles.navClose} onClick={() => onBarExpandChange(false)}>\n <Icon svg={SvgClose} size=\"large\" />\n </div>\n </div>\n )}\n {pinnedNavigation && (\n <NavigationCatalogTrigger\n expanded={effectiveExpanded}\n {...pinnedNavigation}\n trailingAction={toggleAll}\n />\n )}\n {!pinnedNavigation && toggleAll && <div className={Styles.navTools}>{toggleAll}</div>}\n </div>\n );\n\n /*\n * Shared by the region above the divider and the list below it, so a destination renders the\n * same way wherever the host puts it. Two copies of this branch would drift.\n */\n const renderItems = (items: NavigationItemData[] | undefined) =>\n items?.map(item =>\n item.submenu ? (\n <SideNavigationGroupItem\n key={item.id}\n item={item}\n barExpanded={effectiveExpanded}\n submenuOpened={!!item.id && !!submenusOpened?.includes(item.id)}\n onSubmenuOpen={onSubmenuOpenChange}\n navigationComponent={navigationComponent}\n onNavigationLinkClick={onNavigationLinkClick}\n touchDevice={touchDevice}\n />\n ) : (\n <InternalSideNavigationLink\n key={item.id}\n id={item.id}\n to={item.to}\n title={effectiveExpanded ? item.title : (item.collapsedTitle ?? item.title)}\n isActive={item.isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={item.className}\n tag={getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag))}\n navigationComponent={navigationComponent}\n />\n )\n );\n\n return (\n <LayoutPlacementContext.Provider value=\"side\">\n {mobile && drawerOpened && (\n <div className={Styles.navDrawerBackdrop} onClick={closeDrawer} />\n )}\n <div\n className={classNames(\n Styles.nav,\n mobile && Styles.navDrawer,\n mobile && drawerOpened && Styles.navDrawerOpened,\n !mobile && (barExpanded ? Styles.navWide : Styles.navSlim),\n className\n )}\n data-cy=\"side-navigation\"\n onClick={closeDrawer}\n >\n <ThemeProvider mode=\"dark\" className={Styles.navContent}>\n {!scrollHeader && navigationHeader}\n <div ref={regionRef} className={Styles.navScrollRegion}>\n <div ref={scrollRef} className={Styles.navMain} data-cy=\"navigation-scroll\">\n <div>\n {scrollHeader && navigationHeader}\n {(!!top?.length || !!topItems?.length) && (\n <SidebarTop>\n {renderItems(topItems)}\n {top}\n </SidebarTop>\n )}\n\n <div className={Styles.navItems} data-cy=\"navigation-items\">\n {renderItems(mainItems)}\n </div>\n\n {!!bottom && <SidebarBottom>{bottom}</SidebarBottom>}\n </div>\n </div>\n {scrollIndicators && (\n <NavigationScrollIndicators\n {...scrollOverflow}\n expanded={effectiveExpanded}\n onReveal={revealNotification}\n />\n )}\n </div>\n </ThemeProvider>\n\n {!mobile && (\n <ThemeProvider mode=\"dark\" className={Styles.navFooter}>\n <div className={Styles.divider} />\n <Flex direction=\"column\" className={Styles.toggleWrapper}>\n <SideNavigationOptionsToggle\n expanded={barExpanded}\n onExpandedChange={() => onBarExpandChange(!barExpanded)}\n />\n </Flex>\n </ThemeProvider>\n )}\n </div>\n </LayoutPlacementContext.Provider>\n );\n};\nLayoutSidebar.displayName = 'LayoutSidebar';\n\n/** Side Navigation options toggle */\nconst SideNavigationOptionsToggle: FC<{\n expanded: boolean;\n onExpandedChange?(expanded: boolean): void;\n}> = ({ expanded, onExpandedChange }) =>\n withTooltip(\n <Button\n appearance=\"ghost\"\n size=\"small\"\n flexGrow={1}\n justifyContent=\"flex-start\"\n icon={expanded ? SvgCollapse : SvgExpand}\n data-cy=\"navigation-left-options\"\n data-pendo=\"navigation-left-options\"\n aria-label={expanded ? 'Collapse Menu' : 'Expand Menu'}\n aria-expanded={expanded}\n className={Styles.toggle}\n onClick={() => onExpandedChange?.(!expanded)}\n />,\n expanded ? 'Collapse Menu' : 'Expand Menu',\n { placement: 'right', pointerEventsNone: true }\n );\n\n/** Side Navigation menu item */\nconst SideNavigationGroupItem: FC<{\n item: NavigationItemData;\n navigationComponent: FC<NavLinkComponentProps>;\n touchDevice: boolean;\n barExpanded: boolean;\n submenuOpened: boolean;\n onSubmenuOpen: (id: string, opened: boolean) => void;\n onNavigationLinkClick?: (itemId: string, submenuItemId?: string) => void;\n}> = ({\n item,\n onSubmenuOpen,\n onNavigationLinkClick,\n barExpanded,\n submenuOpened,\n navigationComponent,\n touchDevice,\n}) => {\n const {\n sidebar: {\n styles: { popoverContent },\n },\n } = useTitanLayoutContext();\n\n const tag = getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag));\n const isActive = item.isActive ?? getModuleActiveMatcher(item.to, item.submenu);\n\n /*\n * Some destinations (legacy monolith pages) never fire the route-change signal the popover\n * would otherwise rely on to dismiss itself, leaving it stranded open over the loaded page.\n * Closing explicitly on click, rather than waiting on navigation to settle, works everywhere.\n */\n const popoverRef = useRef<{ closePopover: () => void }>(null);\n const handleSubmenuLinkClick = useCallback(\n (itemId: string, submenuItemId?: string) => {\n popoverRef.current?.closePopover();\n onNavigationLinkClick?.(itemId, submenuItemId);\n },\n [onNavigationLinkClick]\n );\n\n return barExpanded ? (\n <InternalSideNavigationGroup\n id={item.id}\n to={getModuleDestination(item.to, item.submenu)}\n title={item.title}\n isActive={isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={item.className}\n tag={tag}\n notificationCount={getModuleNotificationCount(item, submenuOpened)}\n submenuOpened={submenuOpened}\n onSubmenuOpen={onSubmenuOpen}\n navigationComponent={navigationComponent}\n >\n <SideNavigationGroupContent\n parentId={item.id}\n groups={item.submenu?.groups ?? []}\n navigationComponent={navigationComponent}\n onNavigationLinkClick={onNavigationLinkClick}\n />\n </InternalSideNavigationGroup>\n ) : (\n <Popover\n ref={popoverRef}\n placement=\"right-start\"\n openOnHover={!touchDevice}\n delay={200}\n noPadding\n disableCaret\n >\n <Popover.Trigger>\n {(triggerProps: any) => (\n <div {...triggerProps}>\n <InternalSideNavigationLink\n id={item.id}\n to={getModuleDestination(item.to, item.submenu)}\n title={item.collapsedTitle ?? item.title}\n isActive={isActive}\n icon={item.icon}\n iconActive={item.iconActive}\n className={classNames(\n item.className,\n touchDevice && Styles.pointerEventsNone\n )}\n tag={tag}\n notificationCount={getModuleNotificationCount(item, false)}\n navigationComponent={navigationComponent}\n />\n </div>\n )}\n </Popover.Trigger>\n <Popover.Content\n style={popoverContent}\n className={classNames('z-global-nav-i', Styles.submenuPopoverContent)}\n data-cy=\"navigation-item-popover-content\"\n >\n <div className={Styles.submenuPopover}>\n <Text\n variant=\"headline\"\n el=\"h4\"\n size=\"small\"\n className={Styles.submenuPopoverTitle}\n >\n {item.title}\n </Text>\n <Divider className={Styles.submenuPopoverDivider} />\n <div className={Styles.submenuPopoverBody}>\n <SideNavigationGroupContent\n parentId={item.id}\n groups={item.submenu?.groups ?? []}\n navigationComponent={navigationComponent}\n onNavigationLinkClick={handleSubmenuLinkClick}\n />\n </div>\n </div>\n </Popover.Content>\n </Popover>\n );\n};\nconst SideNavigationGroupContent: FC<\n NavigationSubmenuData &\n NavigationComponentProps & {\n parentId: string;\n onNavigationLinkClick?: (itemId: string, submenuItemId?: string) => void;\n }\n> = ({ groups, parentId, navigationComponent, onNavigationLinkClick }) => {\n return (\n <Fragment>\n {groups.map((group, index) => {\n if (!group.links.length) {\n return null;\n }\n\n const title = group.title?.trim() ?? '';\n /* eslint-disable react/no-array-index-key */\n return (\n <div key={`:group:${parentId}:${index}`} className={Styles.submenuGroup}>\n {title && (\n <Text\n variant=\"eyebrow\"\n size=\"small\"\n className={Styles.submenuGroupHeader}\n >\n {title}\n </Text>\n )}\n {group.links.map((link, index) => (\n <InternalSideNavigationGroupLink\n key={`:${parentId}:${link.id}:${index}`}\n id={link.id}\n to={link.to}\n title={link.title}\n isActive={link.isActive}\n className={undefined}\n tag={getCounterTag(link.counter, link.tag)}\n parentId={parentId}\n navigationComponent={navigationComponent}\n onLinkClick={onNavigationLinkClick}\n />\n ))}\n </div>\n );\n /* eslint-enable react/no-array-index-key */\n })}\n </Fragment>\n );\n};\n\nfunction SidebarTop({ children }: any) {\n const list = Children.map(children, child => {\n return child && isValidElement(child) ? child : null;\n });\n return list?.length ? (\n <ThemeProvider mode=\"dark\" className={Styles.navTop} data-cy=\"navigation-items-top\">\n <div className={Styles.navTopItems}>{list}</div>\n {/* Anvil 2 spacing 3 resolves to the design's 12px above and below. */}\n <Divider spacing={3} className={Styles.topDivider} />\n </ThemeProvider>\n ) : null;\n}\n\nfunction SidebarBottom({ children }: any) {\n return (\n <ThemeProvider mode=\"dark\" className={Styles.navBottom} data-cy=\"navigation-items-bottom\">\n <div className={Styles.divider} />\n {children}\n </ThemeProvider>\n );\n}\n"],"names":["Button","Divider","Flex","Icon","Popover","Text","ThemeProvider","SvgClose","SvgCollapseAll","SvgExpandAll","SvgCollapse","SvgExpand","classNames","Children","Fragment","isValidElement","useCallback","useRef","getModuleNotificationCount","getCounterTag","getModuleActiveMatcher","getModuleDestination","getSubmenuGroupTag","LayoutPlacementContext","useTitanLayoutContext","InternalSideNavigationGroup","InternalSideNavigationGroupLink","InternalSideNavigationLink","Styles","NavigationCatalogTrigger","NavigationScrollIndicators","useNavigationScroll","withTooltip","LayoutSidebar","className","mobile","touchDevice","barExpanded","submenusOpened","drawerOpened","onBarExpandChange","onSubmenuOpenChange","onSubmenusOpenChange","onDrawerOpenChange","onNavigationLinkClick","mainItems","pinnedNavigation","top","topItems","scrollIndicators","bottom","navigationComponent","regionRef","scrollRef","revealNotification","scrollOverflow","closeDrawer","effectiveExpanded","expandableIds","filter","item","submenu","groups","some","group","links","length","map","id","hasOpenSubmenus","includes","toggleAllLabel","toggleAll","appearance","size","icon","aria-label","aria-expanded","data-cy","data-pendo","onClick","event","stopPropagation","placement","undefined","scrollHeader","navigationHeader","div","navHeader","navCloseWrapper","navClose","svg","expanded","trailingAction","navTools","renderItems","items","SideNavigationGroupItem","submenuOpened","onSubmenuOpen","to","title","collapsedTitle","isActive","iconActive","tag","counter","Provider","value","navDrawerBackdrop","nav","navDrawer","navDrawerOpened","navWide","navSlim","mode","navContent","ref","navScrollRegion","navMain","SidebarTop","navItems","SidebarBottom","onReveal","navFooter","divider","direction","toggleWrapper","SideNavigationOptionsToggle","onExpandedChange","displayName","flexGrow","justifyContent","toggle","pointerEventsNone","sidebar","styles","popoverContent","popoverRef","handleSubmenuLinkClick","itemId","submenuItemId","current","closePopover","notificationCount","SideNavigationGroupContent","parentId","openOnHover","delay","noPadding","disableCaret","Trigger","triggerProps","Content","style","submenuPopoverContent","submenuPopover","variant","el","submenuPopoverTitle","submenuPopoverDivider","submenuPopoverBody","index","trim","submenuGroup","submenuGroupHeader","link","onLinkClick","children","list","child","navTop","navTopItems","spacing","topDivider","navBottom"],"mappings":"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"}
|
|
@@ -401,7 +401,6 @@
|
|
|
401
401
|
background-color: @bg-color;
|
|
402
402
|
|
|
403
403
|
.toggle-wrapper {
|
|
404
|
-
display: flex;
|
|
405
404
|
padding: @spacing-1 @spacing-1 @spacing-1 14px;
|
|
406
405
|
}
|
|
407
406
|
|
|
@@ -620,6 +619,7 @@
|
|
|
620
619
|
|
|
621
620
|
.submenu-popover-title[data-anv][data-anv] {
|
|
622
621
|
color: @color-white;
|
|
622
|
+
font-size: @typescale-2;
|
|
623
623
|
margin-bottom: @spacing-0;
|
|
624
624
|
margin-top: @spacing-1;
|
|
625
625
|
padding: @spacing-1;
|
|
@@ -69,7 +69,7 @@ import * as styles from './navigation-catalog-trigger.module.less';
|
|
|
69
69
|
* that opens a dialog rather than a destination, so sharing the namespace invites a
|
|
70
70
|
* selector written for a link to match it.
|
|
71
71
|
*/ 'data-cy': 'navigation-catalog-trigger',
|
|
72
|
-
'data-pendo': '
|
|
72
|
+
'data-pendo': 'global-nav-go-to',
|
|
73
73
|
'appearance': 'secondary',
|
|
74
74
|
'size': 'small',
|
|
75
75
|
'aria-label': 'Go to',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx"],"sourcesContent":["import { Button, Chip, Flex, Text, ThemeProvider } from '@servicetitan/anvil2';\nimport IconSearch from '@servicetitan/anvil2/assets/icons/material/round/search.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport { PinnedNavigationConfig } from '../interface';\nimport { NavigationCatalog } from './navigation-catalog';\n\nimport * as styles from './navigation-catalog-trigger.module.less';\n\n/**\n * True when the keystroke should open the catalog rather than reach the focused control.\n *\n * The shortcut is deliberately a modifier chord: a bare key would be swallowed by, or would steal\n * from, every text field on the page.\n */\nconst isCatalogShortcut = (event: KeyboardEvent): boolean =>\n (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'k';\n\n/**\n * The chord as the user's own keyboard prints it.\n *\n * The listener accepts either modifier on every platform, so this only decides which one to\n * advertise. `platform` is deprecated but is still the only reliable signal here; the user agent is\n * the fallback and a wrong guess costs nothing but a mislabelled badge.\n */\nconst shortcutLabel = (): string => {\n const platform =\n typeof window.navigator === 'undefined'\n ? ''\n : window.navigator.platform || window.navigator.userAgent;\n\n return /Mac|iPod|iPhone|iPad/.test(platform) ? '⌘ K' : 'Ctrl K';\n};\n\nexport interface NavigationCatalogTriggerProps extends PinnedNavigationConfig {\n /** Whether the rail is expanded; the compact trigger keeps its label below the icon. */\n expanded: boolean;\n /** Optional sidebar action beside the expanded search trigger. */\n trailingAction?: ReactNode;\n}\n\n/**\n * Sidebar entry point for the navigation catalog.\n *\n * Presented as a labelled control rather than a bare icon: the catalog is the only route to an\n * unpinned destination, so the affordance has to read as \"search everything\" at a glance. It is a\n * button and not an input — the real field lives in the dialog and takes focus on open, and two\n * inputs would mean typing in one and getting results in the other.\n *\n * The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this\n * trigger plus its shortcut are what keep the user from being stranded there.\n */\nexport const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps> = ({\n expanded,\n trailingAction,\n ...catalog\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const wasOpen = useRef(false);\n\n const close = useCallback(() => setIsOpen(false), []);\n const toggle = useCallback(() => setIsOpen(open => !open), []);\n\n useEffect(() => {\n // The dialog unmounts on close, so restore focus after its modal content is removed.\n if (wasOpen.current && !isOpen) {\n triggerRef.current?.focus();\n }\n wasOpen.current = isOpen;\n }, [isOpen]);\n\n useEffect(() => {\n const handler = (event: KeyboardEvent) => {\n if (!isCatalogShortcut(event)) {\n return;\n }\n\n event.preventDefault();\n toggle();\n };\n\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [toggle]);\n\n /*\n * Labelled explicitly so the name stays \"Go to\" whether or not the text is rendered, and so the\n * shortcut badge never reads out as part of it.\n */\n const buttonProps = {\n /*\n * Not `navigation-item-*`: that prefix belongs to the rail's links, and this is a control\n * that opens a dialog rather than a destination, so sharing the namespace invites a\n * selector written for a link to match it.\n */\n 'data-cy': 'navigation-catalog-trigger',\n 'data-pendo': 'navigation-catalog-trigger',\n 'appearance': 'secondary',\n 'size': 'small',\n 'aria-label': 'Go to',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': isOpen,\n 'aria-keyshortcuts': 'Meta+K Control+K',\n 'onClick': toggle,\n } as const;\n\n return (\n <Fragment>\n <div className={classNames(styles.container, !expanded && styles.containerCollapsed)}>\n {expanded ? (\n <Button\n {...buttonProps}\n ref={triggerRef}\n icon={{ before: IconSearch }}\n className={styles.trigger}\n >\n <Flex alignItems=\"center\" justifyContent=\"space-between\" gap={1} grow={1}>\n {/* The ellipsis is part of the label, matching the field in the dialog. */}\n <Text inline size=\"small\" className={styles.triggerLabel}>\n Go to...\n </Text>\n {/* Decorative: the chord is already on the button as aria-keyshortcuts. */}\n <Chip\n label={shortcutLabel()}\n size=\"small\"\n className={styles.shortcut}\n />\n </Flex>\n </Button>\n ) : (\n <Button\n {...buttonProps}\n ref={triggerRef}\n appearance=\"ghost\"\n icon={IconSearch}\n className={styles.triggerCollapsed}\n >\n <span className={styles.collapsedLabel}>Go to...</span>\n </Button>\n )}\n {expanded && trailingAction}\n </div>\n {/*\n * Mounted only while open, so a host that resolves its catalog lazily is not asked for\n * it on every page load, and a second navigation tree is not held inside the sidebar's\n * subtree for a dialog nobody opened.\n *\n * Dark, because the catalog belongs to the rail rather than to the page. The rail paints\n * itself dark whatever the application's mode is, and this is the rail's own menu — the\n * same `ThemeProvider` the rail uses on its own sections.\n *\n * A `ThemeProvider` rather than a second `AnvilProvider`: the latter is the\n * application-level provider, and mounting a nested one alongside the host's meant its\n * unmount tore down state the outer one was still relying on, so closing the catalog\n * left the rail unstyled.\n *\n * This only sets the mode, which is what makes the *text* tokens light. The surface is\n * matched to the rail separately, in `navigation-catalog.module.less` — anvil2's dark is\n * a neutral grey and the rail's is a navy, so the mode alone would leave two visibly\n * different darks.\n */}\n {isOpen && (\n <ThemeProvider mode=\"dark\">\n <NavigationCatalog open onClose={close} {...catalog} />\n </ThemeProvider>\n )}\n </Fragment>\n );\n};\n"],"names":["Button","Chip","Flex","Text","ThemeProvider","IconSearch","classNames","Fragment","useCallback","useEffect","useRef","useState","NavigationCatalog","styles","isCatalogShortcut","event","metaKey","ctrlKey","altKey","key","toLowerCase","shortcutLabel","platform","window","navigator","userAgent","test","NavigationCatalogTrigger","expanded","trailingAction","catalog","isOpen","setIsOpen","triggerRef","wasOpen","close","toggle","open","current","focus","handler","preventDefault","addEventListener","removeEventListener","buttonProps","div","className","container","containerCollapsed","ref","icon","before","trigger","alignItems","justifyContent","gap","grow","inline","size","triggerLabel","label","shortcut","appearance","triggerCollapsed","span","collapsedLabel","mode","onClose"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog-trigger.tsx"],"sourcesContent":["import { Button, Chip, Flex, Text, ThemeProvider } from '@servicetitan/anvil2';\nimport IconSearch from '@servicetitan/anvil2/assets/icons/material/round/search.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, ReactNode, useCallback, useEffect, useRef, useState } from 'react';\nimport { PinnedNavigationConfig } from '../interface';\nimport { NavigationCatalog } from './navigation-catalog';\n\nimport * as styles from './navigation-catalog-trigger.module.less';\n\n/**\n * True when the keystroke should open the catalog rather than reach the focused control.\n *\n * The shortcut is deliberately a modifier chord: a bare key would be swallowed by, or would steal\n * from, every text field on the page.\n */\nconst isCatalogShortcut = (event: KeyboardEvent): boolean =>\n (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === 'k';\n\n/**\n * The chord as the user's own keyboard prints it.\n *\n * The listener accepts either modifier on every platform, so this only decides which one to\n * advertise. `platform` is deprecated but is still the only reliable signal here; the user agent is\n * the fallback and a wrong guess costs nothing but a mislabelled badge.\n */\nconst shortcutLabel = (): string => {\n const platform =\n typeof window.navigator === 'undefined'\n ? ''\n : window.navigator.platform || window.navigator.userAgent;\n\n return /Mac|iPod|iPhone|iPad/.test(platform) ? '⌘ K' : 'Ctrl K';\n};\n\nexport interface NavigationCatalogTriggerProps extends PinnedNavigationConfig {\n /** Whether the rail is expanded; the compact trigger keeps its label below the icon. */\n expanded: boolean;\n /** Optional sidebar action beside the expanded search trigger. */\n trailingAction?: ReactNode;\n}\n\n/**\n * Sidebar entry point for the navigation catalog.\n *\n * Presented as a labelled control rather than a bare icon: the catalog is the only route to an\n * unpinned destination, so the affordance has to read as \"search everything\" at a glance. It is a\n * button and not an input — the real field lives in the dialog and takes focus on open, and two\n * inputs would mean typing in one and getting results in the other.\n *\n * The catalog is never opened automatically; an unpinned navigation is a deliberate state, and this\n * trigger plus its shortcut are what keep the user from being stranded there.\n */\nexport const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps> = ({\n expanded,\n trailingAction,\n ...catalog\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const wasOpen = useRef(false);\n\n const close = useCallback(() => setIsOpen(false), []);\n const toggle = useCallback(() => setIsOpen(open => !open), []);\n\n useEffect(() => {\n // The dialog unmounts on close, so restore focus after its modal content is removed.\n if (wasOpen.current && !isOpen) {\n triggerRef.current?.focus();\n }\n wasOpen.current = isOpen;\n }, [isOpen]);\n\n useEffect(() => {\n const handler = (event: KeyboardEvent) => {\n if (!isCatalogShortcut(event)) {\n return;\n }\n\n event.preventDefault();\n toggle();\n };\n\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [toggle]);\n\n /*\n * Labelled explicitly so the name stays \"Go to\" whether or not the text is rendered, and so the\n * shortcut badge never reads out as part of it.\n */\n const buttonProps = {\n /*\n * Not `navigation-item-*`: that prefix belongs to the rail's links, and this is a control\n * that opens a dialog rather than a destination, so sharing the namespace invites a\n * selector written for a link to match it.\n */\n 'data-cy': 'navigation-catalog-trigger',\n 'data-pendo': 'global-nav-go-to',\n 'appearance': 'secondary',\n 'size': 'small',\n 'aria-label': 'Go to',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': isOpen,\n 'aria-keyshortcuts': 'Meta+K Control+K',\n 'onClick': toggle,\n } as const;\n\n return (\n <Fragment>\n <div className={classNames(styles.container, !expanded && styles.containerCollapsed)}>\n {expanded ? (\n <Button\n {...buttonProps}\n ref={triggerRef}\n icon={{ before: IconSearch }}\n className={styles.trigger}\n >\n <Flex alignItems=\"center\" justifyContent=\"space-between\" gap={1} grow={1}>\n {/* The ellipsis is part of the label, matching the field in the dialog. */}\n <Text inline size=\"small\" className={styles.triggerLabel}>\n Go to...\n </Text>\n {/* Decorative: the chord is already on the button as aria-keyshortcuts. */}\n <Chip\n label={shortcutLabel()}\n size=\"small\"\n className={styles.shortcut}\n />\n </Flex>\n </Button>\n ) : (\n <Button\n {...buttonProps}\n ref={triggerRef}\n appearance=\"ghost\"\n icon={IconSearch}\n className={styles.triggerCollapsed}\n >\n <span className={styles.collapsedLabel}>Go to...</span>\n </Button>\n )}\n {expanded && trailingAction}\n </div>\n {/*\n * Mounted only while open, so a host that resolves its catalog lazily is not asked for\n * it on every page load, and a second navigation tree is not held inside the sidebar's\n * subtree for a dialog nobody opened.\n *\n * Dark, because the catalog belongs to the rail rather than to the page. The rail paints\n * itself dark whatever the application's mode is, and this is the rail's own menu — the\n * same `ThemeProvider` the rail uses on its own sections.\n *\n * A `ThemeProvider` rather than a second `AnvilProvider`: the latter is the\n * application-level provider, and mounting a nested one alongside the host's meant its\n * unmount tore down state the outer one was still relying on, so closing the catalog\n * left the rail unstyled.\n *\n * This only sets the mode, which is what makes the *text* tokens light. The surface is\n * matched to the rail separately, in `navigation-catalog.module.less` — anvil2's dark is\n * a neutral grey and the rail's is a navy, so the mode alone would leave two visibly\n * different darks.\n */}\n {isOpen && (\n <ThemeProvider mode=\"dark\">\n <NavigationCatalog open onClose={close} {...catalog} />\n </ThemeProvider>\n )}\n </Fragment>\n );\n};\n"],"names":["Button","Chip","Flex","Text","ThemeProvider","IconSearch","classNames","Fragment","useCallback","useEffect","useRef","useState","NavigationCatalog","styles","isCatalogShortcut","event","metaKey","ctrlKey","altKey","key","toLowerCase","shortcutLabel","platform","window","navigator","userAgent","test","NavigationCatalogTrigger","expanded","trailingAction","catalog","isOpen","setIsOpen","triggerRef","wasOpen","close","toggle","open","current","focus","handler","preventDefault","addEventListener","removeEventListener","buttonProps","div","className","container","containerCollapsed","ref","icon","before","trigger","alignItems","justifyContent","gap","grow","inline","size","triggerLabel","label","shortcut","appearance","triggerCollapsed","span","collapsedLabel","mode","onClose"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-catalog.d.ts","sourceRoot":"","sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,EAAE,EAAmD,MAAM,OAAO,CAAC;AAS5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"navigation-catalog.d.ts","sourceRoot":"","sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,EAAE,EAAmD,MAAM,OAAO,CAAC;AAS5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAmMtD,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB;IAClE,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CAkNxD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"sourcesContent":["import { Button, Dialog, Icon, SearchField, Text, Tooltip } from '@servicetitan/anvil2';\nimport IconLock from '@servicetitan/anvil2/assets/icons/material/round/lock.svg';\nimport IconStar from '@servicetitan/anvil2/assets/icons/material/round/star.svg';\nimport IconStarBorder from '@servicetitan/anvil2/assets/icons/material/round/star_border.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, MouseEvent, useMemo, useRef, useState } from 'react';\nimport {\n filterCatalogGroups,\n getCatalogChildren,\n getCatalogSubmenuGroups,\n} from '../../../utils/catalog-groups';\nimport { NavigationItemData } from '../../../utils/navigation';\nimport { getCounterTag } from '../../../utils/side-nav';\nimport { BadgeTag } from '../../badge-tag';\nimport { PinnedNavigationConfig } from '../interface';\nimport { useTitanLayoutContext } from '../layout-context';\nimport { NavigationCatalogFooter } from './navigation-catalog-footer';\nimport * as styles from './navigation-catalog.module.less';\n\n/** Shown in place of the pin control for a destination the user may not unpin. */\nconst LockedPin: FC = () => (\n <Tooltip>\n <Tooltip.Trigger>\n <Icon svg={IconLock} className={styles.lock} />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n Cannot be removed from Favorites\n </Tooltip.Content>\n </Tooltip>\n);\n\ninterface PinToggleProps {\n /** Mixed modules stay highlighted but use an outline until all pages are pinned. */\n pinned: boolean | 'mixed';\n label: string;\n onToggle: () => void;\n}\n\nconst PinToggle: FC<PinToggleProps> = ({ pinned, label, onToggle }) => {\n const toggle = (event: MouseEvent) => {\n // The control sits inside the row but is not a navigation affordance.\n event.preventDefault();\n event.stopPropagation();\n onToggle();\n };\n\n return (\n <Tooltip delay={300}>\n <Tooltip.Trigger>\n <Button\n appearance=\"ghost\"\n size=\"small\"\n icon={pinned === true ? IconStar : IconStarBorder}\n className={classNames(styles.star, pinned && styles.starActive)}\n aria-label={label}\n aria-pressed={pinned}\n onClick={toggle}\n />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n {pinned === 'mixed'\n ? 'Add this module to Favorites'\n : pinned\n ? 'Remove from Favorites'\n : 'Add to Favorites'}\n </Tooltip.Content>\n </Tooltip>\n );\n};\n\ninterface CatalogRowsProps {\n item: NavigationItemData;\n /**\n * Every id the module's own control covers, taken from the unfiltered item.\n *\n * `item` may have been narrowed by the search, so deriving this from it would let the control\n * act on only the pages that happen to be visible — silently stranding the rest.\n */\n scopeIds: string[];\n isPinned: (id: string) => boolean;\n isLocked: (id: string) => boolean;\n /** `ownerId` is the module the page belongs to; omitted for the module's own row. */\n onTogglePin: (id: string, ownerId?: string) => void;\n onToggleAll: (ids: string[], shouldPin: boolean) => void;\n onNavigate: () => void;\n}\n\nconst CatalogRows: FC<CatalogRowsProps> = ({\n item,\n scopeIds,\n isPinned,\n isLocked,\n onTogglePin,\n onToggleAll,\n onNavigate,\n}) => {\n const { NavigationComponent } = useTitanLayoutContext();\n const submenuGroups = getCatalogSubmenuGroups(item);\n\n /*\n * Minus anything locked — locked destinations are always present, so the module's control\n * neither reports on them nor acts on them.\n */\n const everything = scopeIds.filter(id => !isLocked(id));\n const anyPinned = everything.some(isPinned);\n const allPinned = everything.length > 0 && everything.every(isPinned);\n const tag = getCounterTag(item.counter, item.tag);\n\n return (\n <div className={styles.moduleGroup}>\n <div className={styles.row}>\n <NavigationComponent\n to={item.to}\n className={styles.link}\n onClick={onNavigate}\n aria-label={item.title}\n >\n {item.icon && (\n <Icon svg={item.icon} size=\"medium\" className={styles.moduleIcon} />\n )}\n <Text inline size=\"small\" className={styles.moduleTitle}>\n {item.title}\n </Text>\n {/*\n * The same counter the rail shows, so a destination that is asking for\n * attention still says so here — the catalog is the only place an unfavourited\n * one is visible at all.\n */}\n {!!tag && <BadgeTag data={tag} className={styles.badge} />}\n </NavigationComponent>\n {/*\n * One control for the whole module: favouriting it takes its pages with it, and\n * unfavouriting it takes them away again.\n *\n * A gold outline marks a partially favourited module. Fill the star only when\n * its full, unfiltered scope is favourited, including pages hidden by search.\n * Clicking the outline adds the remaining pages; clicking the filled star removes all.\n */}\n {isLocked(item.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={allPinned ? true : anyPinned ? 'mixed' : false}\n label={\n allPinned\n ? `Unpin ${item.title} from the navigation`\n : `Pin ${item.title} to the navigation`\n }\n onToggle={() => onToggleAll(everything, !allPinned)}\n />\n )}\n </div>\n {submenuGroups.map(group => (\n <div key={group.links[0].id}>\n {group.title && (\n <Text variant=\"eyebrow\" size=\"small\" className={styles.submenuTitle}>\n {group.title}\n </Text>\n )}\n {group.links.map(child => {\n /*\n * Page titles are only unique within their item — \"Settings\" and \"Reports\" recur\n * across the catalog — and the row that tells them apart is a separate element, so\n * it is not part of any of these names. Anything reading the rows on their own, a\n * screen reader running through the links or a voice command naming one, would\n * otherwise be given the same name several times.\n */\n const qualifiedTitle = `${item.title}: ${child.title}`;\n const childPinned = isPinned(child.id);\n const childTag = getCounterTag(child.counter, child.tag);\n\n return (\n <div key={child.id} className={styles.row}>\n <NavigationComponent\n to={child.to}\n className={classNames(styles.link, styles.childLink)}\n onClick={onNavigate}\n aria-label={qualifiedTitle}\n >\n <Text inline className={styles.childTitle}>\n {child.title}\n </Text>\n {!!childTag && (\n <BadgeTag data={childTag} className={styles.badge} />\n )}\n </NavigationComponent>\n {isLocked(child.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={childPinned}\n label={\n childPinned\n ? `Unpin ${qualifiedTitle} from the navigation`\n : `Pin ${qualifiedTitle} to the navigation`\n }\n onToggle={() => onTogglePin(child.id, item.id)}\n />\n )}\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport interface NavigationCatalogProps extends PinnedNavigationConfig {\n open: boolean;\n onClose: () => void;\n}\n\n/**\n * Every destination the host offers, grouped into sections, with a star on every row.\n *\n * The catalog shows what it is given and nothing more — the host has already resolved what this\n * user may reach, so the catalog cannot offer a destination they do not have.\n */\nexport const NavigationCatalog: FC<NavigationCatalogProps> = ({\n open,\n onClose,\n groups,\n pinnedIds,\n lockedIds,\n onPinnedIdsChange,\n onResetToDefaults,\n}) => {\n const [query, setQuery] = useState('');\n const isSearching = query.trim().length > 0;\n const searchRef = useRef<HTMLInputElement>(null);\n\n /*\n * Resolved here rather than by the host, so a catalog that costs requests to enumerate is paid\n * for on opening the dialog and not on every page load. This component is mounted only while\n * open, so the call happens once per open unless the host changes the function.\n */\n const resolvedGroups = useMemo(() => groups(), [groups]);\n\n const visibleGroups = useMemo(\n () => filterCatalogGroups(resolvedGroups, query),\n [resolvedGroups, query]\n );\n\n /*\n * The pin scope of every item, keyed by id and built before the search narrows anything, so a\n * module's control still covers the pages a query is currently hiding.\n */\n const scopeById = useMemo(\n () =>\n new Map(\n resolvedGroups\n .flatMap(group => group.items)\n .map(item => [\n item.id,\n [item.id, ...getCatalogChildren(item).map(child => child.id)],\n ])\n ),\n [resolvedGroups]\n );\n const pinnedSet = useMemo(() => new Set(pinnedIds), [pinnedIds]);\n const lockedSet = useMemo(() => new Set(lockedIds ?? []), [lockedIds]);\n\n const isPinned = (id: string) => pinnedSet.has(id);\n const isLocked = (id: string) => lockedSet.has(id);\n\n /**\n * Favourites or unfavourites one row, carrying its module with it.\n *\n * A page is reached through its module, so the two travel together: favouriting a page brings\n * the module along when it is not already there, and unfavouriting the last one takes the\n * module with it. Leaving the module behind on its own would not read as an emptier\n * navigation — it would put the module back carrying every page it has.\n *\n * Locked ids never reach a proposal. The controls that would move them are not rendered, so\n * that part is belt and braces — but the host's list is the thing that gets persisted, and a\n * locked destination has no business appearing in or disappearing from it.\n */\n const togglePin = (id: string, ownerId?: string) => {\n if (lockedSet.has(id)) {\n return;\n }\n\n /** The module, when it is a separate row that this page can legitimately move. */\n const owner = ownerId !== id && ownerId && !lockedSet.has(ownerId) ? ownerId : undefined;\n\n if (pinnedSet.has(id)) {\n const removing = new Set([id]);\n\n if (owner) {\n const siblings = (scopeById.get(owner) ?? []).filter(\n scoped => scoped !== owner && scoped !== id\n );\n\n if (!siblings.some(sibling => pinnedSet.has(sibling))) {\n removing.add(owner);\n }\n }\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n // Module first, so it reads ahead of its own page in the saved order.\n const additions = owner && !pinnedSet.has(owner) ? [owner, id] : [id];\n\n onPinnedIdsChange([...pinnedIds, ...additions]);\n };\n\n /*\n * One proposal for the whole set rather than one per row, so a host that persists on every\n * change writes once, and every intermediate list — states the user never asked for — never\n * exists.\n */\n const toggleAll = (ids: string[], shouldPin: boolean) => {\n const actionable = ids.filter(id => !lockedSet.has(id));\n\n if (!shouldPin) {\n const removing = new Set(actionable);\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n const additions = actionable.filter(id => !pinnedSet.has(id));\n\n if (additions.length > 0) {\n onPinnedIdsChange([...pinnedIds, ...additions]);\n }\n };\n\n const close = () => {\n setQuery('');\n onClose();\n };\n\n return (\n <Dialog\n open={open}\n onClose={close}\n size=\"xlarge\"\n aria-label=\"All Navigation\"\n className={styles.dialog}\n data-cy=\"navigation-catalog-dialog\"\n >\n <Dialog.Content className={styles.content}>\n <div className={styles.body}>\n <SearchField\n ref={searchRef}\n size=\"small\"\n className={styles.search}\n placeholder=\"Go to ...\"\n value={query}\n onChange={event => setQuery(event.target.value)}\n onClear={() => setQuery('')}\n aria-label=\"Go to\"\n />\n <div\n className={classNames(styles.results, isSearching && styles.searchResults)}\n >\n {visibleGroups.length === 0 ? (\n <div className={styles.empty}>\n {/*\n * Two different nothings: a search that matched no row, and a host\n * that offered no rows at all. The second is a real state — the\n * catalog is reachable whether or not there is anything in it — and\n * quoting an empty query at the user does not describe it.\n */}\n <Text subdued>\n {isSearching ? (\n <Fragment>\n No navigation items match “{query}”.\n </Fragment>\n ) : (\n 'There are no navigation items to show.'\n )}\n </Text>\n </div>\n ) : (\n <div className={isSearching ? undefined : styles.columns}>\n {visibleGroups.map(group => (\n <div\n key={group.title}\n className={isSearching ? undefined : styles.group}\n >\n {/*\n * Browse uses titled columns. Search stacks matches at full\n * width, keeping only each module's own submenu headings.\n */}\n {!isSearching && (\n <Text\n variant=\"eyebrow\"\n size=\"medium\"\n className={styles.groupTitle}\n >\n {group.title}\n </Text>\n )}\n {group.items.map(item => (\n <CatalogRows\n key={item.id}\n item={item}\n scopeIds={scopeById.get(item.id) ?? [item.id]}\n isPinned={isPinned}\n isLocked={isLocked}\n onTogglePin={togglePin}\n onToggleAll={toggleAll}\n onNavigate={close}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </Dialog.Content>\n <NavigationCatalogFooter\n onRestore={\n onResetToDefaults &&\n (() => {\n onResetToDefaults();\n // The control disappears with the host's reset; keep focus in the dialog.\n searchRef.current?.focus();\n })\n }\n />\n </Dialog>\n );\n};\n"],"names":["Button","Dialog","Icon","SearchField","Text","Tooltip","IconLock","IconStar","IconStarBorder","classNames","Fragment","useMemo","useRef","useState","filterCatalogGroups","getCatalogChildren","getCatalogSubmenuGroups","getCounterTag","BadgeTag","useTitanLayoutContext","NavigationCatalogFooter","styles","LockedPin","Trigger","svg","className","lock","Content","style","pointerEvents","PinToggle","pinned","label","onToggle","toggle","event","preventDefault","stopPropagation","delay","appearance","size","icon","star","starActive","aria-label","aria-pressed","onClick","CatalogRows","item","scopeIds","isPinned","isLocked","onTogglePin","onToggleAll","onNavigate","NavigationComponent","submenuGroups","everything","filter","id","anyPinned","some","allPinned","length","every","tag","counter","div","moduleGroup","row","to","link","title","moduleIcon","inline","moduleTitle","data","badge","map","group","variant","submenuTitle","links","child","qualifiedTitle","childPinned","childTag","childLink","childTitle","NavigationCatalog","open","onClose","groups","pinnedIds","lockedIds","onPinnedIdsChange","onResetToDefaults","query","setQuery","isSearching","trim","searchRef","resolvedGroups","visibleGroups","scopeById","Map","flatMap","items","pinnedSet","Set","lockedSet","has","togglePin","ownerId","owner","undefined","removing","siblings","get","scoped","sibling","add","additions","toggleAll","ids","shouldPin","actionable","close","dialog","data-cy","content","body","ref","search","placeholder","value","onChange","target","onClear","results","searchResults","empty","subdued","columns","groupTitle","onRestore","current","focus"],"mappings":";AAAA,SAASA,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAEC,WAAW,EAAEC,IAAI,EAAEC,OAAO,QAAQ,uBAAuB;AACxF,OAAOC,cAAc,4DAA4D;AACjF,OAAOC,cAAc,4DAA4D;AACjF,OAAOC,oBAAoB,mEAAmE;AAC9F,OAAOC,gBAAgB,aAAa;AACpC,SAAaC,QAAQ,EAAcC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAC5E,SACIC,mBAAmB,EACnBC,kBAAkB,EAClBC,uBAAuB,QACpB,gCAAgC;AAEvC,SAASC,aAAa,QAAQ,0BAA0B;AACxD,SAASC,QAAQ,QAAQ,kBAAkB;AAE3C,SAASC,qBAAqB,QAAQ,oBAAoB;AAC1D,SAASC,uBAAuB,QAAQ,8BAA8B;AACtE,YAAYC,YAAY,mCAAmC;AAE3D,gFAAgF,GAChF,MAAMC,YAAgB,kBAClB,MAACjB;;0BACG,KAACA,QAAQkB,OAAO;0BACZ,cAAA,KAACrB;oBAAKsB,KAAKlB;oBAAUmB,WAAWJ,OAAOK,IAAI;;;0BAE/C,KAACrB,QAAQsB,OAAO;gBAACC,OAAO;oBAAEC,eAAe;gBAAO;0BAAG;;;;AAa3D,MAAMC,YAAgC,CAAC,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE;IAC9D,MAAMC,SAAS,CAACC;QACZ,sEAAsE;QACtEA,MAAMC,cAAc;QACpBD,MAAME,eAAe;QACrBJ;IACJ;IAEA,qBACI,MAAC5B;QAAQiC,OAAO;;0BACZ,KAACjC,QAAQkB,OAAO;0BACZ,cAAA,KAACvB;oBACGuC,YAAW;oBACXC,MAAK;oBACLC,MAAMV,WAAW,OAAOxB,WAAWC;oBACnCiB,WAAWhB,WAAWY,OAAOqB,IAAI,EAAEX,UAAUV,OAAOsB,UAAU;oBAC9DC,cAAYZ;oBACZa,gBAAcd;oBACde,SAASZ;;;0BAGjB,KAAC7B,QAAQsB,OAAO;gBAACC,OAAO;oBAAEC,eAAe;gBAAO;0BAC3CE,WAAW,UACN,iCACAA,SACE,0BACA;;;;AAIxB;AAmBA,MAAMgB,cAAoC,CAAC,EACvCC,IAAI,EACJC,QAAQ,EACRC,QAAQ,EACRC,QAAQ,EACRC,WAAW,EACXC,WAAW,EACXC,UAAU,EACb;IACG,MAAM,EAAEC,mBAAmB,EAAE,GAAGpC;IAChC,MAAMqC,gBAAgBxC,wBAAwBgC;IAE9C;;;KAGC,GACD,MAAMS,aAAaR,SAASS,MAAM,CAACC,CAAAA,KAAM,CAACR,SAASQ;IACnD,MAAMC,YAAYH,WAAWI,IAAI,CAACX;IAClC,MAAMY,YAAYL,WAAWM,MAAM,GAAG,KAAKN,WAAWO,KAAK,CAACd;IAC5D,MAAMe,MAAMhD,cAAc+B,KAAKkB,OAAO,EAAElB,KAAKiB,GAAG;IAEhD,qBACI,MAACE;QAAI1C,WAAWJ,OAAO+C,WAAW;;0BAC9B,MAACD;gBAAI1C,WAAWJ,OAAOgD,GAAG;;kCACtB,MAACd;wBACGe,IAAItB,KAAKsB,EAAE;wBACX7C,WAAWJ,OAAOkD,IAAI;wBACtBzB,SAASQ;wBACTV,cAAYI,KAAKwB,KAAK;;4BAErBxB,KAAKP,IAAI,kBACN,KAACvC;gCAAKsB,KAAKwB,KAAKP,IAAI;gCAAED,MAAK;gCAASf,WAAWJ,OAAOoD,UAAU;;0CAEpE,KAACrE;gCAAKsE,MAAM;gCAAClC,MAAK;gCAAQf,WAAWJ,OAAOsD,WAAW;0CAClD3B,KAAKwB,KAAK;;4BAOd,CAAC,CAACP,qBAAO,KAAC/C;gCAAS0D,MAAMX;gCAAKxC,WAAWJ,OAAOwD,KAAK;;;;oBAUzD1B,SAASH,KAAKW,EAAE,kBACb,KAACrC,+BAED,KAACQ;wBACGC,QAAQ+B,YAAY,OAAOF,YAAY,UAAU;wBACjD5B,OACI8B,YACM,CAAC,MAAM,EAAEd,KAAKwB,KAAK,CAAC,oBAAoB,CAAC,GACzC,CAAC,IAAI,EAAExB,KAAKwB,KAAK,CAAC,kBAAkB,CAAC;wBAE/CvC,UAAU,IAAMoB,YAAYI,YAAY,CAACK;;;;YAIpDN,cAAcsB,GAAG,CAACC,CAAAA,sBACf,MAACZ;;wBACIY,MAAMP,KAAK,kBACR,KAACpE;4BAAK4E,SAAQ;4BAAUxC,MAAK;4BAAQf,WAAWJ,OAAO4D,YAAY;sCAC9DF,MAAMP,KAAK;;wBAGnBO,MAAMG,KAAK,CAACJ,GAAG,CAACK,CAAAA;4BACb;;;;;;yBAMC,GACD,MAAMC,iBAAiB,GAAGpC,KAAKwB,KAAK,CAAC,EAAE,EAAEW,MAAMX,KAAK,EAAE;4BACtD,MAAMa,cAAcnC,SAASiC,MAAMxB,EAAE;4BACrC,MAAM2B,WAAWrE,cAAckE,MAAMjB,OAAO,EAAEiB,MAAMlB,GAAG;4BAEvD,qBACI,MAACE;gCAAmB1C,WAAWJ,OAAOgD,GAAG;;kDACrC,MAACd;wCACGe,IAAIa,MAAMb,EAAE;wCACZ7C,WAAWhB,WAAWY,OAAOkD,IAAI,EAAElD,OAAOkE,SAAS;wCACnDzC,SAASQ;wCACTV,cAAYwC;;0DAEZ,KAAChF;gDAAKsE,MAAM;gDAACjD,WAAWJ,OAAOmE,UAAU;0DACpCL,MAAMX,KAAK;;4CAEf,CAAC,CAACc,0BACC,KAACpE;gDAAS0D,MAAMU;gDAAU7D,WAAWJ,OAAOwD,KAAK;;;;oCAGxD1B,SAASgC,MAAMxB,EAAE,kBACd,KAACrC,+BAED,KAACQ;wCACGC,QAAQsD;wCACRrD,OACIqD,cACM,CAAC,MAAM,EAAED,eAAe,oBAAoB,CAAC,GAC7C,CAAC,IAAI,EAAEA,eAAe,kBAAkB,CAAC;wCAEnDnD,UAAU,IAAMmB,YAAY+B,MAAMxB,EAAE,EAAEX,KAAKW,EAAE;;;+BAxB/CwB,MAAMxB,EAAE;wBA6B1B;;mBAhDMoB,MAAMG,KAAK,CAAC,EAAE,CAACvB,EAAE;;;AAqD3C;AAOA;;;;;CAKC,GACD,OAAO,MAAM8B,oBAAgD,CAAC,EAC1DC,IAAI,EACJC,OAAO,EACPC,MAAM,EACNC,SAAS,EACTC,SAAS,EACTC,iBAAiB,EACjBC,iBAAiB,EACpB;IACG,MAAM,CAACC,OAAOC,SAAS,GAAGrF,SAAS;IACnC,MAAMsF,cAAcF,MAAMG,IAAI,GAAGrC,MAAM,GAAG;IAC1C,MAAMsC,YAAYzF,OAAyB;IAE3C;;;;KAIC,GACD,MAAM0F,iBAAiB3F,QAAQ,IAAMiF,UAAU;QAACA;KAAO;IAEvD,MAAMW,gBAAgB5F,QAClB,IAAMG,oBAAoBwF,gBAAgBL,QAC1C;QAACK;QAAgBL;KAAM;IAG3B;;;KAGC,GACD,MAAMO,YAAY7F,QACd,IACI,IAAI8F,IACAH,eACKI,OAAO,CAAC3B,CAAAA,QAASA,MAAM4B,KAAK,EAC5B7B,GAAG,CAAC9B,CAAAA,OAAQ;gBACTA,KAAKW,EAAE;gBACP;oBAACX,KAAKW,EAAE;uBAAK5C,mBAAmBiC,MAAM8B,GAAG,CAACK,CAAAA,QAASA,MAAMxB,EAAE;iBAAE;aAChE,IAEb;QAAC2C;KAAe;IAEpB,MAAMM,YAAYjG,QAAQ,IAAM,IAAIkG,IAAIhB,YAAY;QAACA;KAAU;IAC/D,MAAMiB,YAAYnG,QAAQ,IAAM,IAAIkG,IAAIf,sBAAAA,uBAAAA,YAAa,EAAE,GAAG;QAACA;KAAU;IAErE,MAAM5C,WAAW,CAACS,KAAeiD,UAAUG,GAAG,CAACpD;IAC/C,MAAMR,WAAW,CAACQ,KAAemD,UAAUC,GAAG,CAACpD;IAE/C;;;;;;;;;;;KAWC,GACD,MAAMqD,YAAY,CAACrD,IAAYsD;QAC3B,IAAIH,UAAUC,GAAG,CAACpD,KAAK;YACnB;QACJ;QAEA,gFAAgF,GAChF,MAAMuD,QAAQD,YAAYtD,MAAMsD,WAAW,CAACH,UAAUC,GAAG,CAACE,WAAWA,UAAUE;QAE/E,IAAIP,UAAUG,GAAG,CAACpD,KAAK;YACnB,MAAMyD,WAAW,IAAIP,IAAI;gBAAClD;aAAG;YAE7B,IAAIuD,OAAO;oBACWV;gBAAlB,MAAMa,WAAW,EAACb,iBAAAA,UAAUc,GAAG,CAACJ,oBAAdV,4BAAAA,iBAAwB,EAAE,EAAE9C,MAAM,CAChD6D,CAAAA,SAAUA,WAAWL,SAASK,WAAW5D;gBAG7C,IAAI,CAAC0D,SAASxD,IAAI,CAAC2D,CAAAA,UAAWZ,UAAUG,GAAG,CAACS,WAAW;oBACnDJ,SAASK,GAAG,CAACP;gBACjB;YACJ;YAEAnB,kBAAkBF,UAAUnC,MAAM,CAAC3B,CAAAA,SAAU,CAACqF,SAASL,GAAG,CAAChF;YAC3D;QACJ;QAEA,sEAAsE;QACtE,MAAM2F,YAAYR,SAAS,CAACN,UAAUG,GAAG,CAACG,SAAS;YAACA;YAAOvD;SAAG,GAAG;YAACA;SAAG;QAErEoC,kBAAkB;eAAIF;eAAc6B;SAAU;IAClD;IAEA;;;;KAIC,GACD,MAAMC,YAAY,CAACC,KAAeC;QAC9B,MAAMC,aAAaF,IAAIlE,MAAM,CAACC,CAAAA,KAAM,CAACmD,UAAUC,GAAG,CAACpD;QAEnD,IAAI,CAACkE,WAAW;YACZ,MAAMT,WAAW,IAAIP,IAAIiB;YAEzB/B,kBAAkBF,UAAUnC,MAAM,CAAC3B,CAAAA,SAAU,CAACqF,SAASL,GAAG,CAAChF;YAC3D;QACJ;QAEA,MAAM2F,YAAYI,WAAWpE,MAAM,CAACC,CAAAA,KAAM,CAACiD,UAAUG,GAAG,CAACpD;QAEzD,IAAI+D,UAAU3D,MAAM,GAAG,GAAG;YACtBgC,kBAAkB;mBAAIF;mBAAc6B;aAAU;QAClD;IACJ;IAEA,MAAMK,QAAQ;QACV7B,SAAS;QACTP;IACJ;IAEA,qBACI,MAAC1F;QACGyF,MAAMA;QACNC,SAASoC;QACTvF,MAAK;QACLI,cAAW;QACXnB,WAAWJ,OAAO2G,MAAM;QACxBC,WAAQ;;0BAER,KAAChI,OAAO0B,OAAO;gBAACF,WAAWJ,OAAO6G,OAAO;0BACrC,cAAA,MAAC/D;oBAAI1C,WAAWJ,OAAO8G,IAAI;;sCACvB,KAAChI;4BACGiI,KAAK/B;4BACL7D,MAAK;4BACLf,WAAWJ,OAAOgH,MAAM;4BACxBC,aAAY;4BACZC,OAAOtC;4BACPuC,UAAUrG,CAAAA,QAAS+D,SAAS/D,MAAMsG,MAAM,CAACF,KAAK;4BAC9CG,SAAS,IAAMxC,SAAS;4BACxBtD,cAAW;;sCAEf,KAACuB;4BACG1C,WAAWhB,WAAWY,OAAOsH,OAAO,EAAExC,eAAe9E,OAAOuH,aAAa;sCAExErC,cAAcxC,MAAM,KAAK,kBACtB,KAACI;gCAAI1C,WAAWJ,OAAOwH,KAAK;0CAOxB,cAAA,KAACzI;oCAAK0I,OAAO;8CACR3C,4BACG,MAACzF;;4CAAS;4CAC4BuF;4CAAM;;yCAG5C;;+CAKZ,KAAC9B;gCAAI1C,WAAW0E,cAAcgB,YAAY9F,OAAO0H,OAAO;0CACnDxC,cAAczB,GAAG,CAACC,CAAAA,sBACf,MAACZ;wCAEG1C,WAAW0E,cAAcgB,YAAY9F,OAAO0D,KAAK;;4CAMhD,CAACoB,6BACE,KAAC/F;gDACG4E,SAAQ;gDACRxC,MAAK;gDACLf,WAAWJ,OAAO2H,UAAU;0DAE3BjE,MAAMP,KAAK;;4CAGnBO,MAAM4B,KAAK,CAAC7B,GAAG,CAAC9B,CAAAA;oDAICwD;qEAHd,KAACzD;oDAEGC,MAAMA;oDACNC,QAAQ,GAAEuD,iBAAAA,UAAUc,GAAG,CAACtE,KAAKW,EAAE,eAArB6C,4BAAAA,iBAA0B;wDAACxD,KAAKW,EAAE;qDAAC;oDAC7CT,UAAUA;oDACVC,UAAUA;oDACVC,aAAa4D;oDACb3D,aAAasE;oDACbrE,YAAYyE;mDAPP/E,KAAKW,EAAE;;;uCAlBfoB,MAAMP,KAAK;;;;;;0BAmC5C,KAACpD;gBACG6H,WACIjD,qBACC,CAAA;wBAEG,0EAA0E;oBAC1EK;oBAFAL;qBAEAK,qBAAAA,UAAU6C,OAAO,cAAjB7C,yCAAAA,mBAAmB8C,KAAK;gBAC5B,CAAA;;;;AAKpB,EAAE"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/titan-layout/navigation-catalog/navigation-catalog.tsx"],"sourcesContent":["import { Button, Dialog, Icon, SearchField, Text, Tooltip } from '@servicetitan/anvil2';\nimport IconLock from '@servicetitan/anvil2/assets/icons/material/round/lock.svg';\nimport IconStar from '@servicetitan/anvil2/assets/icons/material/round/star.svg';\nimport IconStarBorder from '@servicetitan/anvil2/assets/icons/material/round/star_border.svg';\nimport classNames from 'classnames';\nimport { FC, Fragment, MouseEvent, useMemo, useRef, useState } from 'react';\nimport {\n filterCatalogGroups,\n getCatalogChildren,\n getCatalogSubmenuGroups,\n} from '../../../utils/catalog-groups';\nimport { NavigationItemData } from '../../../utils/navigation';\nimport { getCounterTag } from '../../../utils/side-nav';\nimport { BadgeTag } from '../../badge-tag';\nimport { PinnedNavigationConfig } from '../interface';\nimport { useTitanLayoutContext } from '../layout-context';\nimport { NavigationCatalogFooter } from './navigation-catalog-footer';\nimport * as styles from './navigation-catalog.module.less';\n\n/** Shown in place of the pin control for a destination the user may not unpin. */\nconst LockedPin: FC = () => (\n <Tooltip>\n <Tooltip.Trigger>\n <Icon svg={IconLock} className={styles.lock} />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n Cannot be removed from Favorites\n </Tooltip.Content>\n </Tooltip>\n);\n\ninterface PinToggleProps {\n /** Mixed modules stay highlighted but use an outline until all pages are pinned. */\n pinned: boolean | 'mixed';\n label: string;\n onToggle: () => void;\n}\n\nconst PinToggle: FC<PinToggleProps> = ({ pinned, label, onToggle }) => {\n const toggle = (event: MouseEvent) => {\n // The control sits inside the row but is not a navigation affordance.\n event.preventDefault();\n event.stopPropagation();\n onToggle();\n };\n\n return (\n <Tooltip delay={300}>\n <Tooltip.Trigger>\n <Button\n appearance=\"ghost\"\n size=\"small\"\n icon={pinned === true ? IconStar : IconStarBorder}\n className={classNames(styles.star, pinned && styles.starActive)}\n aria-label={label}\n aria-pressed={pinned}\n data-pendo=\"global-nav-favorite-star\"\n onClick={toggle}\n />\n </Tooltip.Trigger>\n <Tooltip.Content style={{ pointerEvents: 'none' }}>\n {pinned === 'mixed'\n ? 'Add this module to Favorites'\n : pinned\n ? 'Remove from Favorites'\n : 'Add to Favorites'}\n </Tooltip.Content>\n </Tooltip>\n );\n};\n\ninterface CatalogRowsProps {\n item: NavigationItemData;\n /**\n * Every id the module's own control covers, taken from the unfiltered item.\n *\n * `item` may have been narrowed by the search, so deriving this from it would let the control\n * act on only the pages that happen to be visible — silently stranding the rest.\n */\n scopeIds: string[];\n isPinned: (id: string) => boolean;\n isLocked: (id: string) => boolean;\n /** `ownerId` is the module the page belongs to; omitted for the module's own row. */\n onTogglePin: (id: string, ownerId?: string) => void;\n onToggleAll: (ids: string[], shouldPin: boolean) => void;\n onNavigate: () => void;\n}\n\nconst CatalogRows: FC<CatalogRowsProps> = ({\n item,\n scopeIds,\n isPinned,\n isLocked,\n onTogglePin,\n onToggleAll,\n onNavigate,\n}) => {\n const { NavigationComponent } = useTitanLayoutContext();\n const submenuGroups = getCatalogSubmenuGroups(item);\n\n /*\n * Minus anything locked — locked destinations are always present, so the module's control\n * neither reports on them nor acts on them.\n */\n const everything = scopeIds.filter(id => !isLocked(id));\n const anyPinned = everything.some(isPinned);\n const allPinned = everything.length > 0 && everything.every(isPinned);\n const tag = getCounterTag(item.counter, item.tag);\n\n return (\n <div className={styles.moduleGroup}>\n <div className={styles.row}>\n <NavigationComponent\n to={item.to}\n className={styles.link}\n onClick={onNavigate}\n aria-label={item.title}\n >\n {item.icon && (\n <Icon svg={item.icon} size=\"medium\" className={styles.moduleIcon} />\n )}\n <Text inline size=\"small\" className={styles.moduleTitle}>\n {item.title}\n </Text>\n {/*\n * The same counter the rail shows, so a destination that is asking for\n * attention still says so here — the catalog is the only place an unfavourited\n * one is visible at all.\n */}\n {!!tag && <BadgeTag data={tag} className={styles.badge} />}\n </NavigationComponent>\n {/*\n * One control for the whole module: favouriting it takes its pages with it, and\n * unfavouriting it takes them away again.\n *\n * A gold outline marks a partially favourited module. Fill the star only when\n * its full, unfiltered scope is favourited, including pages hidden by search.\n * Clicking the outline adds the remaining pages; clicking the filled star removes all.\n */}\n {isLocked(item.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={allPinned ? true : anyPinned ? 'mixed' : false}\n label={\n allPinned\n ? `Unpin ${item.title} from the navigation`\n : `Pin ${item.title} to the navigation`\n }\n onToggle={() => onToggleAll(everything, !allPinned)}\n />\n )}\n </div>\n {submenuGroups.map(group => (\n <div key={group.links[0].id}>\n {group.title && (\n <Text variant=\"eyebrow\" size=\"small\" className={styles.submenuTitle}>\n {group.title}\n </Text>\n )}\n {group.links.map(child => {\n /*\n * Page titles are only unique within their item — \"Settings\" and \"Reports\" recur\n * across the catalog — and the row that tells them apart is a separate element, so\n * it is not part of any of these names. Anything reading the rows on their own, a\n * screen reader running through the links or a voice command naming one, would\n * otherwise be given the same name several times.\n */\n const qualifiedTitle = `${item.title}: ${child.title}`;\n const childPinned = isPinned(child.id);\n const childTag = getCounterTag(child.counter, child.tag);\n\n return (\n <div key={child.id} className={styles.row}>\n <NavigationComponent\n to={child.to}\n className={classNames(styles.link, styles.childLink)}\n onClick={onNavigate}\n aria-label={qualifiedTitle}\n >\n <Text inline className={styles.childTitle}>\n {child.title}\n </Text>\n {!!childTag && (\n <BadgeTag data={childTag} className={styles.badge} />\n )}\n </NavigationComponent>\n {isLocked(child.id) ? (\n <LockedPin />\n ) : (\n <PinToggle\n pinned={childPinned}\n label={\n childPinned\n ? `Unpin ${qualifiedTitle} from the navigation`\n : `Pin ${qualifiedTitle} to the navigation`\n }\n onToggle={() => onTogglePin(child.id, item.id)}\n />\n )}\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport interface NavigationCatalogProps extends PinnedNavigationConfig {\n open: boolean;\n onClose: () => void;\n}\n\n/**\n * Every destination the host offers, grouped into sections, with a star on every row.\n *\n * The catalog shows what it is given and nothing more — the host has already resolved what this\n * user may reach, so the catalog cannot offer a destination they do not have.\n */\nexport const NavigationCatalog: FC<NavigationCatalogProps> = ({\n open,\n onClose,\n groups,\n pinnedIds,\n lockedIds,\n onPinnedIdsChange,\n onResetToDefaults,\n}) => {\n const [query, setQuery] = useState('');\n const isSearching = query.trim().length > 0;\n const searchRef = useRef<HTMLInputElement>(null);\n\n /*\n * Resolved here rather than by the host, so a catalog that costs requests to enumerate is paid\n * for on opening the dialog and not on every page load. This component is mounted only while\n * open, so the call happens once per open unless the host changes the function.\n */\n const resolvedGroups = useMemo(() => groups(), [groups]);\n\n const visibleGroups = useMemo(\n () => filterCatalogGroups(resolvedGroups, query),\n [resolvedGroups, query]\n );\n\n /*\n * The pin scope of every item, keyed by id and built before the search narrows anything, so a\n * module's control still covers the pages a query is currently hiding.\n */\n const scopeById = useMemo(\n () =>\n new Map(\n resolvedGroups\n .flatMap(group => group.items)\n .map(item => [\n item.id,\n [item.id, ...getCatalogChildren(item).map(child => child.id)],\n ])\n ),\n [resolvedGroups]\n );\n const pinnedSet = useMemo(() => new Set(pinnedIds), [pinnedIds]);\n const lockedSet = useMemo(() => new Set(lockedIds ?? []), [lockedIds]);\n\n const isPinned = (id: string) => pinnedSet.has(id);\n const isLocked = (id: string) => lockedSet.has(id);\n\n /**\n * Favourites or unfavourites one row, carrying its module with it.\n *\n * A page is reached through its module, so the two travel together: favouriting a page brings\n * the module along when it is not already there, and unfavouriting the last one takes the\n * module with it. Leaving the module behind on its own would not read as an emptier\n * navigation — it would put the module back carrying every page it has.\n *\n * Locked ids never reach a proposal. The controls that would move them are not rendered, so\n * that part is belt and braces — but the host's list is the thing that gets persisted, and a\n * locked destination has no business appearing in or disappearing from it.\n */\n const togglePin = (id: string, ownerId?: string) => {\n if (lockedSet.has(id)) {\n return;\n }\n\n /** The module, when it is a separate row that this page can legitimately move. */\n const owner = ownerId !== id && ownerId && !lockedSet.has(ownerId) ? ownerId : undefined;\n\n if (pinnedSet.has(id)) {\n const removing = new Set([id]);\n\n if (owner) {\n const siblings = (scopeById.get(owner) ?? []).filter(\n scoped => scoped !== owner && scoped !== id\n );\n\n if (!siblings.some(sibling => pinnedSet.has(sibling))) {\n removing.add(owner);\n }\n }\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n // Module first, so it reads ahead of its own page in the saved order.\n const additions = owner && !pinnedSet.has(owner) ? [owner, id] : [id];\n\n onPinnedIdsChange([...pinnedIds, ...additions]);\n };\n\n /*\n * One proposal for the whole set rather than one per row, so a host that persists on every\n * change writes once, and every intermediate list — states the user never asked for — never\n * exists.\n */\n const toggleAll = (ids: string[], shouldPin: boolean) => {\n const actionable = ids.filter(id => !lockedSet.has(id));\n\n if (!shouldPin) {\n const removing = new Set(actionable);\n\n onPinnedIdsChange(pinnedIds.filter(pinned => !removing.has(pinned)));\n return;\n }\n\n const additions = actionable.filter(id => !pinnedSet.has(id));\n\n if (additions.length > 0) {\n onPinnedIdsChange([...pinnedIds, ...additions]);\n }\n };\n\n const close = () => {\n setQuery('');\n onClose();\n };\n\n return (\n <Dialog\n open={open}\n onClose={close}\n size=\"xlarge\"\n aria-label=\"All Navigation\"\n className={styles.dialog}\n data-cy=\"navigation-catalog-dialog\"\n >\n <Dialog.Content className={styles.content}>\n <div className={styles.body}>\n <SearchField\n ref={searchRef}\n size=\"small\"\n className={styles.search}\n placeholder=\"Go to ...\"\n value={query}\n onChange={event => setQuery(event.target.value)}\n onClear={() => setQuery('')}\n aria-label=\"Go to\"\n />\n <div\n className={classNames(styles.results, isSearching && styles.searchResults)}\n >\n {visibleGroups.length === 0 ? (\n <div className={styles.empty}>\n {/*\n * Two different nothings: a search that matched no row, and a host\n * that offered no rows at all. The second is a real state — the\n * catalog is reachable whether or not there is anything in it — and\n * quoting an empty query at the user does not describe it.\n */}\n <Text subdued>\n {isSearching ? (\n <Fragment>\n No navigation items match “{query}”.\n </Fragment>\n ) : (\n 'There are no navigation items to show.'\n )}\n </Text>\n </div>\n ) : (\n <div className={isSearching ? undefined : styles.columns}>\n {visibleGroups.map(group => (\n <div\n key={group.title}\n className={isSearching ? undefined : styles.group}\n >\n {/*\n * Browse uses titled columns. Search stacks matches at full\n * width, keeping only each module's own submenu headings.\n */}\n {!isSearching && (\n <Text\n variant=\"eyebrow\"\n size=\"medium\"\n className={styles.groupTitle}\n >\n {group.title}\n </Text>\n )}\n {group.items.map(item => (\n <CatalogRows\n key={item.id}\n item={item}\n scopeIds={scopeById.get(item.id) ?? [item.id]}\n isPinned={isPinned}\n isLocked={isLocked}\n onTogglePin={togglePin}\n onToggleAll={toggleAll}\n onNavigate={close}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </Dialog.Content>\n <NavigationCatalogFooter\n onRestore={\n onResetToDefaults &&\n (() => {\n onResetToDefaults();\n // The control disappears with the host's reset; keep focus in the dialog.\n searchRef.current?.focus();\n })\n }\n />\n </Dialog>\n );\n};\n"],"names":["Button","Dialog","Icon","SearchField","Text","Tooltip","IconLock","IconStar","IconStarBorder","classNames","Fragment","useMemo","useRef","useState","filterCatalogGroups","getCatalogChildren","getCatalogSubmenuGroups","getCounterTag","BadgeTag","useTitanLayoutContext","NavigationCatalogFooter","styles","LockedPin","Trigger","svg","className","lock","Content","style","pointerEvents","PinToggle","pinned","label","onToggle","toggle","event","preventDefault","stopPropagation","delay","appearance","size","icon","star","starActive","aria-label","aria-pressed","data-pendo","onClick","CatalogRows","item","scopeIds","isPinned","isLocked","onTogglePin","onToggleAll","onNavigate","NavigationComponent","submenuGroups","everything","filter","id","anyPinned","some","allPinned","length","every","tag","counter","div","moduleGroup","row","to","link","title","moduleIcon","inline","moduleTitle","data","badge","map","group","variant","submenuTitle","links","child","qualifiedTitle","childPinned","childTag","childLink","childTitle","NavigationCatalog","open","onClose","groups","pinnedIds","lockedIds","onPinnedIdsChange","onResetToDefaults","query","setQuery","isSearching","trim","searchRef","resolvedGroups","visibleGroups","scopeById","Map","flatMap","items","pinnedSet","Set","lockedSet","has","togglePin","ownerId","owner","undefined","removing","siblings","get","scoped","sibling","add","additions","toggleAll","ids","shouldPin","actionable","close","dialog","data-cy","content","body","ref","search","placeholder","value","onChange","target","onClear","results","searchResults","empty","subdued","columns","groupTitle","onRestore","current","focus"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"side-nav.test.d.ts","sourceRoot":"","sources":["../../../src/utils/__tests__/side-nav.test.ts"],"names":[],"mappings":""}
|
package/dist/utils/side-nav.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { CounterTagProps } from '../components/counter-tag';
|
|
2
2
|
import { CounterTagData, CounterTagValue } from './counter-tag';
|
|
3
3
|
import { NavigationSubmenuData } from './navigation';
|
|
4
|
+
/**
|
|
5
|
+
* A module with sub-items always routes to the first one, in the nav's existing order, rather
|
|
6
|
+
* than whatever destination the host configured for the module row itself (3.5a). A host-supplied
|
|
7
|
+
* `to` only applies to modules with no submenu.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getModuleDestination(to: string, submenu: NavigationSubmenuData | undefined): string;
|
|
10
|
+
/**
|
|
11
|
+
* Routing a module row to its first sub-item (see `getModuleDestination`) means the row's `to` no
|
|
12
|
+
* longer identifies the module for a host that has no explicit `isActive` of its own — the default
|
|
13
|
+
* "compare href to pathname" behavior would then only match the first sub-item's page, not the rest
|
|
14
|
+
* of the module. Synthesize an equivalent match across the module's own `to` and every sub-item
|
|
15
|
+
* instead, so the row still highlights on any of the module's pages, not just the first.
|
|
16
|
+
*
|
|
17
|
+
* Returns `undefined` when there's nothing to match, so a host can still supply its own `isActive`
|
|
18
|
+
* and take precedence (this only fills the gap left by the default behavior, it doesn't replace it).
|
|
19
|
+
*/
|
|
20
|
+
export declare function getModuleActiveMatcher(to: string, submenu: NavigationSubmenuData | undefined): ((pathname: string) => boolean) | undefined;
|
|
4
21
|
export declare function getSubmenuGroupTag(submenu: NavigationSubmenuData | undefined, defaultTag: CounterTagProps | undefined): CounterTagProps | undefined;
|
|
5
22
|
export declare const getCounterTag: (counter: CounterTagValue | undefined, tag?: CounterTagData) => CounterTagProps | undefined;
|
|
6
23
|
//# sourceMappingURL=side-nav.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"side-nav.d.ts","sourceRoot":"","sources":["../../src/utils/side-nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,wBAAgB,kBAAkB,CAC9B,OAAO,EAAE,qBAAqB,GAAG,SAAS,EAC1C,UAAU,EAAE,eAAe,GAAG,SAAS,GACxC,eAAe,GAAG,SAAS,CAyB7B;AAED,eAAO,MAAM,aAAa,GACtB,SAAS,eAAe,GAAG,SAAS,EACpC,MAAM,cAAc,KACrB,eAAe,GAAG,SAIpB,CAAC"}
|
|
1
|
+
{"version":3,"file":"side-nav.d.ts","sourceRoot":"","sources":["../../src/utils/side-nav.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAC3C,MAAM,CAQR;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAClC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAC3C,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAS7C;AAED,wBAAgB,kBAAkB,CAC9B,OAAO,EAAE,qBAAqB,GAAG,SAAS,EAC1C,UAAU,EAAE,eAAe,GAAG,SAAS,GACxC,eAAe,GAAG,SAAS,CAyB7B;AAED,eAAO,MAAM,aAAa,GACtB,SAAS,eAAe,GAAG,SAAS,EACpC,MAAM,cAAc,KACrB,eAAe,GAAG,SAIpB,CAAC"}
|
package/dist/utils/side-nav.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module with sub-items always routes to the first one, in the nav's existing order, rather
|
|
3
|
+
* than whatever destination the host configured for the module row itself (3.5a). A host-supplied
|
|
4
|
+
* `to` only applies to modules with no submenu.
|
|
5
|
+
*/ export function getModuleDestination(to, submenu) {
|
|
6
|
+
var _ref;
|
|
7
|
+
for (const group of (_ref = submenu === null || submenu === void 0 ? void 0 : submenu.groups) !== null && _ref !== void 0 ? _ref : []){
|
|
8
|
+
if (group.links.length > 0) {
|
|
9
|
+
return group.links[0].to;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Routing a module row to its first sub-item (see `getModuleDestination`) means the row's `to` no
|
|
16
|
+
* longer identifies the module for a host that has no explicit `isActive` of its own — the default
|
|
17
|
+
* "compare href to pathname" behavior would then only match the first sub-item's page, not the rest
|
|
18
|
+
* of the module. Synthesize an equivalent match across the module's own `to` and every sub-item
|
|
19
|
+
* instead, so the row still highlights on any of the module's pages, not just the first.
|
|
20
|
+
*
|
|
21
|
+
* Returns `undefined` when there's nothing to match, so a host can still supply its own `isActive`
|
|
22
|
+
* and take precedence (this only fills the gap left by the default behavior, it doesn't replace it).
|
|
23
|
+
*/ export function getModuleActiveMatcher(to, submenu) {
|
|
24
|
+
var _ref;
|
|
25
|
+
const candidates = [
|
|
26
|
+
...to ? [
|
|
27
|
+
to
|
|
28
|
+
] : [],
|
|
29
|
+
...(_ref = submenu === null || submenu === void 0 ? void 0 : submenu.groups.flatMap((group)=>group.links.map((link)=>link.to))) !== null && _ref !== void 0 ? _ref : []
|
|
30
|
+
];
|
|
31
|
+
return candidates.length > 0 ? (pathname)=>candidates.some((candidate)=>pathname.startsWith(candidate)) : undefined;
|
|
32
|
+
}
|
|
1
33
|
export function getSubmenuGroupTag(submenu, defaultTag) {
|
|
2
34
|
if (!submenu) {
|
|
3
35
|
return defaultTag;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/side-nav.ts"],"sourcesContent":["import { CounterTagProps } from '../components/counter-tag';\nimport { CounterTagData, CounterTagValue } from './counter-tag';\nimport { NavigationSubmenuData } from './navigation';\n\nexport function getSubmenuGroupTag(\n submenu: NavigationSubmenuData | undefined,\n defaultTag: CounterTagProps | undefined\n): CounterTagProps | undefined {\n if (!submenu) {\n return defaultTag;\n }\n\n let tagValue: number | boolean | undefined = undefined;\n\n for (const group of submenu.groups) {\n for (const link of group.links) {\n const ltv: number | boolean | undefined = link.counter ?? link.tag?.value;\n\n if (ltv) {\n if (typeof ltv === 'number') {\n if (typeof tagValue !== 'number') {\n tagValue = 0;\n }\n tagValue += ltv;\n } else if (typeof tagValue !== 'number') {\n tagValue = true;\n }\n }\n }\n }\n\n return tagValue ? getCounterTag(tagValue) : defaultTag;\n}\n\nexport const getCounterTag = (\n counter: CounterTagValue | undefined,\n tag?: CounterTagData\n): CounterTagProps | undefined => {\n const value = counter ?? tag?.value;\n\n return value ? { value, className: '' } : undefined;\n};\n"],"names":["
|
|
1
|
+
{"version":3,"sources":["../../src/utils/side-nav.ts"],"sourcesContent":["import { CounterTagProps } from '../components/counter-tag';\nimport { CounterTagData, CounterTagValue } from './counter-tag';\nimport { NavigationSubmenuData } from './navigation';\n\n/**\n * A module with sub-items always routes to the first one, in the nav's existing order, rather\n * than whatever destination the host configured for the module row itself (3.5a). A host-supplied\n * `to` only applies to modules with no submenu.\n */\nexport function getModuleDestination(\n to: string,\n submenu: NavigationSubmenuData | undefined\n): string {\n for (const group of submenu?.groups ?? []) {\n if (group.links.length > 0) {\n return group.links[0].to;\n }\n }\n\n return to;\n}\n\n/**\n * Routing a module row to its first sub-item (see `getModuleDestination`) means the row's `to` no\n * longer identifies the module for a host that has no explicit `isActive` of its own — the default\n * \"compare href to pathname\" behavior would then only match the first sub-item's page, not the rest\n * of the module. Synthesize an equivalent match across the module's own `to` and every sub-item\n * instead, so the row still highlights on any of the module's pages, not just the first.\n *\n * Returns `undefined` when there's nothing to match, so a host can still supply its own `isActive`\n * and take precedence (this only fills the gap left by the default behavior, it doesn't replace it).\n */\nexport function getModuleActiveMatcher(\n to: string,\n submenu: NavigationSubmenuData | undefined\n): ((pathname: string) => boolean) | undefined {\n const candidates = [\n ...(to ? [to] : []),\n ...(submenu?.groups.flatMap(group => group.links.map(link => link.to)) ?? []),\n ];\n\n return candidates.length > 0\n ? (pathname: string) => candidates.some(candidate => pathname.startsWith(candidate))\n : undefined;\n}\n\nexport function getSubmenuGroupTag(\n submenu: NavigationSubmenuData | undefined,\n defaultTag: CounterTagProps | undefined\n): CounterTagProps | undefined {\n if (!submenu) {\n return defaultTag;\n }\n\n let tagValue: number | boolean | undefined = undefined;\n\n for (const group of submenu.groups) {\n for (const link of group.links) {\n const ltv: number | boolean | undefined = link.counter ?? link.tag?.value;\n\n if (ltv) {\n if (typeof ltv === 'number') {\n if (typeof tagValue !== 'number') {\n tagValue = 0;\n }\n tagValue += ltv;\n } else if (typeof tagValue !== 'number') {\n tagValue = true;\n }\n }\n }\n }\n\n return tagValue ? getCounterTag(tagValue) : defaultTag;\n}\n\nexport const getCounterTag = (\n counter: CounterTagValue | undefined,\n tag?: CounterTagData\n): CounterTagProps | undefined => {\n const value = counter ?? tag?.value;\n\n return value ? { value, className: '' } : undefined;\n};\n"],"names":["getModuleDestination","to","submenu","group","groups","links","length","getModuleActiveMatcher","candidates","flatMap","map","link","pathname","some","candidate","startsWith","undefined","getSubmenuGroupTag","defaultTag","tagValue","ltv","counter","tag","value","getCounterTag","className"],"mappings":"AAIA;;;;CAIC,GACD,OAAO,SAASA,qBACZC,EAAU,EACVC,OAA0C;;IAE1C,KAAK,MAAMC,iBAASD,oBAAAA,8BAAAA,QAASE,MAAM,uCAAI,EAAE,CAAE;QACvC,IAAID,MAAME,KAAK,CAACC,MAAM,GAAG,GAAG;YACxB,OAAOH,MAAME,KAAK,CAAC,EAAE,CAACJ,EAAE;QAC5B;IACJ;IAEA,OAAOA;AACX;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASM,uBACZN,EAAU,EACVC,OAA0C;;IAE1C,MAAMM,aAAa;WACXP,KAAK;YAACA;SAAG,GAAG,EAAE;mBACdC,oBAAAA,8BAAAA,QAASE,MAAM,CAACK,OAAO,CAACN,CAAAA,QAASA,MAAME,KAAK,CAACK,GAAG,CAACC,CAAAA,OAAQA,KAAKV,EAAE,yCAAM,EAAE;KAC/E;IAED,OAAOO,WAAWF,MAAM,GAAG,IACrB,CAACM,WAAqBJ,WAAWK,IAAI,CAACC,CAAAA,YAAaF,SAASG,UAAU,CAACD,cACvEE;AACV;AAEA,OAAO,SAASC,mBACZf,OAA0C,EAC1CgB,UAAuC;IAEvC,IAAI,CAAChB,SAAS;QACV,OAAOgB;IACX;IAEA,IAAIC,WAAyCH;IAE7C,KAAK,MAAMb,SAASD,QAAQE,MAAM,CAAE;QAChC,KAAK,MAAMO,QAAQR,MAAME,KAAK,CAAE;gBACcM;gBAAgBA;YAA1D,MAAMS,OAAoCT,gBAAAA,KAAKU,OAAO,cAAZV,2BAAAA,iBAAgBA,YAAAA,KAAKW,GAAG,cAARX,gCAAAA,UAAUY,KAAK;YAEzE,IAAIH,KAAK;gBACL,IAAI,OAAOA,QAAQ,UAAU;oBACzB,IAAI,OAAOD,aAAa,UAAU;wBAC9BA,WAAW;oBACf;oBACAA,YAAYC;gBAChB,OAAO,IAAI,OAAOD,aAAa,UAAU;oBACrCA,WAAW;gBACf;YACJ;QACJ;IACJ;IAEA,OAAOA,WAAWK,cAAcL,YAAYD;AAChD;AAEA,OAAO,MAAMM,gBAAgB,CACzBH,SACAC;IAEA,MAAMC,QAAQF,oBAAAA,qBAAAA,UAAWC,gBAAAA,0BAAAA,IAAKC,KAAK;IAEnC,OAAOA,QAAQ;QAAEA;QAAOE,WAAW;IAAG,IAAIT;AAC9C,EAAE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/navigation",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.5.0",
|
|
4
4
|
"description": "Navigation components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"less": true,
|
|
36
36
|
"webpack": false
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "f0005b6f4fa3607d3e3a072d5491aa062d4de31a"
|
|
39
39
|
}
|
|
@@ -401,7 +401,6 @@
|
|
|
401
401
|
background-color: @bg-color;
|
|
402
402
|
|
|
403
403
|
.toggle-wrapper {
|
|
404
|
-
display: flex;
|
|
405
404
|
padding: @spacing-1 @spacing-1 @spacing-1 14px;
|
|
406
405
|
}
|
|
407
406
|
|
|
@@ -620,6 +619,7 @@
|
|
|
620
619
|
|
|
621
620
|
.submenu-popover-title[data-anv][data-anv] {
|
|
622
621
|
color: @color-white;
|
|
622
|
+
font-size: @typescale-2;
|
|
623
623
|
margin-bottom: @spacing-0;
|
|
624
624
|
margin-top: @spacing-1;
|
|
625
625
|
padding: @spacing-1;
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { Button, Divider, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
1
|
+
import { Button, Divider, Flex, Icon, Popover, Text, ThemeProvider } from '@servicetitan/anvil2';
|
|
2
2
|
import SvgClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
3
3
|
import SvgCollapseAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_less.svg';
|
|
4
4
|
import SvgExpandAll from '@servicetitan/anvil2/assets/icons/material/round/unfold_more.svg';
|
|
5
5
|
import SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';
|
|
6
6
|
import SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';
|
|
7
7
|
import classNames from 'classnames';
|
|
8
|
-
import { Children, FC, Fragment, ReactElement, isValidElement } from 'react';
|
|
8
|
+
import { Children, FC, Fragment, ReactElement, isValidElement, useCallback, useRef } from 'react';
|
|
9
9
|
import { NavigationItemData, NavigationSubmenuData } from '../../utils/navigation';
|
|
10
10
|
import { NavLinkComponentProps } from '../../utils/navigation-context';
|
|
11
11
|
import { getModuleNotificationCount } from '../../utils/navigation-notifications';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getCounterTag,
|
|
14
|
+
getModuleActiveMatcher,
|
|
15
|
+
getModuleDestination,
|
|
16
|
+
getSubmenuGroupTag,
|
|
17
|
+
} from '../../utils/side-nav';
|
|
13
18
|
import { PinnedNavigationConfig } from './interface';
|
|
14
19
|
import { NavigationComponentProps } from './interface-internal';
|
|
15
20
|
import { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';
|
|
@@ -219,12 +224,12 @@ export const LayoutSidebar: FC<LayoutSidebarProps> = ({
|
|
|
219
224
|
{!mobile && (
|
|
220
225
|
<ThemeProvider mode="dark" className={Styles.navFooter}>
|
|
221
226
|
<div className={Styles.divider} />
|
|
222
|
-
<
|
|
227
|
+
<Flex direction="column" className={Styles.toggleWrapper}>
|
|
223
228
|
<SideNavigationOptionsToggle
|
|
224
229
|
expanded={barExpanded}
|
|
225
230
|
onExpandedChange={() => onBarExpandChange(!barExpanded)}
|
|
226
231
|
/>
|
|
227
|
-
</
|
|
232
|
+
</Flex>
|
|
228
233
|
</ThemeProvider>
|
|
229
234
|
)}
|
|
230
235
|
</div>
|
|
@@ -242,6 +247,8 @@ const SideNavigationOptionsToggle: FC<{
|
|
|
242
247
|
<Button
|
|
243
248
|
appearance="ghost"
|
|
244
249
|
size="small"
|
|
250
|
+
flexGrow={1}
|
|
251
|
+
justifyContent="flex-start"
|
|
245
252
|
icon={expanded ? SvgCollapse : SvgExpand}
|
|
246
253
|
data-cy="navigation-left-options"
|
|
247
254
|
data-pendo="navigation-left-options"
|
|
@@ -251,7 +258,7 @@ const SideNavigationOptionsToggle: FC<{
|
|
|
251
258
|
onClick={() => onExpandedChange?.(!expanded)}
|
|
252
259
|
/>,
|
|
253
260
|
expanded ? 'Collapse Menu' : 'Expand Menu',
|
|
254
|
-
{ placement: 'right' }
|
|
261
|
+
{ placement: 'right', pointerEventsNone: true }
|
|
255
262
|
);
|
|
256
263
|
|
|
257
264
|
/** Side Navigation menu item */
|
|
@@ -279,13 +286,28 @@ const SideNavigationGroupItem: FC<{
|
|
|
279
286
|
} = useTitanLayoutContext();
|
|
280
287
|
|
|
281
288
|
const tag = getSubmenuGroupTag(item.submenu, getCounterTag(item.counter, item.tag));
|
|
289
|
+
const isActive = item.isActive ?? getModuleActiveMatcher(item.to, item.submenu);
|
|
290
|
+
|
|
291
|
+
/*
|
|
292
|
+
* Some destinations (legacy monolith pages) never fire the route-change signal the popover
|
|
293
|
+
* would otherwise rely on to dismiss itself, leaving it stranded open over the loaded page.
|
|
294
|
+
* Closing explicitly on click, rather than waiting on navigation to settle, works everywhere.
|
|
295
|
+
*/
|
|
296
|
+
const popoverRef = useRef<{ closePopover: () => void }>(null);
|
|
297
|
+
const handleSubmenuLinkClick = useCallback(
|
|
298
|
+
(itemId: string, submenuItemId?: string) => {
|
|
299
|
+
popoverRef.current?.closePopover();
|
|
300
|
+
onNavigationLinkClick?.(itemId, submenuItemId);
|
|
301
|
+
},
|
|
302
|
+
[onNavigationLinkClick]
|
|
303
|
+
);
|
|
282
304
|
|
|
283
305
|
return barExpanded ? (
|
|
284
306
|
<InternalSideNavigationGroup
|
|
285
307
|
id={item.id}
|
|
286
|
-
to={item.to}
|
|
308
|
+
to={getModuleDestination(item.to, item.submenu)}
|
|
287
309
|
title={item.title}
|
|
288
|
-
isActive={
|
|
310
|
+
isActive={isActive}
|
|
289
311
|
icon={item.icon}
|
|
290
312
|
iconActive={item.iconActive}
|
|
291
313
|
className={item.className}
|
|
@@ -304,6 +326,7 @@ const SideNavigationGroupItem: FC<{
|
|
|
304
326
|
</InternalSideNavigationGroup>
|
|
305
327
|
) : (
|
|
306
328
|
<Popover
|
|
329
|
+
ref={popoverRef}
|
|
307
330
|
placement="right-start"
|
|
308
331
|
openOnHover={!touchDevice}
|
|
309
332
|
delay={200}
|
|
@@ -315,9 +338,9 @@ const SideNavigationGroupItem: FC<{
|
|
|
315
338
|
<div {...triggerProps}>
|
|
316
339
|
<InternalSideNavigationLink
|
|
317
340
|
id={item.id}
|
|
318
|
-
to={item.to}
|
|
341
|
+
to={getModuleDestination(item.to, item.submenu)}
|
|
319
342
|
title={item.collapsedTitle ?? item.title}
|
|
320
|
-
isActive={
|
|
343
|
+
isActive={isActive}
|
|
321
344
|
icon={item.icon}
|
|
322
345
|
iconActive={item.iconActive}
|
|
323
346
|
className={classNames(
|
|
@@ -351,6 +374,7 @@ const SideNavigationGroupItem: FC<{
|
|
|
351
374
|
parentId={item.id}
|
|
352
375
|
groups={item.submenu?.groups ?? []}
|
|
353
376
|
navigationComponent={navigationComponent}
|
|
377
|
+
onNavigationLinkClick={handleSubmenuLinkClick}
|
|
354
378
|
/>
|
|
355
379
|
</div>
|
|
356
380
|
</div>
|
|
@@ -95,7 +95,7 @@ export const NavigationCatalogTrigger: FC<NavigationCatalogTriggerProps> = ({
|
|
|
95
95
|
* selector written for a link to match it.
|
|
96
96
|
*/
|
|
97
97
|
'data-cy': 'navigation-catalog-trigger',
|
|
98
|
-
'data-pendo': '
|
|
98
|
+
'data-pendo': 'global-nav-go-to',
|
|
99
99
|
'appearance': 'secondary',
|
|
100
100
|
'size': 'small',
|
|
101
101
|
'aria-label': 'Go to',
|
|
@@ -54,6 +54,7 @@ const PinToggle: FC<PinToggleProps> = ({ pinned, label, onToggle }) => {
|
|
|
54
54
|
className={classNames(styles.star, pinned && styles.starActive)}
|
|
55
55
|
aria-label={label}
|
|
56
56
|
aria-pressed={pinned}
|
|
57
|
+
data-pendo="global-nav-favorite-star"
|
|
57
58
|
onClick={toggle}
|
|
58
59
|
/>
|
|
59
60
|
</Tooltip.Trigger>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getModuleActiveMatcher, getModuleDestination } from '../side-nav';
|
|
2
|
+
|
|
3
|
+
describe('getModuleDestination', () => {
|
|
4
|
+
it('routes to the first link of the first non-empty group when a submenu exists', () => {
|
|
5
|
+
expect(
|
|
6
|
+
getModuleDestination('/dispatch/default', {
|
|
7
|
+
groups: [
|
|
8
|
+
{
|
|
9
|
+
title: 'Boards',
|
|
10
|
+
links: [{ id: 'daily', title: 'Daily', to: '/dispatch/daily' }],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
})
|
|
14
|
+
).toBe('/dispatch/daily');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('skips empty groups to find the first group that actually has links', () => {
|
|
18
|
+
expect(
|
|
19
|
+
getModuleDestination('/reports', {
|
|
20
|
+
groups: [
|
|
21
|
+
{ title: 'Empty', links: [] },
|
|
22
|
+
{
|
|
23
|
+
title: 'Reports',
|
|
24
|
+
links: [{ id: 'sales', title: 'Sales', to: '/reports/sales' }],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
).toBe('/reports/sales');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('falls back to the host-supplied destination when there is no submenu', () => {
|
|
32
|
+
expect(getModuleDestination('/dashboard', undefined)).toBe('/dashboard');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to the host-supplied destination when the submenu has no links at all', () => {
|
|
36
|
+
expect(
|
|
37
|
+
getModuleDestination('/dashboard', { groups: [{ title: 'Empty', links: [] }] })
|
|
38
|
+
).toBe('/dashboard');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getModuleActiveMatcher', () => {
|
|
43
|
+
const submenu = {
|
|
44
|
+
groups: [
|
|
45
|
+
{
|
|
46
|
+
title: 'Boards',
|
|
47
|
+
links: [
|
|
48
|
+
{ id: 'daily', title: 'Daily', to: '/dispatch/daily' },
|
|
49
|
+
{ id: 'weekly', title: 'Weekly', to: '/dispatch/weekly' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it('matches a sub-item page even though the row now navigates to a different sub-item', () => {
|
|
56
|
+
const isActive = getModuleActiveMatcher('', submenu)!;
|
|
57
|
+
expect(isActive('/dispatch/weekly')).toBe(true);
|
|
58
|
+
expect(isActive('/dispatch/daily')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('also matches the module own to, when it has one, alongside its sub-items', () => {
|
|
62
|
+
const isActive = getModuleActiveMatcher('/dispatch', submenu)!;
|
|
63
|
+
expect(isActive('/dispatch/routes')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not match an unrelated page', () => {
|
|
67
|
+
const isActive = getModuleActiveMatcher('', submenu)!;
|
|
68
|
+
expect(isActive('/accounting/invoices')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns undefined when there is nothing to match against', () => {
|
|
72
|
+
expect(getModuleActiveMatcher('', undefined)).toBeUndefined();
|
|
73
|
+
expect(
|
|
74
|
+
getModuleActiveMatcher('', { groups: [{ title: 'Empty', links: [] }] })
|
|
75
|
+
).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/utils/side-nav.ts
CHANGED
|
@@ -2,6 +2,48 @@ import { CounterTagProps } from '../components/counter-tag';
|
|
|
2
2
|
import { CounterTagData, CounterTagValue } from './counter-tag';
|
|
3
3
|
import { NavigationSubmenuData } from './navigation';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A module with sub-items always routes to the first one, in the nav's existing order, rather
|
|
7
|
+
* than whatever destination the host configured for the module row itself (3.5a). A host-supplied
|
|
8
|
+
* `to` only applies to modules with no submenu.
|
|
9
|
+
*/
|
|
10
|
+
export function getModuleDestination(
|
|
11
|
+
to: string,
|
|
12
|
+
submenu: NavigationSubmenuData | undefined
|
|
13
|
+
): string {
|
|
14
|
+
for (const group of submenu?.groups ?? []) {
|
|
15
|
+
if (group.links.length > 0) {
|
|
16
|
+
return group.links[0].to;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return to;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Routing a module row to its first sub-item (see `getModuleDestination`) means the row's `to` no
|
|
25
|
+
* longer identifies the module for a host that has no explicit `isActive` of its own — the default
|
|
26
|
+
* "compare href to pathname" behavior would then only match the first sub-item's page, not the rest
|
|
27
|
+
* of the module. Synthesize an equivalent match across the module's own `to` and every sub-item
|
|
28
|
+
* instead, so the row still highlights on any of the module's pages, not just the first.
|
|
29
|
+
*
|
|
30
|
+
* Returns `undefined` when there's nothing to match, so a host can still supply its own `isActive`
|
|
31
|
+
* and take precedence (this only fills the gap left by the default behavior, it doesn't replace it).
|
|
32
|
+
*/
|
|
33
|
+
export function getModuleActiveMatcher(
|
|
34
|
+
to: string,
|
|
35
|
+
submenu: NavigationSubmenuData | undefined
|
|
36
|
+
): ((pathname: string) => boolean) | undefined {
|
|
37
|
+
const candidates = [
|
|
38
|
+
...(to ? [to] : []),
|
|
39
|
+
...(submenu?.groups.flatMap(group => group.links.map(link => link.to)) ?? []),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return candidates.length > 0
|
|
43
|
+
? (pathname: string) => candidates.some(candidate => pathname.startsWith(candidate))
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
5
47
|
export function getSubmenuGroupTag(
|
|
6
48
|
submenu: NavigationSubmenuData | undefined,
|
|
7
49
|
defaultTag: CounterTagProps | undefined
|