@servicetitan/navigation 11.0.0-canary.237.fef17f5.0 → 11.0.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 (96) hide show
  1. package/dist/components/badge-tag.d.ts +1 -1
  2. package/dist/components/badge-tag.d.ts.map +1 -1
  3. package/dist/components/header-navigation/header-navigation-stacked.stories.js +1 -1
  4. package/dist/components/header-navigation/header-navigation-stacked.stories.js.map +1 -1
  5. package/dist/components/header-navigation/header-navigation.stories.js +1 -1
  6. package/dist/components/header-navigation/header-navigation.stories.js.map +1 -1
  7. package/dist/components/left-navigation/header-navigation-tiny.stories.js +2 -2
  8. package/dist/components/left-navigation/header-navigation-tiny.stories.js.map +1 -1
  9. package/dist/components/logo/logo-titan-text.d.ts +1 -1
  10. package/dist/components/logo/logo-titan-text.d.ts.map +1 -1
  11. package/dist/components/profile-dropdown/profile-dropdown.d.ts +12 -7
  12. package/dist/components/profile-dropdown/profile-dropdown.d.ts.map +1 -1
  13. package/dist/components/profile-dropdown/profile-dropdown.js +10 -7
  14. package/dist/components/profile-dropdown/profile-dropdown.js.map +1 -1
  15. package/dist/components/profile-dropdown/profile-dropdown.module.less +20 -6
  16. package/dist/components/titan-layout/layout-context.js +1 -1
  17. package/dist/components/titan-layout/layout-context.js.map +1 -1
  18. package/dist/components/titan-layout/layout-header-links.d.ts.map +1 -1
  19. package/dist/components/titan-layout/layout-header-links.js +1 -1
  20. package/dist/components/titan-layout/layout-header-links.js.map +1 -1
  21. package/dist/components/titan-layout/layout-header.d.ts +2 -0
  22. package/dist/components/titan-layout/layout-header.d.ts.map +1 -1
  23. package/dist/components/titan-layout/layout-header.js +3 -4
  24. package/dist/components/titan-layout/layout-header.js.map +1 -1
  25. package/dist/components/titan-layout/layout-header.module.less +56 -33
  26. package/dist/components/titan-layout/layout-logo.d.ts.map +1 -1
  27. package/dist/components/titan-layout/layout-logo.js +2 -1
  28. package/dist/components/titan-layout/layout-logo.js.map +1 -1
  29. package/dist/components/titan-layout/layout-profile.d.ts.map +1 -1
  30. package/dist/components/titan-layout/layout-profile.js +39 -11
  31. package/dist/components/titan-layout/layout-profile.js.map +1 -1
  32. package/dist/components/titan-layout/layout-profile.stories.d.ts.map +1 -1
  33. package/dist/components/titan-layout/layout-profile.stories.js +1 -1
  34. package/dist/components/titan-layout/layout-profile.stories.js.map +1 -1
  35. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +2 -2
  36. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
  37. package/dist/components/titan-layout/layout-sidebar-links-internal.js +4 -4
  38. package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
  39. package/dist/components/titan-layout/layout-sidebar-links.d.ts.map +1 -1
  40. package/dist/components/titan-layout/layout-sidebar-links.js +10 -3
  41. package/dist/components/titan-layout/layout-sidebar-links.js.map +1 -1
  42. package/dist/components/titan-layout/layout-sidebar.d.ts +2 -0
  43. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
  44. package/dist/components/titan-layout/layout-sidebar.js +6 -4
  45. package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
  46. package/dist/components/titan-layout/layout-sidebar.module.less +25 -5
  47. package/dist/components/titan-layout/notifications-context.d.ts +13 -0
  48. package/dist/components/titan-layout/notifications-context.d.ts.map +1 -0
  49. package/dist/components/titan-layout/notifications-context.js +23 -0
  50. package/dist/components/titan-layout/notifications-context.js.map +1 -0
  51. package/dist/components/titan-layout/titan-layout.d.ts +6 -3
  52. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
  53. package/dist/components/titan-layout/titan-layout.js +77 -22
  54. package/dist/components/titan-layout/titan-layout.js.map +1 -1
  55. package/dist/components/titan-layout/titan-layout.module.less +44 -20
  56. package/dist/components/titan-layout/titan-layout.stories.d.ts +4 -0
  57. package/dist/components/titan-layout/titan-layout.stories.d.ts.map +1 -1
  58. package/dist/components/titan-layout/titan-layout.stories.js +15 -7
  59. package/dist/components/titan-layout/titan-layout.stories.js.map +1 -1
  60. package/dist/test/data.d.ts +4 -1
  61. package/dist/test/data.d.ts.map +1 -1
  62. package/dist/test/data.js +2 -3
  63. package/dist/test/data.js.map +1 -1
  64. package/dist/utils/use-breakpoint.d.ts +1 -0
  65. package/dist/utils/use-breakpoint.d.ts.map +1 -1
  66. package/dist/utils/use-breakpoint.js +3 -2
  67. package/dist/utils/use-breakpoint.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/components/badge-tag.tsx +1 -1
  70. package/src/components/header-navigation/header-navigation-stacked.stories.tsx +1 -1
  71. package/src/components/header-navigation/header-navigation.stories.tsx +1 -1
  72. package/src/components/left-navigation/header-navigation-tiny.stories.tsx +2 -2
  73. package/src/components/logo/logo-titan-text.tsx +1 -1
  74. package/src/components/profile-dropdown/profile-dropdown.module.less +20 -6
  75. package/src/components/profile-dropdown/profile-dropdown.module.less.d.ts +2 -0
  76. package/src/components/profile-dropdown/profile-dropdown.tsx +50 -15
  77. package/src/components/titan-layout/layout-context.tsx +1 -1
  78. package/src/components/titan-layout/layout-header-links.tsx +2 -1
  79. package/src/components/titan-layout/layout-header.module.less +56 -33
  80. package/src/components/titan-layout/layout-header.module.less.d.ts +2 -0
  81. package/src/components/titan-layout/layout-header.tsx +12 -5
  82. package/src/components/titan-layout/layout-logo.tsx +13 -6
  83. package/src/components/titan-layout/layout-profile.stories.tsx +10 -1
  84. package/src/components/titan-layout/layout-profile.tsx +92 -28
  85. package/src/components/titan-layout/layout-sidebar-links-internal.tsx +18 -5
  86. package/src/components/titan-layout/layout-sidebar-links.tsx +16 -3
  87. package/src/components/titan-layout/layout-sidebar.module.less +25 -5
  88. package/src/components/titan-layout/layout-sidebar.module.less.d.ts +1 -0
  89. package/src/components/titan-layout/layout-sidebar.tsx +14 -5
  90. package/src/components/titan-layout/notifications-context.tsx +44 -0
  91. package/src/components/titan-layout/titan-layout.module.less +44 -20
  92. package/src/components/titan-layout/titan-layout.module.less.d.ts +2 -1
  93. package/src/components/titan-layout/titan-layout.stories.tsx +118 -6
  94. package/src/components/titan-layout/titan-layout.tsx +196 -87
  95. package/src/test/data.tsx +2 -3
  96. package/src/utils/use-breakpoint.ts +3 -1
