@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.
Files changed (85) 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/Megamenu/style.css +1 -1
  8. package/build/components/Megamenu/style.css.map +1 -1
  9. package/build/components/Modal/style.css +1 -1
  10. package/build/components/Modal/style.css.map +1 -1
  11. package/build/components/StatusIndicator/style.css +2 -0
  12. package/build/components/StatusIndicator/style.css.map +1 -0
  13. package/build/components/Table/style.css +1 -1
  14. package/build/components/Table/style.css.map +1 -1
  15. package/build/components/index.js +1 -1
  16. package/build/components/index.js.map +1 -1
  17. package/build/components/types/index.d.ts +55 -3
  18. package/build/lib/base.css +1 -1
  19. package/build/lib/base.css.map +1 -1
  20. package/build/lib/components.css +1 -1
  21. package/build/lib/components.css.map +1 -1
  22. package/build/lib/footer.js +1 -1
  23. package/build/lib/footer.js.map +1 -1
  24. package/build/lib/megamenu.css +1 -1
  25. package/build/lib/megamenu.css.map +1 -1
  26. package/build/lib/megamenu.js +1 -1
  27. package/build/lib/megamenu.js.map +1 -1
  28. package/build/lib/scripts.js +1 -1
  29. package/build/lib/scripts.js.map +1 -1
  30. package/build/lib/style.css +1 -1
  31. package/build/lib/style.css.map +1 -1
  32. package/build/lib/utilities.css +1 -1
  33. package/build/lib/utilities.css.map +1 -1
  34. package/package.json +27 -27
  35. package/src/components/Alert/Alert.tsx +2 -2
  36. package/src/components/Alert/data.ts +8 -5
  37. package/src/components/Alert/styles/config.scss +4 -4
  38. package/src/components/Alert/styles/mixins.scss +8 -1
  39. package/src/components/Alert/tests/Alert.test.jsx +11 -1
  40. package/src/components/Bar/Bar.tsx +1 -1
  41. package/src/components/Bar/styles/config.scss +1 -0
  42. package/src/components/Bar/tests/Bar.unit.test.jsx +1 -1
  43. package/src/components/Carousel/Carousel.static.ts +103 -12
  44. package/src/components/Carousel/tests/Carousel.static.test.jsx +134 -0
  45. package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +12 -4
  46. package/src/components/Drawer/Drawer.static.ts +281 -0
  47. package/src/components/Drawer/Drawer.tsx +82 -0
  48. package/src/components/Drawer/DrawerBody.tsx +21 -0
  49. package/src/components/Drawer/DrawerFooter.tsx +21 -0
  50. package/src/components/Drawer/DrawerHeader.tsx +21 -0
  51. package/src/components/Drawer/index.ts +5 -0
  52. package/src/components/Drawer/styles/config.scss +17 -0
  53. package/src/components/Drawer/styles/mixins.scss +119 -0
  54. package/src/components/Drawer/styles/style.scss +71 -0
  55. package/src/components/Drawer/tests/Drawer.unit.test.jsx +149 -0
  56. package/src/components/Forms/SegmentedControl/SegmentedControl.tsx +122 -0
  57. package/src/components/Forms/SegmentedControl/index.ts +5 -0
  58. package/src/components/Forms/SegmentedControl/styles/mixins.scss +52 -0
  59. package/src/components/Forms/SegmentedControl/styles/style.scss +35 -0
  60. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.conformance.test.jsx +36 -0
  61. package/src/components/Forms/SegmentedControl/tests/SegmentedControl.unit.test.jsx +125 -0
  62. package/src/components/Forms/index.ts +1 -0
  63. package/src/components/Icon/index.ts +1 -0
  64. package/src/components/Megamenu/styles/mixins.scss +15 -14
  65. package/src/components/Modal/styles/config.scss +3 -2
  66. package/src/components/StatusIndicator/StatusIndicator.tsx +29 -0
  67. package/src/components/StatusIndicator/index.ts +2 -0
  68. package/src/components/StatusIndicator/styles/config.scss +12 -0
  69. package/src/components/StatusIndicator/styles/mixins.scss +36 -0
  70. package/src/components/StatusIndicator/styles/style.scss +16 -0
  71. package/src/components/StatusIndicator/tests/StatusIndicator.conformance.test.jsx +22 -0
  72. package/src/components/StatusIndicator/tests/StatusIndicator.unit.test.jsx +70 -0
  73. package/src/components/Table/Table.static.ts +23 -10
  74. package/src/components/Table/Table.tsx +7 -2
  75. package/src/components/Table/styles/mixins.scss +59 -0
  76. package/src/components/Table/styles/style.scss +25 -0
  77. package/src/components/Table/tests/Table.static.unit.test.jsx +72 -0
  78. package/src/components/Table/tests/Table.unit.test.jsx +33 -0
  79. package/src/components/index.ts +11 -0
  80. package/src/styles/tokens/overlay.scss +3 -0
  81. package/src/styles/typography/style.scss +0 -13
  82. package/src/styles/utilities/border.scss +4 -0
  83. package/src/styles/utilities/border.styles.unit.test.jsx +9 -0
  84. package/src/styles/utilities/text.scss +12 -0
  85. 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";
@@ -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
 
@@ -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: color-mix(
719
- in srgb,
720
- var(--color-surface-contrast) 75%,
721
- transparent
722
- );
725
+ background-color: overlay.$background;
723
726
  opacity: 0;
724
- transition: opacity 0.2s ease-out;
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: color-mix(
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 0.15s ease-out,
1152
- visibility 0.15s ease-out;
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: 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
+ });
@@ -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((entries) => {
37
+ this.resizeObserver = new ResizeObserver(() => {
36
38
  if (!this.scrollableContainer) return;
37
39
 
38
- const containerRect = this.scrollableContainer.getBoundingClientRect();
39
-
40
- entries.forEach((entry) => {
41
- if (entry.contentRect.width > containerRect.width) {
42
- this.scrollableContainer?.setAttribute("tabindex", "0");
43
- } else {
44
- this.scrollableContainer?.removeAttribute("tabindex");
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={isScrollable || false}
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 {...(isScrollable && { id: captionId })}>
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;