@servicetitan/navigation 13.1.4 → 13.2.0-canary.270.b4a066f.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 (37) hide show
  1. package/dist/components/titan-layout/layout-context.js +1 -0
  2. package/dist/components/titan-layout/layout-context.js.map +1 -1
  3. package/dist/components/titan-layout/layout-header.module.less +1 -1
  4. package/dist/components/titan-layout/layout-sidebar.d.ts +1 -0
  5. package/dist/components/titan-layout/layout-sidebar.d.ts.map +1 -1
  6. package/dist/components/titan-layout/layout-sidebar.js +5 -4
  7. package/dist/components/titan-layout/layout-sidebar.js.map +1 -1
  8. package/dist/components/titan-layout/titan-layout-default.stories.d.ts.map +1 -1
  9. package/dist/components/titan-layout/titan-layout.d.ts +3 -1
  10. package/dist/components/titan-layout/titan-layout.d.ts.map +1 -1
  11. package/dist/components/titan-layout/titan-layout.js +66 -28
  12. package/dist/components/titan-layout/titan-layout.js.map +1 -1
  13. package/dist/components/titan-layout/titan-layout.module.less +24 -25
  14. package/dist/components/titan-layout/titan-layout.module.less.d.ts +1 -3
  15. package/dist/test/data.d.ts +3 -0
  16. package/dist/test/data.d.ts.map +1 -1
  17. package/dist/test/data.js +40 -1
  18. package/dist/test/data.js.map +1 -1
  19. package/dist/test/titan-layout.d.ts +1 -0
  20. package/dist/test/titan-layout.d.ts.map +1 -1
  21. package/dist/test/titan-layout.js +1 -0
  22. package/dist/test/titan-layout.js.map +1 -1
  23. package/dist/utils/use-breakpoint.d.ts +1 -0
  24. package/dist/utils/use-breakpoint.d.ts.map +1 -1
  25. package/dist/utils/use-breakpoint.js +2 -0
  26. package/dist/utils/use-breakpoint.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/components/titan-layout/layout-context.tsx +1 -1
  29. package/src/components/titan-layout/layout-header.module.less +1 -1
  30. package/src/components/titan-layout/layout-sidebar.tsx +13 -2
  31. package/src/components/titan-layout/titan-layout-default.stories.tsx +19 -21
  32. package/src/components/titan-layout/titan-layout.module.less +24 -25
  33. package/src/components/titan-layout/titan-layout.module.less.d.ts +1 -3
  34. package/src/components/titan-layout/titan-layout.tsx +118 -64
  35. package/src/test/data.tsx +37 -1
  36. package/src/test/titan-layout.tsx +2 -0
  37. package/src/utils/use-breakpoint.ts +4 -0
@@ -9,8 +9,7 @@
9
9
  @bg-color-active: rgba(120, 187, 250, 0.2);
10
10
 
11
11
  .layout-anvil1 {
12
- height: calc(100vh - var(--nav-offset-top));
13
- margin-top: var(--nav-offset-top);
12
+ height: 100vh;
14
13
 
15
14
  display: flex;
16
15
  flex-direction: column;
@@ -30,20 +29,12 @@
30
29
  }
31
30
  }
32
31
 
