@orangesk/orange-design-system 3.6.2 → 3.8.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 (100) hide show
  1. package/build/components/Alert/style.css +1 -1
  2. package/build/components/Alert/style.css.map +1 -1
  3. package/build/components/Bar/style.css +1 -1
  4. package/build/components/Bar/style.css.map +1 -1
  5. package/build/components/Drawer/style.css +2 -0
  6. package/build/components/Drawer/style.css.map +1 -0
  7. package/build/components/Grid/style.css.map +1 -1
  8. package/build/components/Megamenu/style.css +1 -1
  9. package/build/components/Megamenu/style.css.map +1 -1
  10. package/build/components/Modal/style.css +1 -1
  11. package/build/components/Modal/style.css.map +1 -1
  12. package/build/components/StatusIndicator/style.css +2 -0
  13. package/build/components/StatusIndicator/style.css.map +1 -0
  14. package/build/components/Table/style.css +1 -1
  15. package/build/components/Table/style.css.map +1 -1
  16. package/build/components/index.js +1 -1
  17. package/build/components/index.js.map +1 -1
  18. package/build/components/types/index.d.ts +89 -3
  19. package/build/lib/base.css +1 -1
  20. package/build/lib/base.css.map +1 -1
  21. package/build/lib/components.css +1 -1
  22. package/build/lib/components.css.map +1 -1
  23. package/build/lib/footer.js +1 -1
  24. package/build/lib/footer.js.map +1 -1
  25. package/build/lib/megamenu.css +1 -1
  26. package/build/lib/megamenu.css.map +1 -1
  27. package/build/lib/megamenu.js +1 -1
  28. package/build/lib/megamenu.js.map +1 -1
  29. package/build/lib/scripts.js +1 -1
  30. package/build/lib/scripts.js.map +1 -1
  31. package/build/lib/style.css +1 -1
  32. package/build/lib/style.css.map +1 -1
  33. package/build/lib/utilities.css +1 -1
  34. package/build/lib/utilities.css.map +1 -1
  35. package/package.json +29 -29
  36. package/src/components/Alert/Alert.tsx +2 -2
  37. package/src/components/Alert/data.ts +8 -5
  38. package/src/components/Alert/styles/config.scss +4 -4
  39. package/src/components/Alert/styles/mixins.scss +8 -1
  40. package/src/components/Alert/tests/Alert.test.jsx +11 -1
  41. package/src/components/Bar/Bar.tsx +1 -1
  42. package/src/components/Bar/styles/config.scss +1 -0
  43. package/src/components/Bar/tests/Bar.unit.test.jsx +1 -1
  44. package/src/components/Carousel/Carousel.static.ts +103 -12
  45. package/src/components/Carousel/tests/Carousel.static.test.jsx +134 -0
  46. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +12 -4
  47. package/src/components/Drawer/Drawer.static.ts +281 -0
  48. package/src/components/Drawer/Drawer.tsx +82 -0
  49. package/src/components/Drawer/DrawerBody.tsx +21 -0
  50. package/src/components/Drawer/DrawerFooter.tsx +21 -0
  51. package/src/components/Drawer/DrawerHeader.tsx +21 -0
  52. package/src/components/Drawer/index.ts +5 -0
  53. package/src/components/Drawer/styles/config.scss +17 -0
  54. package/src/components/Drawer/styles/mixins.scss +119 -0
  55. package/src/components/Drawer/styles/style.scss +71 -0
  56. package/src/components/Drawer/tests/Drawer.unit.test.jsx +149 -0
  57. package/src/components/Footer/data.ts +4 -0
  58. package/src/components/Forms/SegmentedControl/SegmentedControl.tsx +122 -0
  59. package/src/components/Forms/SegmentedControl/index.ts +5 -0
  60. package/src/components/Forms/SegmentedControl/styles/mixins.scss +52 -0
  61. package/src/components/Forms/SegmentedControl/styles/style.scss +35 -0
  62. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.conformance.test.jsx +36 -0
  63. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.unit.test.jsx +125 -0
  64. package/src/components/Forms/SelectionTile/PLAN.md +206 -0
  65. package/src/components/Forms/SelectionTile/SelectionTile.tsx +145 -0
  66. package/src/components/Forms/SelectionTile/index.ts +5 -0
  67. package/src/components/Forms/SelectionTile/styles/mixins.scss +83 -0
  68. package/src/components/Forms/SelectionTile/styles/style.scss +58 -0
  69. package/src/components/Forms/SelectionTile/tests/SelectionTile.conformance.test.jsx +32 -0
  70. package/src/components/Forms/SelectionTile/tests/SelectionTile.unit.test.jsx +146 -0
  71. package/src/components/Forms/index.ts +2 -0
  72. package/src/components/Icon/index.ts +1 -0
  73. package/src/components/Megamenu/Megamenu.tsx +20 -16
  74. package/src/components/Megamenu/MegamenuBlog.tsx +10 -7
  75. package/src/components/Megamenu/MyOrangeMobilePanel.tsx +2 -3
  76. package/src/components/Megamenu/constants.ts +3 -1
  77. package/src/components/Megamenu/styles/mixins.scss +53 -18
  78. package/src/components/Megamenu/styles/style.scss +12 -4
  79. package/src/components/Megamenu/tests/Megamenu.unit.test.jsx +32 -0
  80. package/src/components/Modal/styles/config.scss +3 -2
  81. package/src/components/StatusIndicator/StatusIndicator.tsx +29 -0
  82. package/src/components/StatusIndicator/index.ts +2 -0
  83. package/src/components/StatusIndicator/styles/config.scss +12 -0
  84. package/src/components/StatusIndicator/styles/mixins.scss +36 -0
  85. package/src/components/StatusIndicator/styles/style.scss +16 -0
  86. package/src/components/StatusIndicator/tests/StatusIndicator.conformance.test.jsx +22 -0
  87. package/src/components/StatusIndicator/tests/StatusIndicator.unit.test.jsx +70 -0
  88. package/src/components/Table/Table.static.ts +23 -10
  89. package/src/components/Table/Table.tsx +7 -2
  90. package/src/components/Table/styles/mixins.scss +59 -0
  91. package/src/components/Table/styles/style.scss +25 -0
  92. package/src/components/Table/tests/Table.static.unit.test.jsx +72 -0
  93. package/src/components/Table/tests/Table.unit.test.jsx +33 -0
  94. package/src/components/index.ts +13 -0
  95. package/src/styles/tokens/overlay.scss +3 -0
  96. package/src/styles/typography/style.scss +0 -13
  97. package/src/styles/utilities/border.scss +4 -0
  98. package/src/styles/utilities/border.styles.unit.test.jsx +9 -0
  99. package/src/styles/utilities/text.scss +12 -0
  100. package/src/components/Carousel/CHANGELOG.md +0 -11
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import cx from "classnames";
4
+ import React, { useEffect, useMemo } from "react";
5
+ import { useStatic } from "../../utils/hooks";
6
+ import DrawerStatic from "./Drawer.static";
7
+
8
+ const CLASS_ROOT = "drawer";
9
+
10
+ interface DrawerProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ id: string;
12
+ isActive?: boolean;
13
+ onHide?: () => void;
14
+ children?: React.ReactNode;
15
+ className?: string;
16
+ }
17
+
18
+ const Drawer: React.FC<DrawerProps> = ({
19
+ "aria-label": ariaLabel = "Drawer",
20
+ "aria-labelledby": ariaLabelledBy,
21
+ className,
22
+ children,
23
+ id,
24
+ isActive,
25
+ onHide,
26
+ ...other
27
+ }) => {
28
+ const isReactControlled = isActive !== undefined;
29
+ const drawerConfig = useMemo(
30
+ () => (isReactControlled ? { disablePortal: true } : {}),
31
+ [isReactControlled],
32
+ );
33
+ const [drawerRef, instance] = useStatic(DrawerStatic, drawerConfig);
34
+
35
+ useEffect(() => {
36
+ if (!instance.current || !onHide) return;
37
+
38
+ const handleHide = () => onHide();
39
+ instance.current.instance.on("hide", handleHide);
40
+
41
+ return () => {
42
+ instance.current?.instance.off("hide", handleHide);
43
+ };
44
+ }, [instance, onHide]);
45
+
46
+ useEffect(() => {
47
+ if (!instance.current) return;
48
+
49
+ if (isActive) {
50
+ instance.current.show();
51
+ } else if (isActive === false) {
52
+ instance.current.hide();
53
+ }
54
+ }, [instance, isActive]);
55
+
56
+ return (
57
+ <div
58
+ id={id}
59
+ {...(!isReactControlled && { "data-drawer": true })}
60
+ className={cx(CLASS_ROOT, className)}
61
+ ref={drawerRef}
62
+ role="dialog"
63
+ aria-hidden="true"
64
+ aria-modal="true"
65
+ aria-label={ariaLabelledBy ? undefined : ariaLabel}
66
+ aria-labelledby={ariaLabelledBy}
67
+ >
68
+ <div
69
+ className={`${CLASS_ROOT}__overlay`}
70
+ tabIndex={-1}
71
+ data-a11y-dialog-hide
72
+ />
73
+ <div className={`${CLASS_ROOT}__panel`} role="document" {...other}>
74
+ {children}
75
+ </div>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ Drawer.displayName = "Drawer";
81
+
82
+ export { Drawer };
@@ -0,0 +1,21 @@
1
+ import cx from "classnames";
2
+ import React from "react";
3
+
4
+ interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ const DrawerBody: React.FC<DrawerBodyProps> = ({
10
+ children,
11
+ className,
12
+ ...other
13
+ }) => (
14
+ <div className={cx("drawer__body", className)} {...other}>
15
+ {children}
16
+ </div>
17
+ );
18
+
19
+ DrawerBody.displayName = "DrawerBody";
20
+
21
+ export { DrawerBody };
@@ -0,0 +1,21 @@
1
+ import cx from "classnames";
2
+ import React from "react";
3
+
4
+ interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ const DrawerFooter: React.FC<DrawerFooterProps> = ({
10
+ children,
11
+ className,
12
+ ...other
13
+ }) => (
14
+ <div className={cx("drawer__footer", className)} {...other}>
15
+ {children}
16
+ </div>
17
+ );
18
+
19
+ DrawerFooter.displayName = "DrawerFooter";
20
+
21
+ export { DrawerFooter };
@@ -0,0 +1,21 @@
1
+ import cx from "classnames";
2
+ import React from "react";
3
+
4
+ interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ const DrawerHeader: React.FC<DrawerHeaderProps> = ({
10
+ children,
11
+ className,
12
+ ...other
13
+ }) => (
14
+ <div className={cx("drawer__header", className)} {...other}>
15
+ {children}
16
+ </div>
17
+ );
18
+
19
+ DrawerHeader.displayName = "DrawerHeader";
20
+
21
+ export { DrawerHeader };
@@ -0,0 +1,5 @@
1
+ export { Drawer } from "./Drawer";
2
+ export { default as DrawerStatic } from "./Drawer.static";
3
+ export { DrawerBody } from "./DrawerBody";
4
+ export { DrawerFooter } from "./DrawerFooter";
5
+ export { DrawerHeader } from "./DrawerHeader";
@@ -0,0 +1,17 @@
1
+ @use "../../../styles/tools/convert";
2
+ @use "../../../styles/tokens/overlay";
3
+
4
+ $overlay-bg-color: overlay.$background !default;
5
+ $overlay-z-index: 500 !default;
6
+ $overlay-e: overlay.$transition !default;
7
+
8
+ $overlay: (
9
+ background-color: $overlay-bg-color,
10
+ );
11
+
12
+ $panel: (
13
+ max-width: convert.to-rem(700px),
14
+ max-height: min(80vh, convert.to-rem(520px)),
15
+ desktop-width: convert.to-rem(380px),
16
+ desktop-max-width: 90vw,
17
+ );
@@ -0,0 +1,119 @@
1
+ @use "../../../styles/tokens/space";
2
+ @use "../../../styles/tools/convert";
3
+ @use "../../../styles/tools/generate";
4
+ @use "sass:map";
5
+ @use "./config";
6
+
7
+ @mixin position {
8
+ position: fixed;
9
+ inset: 0;
10
+ }
11
+
12
+ @mixin hide {
13
+ visibility: hidden;
14
+ pointer-events: none;
15
+ transition: visibility 0s linear 0.2s;
16
+ }
17
+
18
+ @mixin drawer {
19
+ @include position;
20
+
21
+ z-index: 999;
22
+ display: flex;
23
+ align-items: flex-end;
24
+ }
25
+
26
+ @mixin overlay {
27
+ @include position;
28
+ @include generate.css-map(config.$overlay);
29
+
30
+ z-index: config.$overlay-z-index;
31
+ width: 0;
32
+ height: 0;
33
+ opacity: 0;
34
+ transform: scale(1.1);
35
+ transition:
36
+ opacity config.$overlay-e,
37
+ transform config.$overlay-e;
38
+ }
39
+
40
+ @mixin overlay-active {
41
+ width: 100%;
42
+ height: 100%;
43
+ opacity: 1;
44
+ transform: scale(1);
45
+ }
46
+
47
+ @mixin panel {
48
+ position: relative;
49
+ z-index: config.$overlay-z-index + 1;
50
+ display: flex;
51
+ flex-flow: column;
52
+ width: 100%;
53
+ max-height: map.get(config.$panel, max-height);
54
+ margin: 0 auto;
55
+ overflow: hidden;
56
+ background: var(--color-surface-primary);
57
+ border-radius: 0;
58
+ transform: translateY(100%);
59
+ transition: transform config.$overlay-e;
60
+ -webkit-overflow-scrolling: touch;
61
+ }
62
+
63
+ @mixin panel-active {
64
+ transform: translateY(0);
65
+ }
66
+
67
+ @mixin desktop {
68
+ justify-content: flex-end;
69
+ }
70
+
71
+ @mixin panel-desktop {
72
+ width: map.get(config.$panel, desktop-width);
73
+ max-width: map.get(config.$panel, desktop-max-width);
74
+ max-height: 100%;
75
+ height: 100%;
76
+ margin: 0;
77
+ transform: translateX(100%);
78
+ }
79
+
80
+ @mixin panel-active-desktop {
81
+ transform: translateX(0);
82
+ }
83
+
84
+ @mixin header {
85
+ position: sticky;
86
+ top: 0;
87
+ z-index: 1;
88
+ flex: 0 0 auto;
89
+ padding: space.get("medium") space.get("small");
90
+ background: var(--color-surface-primary);
91
+
92
+ *:last-child {
93
+ margin-bottom: 0px;
94
+ }
95
+ }
96
+
97
+ @mixin body {
98
+ flex: 1 1 auto;
99
+ min-height: 0;
100
+ overflow-y: auto;
101
+ padding: 0 space.get("small");
102
+
103
+ *:last-child {
104
+ margin-bottom: 0px;
105
+ }
106
+ }
107
+
108
+ @mixin footer {
109
+ flex: 0 0 auto;
110
+ padding: space.get("medium") space.get("small");
111
+
112
+ *:last-child {
113
+ margin-bottom: 0px;
114
+ }
115
+ }
116
+
117
+ @mixin reduced-motion {
118
+ transition-duration: 0.01ms;
119
+ }
@@ -0,0 +1,71 @@
1
+ @use "../../../styles/tokens/breakpoint";
2
+ @use "../../../styles/tokens/space";
3
+ @use "./mixins";
4
+
5
+ @layer components {
6
+ .drawer {
7
+ @include mixins.drawer;
8
+
9
+ &[aria-hidden="true"] {
10
+ @include mixins.hide;
11
+ }
12
+ }
13
+
14
+ .drawer__overlay {
15
+ @include mixins.overlay;
16
+
17
+ .drawer.is-active & {
18
+ @include mixins.overlay-active;
19
+ }
20
+ }
21
+
22
+ .drawer__panel {
23
+ @include mixins.panel;
24
+
25
+ // Keep a form wrapper equivalent to the panel's original flex layout.
26
+ > form {
27
+ display: flex;
28
+ flex: 1 1 auto;
29
+ flex-flow: column;
30
+ min-height: 0;
31
+ }
32
+
33
+ .drawer.is-active & {
34
+ @include mixins.panel-active;
35
+ }
36
+ }
37
+
38
+ .drawer__header {
39
+ @include mixins.header;
40
+ }
41
+
42
+ .drawer__body {
43
+ @include mixins.body;
44
+ }
45
+
46
+ .drawer__footer {
47
+ @include mixins.footer;
48
+ }
49
+
50
+ @include breakpoint.get("lg") {
51
+ .drawer {
52
+ @include mixins.desktop;
53
+ }
54
+
55
+ .drawer__panel {
56
+ @include mixins.panel-desktop;
57
+ }
58
+
59
+ .drawer.is-active .drawer__panel {
60
+ @include mixins.panel-active-desktop;
61
+ }
62
+ }
63
+
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .drawer,
66
+ .drawer__overlay,
67
+ .drawer__panel {
68
+ @include mixins.reduced-motion;
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,149 @@
1
+ import { fireEvent } from "@testing-library/dom";
2
+ import { axe } from "vitest-axe";
3
+
4
+ import Drawer from "../Drawer.static";
5
+
6
+ const instances = [];
7
+
8
+ const renderDrawer = () => {
9
+ document.body.innerHTML = `
10
+ <div id="root">
11
+ <button id="open-drawer" data-a11y-dialog-show="drawer">Open</button>
12
+ </div>
13
+ <div id="root-drawers">
14
+ <div id="drawer" class="drawer" data-drawer role="dialog" aria-modal="true"
15
+ aria-hidden="true" aria-labelledby="drawer-title">
16
+ <div class="drawer__overlay" tabindex="-1" data-a11y-dialog-hide></div>
17
+ <div class="drawer__panel" role="document">
18
+ <header class="drawer__header">
19
+ <h2 id="drawer-title" data-a11y-dialog-initial-focus>Drawer title</h2>
20
+ <button class="drawer__close" data-a11y-dialog-hide>Close</button>
21
+ </header>
22
+ <div class="drawer__body"><p>Drawer content</p></div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ `;
27
+
28
+ const drawer = document.querySelector("#drawer");
29
+ const instance = new Drawer(drawer);
30
+ instances.push(instance);
31
+
32
+ return { drawer, instance, opener: document.querySelector("#open-drawer") };
33
+ };
34
+
35
+ afterEach(() => {
36
+ instances.forEach((instance) => instance.destroy());
37
+ instances.length = 0;
38
+ document.body.innerHTML = "";
39
+ document.body.removeAttribute("data-lock-scrolltop");
40
+ document.body.className = "";
41
+ document.body.style.cssText = "";
42
+ });
43
+
44
+ it("moves the drawer into root-drawers and starts hidden", () => {
45
+ const { drawer } = renderDrawer();
46
+
47
+ expect(drawer.parentElement.id).toBe("root-drawers");
48
+ expect(drawer).toHaveAttribute("aria-hidden", "true");
49
+ });
50
+
51
+ it("warns when the drawer root is missing", () => {
52
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
53
+ document.body.innerHTML = `
54
+ <div id="drawer" class="drawer" data-drawer role="dialog" aria-modal="true">
55
+ <div class="drawer__overlay" tabindex="-1" data-a11y-dialog-hide></div>
56
+ <div class="drawer__panel" role="document"></div>
57
+ </div>
58
+ `;
59
+
60
+ new Drawer(document.querySelector("#drawer"));
61
+
62
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("#root-drawers"));
63
+ warn.mockRestore();
64
+ });
65
+
66
+ it("opens, locks body scroll, and closes when the overlay is clicked", () => {
67
+ const { drawer, opener } = renderDrawer();
68
+
69
+ fireEvent.click(opener);
70
+ expect(drawer).toHaveClass("is-active");
71
+ expect(drawer).not.toHaveAttribute("aria-hidden");
72
+ expect(document.body).toHaveClass("has-modal");
73
+
74
+ fireEvent.click(drawer.querySelector(".drawer__overlay"));
75
+ expect(drawer).not.toHaveClass("is-active");
76
+ expect(drawer).toHaveAttribute("aria-hidden", "true");
77
+ expect(document.body).not.toHaveClass("has-modal");
78
+ });
79
+
80
+ it("closes after a downward mobile swipe", () => {
81
+ Object.defineProperty(window, "innerWidth", {
82
+ configurable: true,
83
+ value: 375,
84
+ });
85
+ const { drawer, opener } = renderDrawer();
86
+ fireEvent.click(opener);
87
+
88
+ fireEvent.pointerDown(drawer.querySelector(".drawer__panel"), {
89
+ pointerId: 1,
90
+ isPrimary: true,
91
+ pointerType: "mouse",
92
+ clientX: 100,
93
+ clientY: 100,
94
+ });
95
+ fireEvent.pointerMove(drawer.querySelector(".drawer__panel"), {
96
+ pointerId: 1,
97
+ isPrimary: true,
98
+ pointerType: "mouse",
99
+ clientX: 100,
100
+ clientY: 200,
101
+ });
102
+ fireEvent.pointerUp(drawer.querySelector(".drawer__panel"), {
103
+ pointerId: 1,
104
+ isPrimary: true,
105
+ pointerType: "mouse",
106
+ clientX: 100,
107
+ clientY: 200,
108
+ });
109
+
110
+ expect(drawer).toHaveAttribute("aria-hidden", "true");
111
+ });
112
+
113
+ it("closes after a rightward desktop swipe", () => {
114
+ Object.defineProperty(window, "innerWidth", {
115
+ configurable: true,
116
+ value: 1280,
117
+ });
118
+ const { drawer, opener } = renderDrawer();
119
+ fireEvent.click(opener);
120
+
121
+ fireEvent.pointerDown(drawer.querySelector(".drawer__panel"), {
122
+ pointerId: 1,
123
+ isPrimary: true,
124
+ pointerType: "mouse",
125
+ clientX: 100,
126
+ clientY: 100,
127
+ });
128
+ fireEvent.pointerMove(drawer.querySelector(".drawer__panel"), {
129
+ pointerId: 1,
130
+ pointerType: "mouse",
131
+ clientX: 220,
132
+ clientY: 100,
133
+ });
134
+ fireEvent.pointerUp(drawer.querySelector(".drawer__panel"), {
135
+ pointerId: 1,
136
+ pointerType: "mouse",
137
+ clientX: 220,
138
+ clientY: 100,
139
+ });
140
+
141
+ expect(drawer).toHaveAttribute("aria-hidden", "true");
142
+ });
143
+
144
+ it("is accessible while open", async () => {
145
+ const { drawer, opener } = renderDrawer();
146
+
147
+ fireEvent.click(opener);
148
+ expect(await axe(drawer)).toHaveNoViolations();
149
+ });
@@ -66,6 +66,10 @@ const data = {
66
66
  label: "Informácie pre koncových užívateľov",
67
67
  url: "/informacie-pre-uzivatelov",
68
68
  },
69
+ {
70
+ label: "ISO certifikáty",
71
+ url: "/orange-slovensko/kto-sme",
72
+ },
69
73
  ],
70
74
  copyright: `© 1997 - ${currentYear} Orange`,
71
75
  };
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import cx from "classnames";
4
+ import React, { createContext, type ReactNode, useContext } from "react";
5
+
6
+ import { Label } from "../Label";
7
+ import { Radio } from "../Radio";
8
+ import type { RadioProps } from "../Radio/Radio";
9
+
10
+ const CLASS_ROOT = "segmented-control";
11
+ interface SegmentedControlContextValue {
12
+ name: string;
13
+ isInvalid: boolean;
14
+ describedBy?: string;
15
+ }
16
+
17
+ const SegmentedControlContext =
18
+ createContext<SegmentedControlContextValue | null>(null);
19
+
20
+ export interface SegmentedControlProps
21
+ extends React.HTMLAttributes<HTMLDivElement> {
22
+ name: string;
23
+ isInvalid?: boolean;
24
+ children?: ReactNode;
25
+ }
26
+
27
+ type SegmentedControlInputProps = Omit<
28
+ RadioProps,
29
+ | "id"
30
+ | "name"
31
+ | "value"
32
+ | "type"
33
+ | "checked"
34
+ | "defaultChecked"
35
+ | "disabled"
36
+ | "isChecked"
37
+ | "isDisabled"
38
+ | "isInvalid"
39
+ | "children"
40
+ >;
41
+
42
+ export interface SegmentedControlItemProps
43
+ extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "htmlFor"> {
44
+ id: string;
45
+ value: string;
46
+ isChecked?: boolean;
47
+ isDisabled?: boolean;
48
+ inputProps?: SegmentedControlInputProps;
49
+ children: ReactNode;
50
+ }
51
+
52
+ const SegmentedControl: React.FC<SegmentedControlProps> = ({
53
+ name,
54
+ isInvalid = false,
55
+ "aria-describedby": describedBy,
56
+ className,
57
+ children,
58
+ ...other
59
+ }) => (
60
+ <div
61
+ className={cx(
62
+ CLASS_ROOT,
63
+ { [`${CLASS_ROOT}--invalid`]: isInvalid },
64
+ className,
65
+ )}
66
+ {...other}
67
+ >
68
+ <SegmentedControlContext.Provider value={{ name, isInvalid, describedBy }}>
69
+ {children}
70
+ </SegmentedControlContext.Provider>
71
+ </div>
72
+ );
73
+
74
+ const SegmentedControlItem: React.FC<SegmentedControlItemProps> = ({
75
+ id,
76
+ value,
77
+ isChecked,
78
+ isDisabled,
79
+ inputProps,
80
+ className,
81
+ children,
82
+ ...other
83
+ }) => {
84
+ const context = useContext(SegmentedControlContext);
85
+
86
+ if (context === null) {
87
+ throw new Error(
88
+ "SegmentedControlItem must be rendered inside SegmentedControl.",
89
+ );
90
+ }
91
+
92
+ const { name, isInvalid, describedBy: groupDescribedBy } = context;
93
+ const describedBy = [groupDescribedBy, inputProps?.["aria-describedby"]]
94
+ .filter(Boolean)
95
+ .join(" ");
96
+
97
+ return (
98
+ <Label
99
+ htmlFor={id}
100
+ className={cx(`${CLASS_ROOT}__item`, className)}
101
+ {...other}
102
+ >
103
+ <Radio
104
+ {...inputProps}
105
+ id={id}
106
+ name={name}
107
+ value={value}
108
+ isChecked={isChecked}
109
+ isDisabled={isDisabled}
110
+ isInvalid={isInvalid}
111
+ aria-invalid={isInvalid || undefined}
112
+ aria-describedby={describedBy || undefined}
113
+ />
114
+ <span className={`${CLASS_ROOT}__label`}>{children}</span>
115
+ </Label>
116
+ );
117
+ };
118
+
119
+ SegmentedControl.displayName = "SegmentedControl";
120
+ SegmentedControlItem.displayName = "SegmentedControlItem";
121
+
122
+ export { SegmentedControl, SegmentedControlItem };
@@ -0,0 +1,5 @@
1
+ export type {
2
+ SegmentedControlItemProps,
3
+ SegmentedControlProps,
4
+ } from "./SegmentedControl";
5
+ export { SegmentedControl, SegmentedControlItem } from "./SegmentedControl";
@@ -0,0 +1,52 @@
1
+ @use "../../../../styles/tokens/space";
2
+ @use "../../../../styles/tools/convert";
3
+
4
+ @mixin base {
5
+ display: inline-flex;
6
+ width: fit-content;
7
+ max-width: 100%;
8
+ background: var(--color-fill-primary);
9
+ }
10
+
11
+ @mixin item {
12
+ position: relative;
13
+ display: flex;
14
+ flex: 1 1 auto;
15
+ align-items: center;
16
+ gap: space.get("small");
17
+ min-height: convert.to-rem(50px);
18
+ min-width: 0;
19
+ padding: convert.to-rem(13px) convert.to-rem(15px);
20
+ font-weight: 700;
21
+ border: 2px solid var(--color-border-strong);
22
+ border-radius: 0;
23
+ }
24
+
25
+ @mixin first-item {
26
+ border-start-start-radius: space.get("small");
27
+ border-end-start-radius: space.get("small");
28
+ }
29
+
30
+ @mixin adjacent-item {
31
+ margin-inline-start: -2px;
32
+ }
33
+
34
+ @mixin last-item {
35
+ border-start-end-radius: space.get("small");
36
+ border-end-end-radius: space.get("small");
37
+ }
38
+
39
+ @mixin selected {
40
+ z-index: 1;
41
+ border-color: var(--color-border-accent);
42
+ }
43
+
44
+ @mixin disabled {
45
+ cursor: not-allowed;
46
+ color: var(--color-text-secondary);
47
+ background: var(--color-surface-subtle);
48
+ }
49
+
50
+ @mixin invalid {
51
+ border-color: var(--color-border-negative);
52
+ }