@servicetitan/navigation 10.7.0 → 11.0.0-canary.237.0505ce7.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 (162) 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-extra.stories.d.ts.map +1 -1
  4. package/dist/components/header-navigation/header-navigation-extra.stories.js +5 -5
  5. package/dist/components/header-navigation/header-navigation-extra.stories.js.map +1 -1
  6. package/dist/components/header-navigation/header-navigation-links.d.ts.map +1 -1
  7. package/dist/components/header-navigation/header-navigation-links.js +2 -2
  8. package/dist/components/header-navigation/header-navigation-links.js.map +1 -1
  9. package/dist/components/header-navigation/header-navigation-stacked.stories.d.ts.map +1 -1
  10. package/dist/components/header-navigation/header-navigation-stacked.stories.js +1 -1
  11. package/dist/components/header-navigation/header-navigation-stacked.stories.js.map +1 -1
  12. package/dist/components/header-navigation/header-navigation.stories.d.ts.map +1 -1
  13. package/dist/components/header-navigation/header-navigation.stories.js +2 -2
  14. package/dist/components/header-navigation/header-navigation.stories.js.map +1 -1
  15. package/dist/components/header-navigation/with-tooltip.d.ts +1 -1
  16. package/dist/components/header-navigation/with-tooltip.d.ts.map +1 -1
  17. package/dist/components/left-navigation/header-navigation-tiny.stories.d.ts.map +1 -1
  18. package/dist/components/left-navigation/header-navigation-tiny.stories.js +2 -2
  19. package/dist/components/left-navigation/header-navigation-tiny.stories.js.map +1 -1
  20. package/dist/components/links.d.ts.map +1 -1
  21. package/dist/components/links.js +7 -7
  22. package/dist/components/links.js.map +1 -1
  23. package/dist/components/logo/logo-company-title.d.ts +1 -0
  24. package/dist/components/logo/logo-company-title.d.ts.map +1 -1
  25. package/dist/components/logo/logo-company-title.js +2 -2
  26. package/dist/components/logo/logo-company-title.js.map +1 -1
  27. package/dist/components/logo/logo-titan-text.d.ts +1 -1
  28. package/dist/components/logo/logo-titan-text.d.ts.map +1 -1
  29. package/dist/components/profile-dropdown/profile-dropdown.d.ts +17 -9
  30. package/dist/components/profile-dropdown/profile-dropdown.d.ts.map +1 -1
  31. package/dist/components/profile-dropdown/profile-dropdown.js +11 -9
  32. package/dist/components/profile-dropdown/profile-dropdown.js.map +1 -1
  33. package/dist/components/profile-dropdown/profile-dropdown.module.less +24 -6
  34. package/dist/components/profile-dropdown/profile-dropdown.stories.js +2 -2
  35. package/dist/components/profile-dropdown/profile-dropdown.stories.js.map +1 -1
  36. package/dist/components/titan-layout/index.d.ts +6 -0
  37. package/dist/components/titan-layout/index.d.ts.map +1 -0
  38. package/dist/components/titan-layout/index.js +6 -0
  39. package/dist/components/titan-layout/index.js.map +1 -0
  40. package/dist/components/titan-layout/interface-internal.d.ts +6 -0
  41. package/dist/components/titan-layout/interface-internal.d.ts.map +1 -0
  42. package/dist/components/titan-layout/interface-internal.js +2 -0
  43. package/dist/components/titan-layout/interface-internal.js.map +1 -0
  44. package/dist/components/titan-layout/interface.d.ts +21 -0
  45. package/dist/components/titan-layout/interface.d.ts.map +1 -0
  46. package/dist/components/titan-layout/interface.js +2 -0
  47. package/dist/components/titan-layout/interface.js.map +1 -0
  48. package/dist/components/titan-layout/layout-context.d.ts +20 -0
  49. package/dist/components/titan-layout/layout-context.d.ts.map +1 -0
  50. package/dist/components/titan-layout/layout-context.js +12 -0
  51. package/dist/components/titan-layout/layout-context.js.map +1 -0
  52. package/dist/components/titan-layout/layout-header-links.d.ts +7 -0
  53. package/dist/components/titan-layout/layout-header-links.d.ts.map +1 -0
  54. package/dist/components/titan-layout/layout-header-links.js +32 -0
  55. package/dist/components/titan-layout/layout-header-links.js.map +1 -0
  56. package/dist/components/titan-layout/layout-header.d.ts +22 -0
  57. package/dist/components/titan-layout/layout-header.d.ts.map +1 -0
  58. package/dist/components/titan-layout/layout-header.js +10 -0
  59. package/dist/components/titan-layout/layout-header.js.map +1 -0
  60. package/dist/components/titan-layout/layout-header.module.less +193 -0
  61. package/dist/components/titan-layout/layout-logo.d.ts +12 -0
  62. package/dist/components/titan-layout/layout-logo.d.ts.map +1 -0
  63. package/dist/components/titan-layout/layout-logo.js +16 -0
  64. package/dist/components/titan-layout/layout-logo.js.map +1 -0
  65. package/dist/components/titan-layout/layout-logo.stories.d.ts +13 -0
  66. package/dist/components/titan-layout/layout-logo.stories.d.ts.map +1 -0
  67. package/dist/components/titan-layout/layout-logo.stories.js +17 -0
  68. package/dist/components/titan-layout/layout-logo.stories.js.map +1 -0
  69. package/dist/components/titan-layout/layout-profile.d.ts +9 -0
  70. package/dist/components/titan-layout/layout-profile.d.ts.map +1 -0
  71. package/dist/components/titan-layout/layout-profile.js +72 -0
  72. package/dist/components/titan-layout/layout-profile.js.map +1 -0
  73. package/dist/components/titan-layout/layout-profile.stories.d.ts +13 -0
  74. package/dist/components/titan-layout/layout-profile.stories.d.ts.map +1 -0
  75. package/dist/components/titan-layout/layout-profile.stories.js +13 -0
  76. package/dist/components/titan-layout/layout-profile.stories.js.map +1 -0
  77. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +46 -0
  78. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -0
  79. package/dist/components/titan-layout/layout-sidebar-links-internal.js +61 -0
  80. package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -0
  81. package/dist/components/titan-layout/layout-sidebar-links.d.ts +6 -0
  82. package/dist/components/titan-layout/layout-sidebar-links.d.ts.map +1 -0
  83. package/dist/components/titan-layout/layout-sidebar-links.js +28 -0
  84. package/dist/components/titan-layout/layout-sidebar-links.js.map +1 -0
  85. package/dist/components/titan-layout/layout-sidebar.d.ts +19 -0
  86. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -0
  87. package/dist/components/titan-layout/layout-sidebar.js +67 -0
  88. package/dist/components/titan-layout/layout-sidebar.js.map +1 -0
  89. package/dist/components/titan-layout/layout-sidebar.module.less +536 -0
  90. package/dist/components/titan-layout/notifications-context.d.ts +13 -0
  91. package/dist/components/titan-layout/notifications-context.d.ts.map +1 -0
  92. package/dist/components/titan-layout/notifications-context.js +23 -0
  93. package/dist/components/titan-layout/notifications-context.js.map +1 -0
  94. package/dist/components/titan-layout/titan-layout.d.ts +40 -0
  95. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -0
  96. package/dist/components/titan-layout/titan-layout.js +192 -0
  97. package/dist/components/titan-layout/titan-layout.js.map +1 -0
  98. package/dist/components/titan-layout/titan-layout.module.less +106 -0
  99. package/dist/components/titan-layout/titan-layout.stories.d.ts +22 -0
  100. package/dist/components/titan-layout/titan-layout.stories.d.ts.map +1 -0
  101. package/dist/components/titan-layout/titan-layout.stories.js +83 -0
  102. package/dist/components/titan-layout/titan-layout.stories.js.map +1 -0
  103. package/dist/components/titan-layout/with-tooltip.d.ts +4 -0
  104. package/dist/components/titan-layout/with-tooltip.d.ts.map +1 -0
  105. package/dist/components/titan-layout/with-tooltip.js +4 -0
  106. package/dist/components/titan-layout/with-tooltip.js.map +1 -0
  107. package/dist/index.d.ts +2 -1
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +2 -1
  110. package/dist/index.js.map +1 -1
  111. package/dist/test/data.d.ts +4 -1
  112. package/dist/test/data.d.ts.map +1 -1
  113. package/dist/test/data.js +5 -6
  114. package/dist/test/data.js.map +1 -1
  115. package/dist/utils/navigation-legacy.d.ts +3 -1
  116. package/dist/utils/navigation-legacy.d.ts.map +1 -1
  117. package/dist/utils/use-breakpoint.d.ts +8 -0
  118. package/dist/utils/use-breakpoint.d.ts.map +1 -0
  119. package/dist/utils/use-breakpoint.js +14 -0
  120. package/dist/utils/use-breakpoint.js.map +1 -0
  121. package/package.json +5 -6
  122. package/src/components/badge-tag.tsx +1 -1
  123. package/src/components/header-navigation/header-navigation-extra.stories.tsx +7 -0
  124. package/src/components/header-navigation/header-navigation-links.tsx +2 -0
  125. package/src/components/header-navigation/header-navigation-stacked.stories.tsx +5 -1
  126. package/src/components/header-navigation/header-navigation.stories.tsx +6 -1
  127. package/src/components/left-navigation/header-navigation-tiny.stories.tsx +8 -2
  128. package/src/components/left-navigation/side-navigation-links.tsx +1 -1
  129. package/src/components/links.tsx +33 -13
  130. package/src/components/logo/logo-company-title.tsx +8 -6
  131. package/src/components/logo/logo-titan-text.tsx +1 -1
  132. package/src/components/profile-dropdown/profile-dropdown.module.less +24 -6
  133. package/src/components/profile-dropdown/profile-dropdown.module.less.d.ts +2 -0
  134. package/src/components/profile-dropdown/profile-dropdown.stories.tsx +4 -4
  135. package/src/components/profile-dropdown/profile-dropdown.tsx +87 -55
  136. package/src/components/titan-layout/index.ts +5 -0
  137. package/src/components/titan-layout/interface-internal.ts +6 -0
  138. package/src/components/titan-layout/interface.ts +26 -0
  139. package/src/components/titan-layout/layout-context.tsx +30 -0
  140. package/src/components/titan-layout/layout-header-links.tsx +144 -0
  141. package/src/components/titan-layout/layout-header.module.less +193 -0
  142. package/src/components/titan-layout/layout-header.module.less.d.ts +18 -0
  143. package/src/components/titan-layout/layout-header.tsx +97 -0
  144. package/src/components/titan-layout/layout-logo.stories.tsx +31 -0
  145. package/src/components/titan-layout/layout-logo.tsx +64 -0
  146. package/src/components/titan-layout/layout-profile.stories.tsx +46 -0
  147. package/src/components/titan-layout/layout-profile.tsx +178 -0
  148. package/src/components/titan-layout/layout-sidebar-links-internal.tsx +278 -0
  149. package/src/components/titan-layout/layout-sidebar-links.tsx +72 -0
  150. package/src/components/titan-layout/layout-sidebar.module.less +536 -0
  151. package/src/components/titan-layout/layout-sidebar.module.less.d.ts +49 -0
  152. package/src/components/titan-layout/layout-sidebar.tsx +304 -0
  153. package/src/components/titan-layout/notifications-context.tsx +44 -0
  154. package/src/components/titan-layout/titan-layout.module.less +106 -0
  155. package/src/components/titan-layout/titan-layout.module.less.d.ts +16 -0
  156. package/src/components/titan-layout/titan-layout.stories.tsx +342 -0
  157. package/src/components/titan-layout/titan-layout.tsx +461 -0
  158. package/src/components/titan-layout/with-tooltip.tsx +16 -0
  159. package/src/index.ts +2 -1
  160. package/src/test/data.tsx +5 -5
  161. package/src/utils/navigation-legacy.ts +3 -1
  162. package/src/utils/use-breakpoint.ts +21 -0
