@servicetitan/navigation 9.0.1 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/components/counter-tag.d.ts +1 -4
  2. package/dist/components/counter-tag.d.ts.map +1 -1
  3. package/dist/components/counter-tag.js.map +1 -1
  4. package/dist/components/left-navigation/header-navigation-tiny-links.d.ts.map +1 -1
  5. package/dist/components/left-navigation/header-navigation-tiny-links.js +1 -1
  6. package/dist/components/left-navigation/header-navigation-tiny-links.js.map +1 -1
  7. package/dist/components/left-navigation/index.d.ts +2 -0
  8. package/dist/components/left-navigation/index.d.ts.map +1 -1
  9. package/dist/components/left-navigation/index.js +2 -0
  10. package/dist/components/left-navigation/index.js.map +1 -1
  11. package/dist/components/left-navigation/interface-internal.d.ts +10 -0
  12. package/dist/components/left-navigation/interface-internal.d.ts.map +1 -0
  13. package/dist/components/left-navigation/interface-internal.js +2 -0
  14. package/dist/components/left-navigation/interface-internal.js.map +1 -0
  15. package/dist/components/left-navigation/interface.d.ts +19 -0
  16. package/dist/components/left-navigation/interface.d.ts.map +1 -0
  17. package/dist/components/left-navigation/interface.js +2 -0
  18. package/dist/components/left-navigation/interface.js.map +1 -0
  19. package/dist/components/left-navigation/side-navigation-context.d.ts +8 -0
  20. package/dist/components/left-navigation/side-navigation-context.d.ts.map +1 -0
  21. package/dist/components/left-navigation/side-navigation-context.js +7 -0
  22. package/dist/components/left-navigation/side-navigation-context.js.map +1 -0
  23. package/dist/components/left-navigation/side-navigation-links-internal.d.ts +23 -0
  24. package/dist/components/left-navigation/side-navigation-links-internal.d.ts.map +1 -0
  25. package/dist/components/left-navigation/side-navigation-links-internal.js +29 -0
  26. package/dist/components/left-navigation/side-navigation-links-internal.js.map +1 -0
  27. package/dist/components/left-navigation/side-navigation-links.d.ts +6 -0
  28. package/dist/components/left-navigation/side-navigation-links.d.ts.map +1 -0
  29. package/dist/components/left-navigation/side-navigation-links.js +26 -0
  30. package/dist/components/left-navigation/side-navigation-links.js.map +1 -0
  31. package/dist/components/left-navigation/side-navigation.d.ts +13 -13
  32. package/dist/components/left-navigation/side-navigation.d.ts.map +1 -1
  33. package/dist/components/left-navigation/side-navigation.js +45 -30
  34. package/dist/components/left-navigation/side-navigation.js.map +1 -1
  35. package/dist/components/left-navigation/side-navigation.module.less +11 -4
  36. package/dist/components/left-navigation/side-navigation.stories.d.ts +5 -1
  37. package/dist/components/left-navigation/side-navigation.stories.d.ts.map +1 -1
  38. package/dist/components/left-navigation/side-navigation.stories.js +21 -2
  39. package/dist/components/left-navigation/side-navigation.stories.js.map +1 -1
  40. package/dist/utils/counter-tag.d.ts +8 -0
  41. package/dist/utils/counter-tag.d.ts.map +1 -0
  42. package/dist/utils/counter-tag.js +2 -0
  43. package/dist/utils/counter-tag.js.map +1 -0
  44. package/dist/utils/side-nav.d.ts +3 -0
  45. package/dist/utils/side-nav.d.ts.map +1 -0
  46. package/dist/utils/side-nav.js +27 -0
  47. package/dist/utils/side-nav.js.map +1 -0
  48. package/package.json +2 -2
  49. package/src/components/counter-tag.tsx +1 -5
  50. package/src/components/left-navigation/header-navigation-tiny-links.tsx +3 -0
  51. package/src/components/left-navigation/index.ts +2 -0
  52. package/src/components/left-navigation/interface-internal.ts +11 -0
  53. package/src/components/left-navigation/interface.ts +23 -0
  54. package/src/components/left-navigation/side-navigation-context.tsx +13 -0
  55. package/src/components/left-navigation/side-navigation-links-internal.tsx +128 -0
  56. package/src/components/left-navigation/side-navigation-links.tsx +51 -0
  57. package/src/components/left-navigation/side-navigation.module.less +11 -4
  58. package/src/components/left-navigation/side-navigation.module.less.d.ts +2 -1
  59. package/src/components/left-navigation/side-navigation.stories.tsx +58 -4
  60. package/src/components/left-navigation/side-navigation.tsx +123 -163
  61. package/src/utils/counter-tag.ts +11 -0
  62. package/src/utils/side-nav.ts +34 -0
