@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.
@@ -60,6 +60,35 @@ const TestStatefulTabs = ({
60
60
  };
61
61
 
62
62
  describe("Tabs", () => {
63
+ beforeEach(() => {
64
+ // jsdom doesn't implement Element.prototype.scrollIntoView or .scrollBy
65
+ Element.prototype.scrollIntoView = jest.fn();
66
+ Element.prototype.scrollBy = jest.fn();
67
+ });
68
+
69
+ const mockScrollDimensions = (
70
+ el: Element,
71
+ {
72
+ scrollWidth,
73
+ clientWidth,
74
+ scrollLeft,
75
+ }: { scrollWidth: number; clientWidth: number; scrollLeft: number }
76
+ ) => {
77
+ Object.defineProperty(el, "scrollWidth", {
78
+ value: scrollWidth,
79
+ configurable: true,
80
+ });
81
+ Object.defineProperty(el, "clientWidth", {
82
+ value: clientWidth,
83
+ configurable: true,
84
+ });
85
+ Object.defineProperty(el, "scrollLeft", {
86
+ value: scrollLeft,
87
+ configurable: true,
88
+ writable: true,
89
+ });
90
+ };
91
+
63
92
  it("should render selected tab", () => {
64
93
  render(
65
94
  <TestStatefulTabs>
@@ -88,7 +117,7 @@ describe("Tabs", () => {
88
117
  tabs: "",
89
118
  });
90
119
  const secondTab = screen.getByText(/second/i);
91
- fireEvent.keyUp(tabs, rightArrowKey);
120
+ fireEvent.keyDown(tabs, rightArrowKey);
92
121
  expect(secondTab).toHaveFocus();
93
122
  });
94
123
 
@@ -106,11 +135,11 @@ describe("Tabs", () => {
106
135
  });
107
136
  const secondTab = screen.getByText(/second/i);
108
137
  const thirdTab = screen.getByText(/third/i);
109
- fireEvent.keyUp(tabs, leftArrowKey);
138
+ fireEvent.keyDown(tabs, leftArrowKey);
110
139
  expect(onSelect).toHaveBeenCalledWith("3");
111
140
  expect(thirdTab).toHaveFocus();
112
141
  onSelect.mockReset();
113
- fireEvent.keyUp(tabs, leftArrowKey);
142
+ fireEvent.keyDown(tabs, leftArrowKey);
114
143
  expect(onSelect).toHaveBeenCalledWith("2");
115
144
  expect(secondTab).toHaveFocus();
116
145
  });
@@ -130,15 +159,15 @@ describe("Tabs", () => {
130
159
  const firstTab = screen.getByText(/first/i);
131
160
  const secondTab = screen.getByText(/second/i);
132
161
  const thirdTab = screen.getByText(/third/i);
133
- fireEvent.keyUp(tabs, rightArrowKey);
162
+ fireEvent.keyDown(tabs, rightArrowKey);
134
163
  expect(onSelect).toHaveBeenCalledWith("2");
135
164
  expect(secondTab).toHaveFocus();
136
165
  onSelect.mockReset();
137
- fireEvent.keyUp(tabs, rightArrowKey);
166
+ fireEvent.keyDown(tabs, rightArrowKey);
138
167
  expect(onSelect).toHaveBeenCalledWith("3");
139
168
  expect(thirdTab).toHaveFocus();
140
169
  onSelect.mockReset();
141
- fireEvent.keyUp(tabs, rightArrowKey);
170
+ fireEvent.keyDown(tabs, rightArrowKey);
142
171
  expect(onSelect).toHaveBeenCalledWith("1");
143
172
  expect(firstTab).toHaveFocus();
144
173
  });
@@ -168,7 +197,7 @@ describe("Tabs", () => {
168
197
  tabs: "",
169
198
  });
170
199
  const secondTab = screen.getByText(/second/i);
171
- fireEvent.keyUp(tabs, rightArrowKey);
200
+ fireEvent.keyDown(tabs, rightArrowKey);
172
201
  expect(onSelect).toHaveBeenCalledWith("2");
173
202
  expect(secondTab).toHaveFocus();
174
203
  rerender(
@@ -177,7 +206,7 @@ describe("Tabs", () => {
177
206
  </TestStatefulTabs>
178
207
  );
179
208
  onSelect.mockReset();
180
- fireEvent.keyUp(tabs, rightArrowKey);
209
+ fireEvent.keyDown(tabs, rightArrowKey);
181
210
  const thirdTab = screen.getByText(/third/i);
182
211
  expect(onSelect).toHaveBeenCalledWith("3");
183
212
  expect(thirdTab).toHaveFocus();
@@ -210,7 +239,7 @@ describe("Tabs", () => {
210
239
  });
211
240
  const secondTab = screen.getByText(/second/i);
212
241
  const thirdTab = screen.getByText(/third/i);
213
- fireEvent.keyUp(tabs, rightArrowKey);
242
+ fireEvent.keyDown(tabs, rightArrowKey);
214
243
  expect(onSelect).toHaveBeenCalledWith("2");
215
244
  expect(secondTab).toHaveFocus();
216
245
  rerender(
@@ -219,15 +248,69 @@ describe("Tabs", () => {
219
248
  </TestStatefulTabs>
220
249
  );
221
250
  onSelect.mockReset();
222
- fireEvent.keyUp(tabs, rightArrowKey);
251
+ fireEvent.keyDown(tabs, rightArrowKey);
223
252
  expect(onSelect).toHaveBeenCalledWith("3");
224
253
  expect(thirdTab).toHaveFocus();
225
254
  onSelect.mockReset();
226
- fireEvent.keyUp(tabs, rightArrowKey);
255
+ fireEvent.keyDown(tabs, rightArrowKey);
227
256
  expect(onSelect).toHaveBeenCalledWith("2");
228
257
  expect(secondTab).toHaveFocus();
229
258
  });
230
259
 
260
+ it("scrolls the focused tab into view on arrow-key navigation", () => {
261
+ render(
262
+ <TestStatefulTabs>
263
+ <Tabs.Button id="1">First</Tabs.Button>
264
+ <Tabs.Button id="2">Second</Tabs.Button>
265
+ <Tabs.Button id="3">Third</Tabs.Button>
266
+ </TestStatefulTabs>
267
+ );
268
+ const tabs = screen.getByDataQaLabel({ tabs: "" });
269
+ const secondTab = screen.getByText(/second/i);
270
+
271
+ fireEvent.keyDown(tabs, rightArrowKey);
272
+
273
+ expect(secondTab.scrollIntoView).toHaveBeenCalledWith({
274
+ block: "nearest",
275
+ inline: "nearest",
276
+ });
277
+ });
278
+
279
+ it("scrolls the activated tab into view on click", () => {
280
+ render(
281
+ <TestStatefulTabs>
282
+ <Tabs.Button id="1">First</Tabs.Button>
283
+ <Tabs.Button id="2">Second</Tabs.Button>
284
+ </TestStatefulTabs>
285
+ );
286
+ const secondTab = screen.getByText(/second/i);
287
+
288
+ fireEvent.click(secondTab);
289
+
290
+ expect(secondTab.scrollIntoView).toHaveBeenCalledWith({
291
+ block: "nearest",
292
+ inline: "nearest",
293
+ });
294
+ });
295
+
296
+ it("forwards aria-label to the tablist", () => {
297
+ render(
298
+ <Tabs
299
+ selectedId="1"
300
+ onSelect={jest.fn()}
301
+ aria-label="Repository sections"
302
+ >
303
+ <Tabs.Button id="1">First</Tabs.Button>
304
+ <Tabs.Button id="2">Second</Tabs.Button>
305
+ </Tabs>
306
+ );
307
+
308
+ const tablist = screen.getByRole("tablist", {
309
+ name: "Repository sections",
310
+ });
311
+ expect(tablist).toBeInTheDocument();
312
+ });
313
+
231
314
  it("should handle dynamically changing tab ids", () => {
232
315
  const onSelect = jest.fn();
233
316
  const { rerender } = render(
@@ -256,16 +339,124 @@ describe("Tabs", () => {
256
339
  const thirdTab = screen.getByText(/third/i);
257
340
  const fourthTab = screen.getByText(/fourth/i);
258
341
 
259
- fireEvent.keyUp(tabs, rightArrowKey);
342
+ fireEvent.keyDown(tabs, rightArrowKey);
260
343
  expect(onSelect).toHaveBeenCalledWith("6");
261
344
  expect(secondTab).toHaveFocus();
262
345
  onSelect.mockReset();
263
- fireEvent.keyUp(tabs, rightArrowKey);
346
+ fireEvent.keyDown(tabs, rightArrowKey);
264
347
  expect(onSelect).toHaveBeenCalledWith("5");
265
348
  expect(thirdTab).toHaveFocus();
266
349
  onSelect.mockReset();
267
- fireEvent.keyUp(tabs, rightArrowKey);
350
+ fireEvent.keyDown(tabs, rightArrowKey);
268
351
  expect(onSelect).toHaveBeenCalledWith("4");
269
352
  expect(fourthTab).toHaveFocus();
270
353
  });
354
+
355
+ describe("wheel redirect", () => {
356
+ const renderOverflowing = () => {
357
+ render(
358
+ <TestStatefulTabs>
359
+ <Tabs.Button id="1">First</Tabs.Button>
360
+ <Tabs.Button id="2">Second</Tabs.Button>
361
+ <Tabs.Button id="3">Third</Tabs.Button>
362
+ </TestStatefulTabs>
363
+ );
364
+ return screen.getByDataQaLabel({ tabs: "" });
365
+ };
366
+
367
+ it("redirects vertical wheel to horizontal scroll mid-list", () => {
368
+ const tabs = renderOverflowing();
369
+ mockScrollDimensions(tabs, {
370
+ scrollWidth: 800,
371
+ clientWidth: 480,
372
+ scrollLeft: 100,
373
+ });
374
+ const wheelEvent = new WheelEvent("wheel", {
375
+ deltaY: 50,
376
+ deltaX: 0,
377
+ cancelable: true,
378
+ bubbles: true,
379
+ });
380
+ jest.spyOn(wheelEvent, "preventDefault");
381
+ tabs.dispatchEvent(wheelEvent);
382
+ expect(wheelEvent.preventDefault).toHaveBeenCalled();
383
+ expect(tabs.scrollBy).toHaveBeenCalledWith({
384
+ left: 50,
385
+ behavior: "instant",
386
+ });
387
+ });
388
+
389
+ it("releases at the end so page scroll can take over", () => {
390
+ const tabs = renderOverflowing();
391
+ mockScrollDimensions(tabs, {
392
+ scrollWidth: 800,
393
+ clientWidth: 480,
394
+ scrollLeft: 320, // at end (320 + 480 = 800)
395
+ });
396
+ const wheelEvent = new WheelEvent("wheel", {
397
+ deltaY: 50,
398
+ deltaX: 0,
399
+ cancelable: true,
400
+ bubbles: true,
401
+ });
402
+ jest.spyOn(wheelEvent, "preventDefault");
403
+ tabs.dispatchEvent(wheelEvent);
404
+ expect(wheelEvent.preventDefault).not.toHaveBeenCalled();
405
+ expect(tabs.scrollBy).not.toHaveBeenCalled();
406
+ });
407
+
408
+ it("releases at the start when scrolling back further", () => {
409
+ const tabs = renderOverflowing();
410
+ mockScrollDimensions(tabs, {
411
+ scrollWidth: 800,
412
+ clientWidth: 480,
413
+ scrollLeft: 0,
414
+ });
415
+ const wheelEvent = new WheelEvent("wheel", {
416
+ deltaY: -50,
417
+ deltaX: 0,
418
+ cancelable: true,
419
+ bubbles: true,
420
+ });
421
+ jest.spyOn(wheelEvent, "preventDefault");
422
+ tabs.dispatchEvent(wheelEvent);
423
+ expect(wheelEvent.preventDefault).not.toHaveBeenCalled();
424
+ });
425
+
426
+ it("does not interfere when wheel input has a horizontal component", () => {
427
+ const tabs = renderOverflowing();
428
+ mockScrollDimensions(tabs, {
429
+ scrollWidth: 800,
430
+ clientWidth: 480,
431
+ scrollLeft: 100,
432
+ });
433
+ const wheelEvent = new WheelEvent("wheel", {
434
+ deltaY: 50,
435
+ deltaX: 30,
436
+ cancelable: true,
437
+ bubbles: true,
438
+ });
439
+ jest.spyOn(wheelEvent, "preventDefault");
440
+ tabs.dispatchEvent(wheelEvent);
441
+ expect(wheelEvent.preventDefault).not.toHaveBeenCalled();
442
+ });
443
+
444
+ it("does nothing when the tab list does not overflow", () => {
445
+ const tabs = renderOverflowing();
446
+ mockScrollDimensions(tabs, {
447
+ scrollWidth: 400,
448
+ clientWidth: 480,
449
+ scrollLeft: 0,
450
+ });
451
+ const wheelEvent = new WheelEvent("wheel", {
452
+ deltaY: 50,
453
+ deltaX: 0,
454
+ cancelable: true,
455
+ bubbles: true,
456
+ });
457
+ jest.spyOn(wheelEvent, "preventDefault");
458
+ tabs.dispatchEvent(wheelEvent);
459
+ expect(wheelEvent.preventDefault).not.toHaveBeenCalled();
460
+ });
461
+ });
271
462
  });
package/src/styles.ts CHANGED
@@ -11,18 +11,75 @@ interface TypeStyleProps extends TypeFullWidth, TypeTabState {}
11
11
 
12
12
  const Container = styled.div<TypeTabsProps>`
13
13
  display: ${(props) => (props.fullWidth ? "flex" : "inline-flex")};
14
- justify-content: space-between;
15
14
  margin: 0;
16
15
  padding: 0;
16
+ min-width: 0;
17
+ max-width: 100%;
18
+ min-height: min-content;
19
+ flex-shrink: 0;
20
+ overflow-x: auto;
21
+ overflow-y: hidden;
22
+ scrollbar-width: none;
23
+ scroll-padding-inline: 48px;
24
+ scroll-behavior: smooth;
17
25
  border-bottom: ${(props) => props.theme.borders[500]}
18
26
  ${(props) => props.theme.colors.container.border.base};
19
27
 
28
+ &::-webkit-scrollbar {
29
+ display: none;
30
+ }
31
+
32
+ &[data-fade-right] {
33
+ -webkit-mask-image: linear-gradient(
34
+ to right,
35
+ black 0,
36
+ black calc(100% - 48px),
37
+ transparent
38
+ );
39
+ mask-image: linear-gradient(
40
+ to right,
41
+ black 0,
42
+ black calc(100% - 48px),
43
+ transparent
44
+ );
45
+ }
46
+
47
+ &[data-fade-left] {
48
+ -webkit-mask-image: linear-gradient(
49
+ to right,
50
+ transparent,
51
+ black 48px,
52
+ black 100%
53
+ );
54
+ mask-image: linear-gradient(to right, transparent, black 48px, black 100%);
55
+ }
56
+
57
+ &[data-fade-left][data-fade-right] {
58
+ -webkit-mask-image: linear-gradient(
59
+ to right,
60
+ transparent,
61
+ black 48px,
62
+ black calc(100% - 48px),
63
+ transparent
64
+ );
65
+ mask-image: linear-gradient(
66
+ to right,
67
+ transparent,
68
+ black 48px,
69
+ black calc(100% - 48px),
70
+ transparent
71
+ );
72
+ }
73
+
20
74
  ${COMMON}
21
75
  `;
22
76
 
23
77
  export const TabButton = styled(Button)<TypeStyleProps>`
24
78
  margin-bottom: -1px;
25
- padding: ${(props) => `${props.theme.space[350]} 0`};
79
+ padding: ${(props) =>
80
+ `${props.theme.space[350]} ${
81
+ props.fullWidth ? 0 : props.theme.space[350]
82
+ }`};
26
83
  color: ${(props) => props.theme.colors.text.headline};
27
84
  border-radius: 0;
28
85
  border-width: 0 0 3px;
@@ -34,14 +91,6 @@ export const TabButton = styled(Button)<TypeStyleProps>`
34
91
  flex-grow: 1;
35
92
  `};
36
93
 
37
- &:not(:last-child) {
38
- ${(props) =>
39
- !props.fullWidth &&
40
- css`
41
- margin-right: ${(props) => props.theme.space[350]};
42
- `};
43
- }
44
-
45
94
  ${(props) =>
46
95
  props.isSelected &&
47
96
  css`