@orangesk/orange-design-system 3.7.0 → 3.9.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 (108) hide show
  1. package/README.md +0 -6
  2. package/build/components/Alert/style.css +1 -1
  3. package/build/components/Alert/style.css.map +1 -1
  4. package/build/components/Bar/style.css +1 -1
  5. package/build/components/Bar/style.css.map +1 -1
  6. package/build/components/Drawer/style.css +2 -0
  7. package/build/components/Drawer/style.css.map +1 -0
  8. package/build/components/Footer/style.css +1 -1
  9. package/build/components/Footer/style.css.map +1 -1
  10. package/build/components/Megamenu/style.css +1 -1
  11. package/build/components/Megamenu/style.css.map +1 -1
  12. package/build/components/Modal/style.css +1 -1
  13. package/build/components/Modal/style.css.map +1 -1
  14. package/build/components/StatusIndicator/style.css +2 -0
  15. package/build/components/StatusIndicator/style.css.map +1 -0
  16. package/build/components/Table/style.css +1 -1
  17. package/build/components/Table/style.css.map +1 -1
  18. package/build/components/index.js +1 -1
  19. package/build/components/index.js.map +1 -1
  20. package/build/components/types/index.d.ts +55 -3
  21. package/build/lib/base.css +1 -1
  22. package/build/lib/base.css.map +1 -1
  23. package/build/lib/components.css +1 -1
  24. package/build/lib/components.css.map +1 -1
  25. package/build/lib/footer.css +1 -1
  26. package/build/lib/footer.css.map +1 -1
  27. package/build/lib/footer.js +1 -1
  28. package/build/lib/footer.js.map +1 -1
  29. package/build/lib/megamenu.css +1 -1
  30. package/build/lib/megamenu.css.map +1 -1
  31. package/build/lib/megamenu.js +1 -1
  32. package/build/lib/megamenu.js.map +1 -1
  33. package/build/lib/reset.css +2 -0
  34. package/build/lib/reset.css.map +1 -0
  35. package/build/lib/reset.js +3 -0
  36. package/build/lib/scripts.js +1 -1
  37. package/build/lib/scripts.js.map +1 -1
  38. package/build/lib/style.css +1 -1
  39. package/build/lib/style.css.map +1 -1
  40. package/build/lib/utilities.css +1 -1
  41. package/build/lib/utilities.css.map +1 -1
  42. package/package.json +27 -33
  43. package/src/components/Alert/Alert.tsx +2 -2
  44. package/src/components/Alert/data.ts +8 -5
  45. package/src/components/Alert/styles/config.scss +4 -4
  46. package/src/components/Alert/styles/mixins.scss +8 -1
  47. package/src/components/Alert/tests/Alert.test.jsx +11 -1
  48. package/src/components/Bar/Bar.tsx +1 -1
  49. package/src/components/Bar/styles/config.scss +1 -0
  50. package/src/components/Bar/tests/Bar.unit.test.jsx +1 -1
  51. package/src/components/Carousel/Carousel.static.ts +103 -12
  52. package/src/components/Carousel/tests/Carousel.static.test.jsx +134 -0
  53. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +12 -4
  54. package/src/components/Drawer/Drawer.static.ts +281 -0
  55. package/src/components/Drawer/Drawer.tsx +82 -0
  56. package/src/components/Drawer/DrawerBody.tsx +21 -0
  57. package/src/components/Drawer/DrawerFooter.tsx +21 -0
  58. package/src/components/Drawer/DrawerHeader.tsx +21 -0
  59. package/src/components/Drawer/index.ts +5 -0
  60. package/src/components/Drawer/styles/config.scss +17 -0
  61. package/src/components/Drawer/styles/mixins.scss +119 -0
  62. package/src/components/Drawer/styles/style.scss +71 -0
  63. package/src/components/Drawer/tests/Drawer.unit.test.jsx +149 -0
  64. package/src/components/Footer/Footer.tsx +1 -4
  65. package/src/components/Footer/styles/mixins.scss +3 -1
  66. package/src/components/Footer/styles/style.scss +31 -0
  67. package/src/components/Footer/tests/Footer.styles.unit.test.jsx +62 -0
  68. package/src/components/Forms/SegmentedControl/SegmentedControl.tsx +122 -0
  69. package/src/components/Forms/SegmentedControl/index.ts +5 -0
  70. package/src/components/Forms/SegmentedControl/styles/mixins.scss +52 -0
  71. package/src/components/Forms/SegmentedControl/styles/style.scss +35 -0
  72. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.conformance.test.jsx +36 -0
  73. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.unit.test.jsx +125 -0
  74. package/src/components/Forms/index.ts +1 -0
  75. package/src/components/Icon/index.ts +1 -0
  76. package/src/components/Megamenu/styles/mixins.scss +24 -18
  77. package/src/components/Modal/styles/config.scss +3 -2
  78. package/src/components/StatusIndicator/StatusIndicator.tsx +29 -0
  79. package/src/components/StatusIndicator/index.ts +2 -0
  80. package/src/components/StatusIndicator/styles/config.scss +12 -0
  81. package/src/components/StatusIndicator/styles/mixins.scss +36 -0
  82. package/src/components/StatusIndicator/styles/style.scss +16 -0
  83. package/src/components/StatusIndicator/tests/StatusIndicator.conformance.test.jsx +22 -0
  84. package/src/components/StatusIndicator/tests/StatusIndicator.unit.test.jsx +70 -0
  85. package/src/components/Table/Table.static.ts +23 -10
  86. package/src/components/Table/Table.tsx +7 -2
  87. package/src/components/Table/styles/mixins.scss +59 -0
  88. package/src/components/Table/styles/style.scss +25 -0
  89. package/src/components/Table/tests/Table.static.unit.test.jsx +72 -0
  90. package/src/components/Table/tests/Table.unit.test.jsx +33 -0
  91. package/src/components/index.ts +11 -0
  92. package/src/styles/tokens/overlay.scss +3 -0
  93. package/src/styles/typography/style.scss +0 -13
  94. package/src/styles/utilities/border.scss +4 -0
  95. package/src/styles/utilities/border.styles.unit.test.jsx +9 -0
  96. package/src/styles/utilities/text.scss +12 -0
  97. package/src/components/Alert/tests/Alert.visual.test.jsx +0 -137
  98. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-custom-title-chromium-linux.png +0 -0
  99. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-danger-chromium-linux.png +0 -0
  100. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-full-width-chromium-linux.png +0 -0
  101. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-success-chromium-linux.png +0 -0
  102. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-title-buttons-chromium-linux.png +0 -0
  103. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-title-chromium-linux.png +0 -0
  104. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-title-description-buttons-chromium-linux.png +0 -0
  105. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-title-description-chromium-linux.png +0 -0
  106. package/src/components/Alert/tests/__screenshots__/Alert.visual.test.jsx/alert-preview-warning-chromium-linux.png +0 -0
  107. package/src/components/Carousel/CHANGELOG.md +0 -11
  108. package/src/components/CarouselHero/tests/CarouselHero.visual.test.jsx +0 -90