@@ -123,6 +123,11 @@
123
123
 
124
124
  .dropdown-section {
125
125
  padding: @spacing-1 @spacing-2;
126
+ position: relative;
127
+
128
+ &.dropdown-section-with-counter {
129
+ padding-right: @spacing-3;
130
+ }
126
131
  }
127
132
 
128
133
  .dropdown-link {
@@ -136,13 +141,22 @@
136
141
  }
137
142
  }
138
143
 
139
- .counter {
140
- color: @color-white;
141
- font-size: @typescale-0;
142
- font-weight: @font-weight-semibold;
144
+ .counter-wrapper {
143
145
  position: absolute;
144
- top: @spacing-half;
145
- margin-left: @spacing-half;
146
+ top: 0;
147
+ bottom: 0;
148
+ right: @spacing-1;
149
+
150
+ display: flex;
151
+ align-items: center;
152
+
153
+ .counter {
154
+ color: @color-white;
155
+ font-size: @typescale-0;
156
+ font-weight: @font-weight-semibold;
157
+ min-width: 12px !important;
158
+ min-height: 12px !important;
159
+ }
146
160
  }
147
161
  }
148
162
 
@@ -4,12 +4,14 @@ export const badge: string;
4
4
  export const badgeNoContent: string;
5
5
  export const badgeWithContent: string;
