@servicetitan/navigation 11.0.0-canary.237.4d902dc.0 → 11.0.0-canary.237.6c48c6d.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 (90) 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 +10 -7
  12. package/dist/components/profile-dropdown/profile-dropdown.d.ts.map +1 -1
  13. package/dist/components/profile-dropdown/profile-dropdown.js +2 -2
  14. package/dist/components/profile-dropdown/profile-dropdown.js.map +1 -1
  15. package/dist/components/profile-dropdown/profile-dropdown.module.less +2 -0
  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.d.ts +2 -0
  19. package/dist/components/titan-layout/layout-header.d.ts.map +1 -1
  20. package/dist/components/titan-layout/layout-header.js +3 -4
  21. package/dist/components/titan-layout/layout-header.js.map +1 -1
  22. package/dist/components/titan-layout/layout-header.module.less +62 -14
  23. package/dist/components/titan-layout/layout-logo.d.ts.map +1 -1
  24. package/dist/components/titan-layout/layout-logo.js +2 -1
  25. package/dist/components/titan-layout/layout-logo.js.map +1 -1
  26. package/dist/components/titan-layout/layout-profile.d.ts.map +1 -1
  27. package/dist/components/titan-layout/layout-profile.js +32 -8
  28. package/dist/components/titan-layout/layout-profile.js.map +1 -1
  29. package/dist/components/titan-layout/layout-profile.stories.d.ts.map +1 -1
  30. package/dist/components/titan-layout/layout-profile.stories.js +1 -1
  31. package/dist/components/titan-layout/layout-profile.stories.js.map +1 -1
  32. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts +2 -2
  33. package/dist/components/titan-layout/layout-sidebar-links-internal.d.ts.map +1 -1
  34. package/dist/components/titan-layout/layout-sidebar-links-internal.js +4 -4
  35. package/dist/components/titan-layout/layout-sidebar-links-internal.js.map +1 -1
  36. package/dist/components/titan-layout/layout-sidebar-links.d.ts.map +1 -1
  37. package/dist/components/titan-layout/layout-sidebar-links.js +11 -4
  38. package/dist/components/titan-layout/layout-sidebar-links.js.map +1 -1
  39. package/dist/components/titan-layout/layout-sidebar.d.ts +2 -1
  40. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
  41. package/dist/components/titan-layout/layout-sidebar.js +6 -4
  42. package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
  43. package/dist/components/titan-layout/layout-sidebar.module.less +29 -21
  44. package/dist/components/titan-layout/notifications-context.d.ts +13 -0
  45. package/dist/components/titan-layout/notifications-context.d.ts.map +1 -0
  46. package/dist/components/titan-layout/notifications-context.js +23 -0
  47. package/dist/components/titan-layout/notifications-context.js.map +1 -0
  48. package/dist/components/titan-layout/titan-layout.d.ts +10 -6
  49. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
  50. package/dist/components/titan-layout/titan-layout.js +84 -21
  51. package/dist/components/titan-layout/titan-layout.js.map +1 -1
  52. package/dist/components/titan-layout/titan-layout.module.less +72 -19
  53. package/dist/components/titan-layout/titan-layout.stories.d.ts +15 -11
  54. package/dist/components/titan-layout/titan-layout.stories.d.ts.map +1 -1
  55. package/dist/components/titan-layout/titan-layout.stories.js +35 -14
  56. package/dist/components/titan-layout/titan-layout.stories.js.map +1 -1
  57. package/dist/test/data.d.ts +4 -1
  58. package/dist/test/data.d.ts.map +1 -1
  59. package/dist/test/data.js +2 -3
  60. package/dist/test/data.js.map +1 -1
  61. package/dist/utils/use-breakpoint.d.ts +1 -0
  62. package/dist/utils/use-breakpoint.d.ts.map +1 -1
  63. package/dist/utils/use-breakpoint.js +2 -1
  64. package/dist/utils/use-breakpoint.js.map +1 -1
  65. package/package.json +2 -2
  66. package/src/components/badge-tag.tsx +1 -1
  67. package/src/components/header-navigation/header-navigation-stacked.stories.tsx +1 -1
  68. package/src/components/header-navigation/header-navigation.stories.tsx +1 -1
  69. package/src/components/left-navigation/header-navigation-tiny.stories.tsx +2 -2
  70. package/src/components/logo/logo-titan-text.tsx +1 -1
  71. package/src/components/profile-dropdown/profile-dropdown.module.less +2 -0
  72. package/src/components/profile-dropdown/profile-dropdown.tsx +13 -6
  73. package/src/components/titan-layout/layout-context.tsx +1 -1
  74. package/src/components/titan-layout/layout-header.module.less +62 -14
  75. package/src/components/titan-layout/layout-header.tsx +12 -5
  76. package/src/components/titan-layout/layout-logo.tsx +13 -6
  77. package/src/components/titan-layout/layout-profile.stories.tsx +10 -1
  78. package/src/components/titan-layout/layout-profile.tsx +60 -25
  79. package/src/components/titan-layout/layout-sidebar-links-internal.tsx +18 -5
  80. package/src/components/titan-layout/layout-sidebar-links.tsx +16 -4
  81. package/src/components/titan-layout/layout-sidebar.module.less +29 -21
  82. package/src/components/titan-layout/layout-sidebar.module.less.d.ts +1 -2
  83. package/src/components/titan-layout/layout-sidebar.tsx +15 -9
  84. package/src/components/titan-layout/notifications-context.tsx +44 -0
  85. package/src/components/titan-layout/titan-layout.module.less +72 -19
  86. package/src/components/titan-layout/titan-layout.module.less.d.ts +5 -0
  87. package/src/components/titan-layout/titan-layout.stories.tsx +166 -19
  88. package/src/components/titan-layout/titan-layout.tsx +212 -74
  89. package/src/test/data.tsx +2 -3
  90. package/src/utils/use-breakpoint.ts +2 -0