@@ -1,6 +1,4 @@
1
1
  import { Icon, Popover, PopoverTriggerProps, Text } from '@servicetitan/anvil2';
2
- import SvgGroupCollapse from '@servicetitan/anvil2/assets/icons/material/round/expand_less.svg';
3
- import SvgGroupExpand from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
4
2
  import SvgCollapse from '@servicetitan/anvil2/assets/icons/st/gnav_menu_collapse.svg';
5
3
  import SvgExpand from '@servicetitan/anvil2/assets/icons/st/gnav_menu_expand.svg';
6
4
  import { Collapsible, Headline } from '@servicetitan/design-system';
@@ -8,29 +6,38 @@ import { Collapsible, Headline } from '@servicetitan/design-system';
8
6
  import classNames from 'classnames';
9
7
  import {
10
8
  CSSProperties,
9
+ Children,
11
10
  FC,
12
11
  Fragment,
13
12
  MouseEvent,
14
13
  ReactElement,
14
+ isValidElement,
15
15
  useCallback,
16
16
  useContext,
17
+ useMemo,
17
18
  } from 'react';
18
19
  import {
19
20
  HeaderNavigationItemData,
20
21
  HeaderNavigationItemSubmenu,
21
22
  HeaderNavigationItemSubmenuLink,
22
- NavLinkComponentProps,
23
23
  } from '../../utils/navigation';
24
24
  import { NavigationComponentContext } from '../../utils/navigation-context';
25
+ import { getSubmenuGroupTag } from '../../utils/side-nav';
25
26
  import { CounterTag } from '../counter-tag';
27
+ import {
28
+ SideNavigationExpandedState,
29
+ SideNavigationLinkProps,
30
+ SideNavigationTriggerProps,
31
+ } from './interface';
32
+ import { NavigationComponentProps, SideNavigationExpandedProps } from './interface-internal';
33
+ import { SideNavigationContext, SideNavigationContextType } from './side-navigation-context';
34
+ import { InternalSideNavigationLink } from './side-navigation-links-internal';
26
35
  import * as Styles from './side-navigation.module.less';
27
36
  import { withTooltip } from './with-tooltip';
28
37
 
