@orangesk/orange-design-system 2.0.0-beta.45 → 2.0.0-beta.47

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 (73) hide show
  1. package/build/components/Breadcrumbs/style.css +1 -1
  2. package/build/components/Breadcrumbs/style.css.map +1 -1
  3. package/build/components/Carousel/style.css +1 -1
  4. package/build/components/Carousel/style.css.map +1 -1
  5. package/build/components/Footer/style.css +1 -1
  6. package/build/components/Footer/style.css.map +1 -1
  7. package/build/components/Grid/style.css +1 -1
  8. package/build/components/Grid/style.css.map +1 -1
  9. package/build/components/Link/style.css +1 -1
  10. package/build/components/Link/style.css.map +1 -1
  11. package/build/components/Megamenu/style.css +1 -1
  12. package/build/components/Megamenu/style.css.map +1 -1
  13. package/build/components/Stepbar/style.css +1 -1
  14. package/build/components/Stepbar/style.css.map +1 -1
  15. package/build/components/Tabs/style.css +1 -1
  16. package/build/components/Tabs/style.css.map +1 -1
  17. package/build/components/index.js +1 -1
  18. package/build/components/index.js.map +1 -1
  19. package/build/components/tsconfig.tsbuildinfo +1 -1
  20. package/build/components/types/index.d.ts +1 -4
  21. package/build/components/types/src/components/CarouselHero/CarouselHero.d.ts +1 -0
  22. package/build/components/types/src/components/Preview/CodeExample.d.ts +1 -0
  23. package/build/components/types/src/components/Preview/PreviewGenerator.d.ts +1 -0
  24. package/build/components/types/src/components/Preview/getElementDisplayName.d.ts +1 -0
  25. package/build/components/types/src/components/Tabs/Tabs.d.ts +0 -4
  26. package/build/components/types/src/components/Tabs/Tabs.static.d.ts +12 -0
  27. package/build/lib/base.css +1 -1
  28. package/build/lib/base.css.map +1 -1
  29. package/build/lib/components.css +1 -1
  30. package/build/lib/components.css.map +1 -1
  31. package/build/lib/footer.css +1 -1
  32. package/build/lib/footer.css.map +1 -1
  33. package/build/lib/megamenu.css +1 -1
  34. package/build/lib/megamenu.css.map +1 -1
  35. package/build/lib/scripts.js +1 -1
  36. package/build/lib/scripts.js.map +1 -1
  37. package/build/lib/style.css +1 -1
  38. package/build/lib/style.css.map +1 -1
  39. package/build/lib/utilities.css +1 -1
  40. package/build/lib/utilities.css.map +1 -1
  41. package/build/search-index.json +5 -5
  42. package/package.json +18 -18
  43. package/src/components/Breadcrumbs/styles/mixins.scss +14 -3
  44. package/src/components/Carousel/styles/mixins.scss +22 -2
  45. package/src/components/CarouselHero/CarouselHero.tsx +20 -6
  46. package/src/components/CarouselHero/tests/CarouselHero.conformance.test.jsx +2 -0
  47. package/src/components/CarouselHero/tests/CarouselHero.unit.test.jsx +78 -9
  48. package/src/components/Footer/styles/mixins.scss +2 -1
  49. package/src/components/Forms/Checkbox/styles/style.scss +13 -6
  50. package/src/components/Forms/InputStepper/InputStepper.tsx +2 -0
  51. package/src/components/Forms/InputStepper/styles/style.scss +25 -8
  52. package/src/components/Forms/InputStepper/tests/InputStepper.unit.test.jsx +8 -0
  53. package/src/components/Link/styles/mixins.scss +0 -1
  54. package/src/components/Megamenu/Megamenu.tsx +2 -2
  55. package/src/components/Megamenu/MegamenuBlog.tsx +2 -2
  56. package/src/components/Megamenu/styles/mixins.scss +20 -12
  57. package/src/components/Preview/CodeExample.tsx +66 -25
  58. package/src/components/Preview/Preview.tsx +26 -13
  59. package/src/components/Preview/PreviewGenerator.tsx +57 -32
  60. package/src/components/Preview/getElementDisplayName.ts +25 -0
  61. package/src/components/Stepbar/styles/config.scss +34 -17
  62. package/src/components/Stepbar/styles/mixins.scss +5 -3
  63. package/src/components/Tabs/Tabs.static.ts +157 -30
  64. package/src/components/Tabs/Tabs.tsx +62 -67
  65. package/src/components/Tabs/styles/config.scss +18 -25
  66. package/src/components/Tabs/styles/mixins.scss +93 -28
  67. package/src/components/Tabs/styles/style.scss +4 -15
  68. package/src/components/Tabs/tests/Tabs.unit.test.jsx +111 -0
  69. package/src/styles/base/globals.scss +2 -0
  70. package/src/styles/shame.scss +16 -3
  71. package/src/styles/tools/convert.scss +8 -0
  72. package/src/styles/utilities/horizontal-scroll.scss +7 -2
  73. package/src/styles/utilities/text.scss +0 -1
