@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.
- package/build/components/Breadcrumbs/style.css +1 -1
- package/build/components/Breadcrumbs/style.css.map +1 -1
- package/build/components/Carousel/style.css +1 -1
- package/build/components/Carousel/style.css.map +1 -1
- package/build/components/Footer/style.css +1 -1
- package/build/components/Footer/style.css.map +1 -1
- package/build/components/Grid/style.css +1 -1
- package/build/components/Grid/style.css.map +1 -1
- package/build/components/Link/style.css +1 -1
- package/build/components/Link/style.css.map +1 -1
- package/build/components/Megamenu/style.css +1 -1
- package/build/components/Megamenu/style.css.map +1 -1
- package/build/components/Stepbar/style.css +1 -1
- package/build/components/Stepbar/style.css.map +1 -1
- package/build/components/Tabs/style.css +1 -1
- package/build/components/Tabs/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/components/tsconfig.tsbuildinfo +1 -1
- package/build/components/types/index.d.ts +1 -4
- package/build/components/types/src/components/CarouselHero/CarouselHero.d.ts +1 -0
- package/build/components/types/src/components/Preview/CodeExample.d.ts +1 -0
- package/build/components/types/src/components/Preview/PreviewGenerator.d.ts +1 -0
- package/build/components/types/src/components/Preview/getElementDisplayName.d.ts +1 -0
- package/build/components/types/src/components/Tabs/Tabs.d.ts +0 -4
- package/build/components/types/src/components/Tabs/Tabs.static.d.ts +12 -0
- package/build/lib/base.css +1 -1
- package/build/lib/base.css.map +1 -1
- package/build/lib/components.css +1 -1
- package/build/lib/components.css.map +1 -1
- package/build/lib/footer.css +1 -1
- package/build/lib/footer.css.map +1 -1
- package/build/lib/megamenu.css +1 -1
- package/build/lib/megamenu.css.map +1 -1
- package/build/lib/scripts.js +1 -1
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/build/lib/utilities.css +1 -1
- package/build/lib/utilities.css.map +1 -1
- package/build/search-index.json +5 -5
- package/package.json +18 -18
- package/src/components/Breadcrumbs/styles/mixins.scss +14 -3
- package/src/components/Carousel/styles/mixins.scss +22 -2
- package/src/components/CarouselHero/CarouselHero.tsx +20 -6
- package/src/components/CarouselHero/tests/CarouselHero.conformance.test.jsx +2 -0
- package/src/components/CarouselHero/tests/CarouselHero.unit.test.jsx +78 -9
- package/src/components/Footer/styles/mixins.scss +2 -1
- package/src/components/Forms/Checkbox/styles/style.scss +13 -6
- package/src/components/Forms/InputStepper/InputStepper.tsx +2 -0
- package/src/components/Forms/InputStepper/styles/style.scss +25 -8
- package/src/components/Forms/InputStepper/tests/InputStepper.unit.test.jsx +8 -0
- package/src/components/Link/styles/mixins.scss +0 -1
- package/src/components/Megamenu/Megamenu.tsx +2 -2
- package/src/components/Megamenu/MegamenuBlog.tsx +2 -2
- package/src/components/Megamenu/styles/mixins.scss +20 -12
- package/src/components/Preview/CodeExample.tsx +66 -25
- package/src/components/Preview/Preview.tsx +26 -13
- package/src/components/Preview/PreviewGenerator.tsx +57 -32
- package/src/components/Preview/getElementDisplayName.ts +25 -0
- package/src/components/Stepbar/styles/config.scss +34 -17
- package/src/components/Stepbar/styles/mixins.scss +5 -3
- package/src/components/Tabs/Tabs.static.ts +157 -30
- package/src/components/Tabs/Tabs.tsx +62 -67
- package/src/components/Tabs/styles/config.scss +18 -25
- package/src/components/Tabs/styles/mixins.scss +93 -28
- package/src/components/Tabs/styles/style.scss +4 -15
- package/src/components/Tabs/tests/Tabs.unit.test.jsx +111 -0
- package/src/styles/base/globals.scss +2 -0
- package/src/styles/shame.scss +16 -3
- package/src/styles/tools/convert.scss +8 -0
- package/src/styles/utilities/horizontal-scroll.scss +7 -2
- 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(
|
|
71
|
+
const jsxCode = generateJSXString(jsxSource, {
|
|
56
72
|
showDefaultProps: false,
|
|
57
73
|
showFunctions: true,
|
|
58
74
|
functionValue: (fn: any) => fn.name || "Function",
|
|
59
|
-
displayName:
|
|
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
|
|
78
|
-
{
|
|
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:
|
|
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
|
-
|
|
538
|
-
const wrapComponentWithTheme = (component: ReactNode) => {
|
|
557
|
+
const wrapRenderedComponentWithTheme = (component: ReactNode) => {
|
|
539
558
|
if (!component) return component;
|
|
540
559
|
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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:
|
|
550
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
(typeof code === "string"
|
|
610
|
+
typeof code === "string"
|
|
586
611
|
? code
|
|
587
|
-
:
|
|
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 =
|
|
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:
|
|
9
|
-
padding-bottom:
|
|
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(--
|
|
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(
|
|
30
|
-
padding-right: convert.to-rem(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
76
|
+
--stepbar-item-background: var(--color-surface-tertiary),
|
|
77
|
+
--stepbar-item-color: #{color.$white},
|
|
69
78
|
),
|
|
70
79
|
next: (
|
|
71
|
-
background: var(--color-surface-
|
|
72
|
-
color: var(--color-
|
|
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(--
|
|
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/
|
|
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: -
|
|
69
|
-
width:
|
|
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.
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
}
|