@@ -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
+ }
@@ -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";
@@ -1,3 +1,4 @@
1
+ export type { IconColor } from "./Icon";
1
2
  export { Icon } from "./Icon";
2
3
  export { Pictogram } from "./Pictogram";
3
4
  export { SpritePathContext, SpritePathProvider } from "./SpritePathContext";
@@ -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
 
@@ -101,11 +102,16 @@
101
102
  }
102
103
 
103
104
  * {
104
- box-sizing: border-box;
105
105
  letter-spacing: convert.to-rem(-0.1px);
106
106
  line-height: convert.to-rem(18px);
107
107
  }
108
108
 
109
+ *,
110
+ *::before,
111
+ *::after {
112
+ box-sizing: border-box;
113
+ }
114
+
109
115
  ::selection {
110
116
  background-color: base.$selection-background;
111
117
  color: base.$text-color;
@@ -262,7 +268,7 @@
262
268
  }
263
269
 
264
270
  @mixin nav-item-lead {
265
- margin-right: convert.to-rem(10px);
271
+ margin-right: 0;
266
272
  }
267
273
 
268
274
  @mixin nav-item-push {
@@ -417,7 +423,7 @@
417
423
  }
418
424
 
419
425
  &.is-active {
420
- color: var(--color-border-accent);
426
+ color: var(--color-border-accent) !important;
421
427
  }