@@ -0,0 +1,461 @@
1
+ import classNames from 'classnames';
2
+ import {
3
+ CSSProperties,
4
+ Children,
5
+ ComponentPropsWithoutRef,
6
+ FC,
7
+ Fragment,
8
+ ReactElement,
9
+ ReactNode,
10
+ isValidElement,
11
+ useCallback,
12
+ useEffect,
13
+ useMemo,
14
+ useRef,
15
+ useState,
16
+ } from 'react';
17
+ import { NavigationItemData } from '../../utils/navigation';
18
+ import { DefaultNavLinkComponent, NavLinkComponentProps } from '../../utils/navigation-context';
19
+ import { useTitanBreakpoint } from '../../utils/use-breakpoint';
20
+ import { TitanLayoutState } from './interface';
21
+ import {
22
+ LayoutContext,
23
+ LayoutPlacementContext,
24
+ TitanLayoutContextType,
25
+ TitanLayoutSidebarContextType,
26
+ } from './layout-context';
27
+ import { LayoutHeader } from './layout-header';
28
+ import { TitanLayoutLogo, TitanLayoutLogoProps } from './layout-logo';
29
+ import { LayoutSidebar } from './layout-sidebar';
30
+ import { TitanLayoutSidebarLink, TitanLayoutSidebarTrigger } from './layout-sidebar-links';
31
+ import { InternalSideNavigationTrigger } from './layout-sidebar-links-internal';
32
+ import { useNotificationsState } from './notifications-context';
33
+ import * as Styles from './titan-layout.module.less';
34
+
35
+ type TitanLayoutChild = ReactElement<TitanLayoutContentProps> | ReactElement<TitanLayoutLogoProps>;
36
+
37
+ export type TitanLayoutProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'style'> & {
38
+ /** layout appearance */
39
+ appearance?: 'legacy' | 'anvil1' | 'anvil2';
40
+
41
+ /** layout's content */
42
+ children?: TitanLayoutChild | TitanLayoutChild[];
43
+
44
+ /** show only content without side and top bars */
45
+ contentOnly?: boolean;
46
+
47
+ /** component used for navigation */
48
+ navigationComponent?: FC<NavLinkComponentProps>;
49
+
50
+ /** data for main navigation links */
51
+ navigationMainItems?: NavigationItemData[];
52
+
53
+ state?: TitanLayoutState;
54
+ onStateChange?: (state: TitanLayoutState) => void;
55
+
56
+ header?: ReactElement;
57
+ top?: ReactElement;
58
+ sideTop?: ReactElement[];
59
+ profile?: ReactElement;
60
+ extraLinks?: ReactElement;
61
+ extraLinksTop?: ReactElement;
62
+ extraText?: string;
63
+ minContentWidth?: number;
64
+ };
65
+
66
+ const defaultSidebarContext: TitanLayoutSidebarContextType = {
67
+ styles: {
68
+ popoverContent: {
69
+ '--background-color-strong': '#24323C',
70
+ 'color': 'var(--color-white)',
71
+ } as CSSProperties,
72
+ },
73
+ };
74
+
75
+ const useVariant = (appearance: TitanLayoutProps['appearance']) =>
76
+ useMemo(() => {
77
+ const isLegacy = appearance === 'legacy';
78
+ const isAnvil1 = appearance === 'anvil1';
79
+ const isAnvil2 = appearance === 'anvil2';
80
+
81
+ return {
82
+ isLegacy,
83
+ isAnvil1,
84
+ isAnvil2,
85
+ isSequent: isLegacy || isAnvil2,
86
+ };
87
+ }, [appearance]);
88
+
89
+ const useLayoutChildren = (children: TitanLayoutProps['children']) =>
90
+ useMemo(
91
+ () =>
92
+ Children.toArray(children).reduce(
93
+ (out, item) => {
94
+ if (
95
+ item &&
96
+ isValidElement(item) &&
97
+ item.type &&
98
+ typeof item.type !== 'string'
99
+ ) {
100
+ if (item.type.name === TitanLayoutContent.name) {
101
+ out.content = item as any;
102
+ } else if (item.type.name === TitanLayoutLogo.name) {
103
+ out.logo = item as any;
104
+ }
105
+ }
106
+ return out;
107
+ },
108
+ {
109
+ logo: <TitanLayoutLogo />,
110
+ } as {
111
+ content?: ReactElement<TitanLayoutContentProps>;
112
+ logo: ReactElement<TitanLayoutLogoProps>;
113
+ }
114
+ ),
115
+ [children]
116
+ );
117
+
118
+ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
119
+ appearance = 'anvil2',
120
+ id,
121
+ children,
122
+ contentOnly,
123
+ navigationComponent,
124
+ header,
125
+ top,
126
+ profile,
127
+ state,
128
+ onStateChange,
129
+ navigationMainItems,
130
+ extraLinks,
131
+ extraLinksTop,
132
+ extraText,
133
+ minContentWidth,
134
+ sideTop,
135
+ }) => {
136
+ const breakpoint = useTitanBreakpoint();
137
+ const context: TitanLayoutContextType = useMemo(
138
+ () => ({
139
+ NavigationComponent: navigationComponent ?? DefaultNavLinkComponent,
140
+ breakpoint,
141
+ isTitanLayout: true,
142
+ sidebar: defaultSidebarContext,
143
+ }),
144
+ [navigationComponent, breakpoint]
145
+ );
146
+ const variant = useVariant(appearance);
147
+ const [mobileDrawerOpened, setMobileDrawerOpened] = useState(false);
148
+ const { content, logo } = useLayoutChildren(children);
149
+ const { hasNotifications, NotificationsContextProvider } = useNotificationsState();
150
+ const [anvil2Styles, setAnvil2Styles] = useState<object>({});
151
+ const updateIndicatorsHeight = useCallback((offset: number) => {
152
+ setAnvil2Styles({ '--offset': `calc(var(--nav-offset-top) + ${offset}px)` });
153
+ }, []);
154
+
155
+ const isMobile = breakpoint.isMobile;
156
+ const hasSideBar = !contentOnly && (!!navigationMainItems?.length || !!sideTop?.length);
157
+ const hasTopBar = !contentOnly;
158
+
159
+ useEffect(() => {
160
+ if (variant.isAnvil1) {
161
+ const bodyClassName = 'of-hidden-i';
162
+ document.body.classList.add(bodyClassName);
163
+ return () => document.body.classList.remove(bodyClassName);
164
+ }
165
+ }, [variant.isAnvil1]);
166
+
167
+ const onBurgerClick = useCallback((e: MouseEvent) => {
168
+ setMobileDrawerOpened(true);
169
+ e.stopPropagation();
170
+ }, []);
171
+
172
+ const onBarExpandChange = useCallback(
173
+ (expanded: boolean) => {
174
+ if (isMobile) {
175
+ setMobileDrawerOpened(false);
176
+ } else {
177
+ onStateChange?.({ navCollapsed: !expanded });
178
+ }
179
+ },
180
+ [onStateChange, isMobile]
181
+ );
182
+ const onSubmenuExpandChange = useCallback(
183
+ (id: string, expanded: boolean) => {
184
+ onStateChange?.({
185
+ navCollapsed: state?.navCollapsed ?? false,
186
+ submenuExpanded: expanded ? id : undefined,
187
+ });
188
+ },
189
+ [state, onStateChange]
190
+ );
191
+ const hasMenuNotifications = useMemo(() => {
192
+ try {
193
+ return (
194
+ navigationMainItems?.some(item => {
195
+ if (item.counter || item.tag?.value) {
196
+ return true;
197
+ } else if (item.submenu) {
198
+ return item.submenu.groups.some(group =>
199
+ group.links.some(link => !!link.counter || !!link.tag?.value)
200
+ );
201
+ }
202
+ return false;
203
+ }) ?? false
204
+ );
205
+ } catch {
206
+ return false;
207
+ }
208
+ }, [navigationMainItems]);
209
+
210
+ const limitContentWidth = useMemo(() => {
211
+ if (variant.isAnvil2 || !minContentWidth) {
212
+ return undefined;
213
+ }
214
+
215
+ if (breakpoint.width < minContentWidth) {
216
+ return minContentWidth;
217
+ }
218
+ }, [variant, minContentWidth, breakpoint.width]);
219
+
220
+ const contentStyles = useMemo(() => {
221
+ if (variant.isAnvil2) {
222
+ return anvil2Styles;
223
+ }
224
+
225
+ if (variant.isLegacy && limitContentWidth) {
226
+ return {
227
+ display: 'flex',
228
+ flexDirection: 'column',
229
+ minHeight: '100vh',
230
+ };
231
+ }
232
+ }, [variant, anvil2Styles, limitContentWidth]);
233
+
234
+ const layoutClass = variant.isLegacy
235
+ ? Styles.layoutLegacy
236
+ : variant.isAnvil1
237
+ ? Styles.layoutAnvil1
238
+ : Styles.layoutAnvil2;
239
+
240
+ return (
241
+ <LayoutContext.Provider value={context}>
242
+ <LayoutPlacementContext.Provider value="unset">
243
+ <div
244
+ id={id}
245
+ className={classNames(
246
+ Styles.layout,
247
+ isMobile ? Styles.layoutMobile : Styles.layoutDesktop,
248
+ {
249
+ [Styles.layoutTop]: hasTopBar,
250
+ [Styles.layoutNavSlim]: !isMobile && hasSideBar && state?.navCollapsed,
251
+ [Styles.layoutNavWide]: !isMobile && hasSideBar && !state?.navCollapsed,
252
+ },
253
+ layoutClass
254
+ )}
255
+ style={contentStyles}
256
+ >
257
+ {variant.isSequent && <div className={Styles.topPlaceholder} />}
258
+ {hasTopBar && (
259
+ <LayoutHeader
260
+ className={Styles.top}
261
+ logo={logo}
262
+ profile={isMobile ? undefined : profile}
263
+ center={top}
264
+ rightText={isMobile ? undefined : extraText}
265
+ right={
266
+ <Fragment>
267
+ {extraLinksTop}
268
+ {!isMobile && extraLinks}
269
+ </Fragment>
270
+ }
271
+ isMobile={isMobile}
272
+ hasNotifications={hasNotifications || hasMenuNotifications}
273
+ onBurgerClick={onBurgerClick}
274
+ />
275
+ )}
276
+
277
+ {hasSideBar && (
278
+ <NotificationsContextProvider>
279
+ <LayoutSidebar
280
+ className={Styles.side}
281
+ mobile={breakpoint.isMobile}
282
+ barExpanded={!state?.navCollapsed}
283
+ onBarExpandChange={onBarExpandChange}
284
+ submenuExpanded={state?.submenuExpanded}
285
+ onSubmenuExpandChange={onSubmenuExpandChange}
286
+ drawerOpened={mobileDrawerOpened}
287
+ onDrawerOpenChange={setMobileDrawerOpened}
288
+ top={sideTop}
289
+ mainItems={navigationMainItems}
290
+ navigationComponent={context.NavigationComponent}
291
+ bottom={
292
+ isMobile ? (
293
+ <Fragment>
294
+ {profile}
295
+ {extraLinks}
296
+ {!!extraText && (
297
+ <InternalSideNavigationTrigger
298
+ id="__extra_text"
299
+ title={extraText}
300
+ submenuExpanded={undefined}
301
+ dataPrefix="navigation-extra-text"
302
+ tag={undefined}
303
+ icon={undefined}
304
+ iconActive={undefined}
305
+ />
306
+ )}
307
+ </Fragment>
308
+ ) : undefined
309
+ }
310
+ />
311
+ </NotificationsContextProvider>
312
+ )}
313
+
314
+ {variant.isAnvil1 ? (
315
+ <LayoutContentAnvil1 header={header} minWidth={limitContentWidth}>
316
+ {content}
317
+ </LayoutContentAnvil1>
318
+ ) : variant.isLegacy ? (
319
+ <LayoutContentLegacy header={header} minWidth={limitContentWidth}>
320
+ {content}
321
+ </LayoutContentLegacy>
322
+ ) : (
323
+ <LayoutContentAnvil2
324
+ header={header}
325
+ onHeaderHeightChange={updateIndicatorsHeight}
326
+ >
327
+ {content}
328
+ </LayoutContentAnvil2>
329
+ )}
330
+ </div>
331
+ </LayoutPlacementContext.Provider>
332
+ </LayoutContext.Provider>
333
+ );
334
+ };
335
+
336
+ const TitanLayoutHeaderObserved: FC<{
337
+ children: ReactNode;
338
+ heightChange?(value: number): void;
339
+ }> = ({ children, heightChange }) => {
340
+ const ref = useRef<HTMLDivElement>(null);
341
+
342
+ useEffect(() => {
343
+ if (ref.current) {
344
+ const updatePosition = () => {
345
+ if (ref.current && heightChange) {
346
+ const pos = ref.current.getBoundingClientRect();
347
+ heightChange(pos.height);
348
+ }
349
+ };
350
+
351
+ const observer = new ResizeObserver(updatePosition);
352
+ observer.observe(ref.current);
353
+
354
+ updatePosition();
355
+ return () => observer.disconnect();
356
+ }
357
+ }, [heightChange]);
358
+
359
+ useEffect(() => {
360
+ return () => {
361
+ heightChange?.(0);
362
+ };
363
+ }, [heightChange]);
364
+ return (
365
+ <div ref={ref} className={Styles.contentHeader} data-cy="layout-content-header">
366
+ {children}
367
+ </div>
368
+ );
369
+ };
370
+ const TitanLayoutHeader: FC<{ children: ReactNode }> = ({ children }) => {
371
+ return (
372
+ <div className={Styles.contentHeader} data-cy="layout-content-header">
373
+ {children}
374
+ </div>
375
+ );
376
+ };
377
+
378
+ export interface TitanLayoutContentProps {
379
+ children: ReactNode;
380
+ }
381
+ const TitanLayoutContent: FC<TitanLayoutContentProps> = ({ children }) => children;
382
+
383
+ const LayoutContentAnvil2: FC<{
384
+ children: ReactNode;
385
+ header?: ReactNode;
386
+ onHeaderHeightChange?: (height: number) => void;
387
+ }> = ({ children, header, onHeaderHeightChange }) => {
388
+ return (
389
+ <Fragment>
390
+ <TitanLayoutHeaderObserved heightChange={onHeaderHeightChange}>
391
+ {header}
392
+ </TitanLayoutHeaderObserved>
393
+ {children}
394
+ </Fragment>
395
+ );
396
+ };
397
+
398
+ const LayoutContentAnvil1: FC<{
399
+ children: ReactNode;
400
+ header?: ReactNode;
401
+ minWidth?: number;
402
+ }> = ({ children, header, minWidth }) => {
403
+ const innerContentStyles: CSSProperties = useMemo(
404
+ () => ({
405
+ ...(minWidth ? { minWidth: `${minWidth}px` } : {}),
406
+ }),
407
+ [minWidth]
408
+ );
409
+
410
+ return (
411
+ <Fragment>
412
+ <TitanLayoutHeader>{header}</TitanLayoutHeader>
413
+ <div
414
+ className={classNames(Styles.content, { 'of-x-auto': !!minWidth })}
415
+ data-cy="layout-content"
416
+ >
417
+ <div
418
+ className="position-relative d-f flex-grow-1 flex-basis-0 of-hidden"
419
+ style={innerContentStyles}
420
+ >
421
+ {children}
422
+ </div>
423
+ </div>
424
+ </Fragment>
425
+ );
426
+ };
427
+
428
+ const LayoutContentLegacy: FC<{
429
+ children: ReactNode;
430
+ header?: ReactNode;
431
+ minWidth: number | undefined;
432
+ }> = ({ children, header, minWidth }) => {
433
+ const innerContentStyles: CSSProperties = useMemo(
434
+ () => ({
435
+ position: 'relative',
436
+ minWidth: `${minWidth}px`,
437
+ }),
438
+ [minWidth]
439
+ );
440
+
441
+ return (
442
+ <Fragment>
443
+ <TitanLayoutHeader>{header}</TitanLayoutHeader>
444
+
445
+ {minWidth ? (
446
+ <div className="of-x-auto flex-basis-0 flex-grow-1">
447
+ <div style={innerContentStyles}>{children}</div>
448
+ </div>
449
+ ) : (
450
+ children
451
+ )}
452
+ </Fragment>
453
+ );
454
+ };
455
+
456
+ export const TitanLayout = Object.assign(TitanLayoutComponent, {
457
+ Content: TitanLayoutContent,
458
+ Logo: TitanLayoutLogo,
459
+ Link: TitanLayoutSidebarLink,
460
+ Trigger: TitanLayoutSidebarTrigger,
461
+ });
@@ -0,0 +1,16 @@
1
+ import { Tooltip, TooltipProps } from '@servicetitan/anvil2';
2
+ import { ReactElement } from 'react';
3
+
4
+ export const withTooltip = (
5
+ element: ReactElement,
6
+ tooltip: string | undefined,
7
+ placement: TooltipProps['placement'] = 'bottom'
8
+ ) =>
9
+ tooltip ? (
10
+ <Tooltip placement={placement}>
11
+ <Tooltip.Content>{tooltip}</Tooltip.Content>
12
+ <Tooltip.Trigger>{element}</Tooltip.Trigger>
13
+ </Tooltip>
14
+ ) : (
15
+ element
16
+ );
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export * from './components/header-navigation';
2
- export * from './components/profile-dropdown/profile-dropdown';
3
2
  export * from './components/logo/logo-company-title';