33
- .layout-anvil2 {
34
- .content-header {
35
- position: sticky;
36
- top: var(--nav-offset-top);
37
- z-index: 989;
38
- }
39
- }
40
-
41
32
  .layout-desktop {
42
33
  padding-left: var(--nav-offset-left);
43
34
 
44
35
  .side {
45
36
  position: fixed;
46
- top: var(--nav-offset-top);
37
+ top: var(--nav-top-height);
47
38
  bottom: 0;
48
39
  left: 0;
49
40
  right: 0;
@@ -52,31 +43,20 @@
52
43
  }
53
44
 
54
45
  .layout-legacy,
55
- .layout-anvil2 {
46
+ .layout-anvil2,
47
+ .layout-anvil1 {
56
48
  .top-placeholder {
57
49
  height: var(--nav-offset-top);
58
50
  }
59
51
  }
60
52
 
61
53
  .layout {
54
+ --nav-top-height: 0px;
62
55
  --nav-offset-top: 0px;
63
56
  --nav-offset-left: 0px;
64
57
  --content-offset-top: var(--nav-offset-top);
65
58
  --offset: var(--content-offset-top);
66
59
 
67
- &.layout-desktop.layout-top-light {
68
- --nav-offset-top: 48px;
69
- }
70
-
71
- &.layout-desktop.layout-top-nav {
72
- --nav-offset-top: 80px;
73
- }
74
-
75
- &.layout-mobile.layout-top-light,
76
- &.layout-mobile.layout-top-nav {
77
- --nav-offset-top: 72px;
78
- }
79
-
80
60
  &.layout-desktop.layout-nav-slim {
81
61
  --nav-offset-left: 64px;
82
62
  }
@@ -98,10 +78,29 @@
98
78
  left: 0;
99
79
  right: 0;
100
80
  }
81
+
82
+ .content-fixed-header {
83
+ position: fixed;
84
+ top: var(--nav-top-height);
85
+ left: var(--nav-offset-left);
86
+ right: 0;
87
+ z-index: 801;
88
+ }
101
89
  }
102
90
 
91
+ /* anvil2 doesn't support scrollable header s above the Page content */
92
+ /* so making them sticky as well */
93
+ .layout-anvil2 {
94
+ .content-header {
95
+ position: sticky;
96
+ top: var(--nav-offset-top);
97
+ z-index: 800;
98
+ }
99
+ }
103
100
  @media print {
104
101
  .layout {
102
+ --nav-top-height: 0px !important;
103
+ --nav-offset-top: 0px !important;
105
104
  --nav-offset-left: 0px !important;
106
105
  --nav-offset-right: 0px !important;
107
106
 
@@ -1,16 +1,14 @@
1
1
  export const __esModule: true;
2
2
  export const content: string;
3
+ export const contentFixedHeader: string;
3
4
  export const contentHeader: string;
4
5
  export const layout: string;
5
6
  export const layoutAnvil1: string;
6
7
  export const layoutAnvil2: string;
7
8
  export const layoutDesktop: string;
8
9
  export const layoutLegacy: string;
9
- export const layoutMobile: string;
10
10
  export const layoutNavSlim: string;
11
11
  export const layoutNavWide: string;
12
- export const layoutTopLight: string;
13
- export const layoutTopNav: string;
14
12
  export const side: string;
15
13
  export const top: string;
16
14
  export const topPlaceholder: string;
@@ -65,6 +65,9 @@ export type TitanLayoutProps = Omit<ComponentPropsWithoutRef<'div'>, 'children'
65
65
  /** content header content */
66
66
  header?: ReactElement;
67
67
 
68
+ /** content header fixed (stocky to top) content */
69
+ headerFixed?: ReactElement;
70
+
68
71
  /** layout header content (center) */
69
72
  top?: ReactElement;
70
73
 
@@ -125,6 +128,43 @@ const useAppearance = (appearance: TitanLayoutProps['appearance']) =>
125
128
  };
126
129
  }, [appearance]);
127
130
 