422
428
  }
423
429
 
@@ -636,7 +642,7 @@
636
642
  min-width: 0;
637
643
  }
638
644
 
639
- > #{config.$class-root}__nav:last-child {
645
+ > #{config.$class-root}__nav:last-child:not(:first-child) {
640
646
  flex: 0 0 auto;
641
647
  width: auto;
642
648
  }
@@ -652,10 +658,16 @@
652
658
 
653
659
  @mixin subnav-item {
654
660
  display: flex;
655
- gap: convert.to-rem(10px);
661
+ column-gap: convert.to-rem(10px);
656
662
  margin-bottom: convert.to-rem(10px) !important;
657
663
 
664
+ > #{config.$class-root}__tag {
665
+ align-self: flex-start;
666
+ }
667
+
658
668
  a {
669
+ min-width: 0;
670
+
659
671
  &:hover,
660
672
  &:focus,
661
673
  &:focus-visible,
@@ -715,13 +727,9 @@
715
727
  left: 0;
716
728
  width: 100%;
717
729
  height: 100%;
718
- background-color: color-mix(
719
- in srgb,
720
- var(--color-surface-contrast) 75%,
721
- transparent
722
- );
730
+ background-color: overlay.$background;
723
731
  opacity: 0;
724
- transition: opacity 0.2s ease-out;
732
+ transition: opacity overlay.$transition;
725
733
  }
726
734
 
727
735
  &.is-active {
@@ -1135,11 +1143,7 @@
1135
1143
  left: 0;
1136
1144
  width: 100%;
1137
1145
  height: 0;
1138
- background-color: color-mix(
1139
- in srgb,
1140
- var(--color-surface-contrast) 75%,
1141
- transparent
1142
- );
1146
+ background-color: overlay.$background;
1143
1147
  border: none;
1144
1148
  appearance: none;
1145
1149
  -webkit-appearance: none;
@@ -1148,8 +1152,8 @@
1148
1152
  opacity: 0;
1149
1153
  visibility: hidden;
1150
1154
  transition:
1151
- opacity 0.15s ease-out,
1152
- visibility 0.15s ease-out;
1155
+ opacity overlay.$transition,
1156
+ visibility overlay.$transition;
1153
1157
  outline: none;
1154
1158
 
1155
1159
  &.is-active {
@@ -1327,6 +1331,8 @@
1327
1331
 
1328
1332
  @mixin tag {
1329
1333
  @include tag.base;
1334
+ flex: 0 0 auto;
1335
+ white-space: nowrap;
1330
1336
  text-decoration: none !important;
1331
1337
 
1332
1338
  @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: rgba(0, 0, 0, 0.4) !default;
6
+ $overlay-bg-color: overlay.$background !default;
6
7
  $overlay-z-index: 500 !default;
7
- $overlay-e: 0.4s ease 0s !default;
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,2 @@
1
+ export type { StatusIndicatorProps } from "./StatusIndicator";
2
+ export { StatusIndicator } from "./StatusIndicator";
@@ -0,0 +1,12 @@
1
+ @use "../../../styles/tokens/space";
2
+ @use "../../../styles/tools/convert";
3
+
4
+ $base: (
5
+ column-gap: space.get("xsmall"),
6
+ );
7
+
8
+ $marker: (
9
+ width: convert.to-rem(12px),
10
+ height: convert.to-rem(12px),
11
+ margin-top: convert.to-rem(3px),
12
+ );
@@ -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,16 @@
1
+ @use "./mixins";
2
+
3
+ @layer components {
4
+ .status-indicator {
5
+ @include mixins.base();
6
+
7
+ &__marker {
8
+ @include mixins.marker();
9
+ @include mixins.marker-types();
10
+ }
11
+
12
+ &__content {
13
+ @include mixins.content();
14
+ }
15
+ }
16
+ }
@@ -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
+ });