@@ -3,6 +3,7 @@
3
3
  import type { ReactNode } from "react";
4
4
  import React from "react";
5
5
  import reactElementToJSXString from "react-element-to-jsx-string";
6
+ import { getElementDisplayName } from "./getElementDisplayName";
6
7
  import { PreviewGenerator } from "./PreviewGenerator";
7
8
 
8
9
  type PreviewProps = React.ComponentProps<typeof PreviewGenerator> & {
@@ -51,21 +52,27 @@ export function Preview({
51
52
  codeTypes = ["html", "jsx"],
52
53
  ...rest
53
54
  }: PreviewProps) {
55
+ const normalizedChildren = React.Children.toArray(children);
56
+ const normalizedCode =
57
+ rest.code == null
58
+ ? undefined
59
+ : typeof rest.code === "string" || typeof rest.code === "function"
60
+ ? rest.code
61
+ : React.Children.toArray(rest.code);
62
+ const jsxSource =
63
+ normalizedCode &&
64
+ typeof normalizedCode !== "string" &&
65
+ typeof normalizedCode !== "function"
66
+ ? normalizedCode
67
+ : normalizedChildren;
68
+ const hasMultipleCodeRoots = React.Children.count(jsxSource) > 1;
69
+
54
70
  // Generate JSX string on server
55
- const jsxCode = generateJSXString(children, {
71
+ const jsxCode = generateJSXString(jsxSource, {
56
72
  showDefaultProps: false,
57
73
  showFunctions: true,
58
74
  functionValue: (fn: any) => fn.name || "Function",
59
- displayName: (el: any) => {
60
- // More robust display name handling
61
- if (!el) return "Unknown";
62
- return (
63
- el?.props?.mdxType ||
64
- el?.type?.displayName ||
65
- el?.type?.name ||
66
- (typeof el?.type === "string" ? el.type : "Component")
67
- );
68
- },
75
+ displayName: getElementDisplayName,
69
76
  filterProps: ["mdxType", "originalType"],
70
77
  useBooleanShorthandSyntax: false,
71
78
  sortProps: false,
@@ -74,8 +81,14 @@ export function Preview({
74
81
 
75
82
  // Pass the pre-generated JSX code to PreviewGenerator
76
83
  return (
77
- <PreviewGenerator {...rest} jsxCode={jsxCode} codeTypes={codeTypes}>
78
- {children}
84
+ <PreviewGenerator
85
+ {...rest}
86
+ code={normalizedCode}
87
+ hasMultipleCodeRoots={hasMultipleCodeRoots}
88
+ jsxCode={jsxCode}
89
+ codeTypes={codeTypes}
90
+ >
91
+ {normalizedChildren}
79
92
  </PreviewGenerator>
80
93
  );
81
94
  }
@@ -17,6 +17,7 @@ import { Card } from "../Card";
17
17
  import { Dropdown, DropdownItem } from "../Dropdown";
18
18
  import { Icon } from "../Icon";
19
19
  import CodeExample from "./CodeExample";
20
+ import { getElementDisplayName } from "./getElementDisplayName";
20
21
  import PreviewTitleBar from "./PreviewTitleBar";
21
22
 
22
23
  import "./styles/style.scss";
@@ -32,16 +33,7 @@ const generateJSXString = (
32
33
  showDefaultProps: false,
33
34
  showFunctions: true,
34
35
  functionValue: (fn: any) => fn.name || "Function",
35
- displayName: (el: any) => {
36
- // More robust display name handling
37
- if (!el) return "Unknown";
38
- return (
39
- el?.props?.mdxType ||
40
- el?.type?.displayName ||
41
- el?.type?.name ||
42
- (typeof el?.type === "string" ? el.type : "Component")
43
- );
44
- },
36
+ displayName: getElementDisplayName,
45
37
  filterProps: ["mdxType", "originalType"],
46
38
  useBooleanShorthandSyntax: false,
47
39
  sortProps: false,
@@ -116,7 +108,6 @@ const PREVIEW_BREAKPOINTS: PreviewBreakpoint[] = [
116
108
  ];
117
109
 
118
110
  const IFRAME_VIEWPORT_GUTTER = 16;
119
-
120
111
  const getScreenLikeMinHeight = (width: number | null): number => {
121
112
  if (width === null) {
122
113
  return 1;
@@ -284,6 +275,7 @@ interface PreviewProps {
284
275
  codeTypes?: string[];
285
276
  enableFullscreen?: boolean;
286
277
  hasCodePreview?: boolean;
278
+ hasMultipleCodeRoots?: boolean;
287
279
  html?: string;
288
280
  jsxCode?: string; // Pre-generated JSX string (from server component)
289
281
  disableBreakpoints?: boolean;
@@ -344,6 +336,7 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
344
336
  codeTypes = ["html", "jsx"],
345
337
  enableFullscreen = true,
346
338
  hasCodePreview = true,
339
+ hasMultipleCodeRoots = false,
347
340
  html,
348
341
  jsxCode,
349
342
  isDark = false,
@@ -419,6 +412,33 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
419
412
 
420
413
  const themeClass = isDarkMode ? "is-dark" : "is-light";
421
414
  const bgClass = getBgClassFromName(previewBackground.label);
415
+ const applyThemeClassToJsxCode = (value?: string): string | undefined => {
416
+ if (!value) return value;
417
+
418
+ const trimmedValue = value.trim();
419
+ if (!trimmedValue) return trimmedValue;
420
+
421
+ if (hasMultipleCodeRoots) {
422
+ return `<div className="${themeClass}">\n${trimmedValue}\n</div>`;
423
+ }
424
+
425
+ return trimmedValue.replace(
426
+ /^<([A-Za-z][\w.]*)((?:\s+[\s\S]*?)?)\s*(\/?)>/,
427
+ (_, tagName: string, rawAttrs = "", selfClosing = "") => {
428
+ const attrs = rawAttrs || "";
429
+
430
+ if (/\sclassName=/.test(attrs)) {
431
+ return `<${tagName}${attrs.replace(
432
+ /className=(['"])(.*?)\1/,
433
+ (_match: string, quote: string, className: string) =>
434
+ `className=${quote}${className} ${themeClass}${quote}`,
435
+ )}${selfClosing ? " /" : ""}>`;
436
+ }
437
+
438
+ return `<${tagName}${attrs}\n className="${themeClass}"${selfClosing ? " /" : ""}>`;
439
+ },
440
+ );
441
+ };
422
442
 
423
443
  const classes = cx(CLASS_ROOT, themeClass, bgClass, className);
424
444
  const wrapperClasses = cx("previewWrapper", {
@@ -534,28 +554,36 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
534
554
  ? children(renderAsFunctionContext)
535
555
  : children;
536
556
 
537
- // Wrap the component to ensure theme classes are applied to the root element
538
- const wrapComponentWithTheme = (component: ReactNode) => {
557
+ const wrapRenderedComponentWithTheme = (component: ReactNode) => {
539
558
  if (!component) return component;
540
559
 
541
- // If it's a single React element, clone it with additional classes
542
- if (React.isValidElement(component)) {
543
- const props = component.props as any;
544
- const existingClassName = props.className || "";
545
- const newClassName = cx(existingClassName, themeClass);
560
+ // Keep wrapper shape stable across SSR/hydration and avoid cloning RSC children.
561
+ return <div className={cx(themeClass)}>{component}</div>;
562
+ };
546
563
 
547
- return React.cloneElement(component, {
564
+ const wrapCodeWithTheme = (component: ReactNode) => {
565
+ if (!component) return component;
566
+
567
+ const normalizedChildren = React.Children.toArray(component);
568
+
569
+ if (
570
+ normalizedChildren.length === 1 &&
571
+ React.isValidElement(normalizedChildren[0])
572
+ ) {
573
+ const element = normalizedChildren[0] as React.ReactElement<any>;
574
+ const props = element.props as { className?: string };
575
+
576
+ return React.cloneElement(element, {
548
577
  ...props,
549
- className: newClassName,
550
- } as any);
578
+ className: cx(props.className, themeClass),
579
+ });
551
580
  }
552
581
 
553
- // For multiple elements or other cases, wrap in a div with theme classes
554
582
  return <div className={cx(themeClass)}>{component}</div>;
555
583
  };
556
584
 
557
585
  const toRender = childrenToRender ? (
558
- wrapComponentWithTheme(childrenToRender)
586
+ wrapRenderedComponentWithTheme(childrenToRender)
559
587
  ) : html ? (
560
588
  <div
561
589
  className={cx(themeClass)}
@@ -574,22 +602,18 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
574
602
  html;
575
603
 
576
604
  // Apply the exact same wrapping logic for code display
577
- const toCode = codeContent
578
- ? wrapComponentWithTheme(codeContent)
579
- : codeContent;
605
+ const toCode = codeContent ? wrapCodeWithTheme(codeContent) : codeContent;
580
606
 
581
607
  // Generate JSX string for the JSX tab
582
608
  // Use pre-generated JSX if provided (from server), otherwise generate client-side
583
609
  const generatedJSXCode =
584
- jsxCode ||
585
- (typeof code === "string"
610
+ typeof code === "string"
586
611
  ? code
587
- : toCode
588
- ? generateJSXString(toCode, codeJSXOptions)
589
- : "");
612
+ : applyThemeClassToJsxCode(jsxCode) ||
613
+ (toCode ? generateJSXString(toCode, codeJSXOptions) : "");
590
614
 
591
615
  // Use toCode for HTML rendering
592
- const codeForHTML = toCode;
616
+ const codeForHTML = codeContent;
593
617
 
594
618
  const previewViewportWidth = disableBreakpoints
595
619
  ? null
@@ -636,6 +660,7 @@ const PreviewGenerator: FunctionComponent<PreviewProps> = ({
636
660
  <CodeExample
637
661
  codeJSXOptions={codeJSXOptions}
638
662
  codeTypes={codeTypes}
663
+ htmlThemeClass={themeClass}
639
664
  jsxCode={generatedJSXCode}
640
665
  htmlCode={codeForHTML}
641
666
  >
@@ -0,0 +1,25 @@
1
+ export const getElementDisplayName = (element: any): string => {
2
+ if (!element) return "Unknown";
3
+
4
+ const type = element.type;
5
+ const clientReferenceId =
6
+ typeof type?.$$id === "string" ? type.$$id.split("#").pop() : undefined;
7
+ const payloadValue = type?._payload?.value;
8
+ const payloadName =
9
+ typeof payloadValue === "string"
10
+ ? payloadValue.split("#").pop()
11
+ : payloadValue?.name;
12
+
13
+ return (
14
+ element?.props?.mdxType ||
15
+ type?.displayName ||
16
+ type?.render?.displayName ||
17
+ type?.render?.name ||
18
+ type?.name ||
19
+ type?.type?.displayName ||
20
+ type?.type?.name ||
21
+ clientReferenceId ||
22
+ payloadName ||
23
+ (typeof type === "string" ? type : "Component")
24
+ );
25
+ };
@@ -5,10 +5,14 @@
5
5
  $item-base: (
6
6
  display: inline-flex,
7
7
  align-items: center,
8
- padding-top: space.get("small"),
9
- padding-bottom: space.get("small"),
8
+ padding-top: convert.to-rem(10px),
9
+ padding-bottom: convert.to-rem(10px),
10
10
  margin: 0 0 0 space.get("xsmall"),
11
- color: var(--color-text-default),
11
+ color: var(--stepbar-item-color),
12
+ background: var(--stepbar-item-background),
13
+ --stepbar-arrow-size: #{convert.to-rem(21px)},
14
+ --stepbar-item-background: var(--color-surface-contrast),
15
+ --stepbar-item-color: var(--color-text-inverse),
12
16
  );
13
17
 
14
18
  $arrow-spacing-small: (
@@ -26,8 +30,8 @@ $arrow-spacing-small: (
26
30
 
27
31
  $arrow-spacing: (
28
32
  default: (
29
- padding-left: convert.to-rem(40px),
30
- padding-right: convert.to-rem(19px),
33
+ padding-left: convert.to-rem(35px),
34
+ padding-right: convert.to-rem(30px),
31
35
  ),
32
36
  first: (
33
37
  padding-left: convert.to-rem(35px),
@@ -40,43 +44,56 @@ $arrow-spacing: (
40
44
  $arrows: (
41
45
  default: (
42
46
  done:
43
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='%23141414'/%3E%3C/svg%3E"),
47
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='%23141414'/%3E%3C/svg%3E"),
44
48
  current:
45
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='%23F16E00'/%3E%3C/svg%3E"),
49
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='%23f15e00'/%3E%3C/svg%3E"),
46
50
  next:
47
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='%23eee'/%3E%3C/svg%3E"),
51
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='white'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='%23dddddd'/%3E%3C/svg%3E"),
48
52
  ),
49
53
  inverse: (
50
54
  done:
51
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='white'/%3E%3C/svg%3E"),
55
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='white'/%3E%3C/svg%3E"),
52
56
  current:
53
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='%23F16E00'/%3E%3C/svg%3E"),
57
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='%23f15e00'/%3E%3C/svg%3E"),
54
58
  next:
55
- url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M0 0L16 20L0 40V0Z' fill='%23eee'/%3E%3C/svg%3E"),
59
+ url("data:image/svg+xml,%3Csvg width='21' height='40' viewBox='0 0 21 40' fill='none' preserveAspectRatio='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0H0V40H5L21 20L5 0Z' fill='%23141414'/%3E%3Cpath d='M0 0L16 20L0 40V0Z' fill='%23595959'/%3E%3C/svg%3E"),
56
60
  ),
57
61
  );
58
62
 
59
63
  $item-done-colors: (
60
64
  default: (
61
- background: var(--color-surface-contrast),
65
+ --stepbar-item-background: var(--color-surface-contrast),
66
+ --stepbar-item-color: var(--color-text-inverse),
67
+ ),
68
+ inverse: (
69
+ --stepbar-item-background: var(--color-surface-contrast),
70
+ --stepbar-item-color: var(--color-text-inverse),
62
71
  ),
63
72
  );
64
73
 
65
74
  $item-colors: (
66
75
  current: (
67
- background: var(--color-surface-tertiary),
68
- color: var(--color-text-default),
76
+ --stepbar-item-background: var(--color-surface-tertiary),
77
+ --stepbar-item-color: #{color.$white},
69
78
  ),
70
79
  next: (
71
- background: var(--color-surface-subtle),
72
- color: var(--color-surface-contrast),
80
+ --stepbar-item-background: var(--color-surface-moderate),
81
+ --stepbar-item-color: var(--color-text-default),
73
82
  ),
74
83
  );
75
84
 
76
85
  $link-colors: (
77
86
  default: (
78
87
  base: (
79
- color: var(--color-text-inverse),
88
+ color: var(--stepbar-item-color),
89
+ ),
90
+ interaction: (
91
+ color: var(--color-text-accent),
92
+ ),
93
+ ),
94
+ inverse: (
95
+ base: (
96
+ color: var(--stepbar-item-color),
80
97
  ),
81
98
  interaction: (
82
99
  color: var(--color-text-accent),
@@ -4,7 +4,7 @@
4
4
  @use "../../../styles/tools/generate";
5
5
  @use "../../../styles/tools/map";
6
6
  @use "../../../styles/tokens/breakpoint";
7
- @use "../../../styles/typography/mixins" as typoMixins;
7
+ @use "../../../styles/typography/config" as typographyConfig;
8
8
  @use "./config";
9
9
 
10
10
  @mixin list-base {
@@ -14,6 +14,7 @@
14
14
  padding-left: 0;
15
15
  margin: 0 0 space.get("large") 0;
16
16
  max-width: none;
17
+ @include generate.css-map(sass-map.get(typographyConfig.$caption, "large"));
17
18
  font-weight: bold;
18
19
  }
19
20
 
@@ -65,10 +66,11 @@
65
66
  position: absolute;
66
67
  z-index: 1;
67
68
  top: 0;
68
- right: -21px;
69
- width: 21px;
69
+ right: calc(var(--stepbar-arrow-size) * -1);
70
+ width: var(--stepbar-arrow-size);
70
71
  height: 100%;
71
72
  background-size: 100% 100%;
73
+ pointer-events: none;
72
74
  }
73
75
  }
74
76
 
@@ -13,12 +13,19 @@ export const configDocs = {
13
13
  };
14
14
 
15
15
  export default class Tabs {
16
+ private static readonly SCROLL_ALIGNMENT_TOLERANCE = 1;
17
+ private static readonly SCROLL_EDGE_TOLERANCE = 1;
18
+
16
19
  private element: HTMLElement;
17
20
  private config: TabsConfig;
18
21
  private tabs: HTMLElement[];
19
22
  private tabPanels: (HTMLElement | null)[];
20
23
  private activeTabIndex: number | null;
21
24
  private rovingTabindex: RovingTabindex | null;
25
+ private resizeObserver: ResizeObserver | null;
26
+ private horizontalScrollHandler: () => void;
27
+ private resizeHandler: () => void;
28
+ private viewportElement: HTMLElement | null;
22
29
 
23
30
  constructor(element: HTMLElement, config?: Partial<TabsConfig>) {
24
31
  this.element = element;
@@ -29,9 +36,14 @@ export default class Tabs {
29
36
  this.activeTabIndex = null;
30
37
 
31
38
  this.rovingTabindex = null;
39
+ this.resizeObserver = null;
40
+ this.viewportElement = null;
32
41
 
33
42
  this.handleClick = this.handleClick.bind(this);
34
43
  this.handleTabFocus = this.handleTabFocus.bind(this);
44
+ this.horizontalScrollHandler =
45
+ this.updateHorizontalOverflowState.bind(this);
46
+ this.resizeHandler = this.updateHorizontalOverflowState.bind(this);
35
47
 
36
48
  (this.element as any).ODS_Tabs = this;
37
49
 
@@ -73,13 +85,17 @@ export default class Tabs {
73
85
  }
74
86
 
75
87
  if (this.activeTabIndex !== null) {
76
- this.toggleTabPanel(this.tabPanels[this.activeTabIndex], "on");
88
+ this.setActiveTab(this.activeTabIndex);
77
89
  } else if (this.tabs.length > 0) {
78
90
  this.activateNthTab(0);
79
91
  }
92
+
93
+ this.setupOverflowObserver();
80
94
  }
81
95
 
82
96
  destroy(): void {
97
+ this.teardownOverflowObserver();
98
+
83
99
  this.tabs.map((tab) => {
84
100
  tab.removeEventListener("click", this.handleClick);
85
101
  tab.removeEventListener("focus", this.handleTabFocus);
@@ -94,6 +110,66 @@ export default class Tabs {
94
110
  (this.element as any).ODS_Tabs = null;
95
111
  }
96
112
 
113
+ private setupOverflowObserver(): void {
114
+ this.teardownOverflowObserver();
115
+
116
+ this.viewportElement = this.element.parentElement?.classList.contains(
117
+ "tab-list__viewport",
118
+ )
119
+ ? this.element.parentElement
120
+ : null;
121
+
122
+ this.element.addEventListener("scroll", this.horizontalScrollHandler, {
123
+ passive: true,
124
+ });
125
+ window.addEventListener("resize", this.resizeHandler);
126
+
127
+ if (typeof ResizeObserver !== "undefined") {
128
+ this.resizeObserver = new ResizeObserver(this.resizeHandler);
129
+ this.resizeObserver.observe(this.element);
130
+ this.tabs.forEach((tab) => this.resizeObserver?.observe(tab));
131
+ }
132
+
133
+ this.updateHorizontalOverflowState();
134
+ }
135
+
136
+ private teardownOverflowObserver(): void {
137
+ this.element.removeEventListener("scroll", this.horizontalScrollHandler);
138
+ window.removeEventListener("resize", this.resizeHandler);
139
+
140
+ if (this.viewportElement) {
141
+ this.viewportElement.classList.remove(
142
+ "has-left-overflow",
143
+ "has-right-overflow",
144
+ );
145
+ this.viewportElement = null;
146
+ }
147
+
148
+ if (this.resizeObserver) {
149
+ this.resizeObserver.disconnect();
150
+ this.resizeObserver = null;
151
+ }
152
+ }
153
+
154
+ private updateHorizontalOverflowState(): void {
155
+ if (!this.viewportElement) return;
156
+
157
+ const maxScrollLeft = this.element.scrollWidth - this.element.clientWidth;
158
+ const hasOverflow = maxScrollLeft > Tabs.SCROLL_EDGE_TOLERANCE;
159
+ const atStart = this.element.scrollLeft <= Tabs.SCROLL_EDGE_TOLERANCE;
160
+ const atEnd =
161
+ this.element.scrollLeft >= maxScrollLeft - Tabs.SCROLL_EDGE_TOLERANCE;
162
+
163
+ this.viewportElement.classList.toggle(
164
+ "has-left-overflow",
165
+ hasOverflow && !atStart,
166
+ );
167
+ this.viewportElement.classList.toggle(
168
+ "has-right-overflow",
169
+ hasOverflow && !atEnd,
170
+ );
171
+ }
172
+
97
173
  update(): void {
98
174
  this.destroy();
99
175
  this.init();
@@ -105,31 +181,80 @@ export default class Tabs {
105
181
  private isSelected = (el: HTMLElement): boolean =>
106
182
  el.getAttribute("aria-selected") === "true";
107
183
 
108
- private handleClick(e: MouseEvent): void {
109
- const clickedTab = e.currentTarget as HTMLElement;
110
-
111
- if (clickedTab.hasAttribute("aria-disabled")) {
112
- e.preventDefault();
113
- return;
114
- }
115
-
184
+ private setActiveTab(index: number, forceCenter: boolean = false): void {
116
185
  this.tabs.map((tab, i) => {
117
- if (tab === clickedTab) {
186
+ if (i === index) {
118
187
  this.activeTabIndex = i;
119
188
  return this.toggleTab(tab, "on");
120
189
  }
121
190
  return this.toggleTab(tab, "off");
122
191
  });
123
192
 
124
- this.tabPanels.map((tabPanel) => {
125
- if (
126
- tabPanel &&
127
- tabPanel.getAttribute("id") === clickedTab.getAttribute("aria-controls")
128
- ) {
193
+ this.tabPanels.map((tabPanel, i) => {
194
+ if (i === index) {
129
195
  return this.toggleTabPanel(tabPanel, "on");
130
196
  }
131
197
  return this.toggleTabPanel(tabPanel, "off");
132
198
  });
199
+
200
+ const activeTab = this.tabs[index];
201
+ if (activeTab) {
202
+ this.scrollTabIntoView(activeTab, forceCenter);
203
+ }
204
+ }
205
+
206
+ private scrollTabIntoView(
207
+ activeTab: HTMLElement,
208
+ forceCenter: boolean = false,
209
+ ): void {
210
+ const maxScrollLeft = Math.max(
211
+ 0,
212
+ this.element.scrollWidth - this.element.clientWidth,
213
+ );
214
+ const contentRect = this.element.getBoundingClientRect();
215
+ const itemRect = activeTab.getBoundingClientRect();
216
+ const itemCenterWithinContent =
217
+ itemRect.left -
218
+ contentRect.left +
219
+ this.element.scrollLeft +
220
+ itemRect.width / 2;
221
+ const targetScrollLeft =
222
+ itemCenterWithinContent - this.element.clientWidth / 2;
223
+ const behavior = window.innerWidth < 768 ? "auto" : "smooth";
224
+ const nextScrollLeft = Math.min(
225
+ maxScrollLeft,
226
+ Math.max(0, targetScrollLeft),
227
+ );
228
+
229
+ const isAlreadyAligned =
230
+ Math.abs(this.element.scrollLeft - nextScrollLeft) <=
231
+ Tabs.SCROLL_ALIGNMENT_TOLERANCE;
232
+ if (!forceCenter && isAlreadyAligned) return;
233
+
234
+ if (typeof this.element.scrollTo === "function") {
235
+ this.element.scrollTo({
236
+ left: nextScrollLeft,
237
+ behavior,
238
+ });
239
+ return;
240
+ }
241
+
242
+ this.element.scrollLeft = nextScrollLeft;
243
+ this.updateHorizontalOverflowState();
244
+ }
245
+
246
+ private handleClick(e: MouseEvent): void {
247
+ const clickedTab = e.currentTarget as HTMLElement;
248
+
249
+ if (clickedTab.hasAttribute("aria-disabled")) {
250
+ e.preventDefault();
251
+ return;
252
+ }
253
+
254
+ const clickedIndex = this.tabs.findIndex((tab) => tab === clickedTab);
255
+ if (clickedIndex >= 0) {
256
+ this.setActiveTab(clickedIndex, true);
257
+ }
133
258
  }
134
259
 
135
260
  private toggleTab(el: HTMLElement, state: "on" | "off"): void {
@@ -161,25 +286,27 @@ export default class Tabs {
161
286
  }
162
287
 
163
288
  activateNthTab(index: number): void {
164
- this.tabs.map((tab, i) => {
165
- if (i === index) {
166
- this.activeTabIndex = i;
167
- return this.toggleTab(tab, "on");
168
- }
169
- return this.toggleTab(tab, "off");
170
- });
171
-
172
- this.tabPanels.map((tabPanel, i) => {
173
- if (i === index) {
174
- return this.toggleTabPanel(tabPanel, "on");
175
- }
176
- return this.toggleTabPanel(tabPanel, "off");
177
- });
289
+ this.setActiveTab(index, true);
178
290
  }
179
291
 
180
- private handleTabFocus(): void {
292
+ private handleTabFocus(event: FocusEvent): void {
181
293
  if (this.rovingTabindex && !this.rovingTabindex.isActive) {
182
294
  this.rovingTabindex.init();
183
295
  }
296
+
297
+ const focusedTab = event.currentTarget as HTMLElement | null;
298
+ if (focusedTab && this.shouldCenterFocusedTab(focusedTab)) {
299
+ this.scrollTabIntoView(focusedTab);
300
+ }
301
+ }
302
+
303
+ private shouldCenterFocusedTab(focusedTab: HTMLElement): boolean {
304
+ if (typeof focusedTab.matches !== "function") return true;
305
+
306
+ try {
307
+ return focusedTab.matches(":focus-visible");
308
+ } catch {
309
+ return true;
310
+ }
184
311
  }
185
312
  }