131
+ enum TopNavVariant {
132
+ None = 'none',
133
+ Tiny = 'tiny',
134
+ Dark = 'dark',
135
+ DarkStacked = 'stacked',
136
+ Mobile = 'mobile',
137
+ }
138
+
139
+ const topBarHeights: Record<TopNavVariant, number> = {
140
+ [TopNavVariant.None]: 0,
141
+ [TopNavVariant.Tiny]: 48,
142
+ [TopNavVariant.Dark]: 48,
143
+ [TopNavVariant.DarkStacked]: 80,
144
+ [TopNavVariant.Mobile]: 72,
145
+ };
146
+
147
+ const useTopNavVariant = (
148
+ hasTopBar: boolean,
149
+ isMobile: boolean,
150
+ isLeftNav: boolean,
151
+ hasTopCenterContent: boolean
152
+ ) => {
153
+ if (!hasTopBar) {
154
+ return TopNavVariant.None;
155
+ }
156
+
157
+ if (isMobile) {
158
+ return TopNavVariant.Mobile;
159
+ }
160
+
161
+ if (isLeftNav) {
162
+ return TopNavVariant.Tiny;
163
+ }
164
+
165
+ return hasTopCenterContent ? TopNavVariant.DarkStacked : TopNavVariant.Dark;
166
+ };
167
+
128
168
  function TitanLayoutComponent({
129
169
  appearance = 'anvil2',
130
170
  navVariant = 'left',
@@ -133,6 +173,7 @@ function TitanLayoutComponent({
133
173
  contentOnly,
134
174
  navigationComponent,
135
175
  header,
176
+ headerFixed,
136
177
  top,
137
178
  profile,
138
179
  state,
@@ -159,19 +200,15 @@ function TitanLayoutComponent({
159
200
  const view = useAppearance(appearance);
160
201
  const [mobileDrawerOpened, setMobileDrawerOpened] = useState(false);
161
202
  const { hasNotifications, NotificationsContextProvider } = useNotificationsState();
162
- const [offsetTopStyles, setOffsetTopStyles] = useState<object>({});
163
- const updateIndicatorsHeight = useCallback((offset: number) => {
164
- setOffsetTopStyles({
165
- '--content-offset-top': `calc(var(--nav-offset-top) + ${offset}px)`,
166
- });
167
- }, []);
203
+ const [headerHeight, setHeaderHeight] = useState(0);
204
+ const [headerFixedHeight, setHeaderFixedHeight] = useState(0);
168
205
 
169
206
  const isMobile = breakpoint.isMobile;
170
207
  const hasSideBar =
171
208
  !contentOnly &&
172
209
  (navVariant === 'left' || (navVariant === 'top' && isMobile)) &&
173
210
  (!!navigationMainItems?.length || !!sideTop?.length);
174
- const hasTopBar = !contentOnly;
211
+ const topBarVariant = useTopNavVariant(!contentOnly, isMobile, navVariant === 'left', !!top);
175
212
 
176
213
  useEffect(() => {
177
214
  if (view.isAnvil1) {
@@ -257,11 +294,15 @@ function TitanLayoutComponent({
257
294
  }
258
295
  }, [view, minContentWidth, breakpoint.width]);
259
296
 
260
- const contentStyles = useMemo(() => {
261
- if (view.isAnvil2 || view.isLegacy) {
262
- return offsetTopStyles;
263
- }
264
- }, [view, offsetTopStyles]);
297
+ const layoutStyles = useMemo(() => {
298
+ const topHeight = topBarHeights[topBarVariant] ?? 0;
299
+
300
+ return {
301
+ '--nav-top-height': `${topHeight}px`,
302
+ '--nav-offset-top': `${topHeight + headerFixedHeight}px`,
303
+ '--content-offset-top': `${topHeight + headerFixedHeight + headerHeight}px`,
304
+ } as CSSProperties;
305
+ }, [topBarVariant, headerFixedHeight, headerHeight]);
265
306
 
266
307
  const layoutClass = view.isLegacy
267
308
  ? Styles.layoutLegacy
@@ -270,7 +311,7 @@ function TitanLayoutComponent({
270
311
  : Styles.layoutAnvil2;
271
312
 
272
313
  const burgerProps: LayoutHeaderProps['burger'] = useMemo(() => {
273
- if (!hasTopBar) {
314
+ if (topBarVariant === TopNavVariant.None) {
274
315
  return undefined;
275
316
  }
276
317
 
@@ -293,7 +334,7 @@ function TitanLayoutComponent({
293
334
  return undefined;
294
335
  }, [
295
336
  isMobile,
296
- hasTopBar,
337
+ topBarVariant,
297
338
  navVariant,
298
339
  state?.navCollapsed,
299
340
  onBurgerClick,
@@ -308,63 +349,59 @@ function TitanLayoutComponent({
308
349
  id={id}
309
350
  className={classNames(
310
351
  Styles.layout,
311
- isMobile ? Styles.layoutMobile : Styles.layoutDesktop,
312
- hasTopBar &&
313
- (navVariant === 'left' || !top
314
- ? Styles.layoutTopLight
315
- : Styles.layoutTopNav),
352
+ !isMobile && Styles.layoutDesktop,
316
353
  {
317
354
  [Styles.layoutNavSlim]: !isMobile && hasSideBar && state?.navCollapsed,
318
355
  [Styles.layoutNavWide]: !isMobile && hasSideBar && !state?.navCollapsed,
319
356
  },
320
357
  layoutClass
321
358
  )}
322
- style={contentStyles}
359
+ style={layoutStyles}
323
360
  >
324
- {view.isSequent && <div className={Styles.topPlaceholder} />}
325
- {hasTopBar &&
326
- (navVariant === 'left' ? (
327
- <LayoutHeader
328
- className={Styles.top}
329
- variant="light"
330
- logo={logo}
331
- profile={isMobile ? undefined : profile}
332
- center={top}
333
- rightText={isMobile ? undefined : extraText}
334
- right={
335
- <Fragment>
336
- {extraLinksTop}
337
- {!isMobile && extraLinks}
338
- </Fragment>
339
- }
340
- isMobile={isMobile}
341
- burger={burgerProps}
342
- />
343
- ) : (
344
- <LayoutHeaderDark
345
- className={Styles.top}
346
- logo={logo}
347
- profile={isMobile ? undefined : profile}
348
- center={top}
349
- rightText={isMobile ? undefined : extraText}
350
- right={
351
- <Fragment>
352
- {extraLinksTop}
353
- {!isMobile && extraLinks}
354
- </Fragment>
355
- }
356
- isMobile={isMobile}
357
- burger={burgerProps}
358
- navigationMainItems={navigationMainItems}
359
- navigationOverflowItems={navigationOverflowItems}
360
- />
361
- ))}
361
+ <div className={Styles.topPlaceholder} />
362
+ {topBarVariant === TopNavVariant.Tiny ? (
363
+ <LayoutHeader
364
+ className={Styles.top}
365
+ variant="light"
366
+ logo={logo}
367
+ profile={isMobile ? undefined : profile}
368
+ center={top}
369
+ rightText={isMobile ? undefined : extraText}
370
+ right={
371
+ <Fragment>
372
+ {extraLinksTop}
373
+ {!isMobile && extraLinks}
374
+ </Fragment>
375
+ }
376
+ isMobile={isMobile}
377
+ burger={burgerProps}
378
+ />
379
+ ) : topBarVariant !== TopNavVariant.None ? (
380
+ <LayoutHeaderDark
381
+ className={Styles.top}
382
+ logo={logo}
383
+ profile={isMobile ? undefined : profile}
384
+ center={top}
385
+ rightText={isMobile ? undefined : extraText}
386
+ right={
387
+ <Fragment>
388
+ {extraLinksTop}
389
+ {!isMobile && extraLinks}
390
+ </Fragment>
391
+ }
392
+ isMobile={isMobile}
393
+ burger={burgerProps}
394
+ navigationMainItems={navigationMainItems}
395
+ navigationOverflowItems={navigationOverflowItems}
396
+ />
397
+ ) : null}
362
398
 
363
399
  {hasSideBar && (
364
400
  <NotificationsContextProvider>
365
401
  <LayoutSidebar
366
402
  className={Styles.side}
367
403
  mobile={breakpoint.isMobile}
404
+ touchDevice={breakpoint.isTouchDevice}
368
405
  barExpanded={!state?.navCollapsed}
369
406
  onBarExpandChange={onBarExpandChange}
370
407
  submenusExpanded={state?.submenusExpanded}
@@ -397,11 +434,26 @@ function TitanLayoutComponent({
397
434
  </NotificationsContextProvider>
398
435
  )}
399
436
 
400
- {view.isSequent && (
401
- <TitanLayoutHeaderObserved heightChange={updateIndicatorsHeight}>
402
- {header}
437
+ {!!headerFixed && (
438
+ <TitanLayoutHeaderObserved
439
+ heightChange={setHeaderFixedHeight}
440
+ className={Styles.contentFixedHeader}
441
+ data-cy="layout-content-fixed-header"
442
+ >
443
+ {headerFixed}
403
444
  </TitanLayoutHeaderObserved>
404
445
  )}
446
+
447
+ {!!header && view.isSequent && (
448
+ <div className={Styles.contentHeader}>
449
+ <TitanLayoutHeaderObserved
450
+ heightChange={setHeaderHeight}
451
+ data-cy="layout-content-header"
452
+ >
453
+ {header}
454
+ </TitanLayoutHeaderObserved>
455
+ </div>
456
+ )}
405
457
  {view.isAnvil1 ? (
406
458
  <LayoutContentAnvil1 header={header} minWidth={limitContentWidth}>
407
459
  {children}
@@ -420,9 +472,11 @@ function TitanLayoutComponent({
420
472
  }
421
473
 
422
474
  const TitanLayoutHeaderObserved: FC<{
423
- children: ReactNode;
475
+ 'children': ReactNode;
476
+ 'className'?: string;
477
+ 'data-cy'?: string;
424
478
  heightChange?(value: number): void;
425
- }> = ({ children, heightChange }) => {
479
+ }> = ({ children, heightChange, ...rest }) => {
426
480
  const ref = useRef<HTMLDivElement>(null);
427
481
 
428
482
  useEffect(() => {
@@ -448,7 +502,7 @@ const TitanLayoutHeaderObserved: FC<{
448
502
  };
449
503
  }, [heightChange]);
450
504
  return (
451
- <div ref={ref} className={Styles.contentHeader} data-cy="layout-content-header">
505
+ <div {...rest} ref={ref}>
452
506
  {children}
453
507
  </div>
454
508
  );
package/src/test/data.tsx CHANGED
@@ -33,7 +33,7 @@ import SvgTasks from '@servicetitan/anvil2/assets/icons/st/gnav_tasks_inactive.s
33
33
  import { BodyText, Popover } from '@servicetitan/design-system';
34
34
 
35
35
  import classNames from 'classnames';
36
- import { FC, Fragment, forwardRef, useState } from 'react';
36
+ import { FC, Fragment, forwardRef, useLayoutEffect, useRef, useState } from 'react';
37
37
  // needed only for storybook and added in root dependencies
38
38
  // eslint-disable-next-line import/no-extraneous-dependencies
39
39
  import { MemoryRouter, Redirect, Switch, useHistory, useLocation } from 'react-router-dom';
@@ -78,6 +78,42 @@ export const LocationInfo: FC<{ className?: string }> = ({ className }) => {
78
78
  return <BodyText className={className}>current location - {location.pathname}</BodyText>;
79
79
  };
80
80
 
81
+ export const CssInfo: FC<{ className?: string }> = ({ className }) => {
82
+ const ref = useRef<HTMLDivElement>(null);
83
+ const [info, setInfo] = useState({ 'offset': '', 'nav-offset-top': '' });
84
+
85
+ useLayoutEffect(() => {
86
+ const interval = setInterval(() => {
87
+ const el = ref.current;
88
+
89
+ if (!el) {
90
+ return;
91
+ }
92
+
93
+ // eslint-disable-next-line no-restricted-globals
94
+ const st = getComputedStyle(el);
95
+
96
+ setInfo({
97
+ 'offset': st.getPropertyValue('--offset'),
98
+ 'nav-offset-top': st.getPropertyValue('--nav-offset-top'),
99
+ });
100
+ }, 100);
101
+
102
+ return () => clearInterval(interval);
103
+ }, []);
104
+
105
+ return (
106
+ <div className={className} ref={ref}>
107
+ <p>CSS variables:</p>
108
+ {Object.entries(info).map(([key, value]) => (
109
+ <p key={key}>
110
+ --{key} - {value}
111
+ </p>
112
+ ))}
113
+ </div>
114
+ );
115
+ };
116
+
81
117
  export const withMemoryRouter = (Story: any) => (
82
118
  <MemoryRouter>
83
119
  <Story />
@@ -3,6 +3,7 @@ import { TitanLayoutProps } from '../components/titan-layout';
3
3
 
4
4
  export interface LayoutContentArgs {
5
5
  header: boolean;
6
+ headerFixed: boolean;
6
7
  sideTop: boolean;
7
8
  extraText: boolean;
8
9
  search: boolean;
@@ -15,6 +16,7 @@ export interface LayoutContentArgs {
15
16
 
16
17
  export const getDefaultArgs = (): LayoutContentArgs => ({
17
18
  header: true,
19
+ headerFixed: true,
18
20
  sideTop: true,
19
21
  extraText: true,
20
22
  search: true,
@@ -4,9 +4,12 @@ import { useMemo } from 'react';
4
4
  export interface TitanBreakpoint {
5
5
  name: BreakpointReturnProps['name'];
6
6
  isMobile: boolean;
7
+ isTouchDevice: boolean;
7
8
  width: number;
8
9
  }
9
10
 
11
+ const isTouchDevice = () => window.matchMedia('(any-pointer: coarse)').matches;
12
+
10
13
  export const useTitanBreakpoint = (): TitanBreakpoint => {
11
14
  const breakpoint = useBreakpoint();
12
15
 
@@ -14,6 +17,7 @@ export const useTitanBreakpoint = (): TitanBreakpoint => {
14
17
  () => ({
15
18
  name: breakpoint?.name ?? 'xl',
16
19
  isMobile: breakpoint ? breakpoint.innerWidth < 768 : false,
20
+ isTouchDevice: isTouchDevice(),
17
21
  width: breakpoint?.innerWidth ?? 0,
18
22
  }),
19
23
  [breakpoint]