@sproutsocial/seeds-react-tabs 1.3.11 → 1.4.11
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +99 -0
- package/dist/esm/index.js +169 -31
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +168 -30
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Tabs.stories.tsx +56 -0
- package/src/Tabs.tsx +110 -10
- package/src/TabsTypes.ts +9 -0
- package/src/__tests__/tabs.test.tsx +205 -14
- package/src/styles.ts +59 -10
package/dist/index.js
CHANGED
|
@@ -49,17 +49,71 @@ var React = require("react");
|
|
|
49
49
|
// src/styles.ts
|
|
50
50
|
var Container = import_styled_components.default.div`
|
|
51
51
|
display: ${(props) => props.fullWidth ? "flex" : "inline-flex"};
|
|
52
|
-
justify-content: space-between;
|
|
53
52
|
margin: 0;
|
|
54
53
|
padding: 0;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
max-width: 100%;
|
|
56
|
+
min-height: min-content;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
overflow-x: auto;
|
|
59
|
+
overflow-y: hidden;
|
|
60
|
+
scrollbar-width: none;
|
|
61
|
+
scroll-padding-inline: 48px;
|
|
62
|
+
scroll-behavior: smooth;
|
|
55
63
|
border-bottom: ${(props) => props.theme.borders[500]}
|
|
56
64
|
${(props) => props.theme.colors.container.border.base};
|
|
57
65
|
|
|
66
|
+
&::-webkit-scrollbar {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&[data-fade-right] {
|
|
71
|
+
-webkit-mask-image: linear-gradient(
|
|
72
|
+
to right,
|
|
73
|
+
black 0,
|
|
74
|
+
black calc(100% - 48px),
|
|
75
|
+
transparent
|
|
76
|
+
);
|
|
77
|
+
mask-image: linear-gradient(
|
|
78
|
+
to right,
|
|
79
|
+
black 0,
|
|
80
|
+
black calc(100% - 48px),
|
|
81
|
+
transparent
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&[data-fade-left] {
|
|
86
|
+
-webkit-mask-image: linear-gradient(
|
|
87
|
+
to right,
|
|
88
|
+
transparent,
|
|
89
|
+
black 48px,
|
|
90
|
+
black 100%
|
|
91
|
+
);
|
|
92
|
+
mask-image: linear-gradient(to right, transparent, black 48px, black 100%);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&[data-fade-left][data-fade-right] {
|
|
96
|
+
-webkit-mask-image: linear-gradient(
|
|
97
|
+
to right,
|
|
98
|
+
transparent,
|
|
99
|
+
black 48px,
|
|
100
|
+
black calc(100% - 48px),
|
|
101
|
+
transparent
|
|
102
|
+
);
|
|
103
|
+
mask-image: linear-gradient(
|
|
104
|
+
to right,
|
|
105
|
+
transparent,
|
|
106
|
+
black 48px,
|
|
107
|
+
black calc(100% - 48px),
|
|
108
|
+
transparent
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
${import_seeds_react_system_props.COMMON}
|
|
59
113
|
`;
|
|
60
114
|
var TabButton = (0, import_styled_components.default)(import_seeds_react_button.Button)`
|
|
61
115
|
margin-bottom: -1px;
|
|
62
|
-
padding: ${(props) => `${props.theme.space[350]} 0`};
|
|
116
|
+
padding: ${(props) => `${props.theme.space[350]} ${props.fullWidth ? 0 : props.theme.space[350]}`};
|
|
63
117
|
color: ${(props) => props.theme.colors.text.headline};
|
|
64
118
|
border-radius: 0;
|
|
65
119
|
border-width: 0 0 3px;
|
|
@@ -69,12 +123,6 @@ var TabButton = (0, import_styled_components.default)(import_seeds_react_button.
|
|
|
69
123
|
flex-grow: 1;
|
|
70
124
|
`};
|
|
71
125
|
|
|
72
|
-
&:not(:last-child) {
|
|
73
|
-
${(props) => !props.fullWidth && import_styled_components.css`
|
|
74
|
-
margin-right: ${(props2) => props2.theme.space[350]};
|
|
75
|
-
`};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
126
|
${(props) => props.isSelected && import_styled_components.css`
|
|
79
127
|
color: ${(props2) => props2.theme.colors.link.base};
|
|
80
128
|
border-color: transparent transparent
|
|
@@ -153,8 +201,70 @@ var Tabs = class extends React2.Component {
|
|
|
153
201
|
static Panel = TabPanel;
|
|
154
202
|
buttonRefs = {};
|
|
155
203
|
tabList = [];
|
|
204
|
+
containerRef = React2.createRef();
|
|
205
|
+
startSentinelRef = React2.createRef();
|
|
206
|
+
endSentinelRef = React2.createRef();
|
|
207
|
+
startObserver = null;
|
|
208
|
+
endObserver = null;
|
|
209
|
+
componentDidMount() {
|
|
210
|
+
const container = this.containerRef.current;
|
|
211
|
+
if (!container) return;
|
|
212
|
+
container.addEventListener("wheel", this.handleWheel, { passive: false });
|
|
213
|
+
const startSentinel = this.startSentinelRef.current;
|
|
214
|
+
const endSentinel = this.endSentinelRef.current;
|
|
215
|
+
if (!startSentinel || !endSentinel) return;
|
|
216
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
217
|
+
this.startObserver = new IntersectionObserver(
|
|
218
|
+
([entry]) => {
|
|
219
|
+
container.toggleAttribute("data-fade-left", !entry?.isIntersecting);
|
|
220
|
+
},
|
|
221
|
+
{ root: container, threshold: 1, rootMargin: "0px 0px 0px 1px" }
|
|
222
|
+
);
|
|
223
|
+
this.endObserver = new IntersectionObserver(
|
|
224
|
+
([entry]) => {
|
|
225
|
+
container.toggleAttribute("data-fade-right", !entry?.isIntersecting);
|
|
226
|
+
},
|
|
227
|
+
{ root: container, threshold: 1, rootMargin: "0px 1px 0px 0px" }
|
|
228
|
+
);
|
|
229
|
+
this.startObserver.observe(startSentinel);
|
|
230
|
+
this.endObserver.observe(endSentinel);
|
|
231
|
+
}
|
|
232
|
+
componentWillUnmount() {
|
|
233
|
+
this.containerRef.current?.removeEventListener("wheel", this.handleWheel);
|
|
234
|
+
this.startObserver?.disconnect();
|
|
235
|
+
this.endObserver?.disconnect();
|
|
236
|
+
this.startObserver = null;
|
|
237
|
+
this.endObserver = null;
|
|
238
|
+
}
|
|
239
|
+
handleWheel = (e) => {
|
|
240
|
+
const container = this.containerRef.current;
|
|
241
|
+
if (!container) return;
|
|
242
|
+
if (e.deltaY === 0 || e.deltaX !== 0) return;
|
|
243
|
+
if (container.scrollWidth <= container.clientWidth) return;
|
|
244
|
+
const atStart = container.scrollLeft <= 0;
|
|
245
|
+
const atEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth;
|
|
246
|
+
if (atEnd && e.deltaY > 0 || atStart && e.deltaY < 0) return;
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
const lineHeight = 16;
|
|
249
|
+
const pixelDelta = e.deltaMode === 1 ? e.deltaY * lineHeight : e.deltaMode === 2 ? e.deltaY * container.clientHeight : e.deltaY;
|
|
250
|
+
container.scrollBy({ left: pixelDelta, behavior: "instant" });
|
|
251
|
+
};
|
|
156
252
|
getSelectedId = () => this.props.selectedId;
|
|
157
|
-
onActivate = (id) =>
|
|
253
|
+
onActivate = (id) => {
|
|
254
|
+
this.props.onSelect(id);
|
|
255
|
+
this.scrollTabIntoView(id);
|
|
256
|
+
};
|
|
257
|
+
scrollTabIntoView = (id) => {
|
|
258
|
+
const button = this.buttonRefs[id]?.current;
|
|
259
|
+
const container = this.containerRef.current;
|
|
260
|
+
if (!button || !container) return;
|
|
261
|
+
const buttonRect = button.getBoundingClientRect();
|
|
262
|
+
const containerRect = container.getBoundingClientRect();
|
|
263
|
+
const fadePadding = 48;
|
|
264
|
+
const isFullyVisible = buttonRect.left >= containerRect.left + fadePadding && buttonRect.right <= containerRect.right - fadePadding;
|
|
265
|
+
if (isFullyVisible) return;
|
|
266
|
+
button.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
267
|
+
};
|
|
158
268
|
onTabMount = (id, ref) => {
|
|
159
269
|
if (!this.tabList.includes(id)) {
|
|
160
270
|
this.tabList.push(id);
|
|
@@ -175,15 +285,16 @@ var Tabs = class extends React2.Component {
|
|
|
175
285
|
}
|
|
176
286
|
};
|
|
177
287
|
selectNextTab = (id) => {
|
|
178
|
-
const buttonRef = this.buttonRefs[id];
|
|
179
288
|
this.props.onSelect(id);
|
|
180
|
-
|
|
289
|
+
this.buttonRefs[id]?.current?.focus?.({ preventScroll: true });
|
|
290
|
+
this.scrollTabIntoView(id);
|
|
181
291
|
};
|
|
182
|
-
keyHandler = (
|
|
183
|
-
switch (keyCode) {
|
|
292
|
+
keyHandler = (e) => {
|
|
293
|
+
switch (e.keyCode) {
|
|
184
294
|
// left arrow
|
|
185
295
|
case 37:
|
|
186
296
|
this.tabList.forEach((id, index) => {
|
|
297
|
+
e.preventDefault();
|
|
187
298
|
if (id === this.getSelectedId()) {
|
|
188
299
|
const count = this.tabList.length;
|
|
189
300
|
const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;
|
|
@@ -195,6 +306,7 @@ var Tabs = class extends React2.Component {
|
|
|
195
306
|
// right arrow
|
|
196
307
|
case 39:
|
|
197
308
|
this.tabList.forEach((id, index) => {
|
|
309
|
+
e.preventDefault();
|
|
198
310
|
if (id === this.getSelectedId()) {
|
|
199
311
|
const count = this.tabList.length;
|
|
200
312
|
const nextIndex = index + 1 < count ? index + 1 : 0;
|
|
@@ -208,30 +320,56 @@ var Tabs = class extends React2.Component {
|
|
|
208
320
|
}
|
|
209
321
|
};
|
|
210
322
|
render() {
|
|
211
|
-
const {
|
|
212
|
-
|
|
323
|
+
const {
|
|
324
|
+
children,
|
|
325
|
+
qa,
|
|
326
|
+
onSelect,
|
|
327
|
+
"aria-label": ariaLabel,
|
|
328
|
+
...rest
|
|
329
|
+
} = this.props;
|
|
330
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
213
331
|
styles_default,
|
|
214
332
|
{
|
|
333
|
+
ref: this.containerRef,
|
|
215
334
|
"data-qa-tabs": "",
|
|
216
|
-
|
|
335
|
+
onKeyDown: this.keyHandler,
|
|
217
336
|
onSelect,
|
|
218
337
|
role: "tablist",
|
|
338
|
+
"aria-label": ariaLabel,
|
|
219
339
|
...qa,
|
|
220
340
|
...rest,
|
|
221
|
-
children:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
341
|
+
children: [
|
|
342
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
343
|
+
"div",
|
|
344
|
+
{
|
|
345
|
+
ref: this.startSentinelRef,
|
|
346
|
+
"aria-hidden": "true",
|
|
347
|
+
style: { flexShrink: 0, width: 1 }
|
|
348
|
+
}
|
|
349
|
+
),
|
|
350
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
351
|
+
TabButtonContext.Provider,
|
|
352
|
+
{
|
|
353
|
+
value: {
|
|
354
|
+
selectedId: this.getSelectedId(),
|
|
355
|
+
fullWidth: this.props.fullWidth,
|
|
356
|
+
onActivate: this.onActivate,
|
|
357
|
+
onTabMount: this.onTabMount,
|
|
358
|
+
onTabUpdate: this.onTabUpdate,
|
|
359
|
+
onTabUnmount: this.onTabUnmount
|
|
360
|
+
},
|
|
361
|
+
children
|
|
362
|
+
}
|
|
363
|
+
),
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
365
|
+
"div",
|
|
366
|
+
{
|
|
367
|
+
ref: this.endSentinelRef,
|
|
368
|
+
"aria-hidden": "true",
|
|
369
|
+
style: { flexShrink: 0, width: 1 }
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
]
|
|
235
373
|
}
|
|
236
374
|
);
|
|
237
375
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/Tabs.tsx","../src/styles.ts","../src/TabsTypes.ts"],"sourcesContent":["import Tabs from \"./Tabs\";\n\nexport default Tabs;\nexport { Tabs };\nexport * from \"./TabsTypes\";\n","import * as React from \"react\";\nimport Container, { TabButton } from \"./styles\";\nimport type {\n TypeTabButtonsProps,\n TypeTabsProps,\n TypeTabPanelProps,\n} from \"./TabsTypes\";\n\ninterface TypeTabButtonRef {\n current: null | React.ElementRef<\"button\">;\n}\ninterface TypeTabButtonContext {\n onActivate: (arg0: string) => void;\n onTabMount: (id: string, ref: TypeTabButtonRef) => void;\n onTabUpdate: (previousId: string, nextId: string) => void;\n onTabUnmount: (id: string) => void;\n selectedId: string;\n fullWidth?: boolean;\n}\n\nconst TabButtonContext = React.createContext<Partial<TypeTabButtonContext>>({});\n\nclass TabItemButton extends React.Component<TypeTabButtonsProps> {\n static override contextType = TabButtonContext;\n // @ts-notes - using the legacy syntax here because `declare` does not play well with babel\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n //@ts-ignore\n context!: React.ContextType<typeof TabButtonContext>;\n\n buttonRef: TypeTabButtonRef = React.createRef<React.ElementRef<\"button\">>();\n\n override componentDidMount() {\n this.context.onTabMount?.(this.props.id, this.buttonRef);\n }\n\n override componentDidUpdate(prevProps: TypeTabButtonsProps) {\n if (prevProps.id !== this.props.id) {\n this.context.onTabUpdate?.(prevProps.id, this.props.id);\n }\n }\n\n override componentWillUnmount() {\n this.context.onTabUnmount?.(this.props.id);\n }\n\n override render() {\n const { children, id, ...rest } = this.props;\n const { selectedId, onActivate, fullWidth } = this.context;\n const isSelected = selectedId === id;\n\n return (\n <TabButton\n key={id}\n innerRef={this.buttonRef}\n id={id}\n onClick={() => onActivate?.(id)}\n isSelected={isSelected}\n tabIndex={isSelected ? 0 : -1}\n fullWidth={fullWidth}\n data-qa-tab-button={id}\n data-qa-tab-button-state={isSelected}\n aria-selected={isSelected}\n role=\"tab\"\n aria-controls={`${id}-panel`}\n {...rest}\n >\n {children}\n </TabButton>\n );\n }\n}\n\nconst TabPanel: React.FC<TypeTabPanelProps> = ({ children, id, ...rest }) => {\n const panelId = `${id}-panel`;\n\n return (\n <div id={panelId} role=\"tabpanel\" aria-labelledby={id} {...rest}>\n {children}\n </div>\n );\n};\n\n/**\n * Render a group of buttons in a tab-heading style\n */\nclass Tabs extends React.Component<TypeTabsProps> {\n static Button = TabItemButton;\n static Panel = TabPanel;\n buttonRefs: Record<string, TypeTabButtonRef | null | undefined> = {};\n tabList: string[] = [];\n\n getSelectedId = () => this.props.selectedId;\n onActivate = (id: string) => this.props.onSelect(id);\n onTabMount = (id: string, ref: TypeTabButtonRef) => {\n if (!this.tabList.includes(id)) {\n this.tabList.push(id);\n this.buttonRefs[id] = ref;\n }\n };\n\n onTabUpdate = (previousId: string, newId: string) => {\n if (this.tabList.includes(previousId)) {\n this.tabList = this.tabList.map((id) => (id === previousId ? newId : id));\n this.buttonRefs[newId] = this.buttonRefs[previousId];\n this.buttonRefs[previousId] = undefined;\n }\n };\n\n onTabUnmount = (id: string) => {\n if (this.tabList.includes(id)) {\n this.tabList = this.tabList.filter((currentId) => currentId !== id);\n this.buttonRefs[id] = undefined;\n }\n };\n\n selectNextTab = (id: string) => {\n const buttonRef = this.buttonRefs[id];\n this.props.onSelect(id);\n buttonRef &&\n buttonRef.current &&\n buttonRef.current.focus &&\n buttonRef.current.focus();\n };\n\n keyHandler = ({ keyCode }: React.KeyboardEvent<HTMLDivElement>) => {\n switch (keyCode) {\n // left arrow\n case 37:\n this.tabList.forEach((id, index) => {\n if (id === this.getSelectedId()) {\n const count = this.tabList.length;\n const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;\n const nextId = this.tabList[nextIndex];\n this.selectNextTab(nextId ? nextId : \"\");\n }\n });\n break;\n\n // right arrow\n case 39:\n this.tabList.forEach((id, index) => {\n if (id === this.getSelectedId()) {\n const count = this.tabList.length;\n const nextIndex = index + 1 < count ? index + 1 : 0;\n const nextId = this.tabList[nextIndex];\n this.selectNextTab(nextId ? nextId : \"\");\n }\n });\n break;\n\n default:\n break;\n }\n };\n\n override render() {\n const { children, qa, onSelect, ...rest } = this.props;\n return (\n <Container\n data-qa-tabs=\"\"\n onKeyUp={this.keyHandler}\n onSelect={onSelect}\n role=\"tablist\"\n {...qa}\n {...rest}\n >\n <TabButtonContext.Provider\n value={{\n selectedId: this.getSelectedId(),\n fullWidth: this.props.fullWidth,\n onActivate: this.onActivate,\n onTabMount: this.onTabMount,\n onTabUpdate: this.onTabUpdate,\n onTabUnmount: this.onTabUnmount,\n }}\n >\n {children}\n </TabButtonContext.Provider>\n </Container>\n );\n }\n}\n\nexport default Tabs;\n","import styled, { css } from \"styled-components\";\nimport { COMMON } from \"@sproutsocial/seeds-react-system-props\";\nimport { Button } from \"@sproutsocial/seeds-react-button\";\nimport { type TypeTabsProps } from \"./TabsTypes\";\n\ninterface TypeTabState {\n isSelected: boolean;\n}\ntype TypeFullWidth = Pick<TypeTabsProps, \"fullWidth\">;\ninterface TypeStyleProps extends TypeFullWidth, TypeTabState {}\n\nconst Container = styled.div<TypeTabsProps>`\n display: ${(props) => (props.fullWidth ? \"flex\" : \"inline-flex\")};\n justify-content: space-between;\n margin: 0;\n padding: 0;\n border-bottom: ${(props) => props.theme.borders[500]}\n ${(props) => props.theme.colors.container.border.base};\n\n ${COMMON}\n`;\n\nexport const TabButton = styled(Button)<TypeStyleProps>`\n margin-bottom: -1px;\n padding: ${(props) => `${props.theme.space[350]} 0`};\n color: ${(props) => props.theme.colors.text.headline};\n border-radius: 0;\n border-width: 0 0 3px;\n width: 100%;\n\n ${(props) =>\n props.fullWidth &&\n css`\n flex-grow: 1;\n `};\n\n &:not(:last-child) {\n ${(props) =>\n !props.fullWidth &&\n css`\n margin-right: ${(props) => props.theme.space[350]};\n `};\n }\n\n ${(props) =>\n props.isSelected &&\n css`\n color: ${(props) => props.theme.colors.link.base};\n border-color: transparent transparent\n ${(props) => props.theme.colors.button.primary.background.base};\n `};\n\n &:hover {\n ${(props) =>\n props.isSelected &&\n css`\n color: ${(props) => props.theme.colors.link.hover};\n `};\n }\n\n &:focus,\n &:active {\n box-shadow: none;\n }\n\n &:active {\n transform: none;\n }\n`;\nexport default Container;\n","import * as React from \"react\";\nimport type {\n TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeButtonProps } from \"@sproutsocial/seeds-react-button\";\n\nexport interface TypeTabButtonsProps extends TypeButtonProps {\n children: React.ReactNode;\n\n /** Should be unique among sibling elements */\n id: string;\n}\n\nexport interface TypeTabPanelProps\n extends React.ComponentPropsWithoutRef<\"div\"> {\n children: React.ReactNode;\n\n /** Should match the corresponding tab button id */\n id: string;\n}\n\nexport interface TypeTabsProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n Omit<\n React.ComponentPropsWithoutRef<\"ul\">,\n keyof TypeSystemCommonProps | \"onSelect\"\n > {\n children: React.ReactNode;\n onSelect: (arg0: string) => void;\n\n /** Whether or not the tabs should stretch to fill the width of their container */\n fullWidth?: boolean;\n qa?: object;\n\n /** ID of the selected tab */\n selectedId: string;\n}\n"],"mappings":"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","names":["React","styled","props"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Tabs.tsx","../src/styles.ts","../src/TabsTypes.ts"],"sourcesContent":["import Tabs from \"./Tabs\";\n\nexport default Tabs;\nexport { Tabs };\nexport * from \"./TabsTypes\";\n","import * as React from \"react\";\nimport Container, { TabButton } from \"./styles\";\nimport type {\n TypeTabButtonsProps,\n TypeTabsProps,\n TypeTabPanelProps,\n} from \"./TabsTypes\";\n\ninterface TypeTabButtonRef {\n current: null | React.ElementRef<\"button\">;\n}\ninterface TypeTabButtonContext {\n onActivate: (arg0: string) => void;\n onTabMount: (id: string, ref: TypeTabButtonRef) => void;\n onTabUpdate: (previousId: string, nextId: string) => void;\n onTabUnmount: (id: string) => void;\n selectedId: string;\n fullWidth?: boolean;\n}\n\nconst TabButtonContext = React.createContext<Partial<TypeTabButtonContext>>({});\n\nclass TabItemButton extends React.Component<TypeTabButtonsProps> {\n static override contextType = TabButtonContext;\n // @ts-notes - using the legacy syntax here because `declare` does not play well with babel\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n //@ts-ignore\n context!: React.ContextType<typeof TabButtonContext>;\n\n buttonRef: TypeTabButtonRef = React.createRef<React.ElementRef<\"button\">>();\n\n override componentDidMount() {\n this.context.onTabMount?.(this.props.id, this.buttonRef);\n }\n\n override componentDidUpdate(prevProps: TypeTabButtonsProps) {\n if (prevProps.id !== this.props.id) {\n this.context.onTabUpdate?.(prevProps.id, this.props.id);\n }\n }\n\n override componentWillUnmount() {\n this.context.onTabUnmount?.(this.props.id);\n }\n\n override render() {\n const { children, id, ...rest } = this.props;\n const { selectedId, onActivate, fullWidth } = this.context;\n const isSelected = selectedId === id;\n\n return (\n <TabButton\n key={id}\n innerRef={this.buttonRef}\n id={id}\n onClick={() => onActivate?.(id)}\n isSelected={isSelected}\n tabIndex={isSelected ? 0 : -1}\n fullWidth={fullWidth}\n data-qa-tab-button={id}\n data-qa-tab-button-state={isSelected}\n aria-selected={isSelected}\n role=\"tab\"\n aria-controls={`${id}-panel`}\n {...rest}\n >\n {children}\n </TabButton>\n );\n }\n}\n\nconst TabPanel: React.FC<TypeTabPanelProps> = ({ children, id, ...rest }) => {\n const panelId = `${id}-panel`;\n\n return (\n <div id={panelId} role=\"tabpanel\" aria-labelledby={id} {...rest}>\n {children}\n </div>\n );\n};\n\n/**\n * Render a group of buttons in a tab-heading style\n */\nclass Tabs extends React.Component<TypeTabsProps> {\n static Button = TabItemButton;\n static Panel = TabPanel;\n buttonRefs: Record<string, TypeTabButtonRef | null | undefined> = {};\n tabList: string[] = [];\n\n containerRef = React.createRef<HTMLDivElement>();\n startSentinelRef = React.createRef<HTMLDivElement>();\n endSentinelRef = React.createRef<HTMLDivElement>();\n startObserver: IntersectionObserver | null = null;\n endObserver: IntersectionObserver | null = null;\n\n override componentDidMount() {\n const container = this.containerRef.current;\n if (!container) return;\n container.addEventListener(\"wheel\", this.handleWheel, { passive: false });\n\n const startSentinel = this.startSentinelRef.current;\n const endSentinel = this.endSentinelRef.current;\n if (!startSentinel || !endSentinel) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n this.startObserver = new IntersectionObserver(\n ([entry]) => {\n container.toggleAttribute(\"data-fade-left\", !entry?.isIntersecting);\n },\n { root: container, threshold: 1.0, rootMargin: \"0px 0px 0px 1px\" }\n );\n this.endObserver = new IntersectionObserver(\n ([entry]) => {\n container.toggleAttribute(\"data-fade-right\", !entry?.isIntersecting);\n },\n { root: container, threshold: 1.0, rootMargin: \"0px 1px 0px 0px\" }\n );\n this.startObserver.observe(startSentinel);\n this.endObserver.observe(endSentinel);\n }\n\n override componentWillUnmount() {\n this.containerRef.current?.removeEventListener(\"wheel\", this.handleWheel);\n this.startObserver?.disconnect();\n this.endObserver?.disconnect();\n this.startObserver = null;\n this.endObserver = null;\n }\n\n handleWheel = (e: WheelEvent) => {\n const container = this.containerRef.current;\n if (!container) return;\n // Only redirect pure-vertical wheel input. Trackpad horizontal swipes\n // (deltaX !== 0) already scroll natively — don't fight them.\n if (e.deltaY === 0 || e.deltaX !== 0) return;\n // No horizontal overflow means there's nothing to scroll.\n if (container.scrollWidth <= container.clientWidth) return;\n // Release at ends so the page can scroll past the tab bar instead of\n // trapping the user. WCAG 2.1.1 / 2.1.2 spirit.\n const atStart = container.scrollLeft <= 0;\n const atEnd =\n container.scrollLeft + container.clientWidth >= container.scrollWidth;\n if ((atEnd && e.deltaY > 0) || (atStart && e.deltaY < 0)) return;\n\n e.preventDefault();\n // Normalize deltaY across deltaMode units (Firefox can emit LINE/PAGE).\n const lineHeight = 16;\n const pixelDelta =\n e.deltaMode === 1\n ? e.deltaY * lineHeight\n : e.deltaMode === 2\n ? e.deltaY * container.clientHeight\n : e.deltaY;\n container.scrollBy({ left: pixelDelta, behavior: \"instant\" });\n };\n\n getSelectedId = () => this.props.selectedId;\n onActivate = (id: string) => {\n this.props.onSelect(id);\n this.scrollTabIntoView(id);\n };\n\n scrollTabIntoView = (id: string) => {\n const button = this.buttonRefs[id]?.current;\n const container = this.containerRef.current;\n if (!button || !container) return;\n const buttonRect = button.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n const fadePadding = 48;\n const isFullyVisible =\n buttonRect.left >= containerRect.left + fadePadding &&\n buttonRect.right <= containerRect.right - fadePadding;\n if (isFullyVisible) return;\n button.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n };\n onTabMount = (id: string, ref: TypeTabButtonRef) => {\n if (!this.tabList.includes(id)) {\n this.tabList.push(id);\n this.buttonRefs[id] = ref;\n }\n };\n\n onTabUpdate = (previousId: string, newId: string) => {\n if (this.tabList.includes(previousId)) {\n this.tabList = this.tabList.map((id) => (id === previousId ? newId : id));\n this.buttonRefs[newId] = this.buttonRefs[previousId];\n this.buttonRefs[previousId] = undefined;\n }\n };\n\n onTabUnmount = (id: string) => {\n if (this.tabList.includes(id)) {\n this.tabList = this.tabList.filter((currentId) => currentId !== id);\n this.buttonRefs[id] = undefined;\n }\n };\n\n selectNextTab = (id: string) => {\n this.props.onSelect(id);\n this.buttonRefs[id]?.current?.focus?.({ preventScroll: true });\n this.scrollTabIntoView(id);\n };\n\n keyHandler = (e: React.KeyboardEvent<HTMLDivElement>) => {\n switch (e.keyCode) {\n // left arrow\n case 37:\n this.tabList.forEach((id, index) => {\n e.preventDefault();\n if (id === this.getSelectedId()) {\n const count = this.tabList.length;\n const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;\n const nextId = this.tabList[nextIndex];\n this.selectNextTab(nextId ? nextId : \"\");\n }\n });\n break;\n\n // right arrow\n case 39:\n this.tabList.forEach((id, index) => {\n e.preventDefault();\n if (id === this.getSelectedId()) {\n const count = this.tabList.length;\n const nextIndex = index + 1 < count ? index + 1 : 0;\n const nextId = this.tabList[nextIndex];\n this.selectNextTab(nextId ? nextId : \"\");\n }\n });\n break;\n\n default:\n break;\n }\n };\n\n override render() {\n const {\n children,\n qa,\n onSelect,\n \"aria-label\": ariaLabel,\n ...rest\n } = this.props;\n return (\n <Container\n ref={this.containerRef}\n data-qa-tabs=\"\"\n onKeyDown={this.keyHandler}\n onSelect={onSelect}\n role=\"tablist\"\n aria-label={ariaLabel}\n {...qa}\n {...rest}\n >\n <div\n ref={this.startSentinelRef}\n aria-hidden=\"true\"\n style={{ flexShrink: 0, width: 1 }}\n />\n <TabButtonContext.Provider\n value={{\n selectedId: this.getSelectedId(),\n fullWidth: this.props.fullWidth,\n onActivate: this.onActivate,\n onTabMount: this.onTabMount,\n onTabUpdate: this.onTabUpdate,\n onTabUnmount: this.onTabUnmount,\n }}\n >\n {children}\n </TabButtonContext.Provider>\n <div\n ref={this.endSentinelRef}\n aria-hidden=\"true\"\n style={{ flexShrink: 0, width: 1 }}\n />\n </Container>\n );\n }\n}\n\nexport default Tabs;\n","import styled, { css } from \"styled-components\";\nimport { COMMON } from \"@sproutsocial/seeds-react-system-props\";\nimport { Button } from \"@sproutsocial/seeds-react-button\";\nimport { type TypeTabsProps } from \"./TabsTypes\";\n\ninterface TypeTabState {\n isSelected: boolean;\n}\ntype TypeFullWidth = Pick<TypeTabsProps, \"fullWidth\">;\ninterface TypeStyleProps extends TypeFullWidth, TypeTabState {}\n\nconst Container = styled.div<TypeTabsProps>`\n display: ${(props) => (props.fullWidth ? \"flex\" : \"inline-flex\")};\n margin: 0;\n padding: 0;\n min-width: 0;\n max-width: 100%;\n min-height: min-content;\n flex-shrink: 0;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n scroll-padding-inline: 48px;\n scroll-behavior: smooth;\n border-bottom: ${(props) => props.theme.borders[500]}\n ${(props) => props.theme.colors.container.border.base};\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n &[data-fade-right] {\n -webkit-mask-image: linear-gradient(\n to right,\n black 0,\n black calc(100% - 48px),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n black 0,\n black calc(100% - 48px),\n transparent\n );\n }\n\n &[data-fade-left] {\n -webkit-mask-image: linear-gradient(\n to right,\n transparent,\n black 48px,\n black 100%\n );\n mask-image: linear-gradient(to right, transparent, black 48px, black 100%);\n }\n\n &[data-fade-left][data-fade-right] {\n -webkit-mask-image: linear-gradient(\n to right,\n transparent,\n black 48px,\n black calc(100% - 48px),\n transparent\n );\n mask-image: linear-gradient(\n to right,\n transparent,\n black 48px,\n black calc(100% - 48px),\n transparent\n );\n }\n\n ${COMMON}\n`;\n\nexport const TabButton = styled(Button)<TypeStyleProps>`\n margin-bottom: -1px;\n padding: ${(props) =>\n `${props.theme.space[350]} ${\n props.fullWidth ? 0 : props.theme.space[350]\n }`};\n color: ${(props) => props.theme.colors.text.headline};\n border-radius: 0;\n border-width: 0 0 3px;\n width: 100%;\n\n ${(props) =>\n props.fullWidth &&\n css`\n flex-grow: 1;\n `};\n\n ${(props) =>\n props.isSelected &&\n css`\n color: ${(props) => props.theme.colors.link.base};\n border-color: transparent transparent\n ${(props) => props.theme.colors.button.primary.background.base};\n `};\n\n &:hover {\n ${(props) =>\n props.isSelected &&\n css`\n color: ${(props) => props.theme.colors.link.hover};\n `};\n }\n\n &:focus,\n &:active {\n box-shadow: none;\n }\n\n &:active {\n transform: none;\n }\n`;\nexport default Container;\n","import * as React from \"react\";\nimport type {\n TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n} from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeButtonProps } from \"@sproutsocial/seeds-react-button\";\n\nexport interface TypeTabButtonsProps extends TypeButtonProps {\n children: React.ReactNode;\n\n /** Should be unique among sibling elements */\n id: string;\n}\n\nexport interface TypeTabPanelProps\n extends React.ComponentPropsWithoutRef<\"div\"> {\n children: React.ReactNode;\n\n /** Should match the corresponding tab button id */\n id: string;\n}\n\nexport interface TypeTabsProps\n extends TypeStyledComponentsCommonProps,\n TypeSystemCommonProps,\n Omit<\n React.ComponentPropsWithoutRef<\"ul\">,\n keyof TypeSystemCommonProps | \"onSelect\"\n > {\n children: React.ReactNode;\n onSelect: (arg0: string) => void;\n\n /** Whether or not the tabs should stretch to fill the width of their container */\n fullWidth?: boolean;\n qa?: object;\n\n /** ID of the selected tab */\n selectedId: string;\n\n /**\n * Accessible label for the tab list. Forwarded to the `role=\"tablist\"`\n * element. Recommended by the WAI-ARIA Tabs pattern so screen readers\n * can announce what the tabs represent (e.g. \"Report sections\").\n * Use `aria-labelledby` instead via `{...rest}` if you have a visible\n * heading to reference.\n */\n \"aria-label\"?: string;\n}\n"],"mappings":"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","names":["React","styled","props"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-tabs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.11",
|
|
4
4
|
"description": "Seeds React Tabs",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"test:watch": "jest --watch --coverage=false"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@sproutsocial/seeds-react-theme": "^
|
|
22
|
-
"@sproutsocial/seeds-react-system-props": "^3.
|
|
23
|
-
"@sproutsocial/seeds-react-button": "^2.
|
|
21
|
+
"@sproutsocial/seeds-react-theme": "^4.1.0",
|
|
22
|
+
"@sproutsocial/seeds-react-system-props": "^3.1.1",
|
|
23
|
+
"@sproutsocial/seeds-react-button": "^2.2.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/react": "^18.0.0",
|
package/src/Tabs.stories.tsx
CHANGED
|
@@ -127,6 +127,62 @@ export const FullWidth: Story = {
|
|
|
127
127
|
name: "Full width",
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
export const Scrollable: Story = {
|
|
131
|
+
render: () => {
|
|
132
|
+
const tabs = [
|
|
133
|
+
"Overview",
|
|
134
|
+
"Notifications",
|
|
135
|
+
"Issues",
|
|
136
|
+
"Pull Requests",
|
|
137
|
+
"Discussions",
|
|
138
|
+
"Actions",
|
|
139
|
+
"Projects",
|
|
140
|
+
"Wiki",
|
|
141
|
+
"Security",
|
|
142
|
+
"Insights",
|
|
143
|
+
];
|
|
144
|
+
const [selectedId, setSelectedId] = useState(tabs[0]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<Box width="450px" mb={400}>
|
|
149
|
+
<Tabs
|
|
150
|
+
selectedId={selectedId ?? ""}
|
|
151
|
+
onSelect={(selected) => setSelectedId(selected)}
|
|
152
|
+
aria-label="Repository sections"
|
|
153
|
+
>
|
|
154
|
+
{tabs.map((tab) => (
|
|
155
|
+
<Tabs.Button key={tab} id={tab}>
|
|
156
|
+
<Text size="300" fontWeight="600" flexWrap={"nowrap"}>
|
|
157
|
+
{tab}
|
|
158
|
+
</Text>
|
|
159
|
+
</Tabs.Button>
|
|
160
|
+
))}
|
|
161
|
+
</Tabs>
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
{tabs.map((tab) => {
|
|
165
|
+
if (selectedId !== tab) return null;
|
|
166
|
+
return (
|
|
167
|
+
<Tabs.Panel id={tab}>
|
|
168
|
+
<Text color="text.body">{tab} Panel</Text>
|
|
169
|
+
</Tabs.Panel>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
name: "Scrollable",
|
|
176
|
+
parameters: {
|
|
177
|
+
docs: {
|
|
178
|
+
description: {
|
|
179
|
+
story:
|
|
180
|
+
"When the tab list overflows its container, the bar scrolls horizontally and a fade indicator appears at each overflowing edge. Arrow-key and click activation auto-scroll the selected tab fully into view past the fade region. The fade uses CSS `mask-image` so it auto-adapts to any container background — no consumer configuration needed.",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
130
186
|
export const RemoveATab: Story = {
|
|
131
187
|
render: (args) => {
|
|
132
188
|
const [state, setState] = useState<TypeTabsStoryState>({
|
package/src/Tabs.tsx
CHANGED
|
@@ -89,8 +89,91 @@ class Tabs extends React.Component<TypeTabsProps> {
|
|
|
89
89
|
buttonRefs: Record<string, TypeTabButtonRef | null | undefined> = {};
|
|
90
90
|
tabList: string[] = [];
|
|
91
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
|
+
|
|
92
158
|
getSelectedId = () => this.props.selectedId;
|
|
93
|
-
onActivate = (id: string) =>
|
|
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
|
+
};
|
|
94
177
|
onTabMount = (id: string, ref: TypeTabButtonRef) => {
|
|
95
178
|
if (!this.tabList.includes(id)) {
|
|
96
179
|
this.tabList.push(id);
|
|
@@ -114,19 +197,17 @@ class Tabs extends React.Component<TypeTabsProps> {
|
|
|
114
197
|
};
|
|
115
198
|
|
|
116
199
|
selectNextTab = (id: string) => {
|
|
117
|
-
const buttonRef = this.buttonRefs[id];
|
|
118
200
|
this.props.onSelect(id);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
buttonRef.current.focus &&
|
|
122
|
-
buttonRef.current.focus();
|
|
201
|
+
this.buttonRefs[id]?.current?.focus?.({ preventScroll: true });
|
|
202
|
+
this.scrollTabIntoView(id);
|
|
123
203
|
};
|
|
124
204
|
|
|
125
|
-
keyHandler = (
|
|
126
|
-
switch (keyCode) {
|
|
205
|
+
keyHandler = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
206
|
+
switch (e.keyCode) {
|
|
127
207
|
// left arrow
|
|
128
208
|
case 37:
|
|
129
209
|
this.tabList.forEach((id, index) => {
|
|
210
|
+
e.preventDefault();
|
|
130
211
|
if (id === this.getSelectedId()) {
|
|
131
212
|
const count = this.tabList.length;
|
|
132
213
|
const nextIndex = index - 1 >= 0 ? index - 1 : count - 1;
|
|
@@ -139,6 +220,7 @@ class Tabs extends React.Component<TypeTabsProps> {
|
|
|
139
220
|
// right arrow
|
|
140
221
|
case 39:
|
|
141
222
|
this.tabList.forEach((id, index) => {
|
|
223
|
+
e.preventDefault();
|
|
142
224
|
if (id === this.getSelectedId()) {
|
|
143
225
|
const count = this.tabList.length;
|
|
144
226
|
const nextIndex = index + 1 < count ? index + 1 : 0;
|
|
@@ -154,16 +236,29 @@ class Tabs extends React.Component<TypeTabsProps> {
|
|
|
154
236
|
};
|
|
155
237
|
|
|
156
238
|
override render() {
|
|
157
|
-
const {
|
|
239
|
+
const {
|
|
240
|
+
children,
|
|
241
|
+
qa,
|
|
242
|
+
onSelect,
|
|
243
|
+
"aria-label": ariaLabel,
|
|
244
|
+
...rest
|
|
245
|
+
} = this.props;
|
|
158
246
|
return (
|
|
159
247
|
<Container
|
|
248
|
+
ref={this.containerRef}
|
|
160
249
|
data-qa-tabs=""
|
|
161
|
-
|
|
250
|
+
onKeyDown={this.keyHandler}
|
|
162
251
|
onSelect={onSelect}
|
|
163
252
|
role="tablist"
|
|
253
|
+
aria-label={ariaLabel}
|
|
164
254
|
{...qa}
|
|
165
255
|
{...rest}
|
|
166
256
|
>
|
|
257
|
+
<div
|
|
258
|
+
ref={this.startSentinelRef}
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
style={{ flexShrink: 0, width: 1 }}
|
|
261
|
+
/>
|
|
167
262
|
<TabButtonContext.Provider
|
|
168
263
|
value={{
|
|
169
264
|
selectedId: this.getSelectedId(),
|
|
@@ -176,6 +271,11 @@ class Tabs extends React.Component<TypeTabsProps> {
|
|
|
176
271
|
>
|
|
177
272
|
{children}
|
|
178
273
|
</TabButtonContext.Provider>
|
|
274
|
+
<div
|
|
275
|
+
ref={this.endSentinelRef}
|
|
276
|
+
aria-hidden="true"
|
|
277
|
+
style={{ flexShrink: 0, width: 1 }}
|
|
278
|
+
/>
|
|
179
279
|
</Container>
|
|
180
280
|
);
|
|
181
281
|
}
|
package/src/TabsTypes.ts
CHANGED
|
@@ -36,4 +36,13 @@ export interface TypeTabsProps
|
|
|
36
36
|
|
|
37
37
|
/** ID of the selected tab */
|
|
38
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;
|
|
39
48
|
}
|