6
6
  export const counter: string;
7
+ export const counterWrapper: string;
7
8
  export const dropdown: string;
8
9
  export const dropdownContent: string;
9
10
  export const dropdownContentBottomLeft: string;
10
11
  export const dropdownContentWrapper: string;
11
12
  export const dropdownLink: string;
12
13
  export const dropdownSection: string;
14
+ export const dropdownSectionWithCounter: string;
13
15
  export const expandIcon: string;
14
16
  export const hint: string;
15
17
  export const hintContent: string;
@@ -6,6 +6,7 @@ import SvgAccountInactive from '@servicetitan/anvil2/assets/icons/st/gnav_accoun
6
6
  import { BodyText, Popover, PopoverPropsStrict } from '@servicetitan/design-system';
7
7
  import classNames from 'classnames';
8
8
  import {
9
+ ComponentPropsWithoutRef,
9
10
  FC,
10
11
  HTMLAttributeAnchorTarget,
11
12
  MouseEvent,
@@ -147,22 +148,42 @@ const ProfileDropdownTrigger: FC<ProfileDropdownTriggerProps> = ({
147
148
  );
148
149
  };
149
150
 
151
+ const useTag = (counter?: CounterTagValue, tag?: CounterTagData) =>
152
+ useMemo(() => {
153
+ const data = getCounterTag(counter, tag);
154
+
155
+ return data ? (
156
+ <div className={Styles.counterWrapper}>
157
+ <CounterTag className={Styles.counter} data={data} />
158
+ </div>
159
+ ) : undefined;
160
+ }, [counter, tag]);
161
+
162
+ export type ProfileItemContent =
163
+ | { children: string; text?: string }
164
+ | { children: ReactNode; text: string };
165
+
150
166
  export interface ProfileDropdownSectionPropsStrict {
151
167
  children: ReactNode;
152
168
  id: string;
153
169
  tooltip?: string;
154
170
  className?: string;
171
+ tag?: CounterTagData;
172
+ counter?: CounterTagValue;
155
173
  onClick?(e: MouseEvent): void;
156
174
  }
157
175
 
158
- export interface ProfileDropdownSectionProps extends ProfileDropdownSectionPropsStrict {
159
- [key: string]: any;
160
- }
176
+ export type ProfileDropdownSectionProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'> &
177
+ ProfileDropdownSectionPropsStrict &
178
+ ProfileItemContent;
161
179
 
162
180
  export const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
163
181
  children,
164
182
  className,
183
+ counter,
165
184
  id,
185
+ tag,
186
+ text,
166
187
  tooltip,
167
188
  onClick,
168
189
  ...rest
@@ -175,17 +196,23 @@ export const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
175
196
  }
176
197
  };
177
198
 