4
3
  export * from './components/logo/logo-titan';
5
4
  export * from './components/logo/logo-titan-text';
6
5
  export * from './components/counter-tag';
7
6
  export * from './components/left-navigation';
7
+ export * from './components/titan-layout';
8
8
  export * from './components/links';
9
9
  export * from './utils/navigation';
10
10
  export {
@@ -17,3 +17,4 @@ export {
17
17
  NavigationActiveLinkMatcher,
18
18
  } from './utils/navigation-context';
19
19
  export * from './utils/navigation-legacy';
20
+ export * from './utils/use-breakpoint';
package/src/test/data.tsx CHANGED
@@ -66,7 +66,6 @@ export const NavLinkMock = forwardRef<any, NavLinkComponentProps>(
66
66
  {...rest}
67
67
  onClick={e => {
68
68
  e.preventDefault();
69
- e.stopPropagation();
70
69
 
71
70
  if (!to.startsWith('http')) {
72
71
  history.replace(to);
@@ -82,10 +81,10 @@ export const NavLinkMock = forwardRef<any, NavLinkComponentProps>(
82
81
  }
83
82
  );
84
83
 
85
- export const LocationInfo = () => {
84
+ export const LocationInfo: FC<{ className?: string }> = ({ className }) => {
86
85
  const location = useLocation();
87
86
 
88
- return <BodyText>current location - {location.pathname}</BodyText>;
87
+ return <BodyText className={className}>current location - {location.pathname}</BodyText>;
89
88
  };
90
89
 
91
90
  const LocationProvider: FC<{ children: any }> = ({ children }) => {
@@ -154,7 +153,7 @@ const getItem = (
154
153
  hint: id,
155
154
  icon: undefined,
156
155
  iconActive: undefined,
157
- ...(data ?? {}),
156
+ ...data,
158
157
  submenu: data.submenu
159
158
  ? {
160
159
  ...data.submenu,
@@ -176,7 +175,7 @@ const getSubItem = (
176
175
  id,
177
176
  to: id,
178
177
  title: id[0].toUpperCase() + id.substring(1),
179
- ...(data ?? {}),
178
+ ...data,
180
179
  });
181
180
 
182
181
  const getGroup = (
@@ -339,6 +338,7 @@ export const CallsNavigationTrigger = () => {
339
338
  id="dialpad"
340
339
  iconName="phone"
341
340
  counter={2}
341
+ title="Calls"
342
342
  icon={SvgPhone}
343
343
  iconActive={SvgPhoneActive}
344
344
  onClick={() => setOpen(!open)}
@@ -54,6 +54,8 @@ export interface HeaderNavigationItemLinkProps {
54
54
  export interface HeaderNavigationTriggerPropsStrict {
55
55
  /** unique identifier */
56
56
  id: string;
57
+ /** item title (used for mobile) */
58
+ title: string;
57
59
  /** tooltip text */
58
60
  tooltip?: string;
59
61
  /** item description */
@@ -80,7 +82,7 @@ export interface HeaderNavigationTriggerPropsStrict {
80
82
  /** svg icon (anvil2) of inactive item */
81
83
  icon: IconProps['svg'] | undefined;
82
84
  /** svg icon (anvil2) of active item */
83
- iconActive: IconProps['svg'] | undefined;
85
+ iconActive?: IconProps['svg'];
84
86
  }
85
87
 
86
88
  export interface HeaderNavigationTriggerProps extends HeaderNavigationTriggerPropsStrict {
@@ -0,0 +1,21 @@
1
+ import { BreakpointReturnProps, useBreakpoint } from '@servicetitan/anvil2';
2
+ import { useMemo } from 'react';
3
+
4
+ export interface TitanBreakpoint {
5
+ name: BreakpointReturnProps['name'];
6
+ isMobile: boolean;
7
+ width: number;
8
+ }
9
+
10
+ export const useTitanBreakpoint = (): TitanBreakpoint => {
11
+ const breakpoint = useBreakpoint();
12
+
13
+ return useMemo(
14
+ () => ({
15
+ name: breakpoint?.name ?? 'xl',
16
+ isMobile: breakpoint ? breakpoint.innerWidth < 768 : false,
17
+ width: breakpoint?.innerWidth ?? 0,
18
+ }),
19
+ [breakpoint]
20
+ );
21
+ };