@@ -11,6 +11,7 @@ import {
11
11
  useCallback,
12
12
  useEffect,
13
13
  useMemo,
14
+ useRef,
14
15
  useState,
15
16
  } from 'react';
16
17
  import { NavigationItemData } from '../../utils/navigation';
@@ -28,33 +29,38 @@ import { TitanLayoutLogo, TitanLayoutLogoProps } from './layout-logo';
28
29
  import { LayoutSidebar } from './layout-sidebar';
29
30
  import { TitanLayoutSidebarLink, TitanLayoutSidebarTrigger } from './layout-sidebar-links';
30
31
  import { InternalSideNavigationTrigger } from './layout-sidebar-links-internal';
32
+ import { useNotificationsState } from './notifications-context';
31
33
  import * as Styles from './titan-layout.module.less';
32
34
 
33
35
  type TitanLayoutChild = ReactElement<TitanLayoutContentProps> | ReactElement<TitanLayoutLogoProps>;
34
36
 
35
37
  export type TitanLayoutProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'style'> & {
36
- empty?: boolean;
37
-
38
+ /** layout appearance */
38
39
  appearance?: 'legacy' | 'anvil1' | 'anvil2';
39
40
 
41
+ /** layout's content */
42
+ children?: TitanLayoutChild | TitanLayoutChild[];
43
+
44
+ /** show only content without side and top bars */
45
+ contentOnly?: boolean;
46
+
40
47
  /** component used for navigation */
41
48
  navigationComponent?: FC<NavLinkComponentProps>;
42
49
 
43
50
  /** data for main navigation links */
44
51
  navigationMainItems?: NavigationItemData[];
45
52
 
46
- /** layout's content */
47
- children?: TitanLayoutChild | TitanLayoutChild[];
48
-
49
53
  state?: TitanLayoutState;
50
54
  onStateChange?: (state: TitanLayoutState) => void;
51
55
 
52
56
  header?: ReactElement;
57
+ top?: ReactElement;
58
+ sideTop?: ReactElement[];
53
59
  profile?: ReactElement;
54
60
  extraLinks?: ReactElement;
55
61
  extraLinksTop?: ReactElement;
56
62
  extraText?: string;
57
- sidebarTop?: ReactElement[];
63
+ minContentWidth?: number;
58
64
  };
59
65
 
60
66
  const defaultSidebarContext: TitanLayoutSidebarContextType = {
@@ -76,7 +82,7 @@ const useVariant = (appearance: TitanLayoutProps['appearance']) =>
76
82
  isLegacy,
77
83
  isAnvil1,
78
84
  isAnvil2,
79
- isSequent: isLegacy || isAnvil1,
85
+ isSequent: isLegacy || isAnvil2,
80
86
  };
