@orangesk/orange-design-system 3.7.0 → 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.
- package/build/components/Alert/style.css +1 -1
- package/build/components/Alert/style.css.map +1 -1
- package/build/components/Bar/style.css +1 -1
- package/build/components/Bar/style.css.map +1 -1
- package/build/components/Drawer/style.css +2 -0
- package/build/components/Drawer/style.css.map +1 -0
- package/build/components/Megamenu/style.css +1 -1
- package/build/components/Megamenu/style.css.map +1 -1
- package/build/components/Modal/style.css +1 -1
- package/build/components/Modal/style.css.map +1 -1
- package/build/components/StatusIndicator/style.css +2 -0
- package/build/components/StatusIndicator/style.css.map +1 -0
- package/build/components/Table/style.css +1 -1
- package/build/components/Table/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/components/types/index.d.ts +55 -3
- 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.js +1 -1
- package/build/lib/footer.js.map +1 -1
- package/build/lib/megamenu.css +1 -1
- package/build/lib/megamenu.css.map +1 -1
- package/build/lib/megamenu.js +1 -1
- package/build/lib/megamenu.js.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/package.json +27 -27
- package/src/components/Alert/Alert.tsx +2 -2
- package/src/components/Alert/data.ts +8 -5
- package/src/components/Alert/styles/config.scss +4 -4
- package/src/components/Alert/styles/mixins.scss +8 -1
- package/src/components/Alert/tests/Alert.test.jsx +11 -1
- package/src/components/Bar/Bar.tsx +1 -1
- package/src/components/Bar/styles/config.scss +1 -0
- package/src/components/Bar/tests/Bar.unit.test.jsx +1 -1
- package/src/components/Carousel/Carousel.static.ts +103 -12
- package/src/components/Carousel/tests/Carousel.static.test.jsx +134 -0
- package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +12 -4
- package/src/components/Drawer/Drawer.static.ts +281 -0
- package/src/components/Drawer/Drawer.tsx +82 -0
- package/src/components/Drawer/DrawerBody.tsx +21 -0
- package/src/components/Drawer/DrawerFooter.tsx +21 -0
- package/src/components/Drawer/DrawerHeader.tsx +21 -0
- package/src/components/Drawer/index.ts +5 -0
- package/src/components/Drawer/styles/config.scss +17 -0
- package/src/components/Drawer/styles/mixins.scss +119 -0
- package/src/components/Drawer/styles/style.scss +71 -0
- package/src/components/Drawer/tests/Drawer.unit.test.jsx +149 -0
- package/src/components/Forms/SegmentedControl/SegmentedControl.tsx +122 -0
- package/src/components/Forms/SegmentedControl/index.ts +5 -0
- package/src/components/Forms/SegmentedControl/styles/mixins.scss +52 -0
- package/src/components/Forms/SegmentedControl/styles/style.scss +35 -0
- package/src/components/Forms/SegmentedControl/tests/SegmentedControl.conformance.test.jsx +36 -0
- package/src/components/Forms/SegmentedControl/tests/SegmentedControl.unit.test.jsx +125 -0
- package/src/components/Forms/index.ts +1 -0
- package/src/components/Icon/index.ts +1 -0
- package/src/components/Megamenu/styles/mixins.scss +15 -14
- package/src/components/Modal/styles/config.scss +3 -2
- package/src/components/StatusIndicator/StatusIndicator.tsx +29 -0
- package/src/components/StatusIndicator/index.ts +2 -0
- package/src/components/StatusIndicator/styles/config.scss +12 -0
- package/src/components/StatusIndicator/styles/mixins.scss +36 -0
- package/src/components/StatusIndicator/styles/style.scss +16 -0
- package/src/components/StatusIndicator/tests/StatusIndicator.conformance.test.jsx +22 -0
- package/src/components/StatusIndicator/tests/StatusIndicator.unit.test.jsx +70 -0
- package/src/components/Table/Table.static.ts +23 -10
- package/src/components/Table/Table.tsx +7 -2
- package/src/components/Table/styles/mixins.scss +59 -0
- package/src/components/Table/styles/style.scss +25 -0
- package/src/components/Table/tests/Table.static.unit.test.jsx +72 -0
- package/src/components/Table/tests/Table.unit.test.jsx +33 -0
- package/src/components/index.ts +11 -0
- package/src/styles/tokens/overlay.scss +3 -0
- package/src/styles/typography/style.scss +0 -13
- package/src/styles/utilities/border.scss +4 -0
- package/src/styles/utilities/border.styles.unit.test.jsx +9 -0
- package/src/styles/utilities/text.scss +12 -0
- 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,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
|
+
});
|
|
@@ -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,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
|
+
}
|