29
- export interface SideNavigationExpandedState {
30
- bar: boolean;
31
- submenus?: string[];
32
- }
33
-
38
+ type OptionalChildren<T> =
39
+ | (ReactElement<T> | false | undefined | null)
40
+ | (ReactElement<T> | false | undefined | null)[];
34
41
  export interface SideNavigationProps {
35
42
  /** container class name */
36
43
  className?: string;
@@ -38,186 +45,128 @@ export interface SideNavigationProps {
38
45
  id?: string;
39
46
  /** main navigation items */
40
47
  items?: HeaderNavigationItemData[];
41
- /** top navigation items */
42
- itemsTop?: HeaderNavigationItemData[];
48
+ /** additional components */
49
+ children?: OptionalChildren<SideNavigationTopProps>;
43
50
  /** is menu expanded */
44
51
  expanded?: SideNavigationExpandedState;
45
52
  /** expand change handler */
46
53
  onExpandedChange?(expanded: SideNavigationExpandedState): void;
47
54
  }
48
55
 
49
- export const SideNavigation: FC<SideNavigationProps> = ({
56
+ const defaultContext: SideNavigationContextType = {
57
+ styles: {
58
+ popoverContent: {
59
+ '--background-color-strong': '#24323C',
60
+ 'color': 'var(--color-white)',
61
+ } as CSSProperties,
62
+ },
63
+ };
64
+
65
+ function SideNavigationElement({
50
66
  className,
67
+ children,
51
68
  expanded,
52
69
  id,
53
70
  items,
54
- itemsTop,
55
71
  onExpandedChange,
56
- }) => {
72
+ }: SideNavigationProps) {
57
73
  const NavigationComponent = useContext(NavigationComponentContext);
74
+
75
+ const { childTop } = useMemo(
76
+ () =>
77
+ Children.toArray(children).reduce(
78
+ (out, item) => {
79
+ if (
80
+ item &&
81
+ isValidElement(item) &&
82
+ item.type &&
83
+ typeof item.type !== 'string' &&
84
+ item.type.name === SideNavigationTop.name
85
+ ) {
86
+ out.childTop = item;
87
+ }
88
+ return out;
89
+ },
90
+ {} as { childTop?: ReactElement }
91
+ ),
92
+ [children]
93
+ );
58
94
  return (
59
- <div
60
- className={classNames(
61
- Styles.sideNav,
62
- expanded?.bar ? Styles.sideNavExpanded : Styles.sideNavSlim,
63
- className
64
- )}
65
- id={id}
66
- data-cy="side-navigation"
67
- >
68
- {!!itemsTop?.length && (
69
- <Fragment>
70
- <div className={Styles.sideNavTop} data-cy="navigation-items-top">
71
- {itemsTop.map(item => (
72
- <SideNavigationItem
95
+ <SideNavigationContext.Provider value={defaultContext}>
96
+ <div
97
+ className={classNames(
98
+ Styles.sideNav,
99
+ expanded?.bar ? Styles.sideNavExpanded : Styles.sideNavSlim,
100
+ className
101
+ )}
102
+ id={id}
103
+ data-cy="side-navigation"
104
+ >
105
+ {childTop}
106
+ <div className={Styles.sideNavContent} data-cy="navigation-items">
107
+ {items?.map(item =>
108
+ item.submenu ? (
109
+ <SideNavigationGroupItem
73
110
  key={item.id}
74
111
  expanded={expanded}
112
+ onExpandedChange={onExpandedChange}
75
113
  navigationComponent={NavigationComponent}
76
114
  {...item}
77
115
  />
78
- ))}
79
- </div>
80
- <div className={Styles.divider} />
81
- </Fragment>
82
- )}
83
- <div className={Styles.sideNavContent} data-cy="navigation-items">
84
- {items?.map(item =>
85
- item.submenu ? (
86
- <SideNavigationGroupItem
87
- key={item.id}
88
- expanded={expanded}
89
- onExpandedChange={onExpandedChange}
90
- navigationComponent={NavigationComponent}
91
- {...item}
92
- />
93
- ) : (
94
- <SideNavigationItem
95
- key={item.id}
96
- expanded={expanded}
97
- navigationComponent={NavigationComponent}
98
- {...item}
99
- />
100
- )
101
- )}
102
- </div>
103
- <div className={Styles.divider} />
104
- <div className={Styles.sideNavBottom}>
105
- <SideNavigationOptionsToggle
106
- expanded={expanded}
107
- onExpandedChange={onExpandedChange}
108
- />
116
+ ) : (
117
+ <InternalSideNavigationLink
118
+ key={item.id}
119
+ submenuExpanded={undefined}
120
+ navigationComponent={NavigationComponent}
121
+ {...item}
122
+ />
123
+ )
124
+ )}
125
+ </div>
126
+ <div className={Styles.divider} />
127
+ <div className={Styles.sideNavBottom}>
128
+ <SideNavigationOptionsToggle
129
+ expanded={expanded}
130
+ onExpandedChange={onExpandedChange}
131
+ />
132
+ </div>
109
133
  </div>
110
- </div>
134
+ </SideNavigationContext.Provider>
111
135
  );
112
- };
113
-
114
- interface NavigationComponentProps {
115
- navigationComponent: FC<NavLinkComponentProps>;
116
136
  }
117
137
 
118
- interface SideNavigationItemProps extends HeaderNavigationItemData, NavigationComponentProps {
119
- expanded?: SideNavigationExpandedState;
138
+ export interface SideNavigationTopProps {
139
+ children: OptionalChildren<SideNavigationLinkProps | SideNavigationTriggerProps>;
120
140
  }
121
-
122
- /** Side Navigation menu item */
123
- const SideNavigationItem: FC<SideNavigationItemProps> = ({
124
- id,
125
- to,
126
- title,
127
- className,
128
- iconClassName,
129
- iconComponent: IconComponent,
130
- icon,
131
- iconActive,
132
- isActive,
133
- navigationComponent: NavigationComponent,
134
- tag,
135
- expanded,
136
- submenu,
137
- }) => {
138
- const iconSwitch = !!icon && !!iconActive && !IconComponent;
139
- const hasSubmenu = !!submenu;
140
-
141
- return (
142
- <NavigationComponent
143
- data-cy={`navigation-item-${id}`}
144
- data-pendo={`navigation-item-${id}`}
145
- key={id}
146
- to={to}
147
- className={classNames(Styles.navigationItem, className, {
148
- [Styles.navigationItemActive]: isActive === true,
149
- [Styles.navigationItemIconSwitch]: iconSwitch,
150
- })}
151
- isActive={typeof isActive === 'function' ? isActive : undefined}
152
- activeClassName={Styles.navigationItemActive}
153
- >
154
- <div className={Styles.navigationItemIconWrapper}>
155
- {IconComponent ? (
156
- <i className={classNames(Styles.navigationIcon, iconClassName)}>
157
- <IconComponent />
158
- </i>
159
- ) : (
160
- <Fragment>
161
- {icon && (
162
- <Icon
163
- svg={icon}
164
- className={classNames(
165
- Styles.navigationIcon,
166
- Styles.navigationIconInactive,
167
- iconClassName
168
- )}
169
- />
170
- )}
171
- {iconActive && (
172
- <Icon
173
- svg={iconActive}
174
- className={classNames(
175
- Styles.navigationIcon,
176
- Styles.navigationIconActive,
177
- iconClassName
178
- )}
179
- />
180
- )}
181
- </Fragment>
182
- )}
183
-
184
- {!!expanded?.bar && <div className={Styles.navigationItemText}>{title}</div>}
185
- {!!tag && <CounterTag data={tag} className={Styles.navigationItemCounter} />}
186
- {hasSubmenu && !!expanded?.bar && (
187
- <Icon
188
- svg={expanded?.submenus?.includes(id) ? SvgGroupCollapse : SvgGroupExpand}
189
- className={Styles.navigationItemGroupToggle}
190
- />
191
- )}
141
+ function SideNavigationTopElement({ children }: SideNavigationTopProps) {
142
+ const list = Children.map(children, child => {
143
+ return child && isValidElement(child) ? child : null;
144
+ });
145
+ return list?.length ? (
146
+ <Fragment>
147
+ <div className={Styles.sideNavTop} data-cy="navigation-items-top">
148
+ {list}
192
149
  </div>
193
-
194
- {!expanded?.bar && (
195
- <div
196
- className={classNames(Styles.navigationItemText, {
197
- [Styles.navigationItemTextSmall]: title.length >= 10,
198
- })}
199
- >
200
- {title}
201
- </div>
202
- )}
203
- </NavigationComponent>
204
- );
205
- };
206
-
207
- const submenuPopoverStyles = { '--background-color-strong': '#24323C' } as CSSProperties;
150
+ <div className={Styles.divider} />
151
+ </Fragment>
152
+ ) : null;
153
+ }
154
+ const SideNavigationTop: FC<SideNavigationTopProps> = SideNavigationTopElement;
208
155
 
209
156
  /** Side Navigation menu item */
210
157
  const SideNavigationGroupItem: FC<
211
- SideNavigationItemProps & {
212
- onExpandedChange: undefined | ((expanded: SideNavigationExpandedState) => void);
213
- }
158
+ HeaderNavigationItemData &
159
+ SideNavigationExpandedProps &
160
+ NavigationComponentProps & {
161
+ onExpandedChange: undefined | ((expanded: SideNavigationExpandedState) => void);
162
+ }
214
163
  > = ({ onExpandedChange, ...props }) => {
164
+ const isSubmenuExpanded = props.expanded?.submenus?.includes(props.id) ?? false;
215
165
  const triggerClick = useCallback(
216
166
  (e: MouseEvent<HTMLDivElement>) => {
217
167
  e.stopPropagation();
218
168
  e.preventDefault();
219
169
 
220
- const isSubmenuExpanded = props.expanded?.submenus?.includes(props.id);
221
170
  onExpandedChange?.({
222
171
  bar: !!props.expanded?.bar,
223
172
  submenus: [
@@ -226,17 +175,20 @@ const SideNavigationGroupItem: FC<
226
175
  ],
227
176
  });
228
177
  },
229
- [props.id, props.expanded, onExpandedChange]
178
+ [props.id, props.expanded, isSubmenuExpanded, onExpandedChange]
230
179
  );
231
180
 
232
- const tag = props.submenu?.groups.some(group => group.links.some(link => !!link.tag))
233
- ? true
234
- : props.tag;
181
+ const tag = getSubmenuGroupTag(props.submenu, props.tag);
182
+ const context = useContext(SideNavigationContext);
235
183
 
236
184
  return props.expanded?.bar ? (
237
185
  <Fragment>
238
186
  <div onClickCapture={triggerClick}>
239
- <SideNavigationItem {...props} tag={tag} />
187
+ <InternalSideNavigationLink
188
+ {...props}
189
+ submenuExpanded={isSubmenuExpanded}
190
+ tag={tag}
191
+ />
240
192
  </div>
241
193
  <Collapsible open={props.expanded?.submenus?.includes(props.id)} animate>
242
194
  <div className={Styles.submenu}>
@@ -252,11 +204,15 @@ const SideNavigationGroupItem: FC<
252
204
  <Popover.Trigger>
253
205
  {(triggerProps: PopoverTriggerProps) => (
254
206
  <div {...triggerProps}>
255
- <SideNavigationItem {...props} tag={tag} />
207
+ <InternalSideNavigationLink
208
+ {...props}
209
+ submenuExpanded={undefined}
210
+ tag={tag}
211
+ />
256
212
  </div>
257
213
  )}
258
214
  </Popover.Trigger>
259
- <Popover.Content style={submenuPopoverStyles} className="z-global-nav-i">
215
+ <Popover.Content style={context.styles.popoverContent} className="z-global-nav-i">
260
216
  <div className={Styles.submenuPopover}>
261
217
  <Headline size="small" className="c-white m-b-half-i m-t-1">
262
218
  {props.title}
@@ -329,7 +285,7 @@ const SideNavigationGroupLink: FC<HeaderNavigationItemSubmenuLink & NavigationCo
329
285
  };
330
286
 
331
287
  /** Side Navigation options toggle */
332
- export const SideNavigationOptionsToggle: FC<{
288
+ const SideNavigationOptionsToggle: FC<{
333
289
  expanded?: SideNavigationExpandedState;
334
290
  onExpandedChange?(expanded: SideNavigationExpandedState): void;
335
291
  }> = ({ expanded, onExpandedChange }) =>
@@ -351,3 +307,7 @@ export const SideNavigationOptionsToggle: FC<{
351
307
  expanded?.bar ? undefined : 'Expand Menu',
352
308
  'right'
353
309
  );
310
+
311
+ export const SideNavigation = Object.assign(SideNavigationElement, {
312
+ Top: SideNavigationTop,
313
+ });
@@ -0,0 +1,11 @@
1
+ import { CounterTagProps } from '../components/counter-tag';
2
+
3
+ export interface CounterTagPropsStrict {
4
+ value: number | boolean;
5
+ className?: string;
6
+ }
7
+
8
+ export type CounterTagType = boolean | number | CounterTagProps;
9
+
10
+ export const isCounterPropsObject = (tag?: CounterTagType): tag is CounterTagProps =>
11
+ !!tag && (tag as any).value !== undefined;
@@ -0,0 +1,34 @@
1
+ import { isCounterPropsObject } from './counter-tag';
2
+ import { HeaderNavigationItemData, HeaderNavigationItemSubmenu } from './navigation';
3
+
4
+ export function getSubmenuGroupTag(
5
+ submenu: HeaderNavigationItemSubmenu | undefined,
6
+ defaultTag: HeaderNavigationItemData['tag']
7
+ ): HeaderNavigationItemData['tag'] {
8
+ if (!submenu) {
9
+ return defaultTag;
10
+ }
11
+
12
+ let tagValue: number | boolean | undefined = undefined;
13
+
14
+ for (const group of submenu.groups) {
15
+ for (const link of group.links) {
16
+ const ltv: number | boolean | undefined = isCounterPropsObject(link.tag)
17
+ ? link.tag.value
18
+ : link.tag;
19
+
20
+ if (ltv) {
21
+ if (typeof ltv === 'number') {
22
+ if (typeof tagValue !== 'number') {
23
+ tagValue = 0;
24
+ }
25
+ tagValue += ltv;
26
+ } else if (typeof tagValue !== 'number') {
27
+ tagValue = true;
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ return tagValue ?? defaultTag;
34
+ }