@purpurds/tabs 5.18.3 → 5.19.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purpurds/tabs",
3
- "version": "5.18.3",
3
+ "version": "5.19.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "main": "./dist/tabs.cjs.js",
6
6
  "types": "./dist/tabs.d.ts",
@@ -16,9 +16,9 @@
16
16
  "dependencies": {
17
17
  "classnames": "~2.5.0",
18
18
  "@radix-ui/react-tabs": "~1.0.4",
19
- "@purpurds/tokens": "5.18.3",
20
- "@purpurds/icon": "5.18.3",
21
- "@purpurds/paragraph": "5.18.3"
19
+ "@purpurds/icon": "5.19.1",
20
+ "@purpurds/tokens": "5.19.1",
21
+ "@purpurds/paragraph": "5.19.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@rushstack/eslint-patch": "~1.10.0",
@@ -42,7 +42,8 @@
42
42
  "typescript": "^5.5.4",
43
43
  "vite": "5.3.4",
44
44
  "vitest": "~1.5.0",
45
- "@purpurds/component-rig": "1.0.0"
45
+ "@purpurds/component-rig": "1.0.0",
46
+ "@purpurds/button": "5.19.1"
46
47
  },
47
48
  "scripts": {
48
49
  "build:dev": "vite",
@@ -1,9 +1,13 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
+ import { useArgs } from "@storybook/preview-api";
3
4
 
4
5
  import { Tabs } from "./tabs";
5
6
  import { tabsVariants } from "./tabs.utils";
6
7
 
8
+ import { Button } from "@purpurds/button";
9
+ import "@purpurds/button/styles";
10
+
7
11
  const meta = {
8
12
  title: "Components/Tabs",
9
13
  component: Tabs,
@@ -32,6 +36,26 @@ const meta = {
32
36
  },
33
37
  },
34
38
  },
39
+ value: {
40
+ control: { type: "text" },
41
+ table: {
42
+ type: {
43
+ summary: "The controlled value of the tabs. Must be used in conjunction with onChange.",
44
+ },
45
+ },
46
+ },
47
+ defaultValue: {
48
+ control: { type: "text" },
49
+ table: {
50
+ type: { summary: "The value of the tab to select by default, if uncontrolled" },
51
+ },
52
+ },
53
+ onChange: {
54
+ table: {
55
+ type: { summary: "Event handler called when the value changes." },
56
+ disabled: true,
57
+ },
58
+ },
35
59
  fullWidth: {
36
60
  table: { type: { summary: "boolean" } },
37
61
  },
@@ -103,3 +127,70 @@ export const Showcase: Story = {
103
127
  </div>
104
128
  ),
105
129
  };
130
+
131
+ export const Controlled: Story = {
132
+ args: {
133
+ variant: tabsVariants[0],
134
+ fullWidth: false,
135
+ children: [
136
+ <Tabs.Content
137
+ key="1"
138
+ tabId={`${tabId}-1`}
139
+ name={`${name}-1`}
140
+ style={{ padding: "var(--purpur-spacing-250)" }}
141
+ >
142
+ <div>Content 1</div>
143
+ </Tabs.Content>,
144
+ <Tabs.Content
145
+ key="2"
146
+ tabId={`${tabId}-2`}
147
+ name={`${name}-2`}
148
+ style={{ padding: "var(--purpur-spacing-250)" }}
149
+ >
150
+ <div>Content 2</div>
151
+ </Tabs.Content>,
152
+ <Tabs.Content
153
+ key="3"
154
+ tabId={`${tabId}-3`}
155
+ name={`${name}-3`}
156
+ style={{ padding: "var(--purpur-spacing-250)" }}
157
+ >
158
+ <div>Content 3</div>
159
+ </Tabs.Content>,
160
+ ],
161
+ negative: false,
162
+ value: "tab-2",
163
+ },
164
+ render: ({ children, ...args }) => {
165
+ const [{ localValue = args.value }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
166
+ const setValue = (value: string | undefined) => updateArgs({ value });
167
+ return (
168
+ <>
169
+ <div
170
+ style={{
171
+ padding: "0 var(--purpur-spacing-250)",
172
+ }}
173
+ >
174
+ <Button onClick={() => setValue("tab-1")} variant="text" size="sm">
175
+ Click here for Tab 1
176
+ </Button>
177
+ </div>
178
+ <div
179
+ style={{
180
+ padding: "var(--purpur-spacing-250)",
181
+ background: args.negative
182
+ ? "var(--purpur-color-purple-900)"
183
+ : args.variant === "contained"
184
+ ? "var(--purpur-color-gray-50)"
185
+ : "transparent",
186
+ color: `var(--purpur-color-text-default${args.negative ? "-negative" : ""}`,
187
+ }}
188
+ >
189
+ <Tabs {...args} value={localValue} onChange={(event) => setValue(event.detail.value)}>
190
+ {children}
191
+ </Tabs>
192
+ </div>
193
+ </>
194
+ );
195
+ },
196
+ };
package/src/tabs.test.tsx CHANGED
@@ -148,4 +148,21 @@ describe("Tabs", () => {
148
148
  );
149
149
  }).toThrow("tabId must be unique");
