@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,35 @@
|
|
|
1
|
+
@use "./mixins";
|
|
2
|
+
|
|
3
|
+
@layer components {
|
|
4
|
+
.segmented-control {
|
|
5
|
+
@include mixins.base;
|
|
6
|
+
|
|
7
|
+
&__item {
|
|
8
|
+
@include mixins.item;
|
|
9
|
+
|
|
10
|
+
&:first-child {
|
|
11
|
+
@include mixins.first-item;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
& + & {
|
|
15
|
+
@include mixins.adjacent-item;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&:last-child {
|
|
19
|
+
@include mixins.last-item;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&:has(:checked) {
|
|
23
|
+
@include mixins.selected;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:has(:disabled) {
|
|
27
|
+
@include mixins.disabled;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.segmented-control--invalid &:has(input) {
|
|
31
|
+
@include mixins.invalid;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { axe } from "vitest-axe";
|
|
3
|
+
|
|
4
|
+
import { Fieldset } from "../../Fieldset";
|
|
5
|
+
import { Message } from "../../Message";
|
|
6
|
+
import { SegmentedControl, SegmentedControlItem } from "..";
|
|
7
|
+
|
|
8
|
+
it("is accessible as a labelled radio group with feedback", async () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<Fieldset legend="Payment method">
|
|
11
|
+
<SegmentedControl
|
|
12
|
+
name="payment-method"
|
|
13
|
+
isInvalid
|
|
14
|
+
aria-describedby="payment-method-error"
|
|
15
|
+
>
|
|
16
|
+
<SegmentedControlItem id="payment-card" value="card">
|
|
17
|
+
Card
|
|
18
|
+
</SegmentedControlItem>
|
|
19
|
+
<SegmentedControlItem id="payment-cash" value="cash">
|
|
20
|
+
Cash
|
|
21
|
+
</SegmentedControlItem>
|
|
22
|
+
</SegmentedControl>
|
|
23
|
+
<Message id="payment-method-error" type="error">
|
|
24
|
+
Choose a payment method.
|
|
25
|
+
</Message>
|
|
26
|
+
</Fieldset>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(container).toHTMLValidate({
|
|
30
|
+
rules: {
|
|
31
|
+
"element-permitted-content": "off",
|
|
32
|
+
"no-redundant-for": "off",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
36
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { Fieldset } from "../../Fieldset";
|
|
4
|
+
import { SegmentedControl, SegmentedControlItem } from "..";
|
|
5
|
+
|
|
6
|
+
describe("SegmentedControl", () => {
|
|
7
|
+
it("renders any number of choices as one native radio group", () => {
|
|
8
|
+
render(
|
|
9
|
+
<Fieldset legend="Number type">
|
|
10
|
+
<SegmentedControl name="numberType">
|
|
11
|
+
<SegmentedControlItem id="number-new" value="new">
|
|
12
|
+
New number
|
|
13
|
+
</SegmentedControlItem>
|
|
14
|
+
<SegmentedControlItem id="number-existing" value="existing">
|
|
15
|
+
Existing number
|
|
16
|
+
</SegmentedControlItem>
|
|
17
|
+
<SegmentedControlItem id="number-transfer" value="transfer">
|
|
18
|
+
Transfer number
|
|
19
|
+
</SegmentedControlItem>
|
|
20
|
+
</SegmentedControl>
|
|
21
|
+
</Fieldset>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const choices = screen.getAllByRole("radio");
|
|
25
|
+
|
|
26
|
+
expect(screen.getByRole("group", { name: "Number type" })).toContainElement(
|
|
27
|
+
choices[0],
|
|
28
|
+
);
|
|
29
|
+
expect(choices).toHaveLength(3);
|
|
30
|
+
expect(choices.map((choice) => choice.name)).toEqual([
|
|
31
|
+
"numberType",
|
|
32
|
+
"numberType",
|
|
33
|
+
"numberType",
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("selects an option through its label and preserves radio exclusivity", () => {
|
|
38
|
+
render(
|
|
39
|
+
<SegmentedControl name="delivery">
|
|
40
|
+
<SegmentedControlItem id="delivery-email" value="email">
|
|
41
|
+
Email
|
|
42
|
+
</SegmentedControlItem>
|
|
43
|
+
<SegmentedControlItem id="delivery-sms" value="sms">
|
|
44
|
+
SMS
|
|
45
|
+
</SegmentedControlItem>
|
|
46
|
+
</SegmentedControl>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const email = screen.getByRole("radio", { name: "Email" });
|
|
50
|
+
const sms = screen.getByRole("radio", { name: "SMS" });
|
|
51
|
+
|
|
52
|
+
fireEvent.click(screen.getByText("Email"));
|
|
53
|
+
expect(email).toBeChecked();
|
|
54
|
+
|
|
55
|
+
fireEvent.click(screen.getByText("SMS"));
|
|
56
|
+
expect(email).not.toBeChecked();
|
|
57
|
+
expect(sms).toBeChecked();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("forwards native radio states and attributes", () => {
|
|
61
|
+
render(
|
|
62
|
+
<SegmentedControl name="plan" className="custom-control">
|
|
63
|
+
<SegmentedControlItem
|
|
64
|
+
id="plan-standard"
|
|
65
|
+
value="standard"
|
|
66
|
+
isChecked
|
|
67
|
+
isDisabled
|
|
68
|
+
inputProps={{ "aria-describedby": "plan-hint" }}
|
|
69
|
+
className="custom-item"
|
|
70
|
+
>
|
|
71
|
+
Standard
|
|
72
|
+
</SegmentedControlItem>
|
|
73
|
+
</SegmentedControl>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const choice = screen.getByRole("radio", { name: "Standard" });
|
|
77
|
+
|
|
78
|
+
expect(choice).toBeChecked();
|
|
79
|
+
expect(choice).toBeDisabled();
|
|
80
|
+
expect(choice).toHaveAttribute("value", "standard");
|
|
81
|
+
expect(choice).toHaveAttribute("aria-describedby", "plan-hint");
|
|
82
|
+
expect(choice.closest(".segmented-control")).toHaveClass("custom-control");
|
|
83
|
+
expect(choice.closest(".segmented-control__item")).toHaveClass(
|
|
84
|
+
"custom-item",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("marks the whole radio group as invalid", () => {
|
|
89
|
+
render(
|
|
90
|
+
<SegmentedControl name="payment" isInvalid data-testid="payment-method">
|
|
91
|
+
<SegmentedControlItem id="payment-card" value="card" isChecked>
|
|
92
|
+
Card
|
|
93
|
+
</SegmentedControlItem>
|
|
94
|
+
<SegmentedControlItem id="payment-cash" value="cash">
|
|
95
|
+
Cash
|
|
96
|
+
</SegmentedControlItem>
|
|
97
|
+
</SegmentedControl>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByTestId("payment-method")).toHaveClass(
|
|
101
|
+
"segmented-control--invalid",
|
|
102
|
+
);
|
|
103
|
+
for (const choice of screen.getAllByRole("radio")) {
|
|
104
|
+
expect(choice).toHaveAttribute("aria-invalid", "true");
|
|
105
|
+
expect(choice).toHaveClass("is-invalid");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("associates group feedback with every radio", () => {
|
|
110
|
+
render(
|
|
111
|
+
<SegmentedControl name="contact" aria-describedby="contact-error">
|
|
112
|
+
<SegmentedControlItem id="contact-email" value="email">
|
|
113
|
+
Email
|
|
114
|
+
</SegmentedControlItem>
|
|
115
|
+
<SegmentedControlItem id="contact-phone" value="phone">
|
|
116
|
+
Phone
|
|
117
|
+
</SegmentedControlItem>
|
|
118
|
+
</SegmentedControl>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
for (const choice of screen.getAllByRole("radio")) {
|
|
122
|
+
expect(choice).toHaveAttribute("aria-describedby", "contact-error");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -11,6 +11,7 @@ export { Label } from "./Label";
|
|
|
11
11
|
export { Message } from "./Message";
|
|
12
12
|
export { Radio } from "./Radio";
|
|
13
13
|
export { RangeSlider } from "./RangeSlider";
|
|
14
|
+
export { SegmentedControl, SegmentedControlItem } from "./SegmentedControl";
|
|
14
15
|
export { Select } from "./Select";
|
|
15
16
|
export { SelectionTile } from "./SelectionTile";
|
|
16
17
|
export { TextArea } from "./TextArea";
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
@use "../../Button/styles/mixins" as button;
|
|
18
18
|
@use "../../Card/styles/mixins" as card;
|
|
19
19
|
@use "../../../styles/typography/mixins" as typography;
|
|
20
|
+
@use "../../../styles/tokens/overlay";
|
|
20
21
|
|
|
21
22
|
@use "./config";
|
|
22
23
|
|
|
@@ -652,10 +653,16 @@
|
|
|
652
653
|
|
|
653
654
|
@mixin subnav-item {
|
|
654
655
|
display: flex;
|
|
655
|
-
gap: convert.to-rem(10px);
|
|
656
|
+
column-gap: convert.to-rem(10px);
|
|
656
657
|
margin-bottom: convert.to-rem(10px) !important;
|
|
657
658
|
|
|
659
|
+
> #{config.$class-root}__tag {
|
|
660
|
+
align-self: flex-start;
|
|
661
|
+
}
|
|
662
|
+
|
|
658
663
|
a {
|
|
664
|
+
min-width: 0;
|
|
665
|
+
|
|
659
666
|
&:hover,
|
|
660
667
|
&:focus,
|
|
661
668
|
&:focus-visible,
|
|
@@ -715,13 +722,9 @@
|
|
|
715
722
|
left: 0;
|
|
716
723
|
width: 100%;
|
|
717
724
|
height: 100%;
|
|
718
|
-
background-color:
|
|
719
|
-
in srgb,
|
|
720
|
-
var(--color-surface-contrast) 75%,
|
|
721
|
-
transparent
|
|
722
|
-
);
|
|
725
|
+
background-color: overlay.$background;
|
|
723
726
|
opacity: 0;
|
|
724
|
-
transition: opacity
|
|
727
|
+
transition: opacity overlay.$transition;
|
|
725
728
|
}
|
|
726
729
|
|
|
727
730
|
&.is-active {
|
|
@@ -1135,11 +1138,7 @@
|
|
|
1135
1138
|
left: 0;
|
|
1136
1139
|
width: 100%;
|
|
1137
1140
|
height: 0;
|
|
1138
|
-
background-color:
|
|
1139
|
-
in srgb,
|
|
1140
|
-
var(--color-surface-contrast) 75%,
|
|
1141
|
-
transparent
|
|
1142
|
-
);
|
|
1141
|
+
background-color: overlay.$background;
|
|
1143
1142
|
border: none;
|
|
1144
1143
|
appearance: none;
|
|
1145
1144
|
-webkit-appearance: none;
|
|
@@ -1148,8 +1147,8 @@
|
|
|
1148
1147
|
opacity: 0;
|
|
1149
1148
|
visibility: hidden;
|
|
1150
1149
|
transition:
|
|
1151
|
-
opacity
|
|
1152
|
-
visibility
|
|
1150
|
+
opacity overlay.$transition,
|
|
1151
|
+
visibility overlay.$transition;
|
|
1153
1152
|
outline: none;
|
|
1154
1153
|
|
|
1155
1154
|
&.is-active {
|
|
@@ -1327,6 +1326,8 @@
|
|
|
1327
1326
|
|
|
1328
1327
|
@mixin tag {
|
|
1329
1328
|
@include tag.base;
|
|
1329
|
+
flex: 0 0 auto;
|
|
1330
|
+
white-space: nowrap;
|
|
1330
1331
|
text-decoration: none !important;
|
|
1331
1332
|
|
|
1332
1333
|
@each $name, $props in tagConfig.$sizes {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
@use "../../../styles/tokens/space";
|
|
2
2
|
@use "../../../styles/tokens/color";
|
|
3
|
+
@use "../../../styles/tokens/overlay";
|
|
3
4
|
@use "../../../styles/tools/convert";
|
|
4
5
|
|
|
5
|
-
$overlay-bg-color:
|
|
6
|
+
$overlay-bg-color: overlay.$background !default;
|
|
6
7
|
$overlay-z-index: 500 !default;
|
|
7
|
-
$overlay-e:
|
|
8
|
+
$overlay-e: overlay.$transition !default;
|
|
8
9
|
|
|
9
10
|
$overlay: (
|
|
10
11
|
background-color: $overlay-bg-color,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import cx from "classnames";
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export interface StatusIndicatorProps extends HTMLAttributes<HTMLSpanElement> {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
type: "success" | "warning" | "danger";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CLASS_ROOT = "status-indicator";
|
|
11
|
+
|
|
12
|
+
const StatusIndicator: React.FC<StatusIndicatorProps> = ({
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
type,
|
|
16
|
+
...other
|
|
17
|
+
}) => (
|
|
18
|
+
<span
|
|
19
|
+
className={cx(CLASS_ROOT, `${CLASS_ROOT}--${type}`, className)}
|
|
20
|
+
{...other}
|
|
21
|
+
>
|
|
22
|
+
<span className={`${CLASS_ROOT}__marker`} aria-hidden="true" />
|
|
23
|
+
<span className={`${CLASS_ROOT}__content`}>{children}</span>
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
StatusIndicator.displayName = "StatusIndicator";
|
|
28
|
+
|
|
29
|
+
export { StatusIndicator };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
@use "../../../styles/tools/generate";
|
|
2
|
+
@use "../../../styles/typography/config" as typography;
|
|
3
|
+
@use "./config";
|
|
4
|
+
|
|
5
|
+
@mixin base($config: config.$base) {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: flex-start;
|
|
8
|
+
|
|
9
|
+
@include generate.css-map($config);
|
|
10
|
+
@include generate.css-map(typography.$caption, "default");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@mixin marker($config: config.$marker) {
|
|
14
|
+
flex: 0 0 auto;
|
|
15
|
+
border-radius: 50%;
|
|
16
|
+
|
|
17
|
+
@include generate.css-map($config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@mixin content() {
|
|
21
|
+
min-width: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@mixin marker-types() {
|
|
25
|
+
.status-indicator--success & {
|
|
26
|
+
background-color: var(--color-icon-positive);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.status-indicator--warning & {
|
|
30
|
+
background-color: var(--color-icon-warning);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.status-indicator--danger & {
|
|
34
|
+
background-color: var(--color-icon-negative);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { axe } from "vitest-axe";
|
|
3
|
+
|
|
4
|
+
import { StatusIndicator } from "../";
|
|
5
|
+
|
|
6
|
+
const example = (
|
|
7
|
+
<>
|
|
8
|
+
<StatusIndicator type="success">Available</StatusIndicator>
|
|
9
|
+
<StatusIndicator type="warning">Limited availability</StatusIndicator>
|
|
10
|
+
<StatusIndicator type="danger">Unavailable</StatusIndicator>
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
it("is valid html", () => {
|
|
15
|
+
const { container } = render(example);
|
|
16
|
+
expect(container).toHTMLValidate();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("is accessible", async () => {
|
|
20
|
+
const { container } = render(example);
|
|
21
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
22
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import * as components from "../../";
|
|
4
|
+
|
|
5
|
+
describe("StatusIndicator", () => {
|
|
6
|
+
it("is available from the public component entry point", () => {
|
|
7
|
+
expect(components.StatusIndicator).toBeTypeOf("function");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it.each([
|
|
11
|
+
["success", "status-indicator--success"],
|
|
12
|
+
["warning", "status-indicator--warning"],
|
|
13
|
+
["danger", "status-indicator--danger"],
|
|
14
|
+
])("renders the %s type", (type, expectedClass) => {
|
|
15
|
+
const { getByText } = render(
|
|
16
|
+
<components.StatusIndicator type={type}>
|
|
17
|
+
{type}
|
|
18
|
+
</components.StatusIndicator>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(getByText(type).closest(".status-indicator")).toHaveClass(
|
|
22
|
+
"status-indicator",
|
|
23
|
+
expectedClass,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders a decorative marker before the label", () => {
|
|
28
|
+
const { getByTestId } = render(
|
|
29
|
+
<components.StatusIndicator data-testid="status" type="success">
|
|
30
|
+
Available
|
|
31
|
+
</components.StatusIndicator>,
|
|
32
|
+
);
|
|
33
|
+
const statusIndicator = getByTestId("status");
|
|
34
|
+
const marker = statusIndicator.firstElementChild;
|
|
35
|
+
|
|
36
|
+
expect(marker).toHaveClass("status-indicator__marker");
|
|
37
|
+
expect(marker).toHaveAttribute("aria-hidden", "true");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("groups text and inline controls in one wrapping content item", () => {
|
|
41
|
+
const { getByTestId, getByRole } = render(
|
|
42
|
+
<components.StatusIndicator data-testid="status" type="danger">
|
|
43
|
+
Temporarily unavailable <button type="button">More information</button>
|
|
44
|
+
</components.StatusIndicator>,
|
|
45
|
+
);
|
|
46
|
+
const content = getByTestId("status").querySelector(
|
|
47
|
+
".status-indicator__content",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(content).toHaveTextContent("Temporarily unavailable");
|
|
51
|
+
expect(content).toContainElement(
|
|
52
|
+
getByRole("button", { name: "More information" }),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("passes native span props and additional classes", () => {
|
|
57
|
+
const { getByTestId } = render(
|
|
58
|
+
<components.StatusIndicator
|
|
59
|
+
className="custom-class"
|
|
60
|
+
data-testid="status"
|
|
61
|
+
type="warning"
|
|
62
|
+
>
|
|
63
|
+
Limited
|
|
64
|
+
</components.StatusIndicator>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(getByTestId("status")).toHaveClass("custom-class");
|
|
68
|
+
expect(getByTestId("status")).toHaveTextContent("Limited");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import toggle from "../../scripts/modules/toggleUtil";
|
|
2
2
|
|
|
3
|
+
const SCROLLABLE_OVERFLOW_CLASS = "table-scrollable--overflowing";
|
|
4
|
+
|
|
3
5
|
interface TableConfig {
|
|
4
6
|
expandButtonSelector: string;
|
|
5
7
|
scrollableContainerSelector: string;
|
|
@@ -32,18 +34,26 @@ export default class Table {
|
|
|
32
34
|
|
|
33
35
|
(this.element as any).ODS_Table = this;
|
|
34
36
|
|
|
35
|
-
this.resizeObserver = new ResizeObserver((
|
|
37
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
36
38
|
if (!this.scrollableContainer) return;
|
|
37
39
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const overflowX = getComputedStyle(this.scrollableContainer).overflowX;
|
|
41
|
+
const canScroll = overflowX === "auto" || overflowX === "scroll";
|
|
42
|
+
const hasOverflow =
|
|
43
|
+
this.scrollableContainer.scrollWidth >
|
|
44
|
+
this.scrollableContainer.clientWidth;
|
|
45
|
+
const isScrollable = canScroll && hasOverflow;
|
|
46
|
+
|
|
47
|
+
this.scrollableContainer.classList.toggle(
|
|
48
|
+
SCROLLABLE_OVERFLOW_CLASS,
|
|
49
|
+
isScrollable,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (isScrollable) {
|
|
53
|
+
this.scrollableContainer.setAttribute("tabindex", "0");
|
|
54
|
+
} else {
|
|
55
|
+
this.scrollableContainer.removeAttribute("tabindex");
|
|
56
|
+
}
|
|
47
57
|
});
|
|
48
58
|
|
|
49
59
|
this.init();
|
|
@@ -120,6 +130,7 @@ export default class Table {
|
|
|
120
130
|
);
|
|
121
131
|
if (this.scrollableContainer) {
|
|
122
132
|
this.resizeObserver.observe(this.element);
|
|
133
|
+
this.resizeObserver.observe(this.scrollableContainer);
|
|
123
134
|
return true;
|
|
124
135
|
}
|
|
125
136
|
return undefined;
|
|
@@ -131,6 +142,8 @@ export default class Table {
|
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
this.resizeObserver.disconnect();
|
|
145
|
+
this.scrollableContainer?.classList.remove(SCROLLABLE_OVERFLOW_CLASS);
|
|
146
|
+
this.scrollableContainer?.removeAttribute("tabindex");
|
|
134
147
|
|
|
135
148
|
this.expandButtons = [];
|
|
136
149
|
}
|
|
@@ -26,6 +26,8 @@ interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
|
|
|
26
26
|
isResponsive?: boolean;
|
|
27
27
|
/** If table has horizontal scrollbar */
|
|
28
28
|
isScrollable?: boolean;
|
|
29
|
+
/** Keep the first column visible while the table scrolls horizontally */
|
|
30
|
+
isFirstColumnPinned?: boolean;
|
|
29
31
|
/** Different colors for even and odd rows. */
|
|
30
32
|
isStriped?: boolean;
|
|
31
33
|
/** rows to print out in the table. */
|
|
@@ -45,6 +47,7 @@ const Table: React.FC<TableProps> = ({
|
|
|
45
47
|
isResponsive,
|
|
46
48
|
isStriped,
|
|
47
49
|
isScrollable,
|
|
50
|
+
isFirstColumnPinned,
|
|
48
51
|
expandableRowsCaptions = {
|
|
49
52
|
header: "Rozšíriteľný",
|
|
50
53
|
emptyRow: "Nerozšíriteľný riadok",
|
|
@@ -60,11 +63,13 @@ const Table: React.FC<TableProps> = ({
|
|
|
60
63
|
[`${CLASS_ROOT}--striped`]: isStriped,
|
|
61
64
|
[`${CLASS_ROOT}--compact`]: isCompact,
|
|
62
65
|
[`${CLASS_ROOT}--responsive`]: isResponsive,
|
|
66
|
+
[`${CLASS_ROOT}--pinned-first-column`]: isFirstColumnPinned,
|
|
63
67
|
},
|
|
64
68
|
className,
|
|
65
69
|
);
|
|
66
70
|
|
|
67
71
|
const captionId = `${idPrefix}-caption`;
|
|
72
|
+
const hasScrollableRegion = isScrollable || isFirstColumnPinned;
|
|
68
73
|
|
|
69
74
|
const expandableRows = {
|
|
70
75
|
hasExpandableRows: rows.some(
|
|
@@ -98,7 +103,7 @@ const Table: React.FC<TableProps> = ({
|
|
|
98
103
|
const table = (
|
|
99
104
|
<TableContext.Provider value={context}>
|
|
100
105
|
<ConditionalWrapper
|
|
101
|
-
condition={
|
|
106
|
+
condition={hasScrollableRegion || false}
|
|
102
107
|
wrapper={
|
|
103
108
|
<div
|
|
104
109
|
className={`${CLASS_ROOT}-scrollable`}
|
|
@@ -114,7 +119,7 @@ const Table: React.FC<TableProps> = ({
|
|
|
114
119
|
{...other}
|
|
115
120
|
>
|
|
116
121
|
{caption && (
|
|
117
|
-
<caption {...(
|
|
122
|
+
<caption {...(hasScrollableRegion && { id: captionId })}>
|
|
118
123
|
{caption}
|
|
119
124
|
</caption>
|
|
120
125
|
)}
|
|
@@ -57,17 +57,33 @@
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
@mixin scrollable-wrapper {
|
|
60
|
+
container-type: inline-size;
|
|
60
61
|
display: block;
|
|
61
62
|
width: 100%;
|
|
62
63
|
overflow-x: auto;
|
|
63
64
|
-webkit-overflow-scrolling: touch;
|
|
64
65
|
|
|
66
|
+
caption {
|
|
67
|
+
position: sticky;
|
|
68
|
+
left: 0;
|
|
69
|
+
width: 100cqw;
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
td,
|
|
66
73
|
th {
|
|
67
74
|
white-space: nowrap;
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
@mixin responsive-scrollable-wrapper {
|
|
79
|
+
overflow-x: visible;
|
|
80
|
+
|
|
81
|
+
td,
|
|
82
|
+
th {
|
|
83
|
+
white-space: normal;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
@mixin sort-button {
|
|
72
88
|
position: relative;
|
|
73
89
|
display: inline-block;
|
|
@@ -117,6 +133,49 @@
|
|
|
117
133
|
width: auto;
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
@mixin pinned-first-column {
|
|
137
|
+
tr > :first-child {
|
|
138
|
+
position: sticky;
|
|
139
|
+
left: 0;
|
|
140
|
+
z-index: 1;
|
|
141
|
+
background: var(
|
|
142
|
+
--table-pinned-column-background,
|
|
143
|
+
var(--color-background-primary)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
&::after {
|
|
147
|
+
content: "";
|
|
148
|
+
display: none;
|
|
149
|
+
position: absolute;
|
|
150
|
+
top: 0;
|
|
151
|
+
right: -12px;
|
|
152
|
+
bottom: 0;
|
|
153
|
+
width: 12px;
|
|
154
|
+
background: linear-gradient(
|
|
155
|
+
to right,
|
|
156
|
+
color-mix(in srgb, var(--color-text-default) 10%, transparent),
|
|
157
|
+
transparent
|
|
158
|
+
);
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
thead > tr > :first-child {
|
|
164
|
+
z-index: 2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@mixin unpin-first-column {
|
|
169
|
+
tr > :first-child {
|
|
170
|
+
position: static;
|
|
171
|
+
background: transparent;
|
|
172
|
+
|
|
173
|
+
&::after {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
120
179
|
@mixin responsive-table {
|
|
121
180
|
thead {
|
|
122
181
|
display: none;
|