81
87
  }, [appearance]);
82
88
 
@@ -113,8 +119,10 @@ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
113
119
  appearance = 'anvil2',
114
120
  id,
115
121
  children,
122
+ contentOnly,
116
123
  navigationComponent,
117
124
  header,
125
+ top,
118
126
  profile,
119
127
  state,
120
128
  onStateChange,
@@ -122,10 +130,10 @@ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
122
130
  extraLinks,
123
131
  extraLinksTop,
124
132
  extraText,
125
- sidebarTop,
133
+ minContentWidth,
134
+ sideTop,
126
135
  }) => {
127
136
  const breakpoint = useTitanBreakpoint();
128
- const isMobile = breakpoint.isMobile;
129
137
  const context: TitanLayoutContextType = useMemo(
130
138
  () => ({
131
139
  NavigationComponent: navigationComponent ?? DefaultNavLinkComponent,
@@ -138,21 +146,19 @@ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
138
146
  const variant = useVariant(appearance);
139
147
  const [mobileDrawerOpened, setMobileDrawerOpened] = useState(false);
140
148
  const { content, logo } = useLayoutChildren(children);
149
+ const { hasNotifications, NotificationsContextProvider } = useNotificationsState();
150
+
151
+ const isMobile = breakpoint.isMobile;
152
+ const hasSideBar = !contentOnly && (!!navigationMainItems?.length || !!sideTop?.length);
153
+ const hasTopBar = !contentOnly;
141
154
 
142
155
  useEffect(() => {
143
- if (!isMobile) {
144
- setMobileDrawerOpened(false);
145
- return;
156
+ if (variant.isAnvil1) {
157
+ const bodyClassName = 'of-hidden-i';
158
+ document.body.classList.add(bodyClassName);
159
+ return () => document.body.classList.remove(bodyClassName);
146
160
  }
147
-
148
- const listener = () => {
149
- setMobileDrawerOpened(false);
150
- };
151
-
152
- document.addEventListener('click', listener);
153
-
154
- return () => document.removeEventListener('click', listener);
155
- }, [isMobile]);
161
+ }, [variant.isAnvil1]);
156
162
 
157
163
  const onBurgerClick = useCallback((e: MouseEvent) => {
158
164
  setMobileDrawerOpened(true);
@@ -178,8 +184,34 @@ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
178
184
  },
179
185
  [state, onStateChange]
180
186
  );
187
+ const hasMenuNotifications = useMemo(() => {
188
+ try {
189
+ return (
190
+ navigationMainItems?.some(item => {
191
+ if (item.counter || item.tag?.value) {
192
+ return true;
193
+ } else if (item.submenu) {
194
+ return item.submenu.groups.some(group =>
195
+ group.links.some(link => !!link.counter || !!link.tag?.value)
196
+ );
197
+ }
198
+ return false;
199
+ }) ?? false
200
+ );
201
+ } catch {
202
+ return false;
203
+ }
204
+ }, [navigationMainItems]);
181
205
 
182
- const [layoutStyles] = useState<object>({});
206
+ const limitContentWidth = useMemo(() => {
207
+ if (variant.isAnvil2 || !minContentWidth) {
208
+ return undefined;
209
+ }
210
+
211
+ if (variant.isAnvil1 && breakpoint.width < minContentWidth) {
212
+ return minContentWidth;
213
+ }
214
+ }, [variant, minContentWidth, breakpoint.width]);
183
215
 
184
216
  const layoutClass = variant.isLegacy
185
217
  ? Styles.layoutLegacy
@@ -194,77 +226,183 @@ const TitanLayoutComponent: FC<TitanLayoutProps> = ({
194
226
  id={id}
195
227
  className={classNames(
196
228
  Styles.layout,
197
- isMobile
198
- ? Styles.layoutMobile
199
- : state?.navCollapsed
200
- ? Styles.layoutNavSlim
201
- : Styles.layoutNavWide,
229
+ isMobile ? Styles.layoutMobile : Styles.layoutDesktop,
230
+ {
231
+ [Styles.layoutTop]: hasTopBar,
232
+ [Styles.layoutNavSlim]: !isMobile && hasSideBar && state?.navCollapsed,
233
+ [Styles.layoutNavWide]: !isMobile && hasSideBar && !state?.navCollapsed,
234
+ },
202
235
  layoutClass
203
236
  )}
204
- style={layoutStyles}
205
237
  >
206
238
  {variant.isSequent && <div className={Styles.topPlaceholder} />}
207
- <LayoutHeader
208
- className={Styles.top}
209
- logo={logo}
210
- profile={isMobile ? undefined : profile}
211
- center={header}
212
- rightText={isMobile ? undefined : extraText}
213
- right={
214
- <Fragment>
215
- {extraLinksTop}
216
- {!isMobile && extraLinks}
217
- </Fragment>
218
- }
219
- onBurgerClick={onBurgerClick}
220
- />
221
-
222
- <LayoutSidebar
223
- flex={!variant.isSequent}
224
- mobile={breakpoint.isMobile}
225
- barExpanded={isMobile ? mobileDrawerOpened : !state?.navCollapsed}
226
- submenuExpanded={state?.submenuExpanded}
227
- onBarExpandChange={onBarExpandChange}
228
- onSubmenuExpandChange={onSubmenuExpandChange}
229
- top={sidebarTop}
230
- mainItems={navigationMainItems}
231
- navigationComponent={context.NavigationComponent}
232
- bottom={
233
- isMobile ? (
239
+ {hasTopBar && (
240
+ <LayoutHeader
241
+ className={Styles.top}
242
+ logo={logo}
243
+ profile={isMobile ? undefined : profile}
244
+ center={top}
245
+ rightText={isMobile ? undefined : extraText}
246
+ right={
234
247
  <Fragment>
235
- {profile}
236
- {extraLinks}
237
- {!!extraText && (
238
- <InternalSideNavigationTrigger
239
- id="__extra_text"
240
- title={extraText}
241
- submenuExpanded={undefined}
242
- dataPrefix="navigation-extra-text"
243
- tag={undefined}
244
- icon={undefined}
245
- iconActive={undefined}
246
- />
247
- )}
248
+ {extraLinksTop}
249
+ {!isMobile && extraLinks}
248
250
  </Fragment>
249
- ) : undefined
250
- }
251
- />
251
+ }
252
+ isMobile={isMobile}
253
+ hasNotifications={hasNotifications || hasMenuNotifications}
254
+ onBurgerClick={onBurgerClick}
255
+ />
256
+ )}
252
257
 
253
- {content}
258
+ {hasSideBar && (
259
+ <NotificationsContextProvider>
260
+ <LayoutSidebar
261
+ className={Styles.side}
262
+ mobile={breakpoint.isMobile}
263
+ barExpanded={!state?.navCollapsed}
264
+ onBarExpandChange={onBarExpandChange}
265
+ submenuExpanded={state?.submenuExpanded}
266
+ onSubmenuExpandChange={onSubmenuExpandChange}
267
+ drawerOpened={mobileDrawerOpened}
268
+ onDrawerOpenChange={setMobileDrawerOpened}
269
+ top={sideTop}
270
+ mainItems={navigationMainItems}
271
+ navigationComponent={context.NavigationComponent}
272
+ bottom={
273
+ isMobile ? (
274
+ <Fragment>
275
+ {profile}
276
+ {extraLinks}
277
+ {!!extraText && (
278
+ <InternalSideNavigationTrigger
279
+ id="__extra_text"
280
+ title={extraText}
281
+ submenuExpanded={undefined}
282
+ dataPrefix="navigation-extra-text"
283
+ tag={undefined}
284
+ icon={undefined}
285
+ iconActive={undefined}
286
+ />
287
+ )}
288
+ </Fragment>
289
+ ) : undefined
290
+ }
291
+ />
292
+ </NotificationsContextProvider>
293
+ )}
294
+ <LayoutContent
295
+ header={header}
296
+ anvil2={variant.isAnvil2}
297
+ anvil1={variant.isAnvil1}
298
+ minWidth={limitContentWidth}
299
+ >
300
+ {content}
301
+ </LayoutContent>
254
302
  </div>
255
303
  </LayoutPlacementContext.Provider>
256
304
  </LayoutContext.Provider>
257
305
  );
258
306
  };
259
307
 
308
+ const TitanLayoutHeaderObserved: FC<{
309
+ children: ReactNode;
310
+ heightChange?(value: number): void;
311
+ }> = ({ children, heightChange }) => {
312
+ const ref = useRef<HTMLDivElement>(null);
313
+
314
+ useEffect(() => {
315
+ if (ref.current) {
316
+ const updatePosition = () => {
317
+ if (ref.current && heightChange) {
318
+ const pos = ref.current.getBoundingClientRect();
319
+ heightChange(pos.height);
320
+ }
321
+ };
322
+
323
+ const observer = new ResizeObserver(updatePosition);
324
+ observer.observe(ref.current);
325
+
326
+ updatePosition();
327
+ return () => observer.disconnect();
328
+ }
329
+ }, [heightChange]);
330
+
331
+ useEffect(() => {
332
+ return () => {
333
+ heightChange?.(0);
334
+ };
335
+ }, [heightChange]);
336
+ return (
337
+ <div ref={ref} className={Styles.contentHeader} data-cy="layout-content-header">
338
+ {children}
339
+ </div>
340
+ );
341
+ };
342
+
260
343
  export interface TitanLayoutContentProps {
261
344
  children: ReactNode;
262
345
  }
263
346
  const TitanLayoutContent: FC<TitanLayoutContentProps> = ({ children }) => children;
264
347
 
348
+ const LayoutContent: FC<{
349
+ children: ReactNode;
350
+ header?: ReactNode;
351
+ anvil1: boolean;
352
+ anvil2: boolean;
353
+ minWidth: number | undefined;
354
+ }> = ({ anvil1, anvil2, children, header, minWidth }) => {
355
+ const [anvil2Styles, setAnvil2Styles] = useState<object>({});
356
+ const updateIndicatorsHeight = useCallback((offset: number) => {
357
+ setAnvil2Styles({ '--offset': `calc(var(--nav-offset-top) + ${offset}px)` });
358
+ }, []);
359
+
360
+ const contentStyles: CSSProperties = useMemo(
361
+ () => ({
362
+ ...(anvil1 && minWidth ? { overflowX: 'auto' } : {}),
363
+ ...(anvil2 ? anvil2Styles : {}),
364
+ }),
365
+ [anvil1, anvil2, minWidth, anvil2Styles]
366
+ );
367
+
368
+ const innerContentStyles: CSSProperties = useMemo(
369
+ () => ({
370
+ ...(anvil1 && minWidth ? { minWidth: `${minWidth}px` } : {}),
371
+ }),
372
+ [anvil1, minWidth]
373
+ );
374
+
375
+ return (
376
+ <Fragment>
377
+ {!!header &&
378
+ (anvil2 ? (
379
+ <TitanLayoutHeaderObserved heightChange={updateIndicatorsHeight}>
380
+ {header}
381
+ </TitanLayoutHeaderObserved>
382
+ ) : (
383
+ <div className={Styles.contentHeader} data-cy="layout-content-header">
384
+ {header}
385
+ </div>
386
+ ))}
387
+ <div className={Styles.content} style={contentStyles} data-cy="layout-content">
388
+ {anvil1 ? (
389
+ <div
390
+ className="position-relative d-f flex-grow-1 flex-basis-0 of-hidden"
391
+ style={innerContentStyles}
392
+ >
393
+ {children}
394
+ </div>
395
+ ) : (
396
+ children
397
+ )}
398
+ </div>
399
+ </Fragment>
400
+ );
401
+ };
402
+
265
403
  export const TitanLayout = Object.assign(TitanLayoutComponent, {
266
404
  Content: TitanLayoutContent,
267
405
  Logo: TitanLayoutLogo,
268
- SidebarLink: TitanLayoutSidebarLink,
269
- SidebarTrigger: TitanLayoutSidebarTrigger,
406
+ Link: TitanLayoutSidebarLink,
407
+ Trigger: TitanLayoutSidebarTrigger,
270
408
  });
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 }) => {
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
4
4
  export interface TitanBreakpoint {
5
5
  name: BreakpointReturnProps['name'];
6
6
  isMobile: boolean;
7
+ width: number;
7
8
  }
8
9
 
9
10
  export const useTitanBreakpoint = (): TitanBreakpoint => {
@@ -13,6 +14,7 @@ export const useTitanBreakpoint = (): TitanBreakpoint => {
13
14
  () => ({
14
15
  name: breakpoint?.name ?? 'xl',
15
16
  isMobile: breakpoint ? breakpoint.innerWidth <= 768 : false,
17
+ width: breakpoint?.innerWidth ?? 0,
16
18
  }),
17
19
  [breakpoint]
18
20
  );