150
150
  });
151
+
152
+ it("should render controlled component", () => {
153
+ const testId = "tab-content";
154
+ render(
155
+ <Tabs value="tab-2">
156
+ <TabContent tabId="tab-1" name="Tab name 1" data-testid={`${testId}-1`}>
157
+ <div>Content</div>
158
+ </TabContent>
159
+ <TabContent tabId="tab-2" name="Tab name 2" data-testid={`${testId}-2`}>
160
+ <div>Content 2</div>
161
+ </TabContent>
162
+ </Tabs>
163
+ );
164
+
165
+ expect(screen.queryByTestId(`${testId}-1`)?.getAttribute("data-state")).toBe("inactive");
166
+ expect(screen.queryByTestId(`${testId}-2`)?.getAttribute("data-state")).toBe("active");
167
+ });
151
168
  });
package/src/tabs.tsx CHANGED
@@ -13,6 +13,10 @@ type TabsProps = {
13
13
  variant?: TabsVariant;
14
14
  negative?: boolean;
15
15
  fullWidth?: boolean;
16
+ /**
17
+ * The controlled value of the tabs. Must be used in conjunction with onChange.
18
+ */
19
+ value?: string;
16
20
  /**
17
21
  * Event handler called when the value changes.
18
22
  * */
@@ -73,6 +77,7 @@ export const Tabs: TabsCmp<TabsProps> = ({
73
77
  className,
74
78
  defaultValue,
75
79
  "data-testid": dataTestId,
80
+ value,
76
81
  ...props
77
82
  }) => {
78
83
  const [scrollClasses, setScrollClasses] = useState<{ [key: string]: boolean }>({});
@@ -98,6 +103,12 @@ export const Tabs: TabsCmp<TabsProps> = ({
98
103
  return defaultIndex >= 0 ? defaultIndex : 0;
99
104
  };
100
105
 
106
+ useEffect(() => {
107
+ if (value) {
108
+ setActiveIndex(tabContentChildren.findIndex((child) => child.props.tabId === value));
109
+ }
110
+ }, [tabContentChildren, value]);
111
+
101
112
  const [activeIndex, setActiveIndex] = useState(getDefaultTabIndex);
102
113
 
103
114
  if (new Set(tabIds).size !== tabIds.length) {
@@ -118,11 +129,11 @@ export const Tabs: TabsCmp<TabsProps> = ({
118
129
  setSelectedTriggerWidth(activeTabElement?.getBoundingClientRect().width || 0);
119
130
  };
120
131
 
121
- const handleTabChange = (value: string) => {
122
- if (isLineVariant) {
123
- setActiveIndex(tabContentChildren.findIndex((child) => child.props.tabId === value));
132
+ const handleTabChange = (_value: string) => {
133
+ if (isLineVariant && !value) {
134
+ setActiveIndex(tabContentChildren.findIndex((child) => child.props.tabId === _value));
124
135
  }
125
- onChange?.(createTabChangeDetailEvent(value));
136
+ onChange?.(createTabChangeDetailEvent(_value));
126
137
  };
127
138
 
128
139
  const handleScrollButtonClick = (side: "left" | "right") => {
@@ -207,6 +218,7 @@ export const Tabs: TabsCmp<TabsProps> = ({
207
218
  onValueChange={handleTabChange}
208
219
  data-testid={dataTestId}
209
220
  className={classNames}
221
+ value={value}
210
222
  {...props}
211
223
  >
212
224
  <div className={cx(`${rootClassName}__container`)}>