@sproutsocial/seeds-react-tabs 1.4.14 → 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/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
- }