199
+ const tagElement = useTag(counter, tag);
200
+
178
201
  return withTooltip(
179
202
  <div
180
- className={classNames(Styles.dropdownSection, className, {
181
- 'cursor-pointer': !!onClick,
182
- })}
203
+ className={classNames(
204
+ Styles.dropdownSection,
205
+ tagElement && Styles.dropdownSectionWithCounter,
206
+ !!onClick && Styles.dropdownLink,
207
+ className
208
+ )}
183
209
  onClick={clickHandler}
184
210
  data-cy={`profile-dropdown-section-${id}`}
185
211
  data-pendo={`profile-dropdown-section-${id}`}
186
212
  {...rest}
187
213
  >
188
214
  {children}
215
+ {tagElement}
189
216
  </div>,
190
217
  tooltip,
191
218
  'left'
@@ -206,9 +233,9 @@ export interface ProfileDropdownLinkPropsStrict {
206
233
  counter?: CounterTagValue;
207
234
  }
208
235
 
209
- export interface ProfileDropdownLinkProps extends ProfileDropdownLinkPropsStrict {
210
- [key: string]: any;
211
- }
236
+ export type ProfileDropdownLinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'children'> &
237
+ ProfileDropdownLinkPropsStrict &
238
+ ProfileItemContent;
212
239
 
213
240
  export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
214
241
  children,
@@ -218,6 +245,7 @@ export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
218
245
  counter,
219
246
  tag,
220
247
  target,
248
+ text,
221
249
  to,
222
250
  tooltip,
223
251
  onClick,
@@ -227,15 +255,17 @@ export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
227
255
 
228
256
  const isExternalLink = external ?? to?.startsWith('http');
229
257
 
230
- const tagElement = useMemo(
231
- () => <CounterTag data={getCounterTag(counter, tag)} className={Styles.counter} />,
232
- [counter, tag]
233
- );
258
+ const tagElement = useTag(counter, tag);
234
259
 
235
260
  return withTooltip(
236
261
  isExternalLink ? (
237
262
  <a
238
- className={classNames(Styles.dropdownSection, Styles.dropdownLink, className)}
263
+ className={classNames(
264
+ Styles.dropdownSection,
265
+ tagElement && Styles.dropdownSectionWithCounter,
266
+ Styles.dropdownLink,
267
+ className
268
+ )}
239
269
  href={to}
240
270
  target={target}
241
271
  data-cy={`profile-dropdown-link-${id}`}
@@ -247,7 +277,12 @@ export const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = ({
247
277
  </a>
248
278
  ) : (
249
279
  <NavigationComponent
250
- className={classNames(Styles.dropdownSection, Styles.dropdownLink, className)}
280
+ className={classNames(
281
+ Styles.dropdownSection,
282
+ Styles.dropdownLink,
283
+ { [Styles.dropdownSectionWithCounter]: !!tagElement },
284
+ className
285
+ )}
251
286
  target={target}
252
287
  to={to}
253
288
  data-cy={`profile-dropdown-link-${id}`}
@@ -19,7 +19,7 @@ export interface TitanLayoutContextType {
19
19
 
20
20
  export const LayoutContext = createContext<TitanLayoutContextType>({
21
21
  NavigationComponent: DefaultNavLinkComponent,
22
- breakpoint: { name: 'lg', isMobile: false },
22
+ breakpoint: { name: 'lg', isMobile: false, width: 0 },
23
23
  isTitanLayout: false,
24
24
  sidebar: { styles: { popoverContent: {} } },
25
25
  });
@@ -134,7 +134,8 @@ export const LayoutHeaderNavigationTrigger: FC<HeaderNavigationTriggerProps> = (
134
134
  >
135
135
  <HeaderNavigationItemContent
136
136
  tag={getCounterTag(counter, tag)}
137
- icon={isActive && iconActive ? iconActive : icon}
137
+ icon={icon}
138
+ iconActive={iconActive}
138
139
  label={label}
139
140
  labelClassName={labelClassName}
140
141
  />
@@ -4,12 +4,14 @@
4
4
  @size-links-tiny: 24px;
5
5
 
6
6
  .header {
7
+ --nav-top-content-height: 32px;
7
8
  display: flex;
8
9
  justify-content: space-between;
9
10
 
10
11
  background-color: @color-white;
11
12
  color: @color-black;
12
- border-bottom: 1px solid @color-neutral-60;
13
+ box-sizing: border-box;
14
+ outline: 1px solid @color-neutral-60;
13
15
 
14
16
  & > * {
15
17
  overflow-y: hidden;
@@ -20,6 +22,10 @@
20
22
  align-items: center;
21
23
  }
22
24
 
25
+ .he-top-center {
26
+ overflow: hidden;
27
+ }
28
+
23
29
  .he-top-right {
24
30
  & > * {
25
31
  color: @color-black;
@@ -57,7 +63,7 @@
57
63
  }
58
64
 
59
65
  &.navigation-item-icon-state.navigation-item-active {
60
- .navigation-icon:not(.navigation-icon-active) {
66
+ .navigation-icon[data-anv][data-anv]:not(.navigation-icon-active) {
61
67
  display: none;
62
68
  }
63
69
 
@@ -84,28 +90,59 @@
84
90
  }
85
91
  }
86
92
 
93
+ .header-mobile {
94
+ padding: @spacing-2 @spacing-0;
95
+ height: var(--nav-offset-top);
96
+
97
+ --nav-top-content-height: 40px;
98
+
99
+ .navigation-link {
100
+ padding: 10px;
101
+ }
102
+
103
+ .he-top-center {
104
+ max-width: unset;
105
+ flex: 1;
106
+ margin-left: @spacing-3;
107
+ margin-right: @spacing-3;
108
+ }
109
+
110
+ .he-top-left {
111
+ margin-left: @spacing-half;
112
+ }
113
+
114
+ .he-top-right {
115
+ margin-right: @spacing-half;
116
+ }
117
+ }
118
+
87
119
  // desktop
88
- @media only screen and (min-width: 768px) {
89
- .header {
90
- .navigation-link {
91
- margin: 6px 2px;
92
- padding: 6px 6px;
93
- }
120
+ .header-desktop {
121
+ height: var(--nav-offset-top);
122
+ .navigation-link {
123
+ margin: 6px 2px;
124
+ padding: 6px 6px;
94
125
 
95
- .he-top-left {
96
- padding-left: @spacing-1;
97
- }
98
- .he-top-center {
99
- flex: 1;
100
- margin-left: @spacing-2;
101
- margin-right: @spacing-2;
102
- max-width: 400px;
126
+ .navigation-item-counter {
127
+ min-width: 12px !important;
128
+ height: 12px !important;
103
129
  }
104
130
  }
131
+
132
+ .he-top-left {
133
+ padding-left: @spacing-1;
134
+ }
135
+ .he-top-center {
136
+ flex: 1;
137
+ margin-left: @spacing-2;
138
+ margin-right: @spacing-2;
139
+ max-width: 400px;
140
+ }
105
141
  }
142
+
106
143
  // desktop wide
107
144
  @media only screen and (min-width: 1200px) {
108
- .header {
145
+ .header-desktop {
109
146
  display: grid;
110
147
  grid-template-columns: repeat(3, 1fr);
111
148
  grid-template-rows: 48px;
@@ -122,21 +159,6 @@
122
159
  }
123
160
  }
124
161
 
125
- // mobile
126
- @media only screen and (max-width: 768px) {
127
- .header {
128
- display: grid;
129
- grid-template-columns: repeat(3, 1fr);
130
- grid-template-rows: 44px;
131
-
132
- padding: @spacing-1 @spacing-half;
133
-
134
- .navigation-link {
135
- padding: 10px;
136
- }
137
- }
138
- }
139
-
140
162
  .navigation-link {
141
163
  // styles specific to extra nav links
142
164
  color: @color-black;
@@ -150,7 +172,8 @@
150
172
  color: @color-white;
151
173
  font-weight: @font-weight-semibold;
152
174
  font-size: 8px !important;
153
- min-width: 12px;
175
+ min-width: 16px !important;
176
+ height: 16px !important;
154
177
  position: absolute;
155
178
  top: 4px;
156
179
  right: -2px;
@@ -4,6 +4,8 @@ export const heTopLeft: string;
4
4
  export const heTopRight: string;
5
5
  export const heTopRightText: string;
6
6
  export const header: string;
7
+ export const headerDesktop: string;
8
+ export const headerMobile: string;
7
9
  export const navigationIcon: string;
8
10
  export const navigationIconActive: string;
9
11
  export const navigationItemActive: string;
@@ -1,7 +1,7 @@
1
1
  import SvgBurgerMenu from '@servicetitan/anvil2/assets/icons/material/round/menu.svg';
2
2
  import classNames from 'classnames';
3
3
  import { ComponentPropsWithoutRef, FC, ReactElement, ReactNode } from 'react';
4
- import { LayoutPlacementContext, useTitanLayoutContext } from './layout-context';
4
+ import { LayoutPlacementContext } from './layout-context';
5
5
  import { LayoutHeaderNavigationTrigger } from './layout-header-links';
6
6
  import * as Styles from './layout-header.module.less';
7
7
  import { TitanLayoutLogoProps } from './layout-logo';
@@ -25,6 +25,8 @@ export type LayoutHeaderProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'
25
25
  logo: ReactElement<TitanLayoutLogoProps>;
26
26
  profile?: ReactElement;
27
27
 
28
+ isMobile: boolean;
29
+ hasNotifications: boolean;
28
30
  onBurgerClick?: (e: MouseEvent) => void;
29
31
  };
30
32
 
@@ -35,22 +37,26 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({
35
37
  rightClassName,
36
38
  center,
37
39
  centerClassName,
40
+ isMobile,
41
+ hasNotifications,
38
42
  logo,
39
43
  profile,
40
44
  onBurgerClick,
41
45
  ...rest
42
46
  }) => {
43
- const { breakpoint } = useTitanLayoutContext();
44
-
45
47
  return (
46
48
  <LayoutPlacementContext.Provider value="top">
47
49
  <div
48
- className={classNames(Styles.header, className)}
50
+ className={classNames(
51
+ Styles.header,
52
+ isMobile ? Styles.headerMobile : Styles.headerDesktop,
53
+ className
54
+ )}
49
55
  {...rest}
50
56
  data-cy="header-navigation"
51
57
  >
52
58
  <div className={classNames(Styles.heTopLeft)} data-cy="navigation-left">
53
- {breakpoint.isMobile && (
59
+ {isMobile && (
54
60
  <LayoutHeaderNavigationTrigger
55
61
  id="burger"
56
62
  title=""
@@ -58,6 +64,7 @@ export const LayoutHeader: FC<LayoutHeaderProps> = ({
58
64
  iconActive={SvgBurgerMenu}
59
65
  className="m-r-1"
60
66
  onClick={onBurgerClick}
67
+ tag={{ value: hasNotifications }}
61
68
  />
62
69
  )}
63
70
  {logo}
@@ -33,20 +33,27 @@ export const TitanLayoutLogo: FC<TitanLayoutLogoProps> = ({
33
33
  const Wrapper = logoWrapper;
34
34
  const logoSize = isMobile ? 44 : 56;
35
35
  const logoCompanySize = 48;
36
+ const showCompanyTitle = title === true && !isMobile;
36
37
 
37
38
  return (
38
- <div className={classNames('d-f align-items-center', className)}>
39
- {typeof title === 'string' ? (
39
+ <div
40
+ className={classNames(
41
+ 'd-f align-items-center',
42
+ { 'p-t-half': showCompanyTitle },
43
+ className
44
+ )}
45
+ >
46
+ {showCompanyTitle ? (
47
+ <Wrapper>
48
+ <LogoCompanyTitle height={logoCompanySize} />
49
+ </Wrapper>
50
+ ) : typeof title === 'string' ? (
40
51
  <Fragment>
41
52
  <LogoTitan size={logoSize} mantleFill={mantleFill} logoWrapper={Wrapper} />
42
53
  {!isMobile && (
43
54
  <LogoTitanTitle className="c-inherit m-l-1">{title}</LogoTitanTitle>
44
55
  )}
45
56
  </Fragment>
46
- ) : title === true && !isMobile ? (
47
- <Wrapper className="">
48
- <LogoCompanyTitle height={logoCompanySize} />
49
- </Wrapper>
50
57
  ) : (
51
58
  <LogoTitan size={logoSize} mantleFill={mantleFill} logoWrapper={Wrapper} />
52
59
  )}
@@ -18,7 +18,7 @@ export default {
18
18
 
19
19
  export const ProfileDefault = withTitanLayout(
20
20
  <ProfileDropdown>
21
- <ProfileDropdown.Link id="first" to="https://google.com">
21
+ <ProfileDropdown.Link id="first" to="https://google.com" external>
22
22
  first link
23
23
  </ProfileDropdown.Link>
24
24
  <ProfileDropdown.Section id="second" onClick={() => alert('second click')}>
@@ -33,5 +33,14 @@ export const ProfileDefault = withTitanLayout(
33
33
  third link
34
34
  </ProfileDropdown.Link>
35
35
  <ProfileDropdown.Divider />
36
+ <ProfileDropdown.Section
37
+ id="forth"
38
+ onClick={() => alert('forth click')}
39
+ text="Sign Out user"
40
+ >
41
+ Sign Out
42
+ <span className="c-neutral-60 m-l-1">user</span>
43
+ </ProfileDropdown.Section>
44
+ <ProfileDropdown.Divider />
36
45
  </ProfileDropdown>
37
46
  );
@@ -1,8 +1,8 @@
1
1
  import SvgAccountActive from '@servicetitan/anvil2/assets/icons/st/gnav_account_active.svg';
2
2
  import SvgAccountInactive from '@servicetitan/anvil2/assets/icons/st/gnav_account_inactive.svg';
3
3
 
4
- import { FC, useState } from 'react';
5
- import { NavigationComponentContext } from '../../utils/navigation-context';
4
+ import { FC, MouseEvent, useEffect, useState } from 'react';
5
+ import { NavLinkComponentProps, NavigationComponentContext } from '../../utils/navigation-context';
6
6
  import {
7
7
  ProfileDropdown as DesktopProfileDropdown,
8
8
  ProfileDropdownLinkProps,
@@ -17,6 +17,7 @@ import {
17
17
  InternalSideNavigationGroupLink,
18
18
  InternalSideNavigationGroupTrigger,
19
19
  } from './layout-sidebar-links-internal';
20
+ import { useNotificationsContext, useNotificationsState } from './notifications-context';
20
21
 
21
22
  export type {
22
23
  ProfileDropdownProps,
@@ -24,14 +25,28 @@ export type {
24
25
  ProfileDropdownLinkProps,
25
26
  } from '../profile-dropdown/profile-dropdown';
26
27
 
28
+ const ExternalNavComponent: FC<NavLinkComponentProps> = ({
29
+ children,
30
+ isActive,
31
+ to,
32
+ activeClassName,
33
+ ...props
34
+ }) => (
35
+ <a {...props} href={to}>
36
+ {children}
37
+ </a>
38
+ );
39
+
27
40
  const ProfileDropdownContent: FC<ProfileDropdownProps> = props => {
28
- const { breakpoint, NavigationComponent } = useTitanLayoutContext();
41
+ const { isTitanLayout, breakpoint, NavigationComponent } = useTitanLayoutContext();
29
42
  return breakpoint.isMobile ? (
30
43
  <MobileProfileDropdown {...props} navigationComponent={NavigationComponent} />
31
- ) : (
44
+ ) : isTitanLayout ? (
32
45
  <NavigationComponentContext.Provider value={NavigationComponent}>
33
46
  <DesktopProfileDropdown {...props} />
34
47
  </NavigationComponentContext.Provider>
48
+ ) : (
49
+ <DesktopProfileDropdown {...props} />
35
50
  );
36
51
  };
37
52
  ProfileDropdownContent.displayName = 'ProfileDropdown';
@@ -40,24 +55,37 @@ const MobileProfileDropdown: FC<ProfileDropdownProps & NavigationComponentProps>
40
55
  children,
41
56
  ...props
42
57
  }) => {
58
+ const id = '__profile';
43
59
  const [expanded, setExpanded] = useState(false);
44
- const onExpandToggle = () => setExpanded(!expanded);
60
+ const { hasNotifications, NotificationsContextProvider } = useNotificationsState();
61
+ const { onNotificationsUpdate } = useNotificationsContext();
62
+ const onExpandToggle = (e: MouseEvent<never>) => {
63
+ e.stopPropagation();
64
+ setExpanded(!expanded);
65
+ };
66
+
67
+ useEffect(() => {
68
+ onNotificationsUpdate(id, hasNotifications);
69
+ }, [hasNotifications, onNotificationsUpdate]);
70
+
45
71
  return (
46
- <InternalSideNavigationGroup
47
- id="__profile"
48
- to={undefined}
49
- title="Profile"
50
- icon={SvgAccountInactive}
51
- iconActive={SvgAccountActive}
52
- isActive={expanded}
53
- {...props}
54
- submenuExpanded={expanded}
55
- onExpandToggle={onExpandToggle}
56
- onClick={onExpandToggle}
57
- tag={undefined}
58
- >
59
- {children}
60
- </InternalSideNavigationGroup>
72
+ <NotificationsContextProvider>
73
+ <InternalSideNavigationGroup
74
+ id={id}
75
+ to={undefined}
76
+ title="Profile"
77
+ icon={SvgAccountInactive}
78
+ iconActive={SvgAccountActive}
79
+ isActive={expanded}
80
+ {...props}
81
+ submenuExpanded={expanded}
82
+ onExpandToggle={onExpandToggle}
83
+ onClick={onExpandToggle}
84
+ tag={{ value: hasNotifications }}
85
+ >
86
+ {children}
87
+ </InternalSideNavigationGroup>
88
+ </NotificationsContextProvider>
61
89
  );
62
90
  };
63
91
 
@@ -70,6 +98,25 @@ const ProfileDropdownDivider: FC = () => {
70
98
  );
71
99
  };
72
100
 
101
+ const getText = (children: any, text: any): string | undefined => {
102
+ if (typeof children === 'string') {
103
+ return children;
104
+ }
105
+
106
+ if (typeof text === 'string') {
107
+ return text;
108
+ }
109
+
110
+ return undefined;
111
+ };
112
+
113
+ const getTag = (
114
+ tag: ProfileDropdownLinkProps['tag'],
115
+ counter: ProfileDropdownLinkProps['counter']
116
+ ): boolean => {
117
+ return !!tag?.value || !!counter;
118
+ };
119
+
73
120
  const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
74
121
  const { breakpoint } = useTitanLayoutContext();
75
122
  return breakpoint.isMobile ? (
@@ -78,10 +125,19 @@ const ProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
78
125
  <DesktopProfileDropdown.Section {...props} />
79
126
  );
80
127
  };
81
- const MobileProfileDropdownSection: FC<ProfileDropdownSectionProps> = props => {
82
- const title = typeof props.children === 'string' ? props.children : undefined;
83
- return title ? (
84
- <InternalSideNavigationGroupTrigger id={props.id} title={title} onClick={props.onClick} />
128
+ const MobileProfileDropdownSection: FC<ProfileDropdownSectionProps> = ({
129
+ children,
130
+ text,
131
+ tooltip,
132
+ title,
133
+ ...props
134
+ }) => {
135
+ const sectionText = getText(children, text);
136
+ const { onNotificationsUpdate } = useNotificationsContext();
137
+ onNotificationsUpdate(props.id, getTag(props.tag, props.counter));
138
+
139
+ return sectionText ? (
140
+ <InternalSideNavigationGroupTrigger {...props} title={sectionText} />
85
141
  ) : null;
86
142
  };
87
143
 
@@ -94,17 +150,25 @@ const ProfileDropdownLink: FC<ProfileDropdownLinkProps> = props => {
94
150
  );
95
151
  };
96
152
  const MobileProfileDropdownLink: FC<ProfileDropdownLinkProps & NavigationComponentProps> = ({
153
+ external,
97
154
  to,
155
+ tooltip,
156
+ text,
157
+ children,
98
158
  navigationComponent,
99
159
  ...props
100
160
  }) => {
101
- const title = typeof props.children === 'string' ? props.children : undefined;
102
- return title ? (
161
+ const { onNotificationsUpdate } = useNotificationsContext();
162
+ const linkText = getText(children, text);
163
+ const isExternalLink = external ?? to?.startsWith('http');
164
+ onNotificationsUpdate(props.id, getTag(props.tag, props.counter));
165
+
166
+ return linkText ? (
103
167
  <InternalSideNavigationGroupLink
104
168
  {...props}
105
169
  to={to}
106
- title={title}
107
- navigationComponent={navigationComponent}
170
+ title={linkText}
171
+ navigationComponent={isExternalLink ? ExternalNavComponent : navigationComponent}
108
172
  />
109
173
  ) : null;
110
174
  };