@sproutsocial/seeds-react-tabs 1.4.13 → 1.4.15
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/dist/esm/index.js +168 -49
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +167 -48
- package/dist/index.js.map +1 -1
- package/dist/tabs.css +122 -0
- package/package.json +16 -5
- package/.eslintignore +0 -6
- package/.eslintrc.js +0 -4
- package/.turbo/turbo-build.log +0 -21
- package/CHANGELOG.md +0 -383
- package/jest.config.js +0 -9
- package/src/Tabs.stories.tsx +0 -258
- package/src/Tabs.tsx +0 -284
- package/src/TabsTypes.ts +0 -48
- package/src/__tests__/Tabs.typetest.tsx +0 -20
- package/src/__tests__/tabs.test.tsx +0 -462
- package/src/index.ts +0 -5
- package/src/styled.d.ts +0 -7
- package/src/styles.ts +0 -119
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -12
package/src/Tabs.tsx
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import Container, { TabButton } from "./styles";
|
|
3
|
-
import type {
|
|
4
|
-
TypeTabButtonsProps,
|
|
5
|
-
TypeTabsProps,
|
|
6
|
-
TypeTabPanelProps,
|
|
7
|
-
} from "./TabsTypes";
|
|
8
|
-
|
|
9
|
-
interface TypeTabButtonRef {
|
|
10
|
-
current: null | React.ElementRef<"button">;
|
|
11
|
-
}
|
|
12
|
-
interface TypeTabButtonContext {
|
|
13
|
-
onActivate: (arg0: string) => void;
|
|
14
|
-
onTabMount: (id: string, ref: TypeTabButtonRef) => void;
|
|
15
|
-
onTabUpdate: (previousId: string, nextId: string) => void;
|
|
16
|
-
onTabUnmount: (id: string) => void;
|
|
17
|
-
selectedId: string;
|
|
18
|
-
fullWidth?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const TabButtonContext = React.createContext<Partial<TypeTabButtonContext>>({});
|
|
22
|
-
|
|
23
|
-
class TabItemButton extends React.Component<TypeTabButtonsProps> {
|
|
24
|
-
static override contextType = TabButtonContext;
|
|
25
|
-
// @ts-notes - using the legacy syntax here because `declare` does not play well with babel
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
27
|
-
//@ts-ignore
|
|
28
|
-
context!: React.ContextType<typeof TabButtonContext>;
|
|
29
|
-
|
|
30
|
-
buttonRef: TypeTabButtonRef = React.createRef<React.ElementRef<"button">>();
|
|
31
|
-
|
|
32
|
-
override componentDidMount() {
|
|
33
|
-
this.context.onTabMount?.(this.props.id, this.buttonRef);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
override componentDidUpdate(prevProps: TypeTabButtonsProps) {
|
|
37
|
-
if (prevProps.id !== this.props.id) {
|
|
38
|
-
this.context.onTabUpdate?.(prevProps.id, this.props.id);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
override componentWillUnmount() {
|
|
43
|
-
this.context.onTabUnmount?.(this.props.id);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
override render() {
|
|
47
|
-
const { children, id, ...rest } = this.props;
|
|
48
|
-
const { selectedId, onActivate, fullWidth } = this.context;
|
|
49
|
-
const isSelected = selectedId === id;
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<TabButton
|
|
53
|
-
key={id}
|
|
54
|
-
innerRef={this.buttonRef}
|
|
55
|
-
id={id}
|
|
56
|
-
onClick={() => onActivate?.(id)}
|
|
57
|
-
isSelected={isSelected}
|
|
58
|
-
tabIndex={isSelected ? 0 : -1}
|
|
59
|
-
fullWidth={fullWidth}
|
|
60
|
-
data-qa-tab-button={id}
|
|
61
|
-
data-qa-tab-button-state={isSelected}
|
|
62
|
-
aria-selected={isSelected}
|
|
63
|
-
role="tab"
|
|
64
|
-
aria-controls={`${id}-panel`}
|
|
65
|
-
{...rest}
|
|
66
|
-
>
|
|
67
|
-
{children}
|
|
68
|
-
</TabButton>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const TabPanel: React.FC<TypeTabPanelProps> = ({ children, id, ...rest }) => {
|
|
74
|
-
const panelId = `${id}-panel`;
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div id={panelId} role="tabpanel" aria-labelledby={id} {...rest}>
|
|
78
|
-
{children}
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Render a group of buttons in a tab-heading style
|
|
85
|
-
*/
|
|
86
|
-
class Tabs extends React.Component<TypeTabsProps> {
|
|
87
|
-
static Button = TabItemButton;
|
|
88
|
-
static Panel = TabPanel;
|
|
89
|
-
buttonRefs: Record<string, TypeTabButtonRef | null | undefined> = {};
|
|
90
|
-
tabList: string[] = [];
|
|
91
|
-
|
|
92
|
-
containerRef = React.createRef<HTMLDivElement>();
|
|
93
|
-
startSentinelRef = React.createRef<HTMLDivElement>();
|
|
94
|
-
endSentinelRef = React.createRef<HTMLDivElement>();
|
|
95
|
-
startObserver: IntersectionObserver | null = null;
|
|
96
|
-
endObserver: IntersectionObserver | null = null;
|
|
97
|
-
|
|
98
|
-
override componentDidMount() {
|
|
99
|
-
const container = this.containerRef.current;
|
|
100
|
-
if (!container) return;
|
|
101
|
-
container.addEventListener("wheel", this.handleWheel, { passive: false });
|
|
102
|
-
|
|
103
|
-
const startSentinel = this.startSentinelRef.current;
|
|
104
|
-
const endSentinel = this.endSentinelRef.current;
|
|
105
|
-
if (!startSentinel || !endSentinel) return;
|
|
106
|
-
if (typeof IntersectionObserver === "undefined") return;
|
|
107
|
-
this.startObserver = new IntersectionObserver(
|
|
108
|
-
([entry]) => {
|
|
109
|
-
container.toggleAttribute("data-fade-left", !entry?.isIntersecting);
|
|
110
|
-
},
|
|
111
|
-
{ root: container, threshold: 1.0, rootMargin: "0px 0px 0px 1px" }
|
|
112
|
-
);
|
|
113
|
-
this.endObserver = new IntersectionObserver(
|
|
114
|
-
([entry]) => {
|
|
115
|
-
container.toggleAttribute("data-fade-right", !entry?.isIntersecting);
|
|
116
|
-
},
|
|
117
|
-
{ root: container, threshold: 1.0, rootMargin: "0px 1px 0px 0px" }
|
|
118
|
-
);
|
|
119
|
-
this.startObserver.observe(startSentinel);
|
|
120
|
-
this.endObserver.observe(endSentinel);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
override componentWillUnmount() {
|
|
124
|
-
this.containerRef.current?.removeEventListener("wheel", this.handleWheel);
|
|
125
|
-
this.startObserver?.disconnect();
|
|
126
|
-
this.endObserver?.disconnect();
|
|
127
|
-
this.startObserver = null;
|
|
128
|
-
this.endObserver = null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
handleWheel = (e: WheelEvent) => {
|
|
132
|
-
const container = this.containerRef.current;
|
|
133
|
-
if (!container) return;
|
|
134
|
-
// Only redirect pure-vertical wheel input. Trackpad horizontal swipes
|
|
135
|
-
// (deltaX !== 0) already scroll natively — don't fight them.
|
|
136
|
-
if (e.deltaY === 0 || e.deltaX !== 0) return;
|
|
137
|
-
// No horizontal overflow means there's nothing to scroll.
|
|
138
|
-
if (container.scrollWidth <= container.clientWidth) return;
|
|
139
|
-
// Release at ends so the page can scroll past the tab bar instead of
|
|
140
|
-
// trapping the user. WCAG 2.1.1 / 2.1.2 spirit.
|
|
141
|
-
const atStart = container.scrollLeft <= 0;
|
|
142
|
-
const atEnd =
|
|
143
|
-
container.scrollLeft + container.clientWidth >= container.scrollWidth;
|
|
144
|
-
if ((atEnd && e.deltaY > 0) || (atStart && e.deltaY < 0)) return;
|
|
145
|
-
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
// Normalize deltaY across deltaMode units (Firefox can emit LINE/PAGE).
|
|
148
|
-
const lineHeight = 16;
|
|
149
|
-
const pixelDelta =
|
|
150
|
-
e.deltaMode === 1
|
|
151
|
-
? e.deltaY * lineHeight
|
|
152
|
-
: e.deltaMode === 2
|
|
153
|
-
? e.deltaY * container.clientHeight
|
|
154
|
-
: e.deltaY;
|
|
155
|
-
container.scrollBy({ left: pixelDelta, behavior: "instant" });
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
getSelectedId = () => this.props.selectedId;
|
|
159
|
-
onActivate = (id: string) => {
|
|
160
|
-
this.props.onSelect(id);
|
|
161
|
-
this.scrollTabIntoView(id);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
scrollTabIntoView = (id: string) => {
|
|
165
|
-
const button = this.buttonRefs[id]?.current;
|
|
166
|
-
const container = this.containerRef.current;
|
|
167
|
-
if (!button || !container) return;
|
|
168
|
-
const buttonRect = button.getBoundingClientRect();
|
|
169
|
-
const containerRect = container.getBoundingClientRect();
|
|
170
|
-
const fadePadding = 48;
|
|
171
|
-
const isFullyVisible =
|
|
172
|
-
buttonRect.left >= containerRect.left + fadePadding &&
|
|
173
|
-
buttonRect.right <= containerRect.right - fadePadding;
|
|
174
|
-
if (isFullyVisible) return;
|
|
175
|
-
button.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
176
|
-
};
|
|
177
|
-
onTabMount = (id: string, ref: TypeTabButtonRef) => {
|
|
178
|
-
if (!this.tabList.includes(id)) {
|
|
179
|
-
this.tabList.push(id);
|
|
180
|
-
this.buttonRefs[id] = ref;
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
onTabUpdate = (previousId: string, newId: string) => {
|
|
185
|
-
if (this.tabList.includes(previousId)) {
|
|
186
|
-
this.tabList = this.tabList.map((id) => (id === previousId ? newId : id));
|
|
187
|
-
this.buttonRefs[newId] = this.buttonRefs[previousId];
|
|
188
|
-
this.buttonRefs[previousId] = undefined;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
onTabUnmount = (id: string) => {
|
|
193
|
-
if (this.tabList.includes(id)) {
|
|
194
|
-
this.tabList = this.tabList.filter((currentId) => currentId !== id);
|
|
195
|
-
this.buttonRefs[id] = undefined;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
selectNextTab = (id: string) => {
|
|
200
|
-
this.props.onSelect(id);
|
|
201
|
-
this.buttonRefs[id]?.current?.focus?.({ preventScroll: true });
|
|
202
|
-
this.scrollTabIntoView(id);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
keyHandler = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
206
|
-
switch (e.keyCode) {
|
|
207
|
-
// left arrow
|
|
208
|
-
case 37:
|
|
209
|
-
this.tabList.forEach((id, index) => {
|
|
210
|
-
e.preventDefault();
|
|
211
|
-
if (id === this.getSelectedId()) {
|
|
212
|
-
const count = this.tabList.length;
|
|
213
|
-
const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;
|
|
214
|
-
const nextId = this.tabList[nextIndex];
|
|
215
|
-
this.selectNextTab(nextId ? nextId : "");
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
break;
|
|
219
|
-
|
|
220
|
-
// right arrow
|
|
221
|
-
case 39:
|
|
222
|
-
this.tabList.forEach((id, index) => {
|
|
223
|
-
e.preventDefault();
|
|
224
|
-
if (id === this.getSelectedId()) {
|
|
225
|
-
const count = this.tabList.length;
|
|
226
|
-
const nextIndex = index + 1 < count ? index + 1 : 0;
|
|
227
|
-
const nextId = this.tabList[nextIndex];
|
|
228
|
-
this.selectNextTab(nextId ? nextId : "");
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
break;
|
|
232
|
-
|
|
233
|
-
default:
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
override render() {
|
|
239
|
-
const {
|
|
240
|
-
children,
|
|
241
|
-
qa,
|
|
242
|
-
onSelect,
|
|
243
|
-
"aria-label": ariaLabel,
|
|
244
|
-
...rest
|
|
245
|
-
} = this.props;
|
|
246
|
-
return (
|
|
247
|
-
<Container
|
|
248
|
-
ref={this.containerRef}
|
|
249
|
-
data-qa-tabs=""
|
|
250
|
-
onKeyDown={this.keyHandler}
|
|
251
|
-
onSelect={onSelect}
|
|
252
|
-
role="tablist"
|
|
253
|
-
aria-label={ariaLabel}
|
|
254
|
-
{...qa}
|
|
255
|
-
{...rest}
|
|
256
|
-
>
|
|
257
|
-
<div
|
|
258
|
-
ref={this.startSentinelRef}
|
|
259
|
-
aria-hidden="true"
|
|
260
|
-
style={{ flexShrink: 0, width: 1 }}
|
|
261
|
-
/>
|
|
262
|
-
<TabButtonContext.Provider
|
|
263
|
-
value={{
|
|
264
|
-
selectedId: this.getSelectedId(),
|
|
265
|
-
fullWidth: this.props.fullWidth,
|
|
266
|
-
onActivate: this.onActivate,
|
|
267
|
-
onTabMount: this.onTabMount,
|
|
268
|
-
onTabUpdate: this.onTabUpdate,
|
|
269
|
-
onTabUnmount: this.onTabUnmount,
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
{children}
|
|
273
|
-
</TabButtonContext.Provider>
|
|
274
|
-
<div
|
|
275
|
-
ref={this.endSentinelRef}
|
|
276
|
-
aria-hidden="true"
|
|
277
|
-
style={{ flexShrink: 0, width: 1 }}
|
|
278
|
-
/>
|
|
279
|
-
</Container>
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export default Tabs;
|
package/src/TabsTypes.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import type {
|
|
3
|
-
TypeStyledComponentsCommonProps,
|
|
4
|
-
TypeSystemCommonProps,
|
|
5
|
-
} from "@sproutsocial/seeds-react-system-props";
|
|
6
|
-
import type { TypeButtonProps } from "@sproutsocial/seeds-react-button";
|
|
7
|
-
|
|
8
|
-
export interface TypeTabButtonsProps extends TypeButtonProps {
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
|
|
11
|
-
/** Should be unique among sibling elements */
|
|
12
|
-
id: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface TypeTabPanelProps
|
|
16
|
-
extends React.ComponentPropsWithoutRef<"div"> {
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
|
|
19
|
-
/** Should match the corresponding tab button id */
|
|
20
|
-
id: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface TypeTabsProps
|
|
24
|
-
extends TypeStyledComponentsCommonProps,
|
|
25
|
-
TypeSystemCommonProps,
|
|
26
|
-
Omit<
|
|
27
|
-
React.ComponentPropsWithoutRef<"ul">,
|
|
28
|
-
keyof TypeSystemCommonProps | "onSelect"
|
|
29
|
-
> {
|
|
30
|
-
children: React.ReactNode;
|
|
31
|
-
onSelect: (arg0: string) => void;
|
|
32
|
-
|
|
33
|
-
/** Whether or not the tabs should stretch to fill the width of their container */
|
|
34
|
-
fullWidth?: boolean;
|
|
35
|
-
qa?: object;
|
|
36
|
-
|
|
37
|
-
/** ID of the selected tab */
|
|
38
|
-
selectedId: string;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Accessible label for the tab list. Forwarded to the `role="tablist"`
|
|
42
|
-
* element. Recommended by the WAI-ARIA Tabs pattern so screen readers
|
|
43
|
-
* can announce what the tabs represent (e.g. "Report sections").
|
|
44
|
-
* Use `aria-labelledby` instead via `{...rest}` if you have a visible
|
|
45
|
-
* heading to reference.
|
|
46
|
-
*/
|
|
47
|
-
"aria-label"?: string;
|
|
48
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import Tabs from "../";
|
|
3
|
-
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
-
function TabsTypes() {
|
|
6
|
-
const tabOne = "Tab one";
|
|
7
|
-
const mockHandler = () => {};
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<>
|
|
11
|
-
<Tabs selectedId={tabOne} onSelect={mockHandler}>
|
|
12
|
-
<Tabs.Button id={tabOne}>{tabOne}</Tabs.Button>
|
|
13
|
-
</Tabs>
|
|
14
|
-
{/* @ts-expect-error - test that invalid onSelect is rejected */}
|
|
15
|
-
<Tabs selectedId={3} onSelect="invalid">
|
|
16
|
-
<Tabs.Button id={tabOne}>{tabOne}</Tabs.Button>
|
|
17
|
-
</Tabs>
|
|
18
|
-
</>
|
|
19
|
-
);
|
|
20
|
-
}
|