@instructure/ui-tabs 11.7.10 → 11.7.11-snapshot-4

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.
@@ -16,7 +16,7 @@ var _keycode = _interopRequireDefault(require("keycode"));
16
16
  var _latest = require("@instructure/ui-view/latest");
17
17
  var _uiReactUtils = require("@instructure/ui-react-utils");
18
18
  var _console = require("@instructure/console");
19
- var _uiFocusable = require("@instructure/ui-focusable");
19
+ var _uiI18n = require("@instructure/ui-i18n");
20
20
  var _uiDomUtils = require("@instructure/ui-dom-utils");
21
21
  var _debounce = require("@instructure/debounce");
22
22
  var _uiUtils = require("@instructure/ui-utils");
@@ -26,7 +26,7 @@ var _index = require("./Tab/index.js");
26
26
  var _index2 = require("./Panel/index.js");
27
27
  var _props = require("./props.js");
28
28
  var _jsxRuntime = require("@emotion/react/jsx-runtime");
29
- var _dec, _dec2, _class;
29
+ var _dec, _dec2, _dec3, _class;
30
30
  /*
31
31
  * The MIT License (MIT)
32
32
  *
@@ -55,19 +55,19 @@ var _dec, _dec2, _class;
55
55
  category: components
56
56
  ---
57
57
  **/
58
- let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emotion.withStyleNew)(_styles.default), _dec(_class = _dec2(_class = class Tabs extends _react.Component {
58
+ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emotion.withStyleNew)(_styles.default), _dec3 = (0, _uiI18n.textDirectionContextConsumer)(), _dec(_class = _dec2(_class = _dec3(_class = class Tabs extends _react.Component {
59
59
  static displayName = 'Tabs';
60
60
  static componentId = 'Tabs';
61
61
  static allowedProps = _props.allowedProps;
62
62
  static defaultProps = {
63
63
  variant: 'default',
64
64
  shouldFocusOnRender: false,
65
- tabOverflow: 'stack'
65
+ tabOverflow: 'stack',
66
+ activationMode: 'auto'
66
67
  };
67
68
  static Panel = _index2.Panel;
68
69
  static Tab = _index.Tab;
69
70
  _tabList = null;
70
- _focusable = null;
71
71
  _tabListPosition;
72
72
  _debounced;
73
73
  _resizeListener;
@@ -197,26 +197,74 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
197
197
  }
198
198
  }
199
199
  }
200
+ get isRtl() {
201
+ return this.props.dir === _uiI18n.textDirectionContextConsumer.DIRECTION.rtl;
202
+ }
200
203
  handleTabClick = (event, {
201
204
  index
202
205
  }) => {
203
- const nextTab = this.getNextTab(index, 0);
204
- this.fireOnChange(event, nextTab);
206
+ const target = this.getNextTab(this.getTabs(), index, 0);
207
+ if (target) {
208
+ this.fireOnChange(event, target);
209
+ }
205
210
  };
206
211
  handleTabKeyDown = (event, {
207
212
  index
208
213
  }) => {
209
- let nextTab;
210
- if (event.keyCode === _keycode.default.codes.up || event.keyCode === _keycode.default.codes.left) {
211
- // Select next tab to the left
212
- nextTab = this.getNextTab(index, -1);
213
- } else if (event.keyCode === _keycode.default.codes.down || event.keyCode === _keycode.default.codes.right) {
214
- // Select next tab to the right
215
- nextTab = this.getNextTab(index, 1);
214
+ const tabs = this.getTabs();
215
+ let target;
216
+ let isActivation = false;
217
+ switch (event.keyCode) {
218
+ case _keycode.default.codes.left:
219
+ target = this.getNextTab(tabs, index, this.isRtl ? 1 : -1);
220
+ break;
221
+ case _keycode.default.codes.right:
222
+ target = this.getNextTab(tabs, index, this.isRtl ? -1 : 1);
223
+ break;
224
+ case _keycode.default.codes.up:
225
+ target = this.getNextTab(tabs, index, -1);
226
+ break;
227
+ case _keycode.default.codes.down:
228
+ target = this.getNextTab(tabs, index, 1);
229
+ break;
230
+ case _keycode.default.codes.home:
231
+ target = this.getEdgeTab(tabs, 'first');
232
+ break;
233
+ case _keycode.default.codes.end:
234
+ target = this.getEdgeTab(tabs, 'last');
235
+ break;
236
+ case _keycode.default.codes.enter:
237
+ case _keycode.default.codes.space:
238
+ target = this.getNextTab(tabs, index, 0);
239
+ isActivation = true;
240
+ break;
241
+ default:
242
+ return;
216
243
  }
217
- if (nextTab) {
218
- event.preventDefault();
219
- this.fireOnChange(event, nextTab);
244
+ event.stopPropagation();
245
+ event.preventDefault();
246
+ if (!target) {
247
+ return;
248
+ }
249
+ if (isActivation || this.props.activationMode !== 'manual') {
250
+ this.fireOnChange(event, target);
251
+ }
252
+ this.focusTab(target.tabDomId);
253
+ };
254
+ handleTabFocus = (_event, {
255
+ id
256
+ }) => {
257
+ if (this.state.focusedTabId !== id) {
258
+ this.setState({
259
+ focusedTabId: id
260
+ });
261
+ }
262
+ };
263
+ handleTabListBlur = event => {
264
+ if (!(0, _uiDomUtils.contains)(event.currentTarget, event.relatedTarget)) {
265
+ this.setState({
266
+ focusedTabId: undefined
267
+ });
220
268
  }
221
269
  };
222
270
  handleResize = () => {
@@ -225,67 +273,129 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
225
273
  });
226
274
  this._tabListPosition = (0, _uiDomUtils.getBoundingClientRect)(this._tabList);
227
275
  };
228
- getNextTab(startIndex, step) {
229
- const tabs = _react.Children.toArray(this.props.children).map(child => (0, _uiReactUtils.matchComponentTypes)(child, [_index2.Panel]) && child);
276
+
277
+ /**
278
+ * The single source of truth for tab order, ids and state.
279
+ */
280
+ getTabs() {
281
+ const panels = [];
282
+ _react.Children.toArray(this.props.children).forEach((child, index) => {
283
+ if ((0, _uiReactUtils.matchComponentTypes)(child, [_index2.Panel])) {
284
+ panels.push({
285
+ index,
286
+ panel: child
287
+ });
288
+ }
289
+ });
290
+ const explicitlySelected = panels.find(({
291
+ panel
292
+ }) => panel.props.isSelected && !panel.props.isDisabled);
293
+ const fallbackIndex = explicitlySelected ? explicitlySelected.index : 0;
294
+ return panels.map(({
295
+ index,
296
+ panel
297
+ }) => {
298
+ const baseId = panel.props.id || this.props.deterministicId(`Tabs_${index}`);
299
+ const isDisabled = !!panel.props.isDisabled;
300
+ return {
301
+ index,
302
+ tabDomId: `tab-${baseId}`,
303
+ panelDomId: panel.props.id || `panel-${baseId}`,
304
+ isDisabled,
305
+ isSelected: !isDisabled && (!!panel.props.isSelected || index === fallbackIndex),
306
+ panel
307
+ };
308
+ });
309
+ }
310
+ getNextTab(tabs, startIndex, step) {
230
311
  const count = tabs.length;
231
- const change = step < 0 ? step + count : step;
232
- (0, _console.logError)(startIndex >= 0 && startIndex < count, `[Tabs] Invalid tab index: '${startIndex}'.`);
233
- let nextIndex = startIndex;
234
- let nextTab;
235
- do {
236
- nextIndex = (nextIndex + change) % count;
237
- nextTab = tabs[nextIndex];
238
- } while (nextTab && nextTab.props && nextTab.props.isDisabled);
239
- (0, _console.logError)(nextIndex >= 0 && nextIndex < count, `[Tabs] Invalid tab index: '${nextIndex}'.`);
240
- return {
241
- index: nextIndex,
242
- id: nextTab.props.id
243
- };
312
+ const from = tabs.findIndex(tab => tab.index === startIndex);
313
+ (0, _console.logError)(from >= 0, `[Tabs] Invalid tab index: '${startIndex}'.`);
314
+ if (from < 0) {
315
+ return undefined;
316
+ }
317
+ if (step === 0) {
318
+ return tabs[from];
319
+ }
320
+ for (let offset = 1; offset <= count; offset++) {
321
+ const candidate = tabs[((from + step * offset) % count + count) % count];
322
+ if (!candidate.isDisabled) {
323
+ return candidate;
324
+ }
325
+ }
326
+ return undefined;
327
+ }
328
+ getEdgeTab(tabs, edge) {
329
+ const enabled = tabs.filter(tab => !tab.isDisabled);
330
+ return edge === 'first' ? enabled[0] : enabled[enabled.length - 1];
331
+ }
332
+ getRovingTabId(tabs) {
333
+ const {
334
+ focusedTabId
335
+ } = this.state;
336
+ const focused = tabs.find(tab => tab.tabDomId === focusedTabId && !tab.isDisabled);
337
+ return focused?.tabDomId ?? tabs.find(tab => tab.isSelected)?.tabDomId ?? tabs.find(tab => !tab.isDisabled)?.tabDomId;
244
338
  }
245
339
  fireOnChange(event, {
246
340
  index,
247
- id
341
+ panel,
342
+ tabDomId
248
343
  }) {
249
344
  if (typeof this.props.onRequestTabChange === 'function') {
250
345
  this.props.onRequestTabChange(event, {
251
346
  index,
252
- id
347
+ id: panel.props.id
253
348
  });
254
349
  }
255
-
256
- // this is needed because keypress cancels scrolling. So we have to trigger the scrolling
350
+ this.scrollTabIntoView(tabDomId);
351
+ }
352
+ scrollTabIntoView(tabDomId) {
257
353
  // one "tick" later than the keypress
258
354
  setTimeout(() => {
259
355
  if (this.state.withTabListOverflow) {
260
- const tab = id ? this._tabList.querySelector(`#tab-${CSS.escape(id)}`) : null;
261
- this.showActiveTabIfOverlayed(tab);
356
+ this.showActiveTabIfOverlayed(this._tabList.querySelector(`#${CSS.escape(tabDomId)}`));
262
357
  }
263
358
  }, 0);
264
359
  }
265
- createTab(index, generatedId, selected, panel) {
266
- const id = panel.props.id || generatedId;
360
+ createTab(tab, isTabbable) {
361
+ const {
362
+ index,
363
+ tabDomId,
364
+ panelDomId,
365
+ isSelected,
366
+ isDisabled,
367
+ panel
368
+ } = tab;
267
369
  return (0, _jsxRuntime.jsx)(_index.Tab, {
268
370
  variant: this.props.variant,
269
- id: `tab-${id}`,
270
- controls: panel.props.id || `panel-${id}`,
371
+ id: tabDomId,
372
+ controls: panelDomId,
271
373
  index: index,
272
- isSelected: selected,
273
- isDisabled: panel.props.isDisabled,
374
+ isSelected: isSelected,
375
+ isDisabled: isDisabled,
376
+ isTabbable: isTabbable,
274
377
  onClick: this.handleTabClick,
275
378
  onKeyDown: this.handleTabKeyDown,
379
+ onFocus: this.handleTabFocus,
276
380
  isOverflowScroll: this.props.tabOverflow === 'scroll',
277
381
  children: panel.props.renderTitle
278
382
  }, `tab-${index}`);
279
383
  }
280
- clonePanel(index, generatedId, selected, panel, activePanel) {
281
- const id = panel.props.id || generatedId;
384
+ clonePanel(tab, activePanel) {
385
+ const {
386
+ index,
387
+ tabDomId,
388
+ panelDomId,
389
+ isSelected,
390
+ panel
391
+ } = tab;
282
392
 
283
393
  // fixHeight can be 0, so simply `fixheight` could return falsy value
284
394
  const hasFixedHeight = typeof this.props.fixHeight !== 'undefined';
285
395
  const commonProps = {
286
- id: panel.props.id || `panel-${id}`,
287
- labelledBy: `tab-${id}`,
288
- isSelected: selected,
396
+ id: panelDomId,
397
+ labelledBy: tabDomId,
398
+ isSelected,
289
399
  variant: this.props.variant,
290
400
  maxHeight: !hasFixedHeight ? this.props.maxHeight : undefined,
291
401
  minHeight: !hasFixedHeight ? this.props.minHeight : '100%'
@@ -311,14 +421,19 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
311
421
  });
312
422
  }
313
423
  }
314
- handleFocusableRef = el => {
315
- this._focusable = el;
316
- };
317
424
  handleTabListRef = el => {
318
425
  this._tabList = el;
319
426
  };
320
427
  focus() {
321
- this._focusable && typeof this._focusable.focus === 'function' && this._focusable.focus();
428
+ this.focusTab(this.getRovingTabId(this.getTabs()));
429
+ }
430
+ focusTab(tabDomId) {
431
+ if (!tabDomId) {
432
+ return;
433
+ }
434
+ const tab = this._tabList?.querySelector(`#${CSS.escape(tabDomId)}`);
435
+ tab?.focus();
436
+ this.scrollTabIntoView(tabDomId);
322
437
  }
323
438
  handleScroll = event => {
324
439
  if (this.props.tabOverflow !== 'scroll' || !this.state.withTabListOverflow) {
@@ -346,27 +461,24 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
346
461
  onRequestTabChange,
347
462
  tabOverflow,
348
463
  styles,
464
+ dir,
349
465
  ...props
350
466
  } = this.props;
351
- const activePanels = _react.Children.toArray(children).filter(child => (0, _uiReactUtils.matchComponentTypes)(child, [_index2.Panel])).filter(child => child.props.active);
467
+ const tabDescriptors = this.getTabs();
468
+ const rovingTabId = this.getRovingTabId(tabDescriptors);
469
+ const tabsByIndex = new Map(tabDescriptors.map(tab => [tab.index, tab]));
470
+ const activePanels = tabDescriptors.map(tab => tab.panel).filter(panel => panel.props.active);
352
471
  if (activePanels.length > 1) {
353
472
  (0, _console.logError)(false, `[Tabs] Only one Panel can be marked as active.`);
354
473
  }
355
- const selectedChildIndex = _react.Children.toArray(children).filter(child => (0, _uiReactUtils.matchComponentTypes)(child, [_index2.Panel])).findIndex(child => child.props.isSelected && !child.props.isDisabled);
356
- const selectedIndex = selectedChildIndex >= 0 ? selectedChildIndex : 0;
357
- _react.Children.toArray(children).map((child, index) => {
358
- if ((0, _uiReactUtils.matchComponentTypes)(child, [_index2.Panel])) {
359
- const selected = !child.props.isDisabled && (child.props.isSelected || selectedIndex === index);
360
- const id = this.props.deterministicId(`Tabs_${index}`);
361
- tabs.push(this.createTab(index, id, selected, child));
362
- if (activePanels.length === 1) {
363
- panels.push(this.clonePanel(index, id, selected, child, activePanels[0]));
364
- } else {
365
- panels.push(this.clonePanel(index, id, selected, child));
366
- }
367
- } else {
474
+ _react.Children.toArray(children).forEach((child, index) => {
475
+ const tab = tabsByIndex.get(index);
476
+ if (!tab) {
368
477
  panels.push(child);
478
+ return;
369
479
  }
480
+ tabs.push(this.createTab(tab, tab.tabDomId === rovingTabId));
481
+ panels.push(this.clonePanel(tab, activePanels.length === 1 ? activePanels[0] : undefined));
370
482
  });
371
483
  const withScrollFade = tabOverflow === 'scroll' && this.state.withTabListOverflow;
372
484
 
@@ -385,23 +497,21 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
385
497
  as: "div",
386
498
  css: styles?.container,
387
499
  "data-cid": "Tabs",
388
- children: [(0, _jsxRuntime.jsx)(_uiFocusable.Focusable, {
389
- ref: this.handleFocusableRef,
390
- children: () => (0, _jsxRuntime.jsx)(_latest.View, {
500
+ children: [(0, _jsxRuntime.jsx)(_latest.View, {
501
+ as: "div",
502
+ position: "relative",
503
+ borderRadius: "medium",
504
+ shouldAnimateFocus: false,
505
+ css: styles?.tabs,
506
+ children: (0, _jsxRuntime.jsxs)(_latest.View, {
391
507
  as: "div",
392
- position: "relative",
393
- borderRadius: "medium",
394
- shouldAnimateFocus: false,
395
- css: styles?.tabs,
396
- children: (0, _jsxRuntime.jsxs)(_latest.View, {
397
- as: "div",
398
- role: "tablist",
399
- css: styles?.tabList,
400
- "aria-label": screenReaderLabel,
401
- elementRef: this.handleTabListRef,
402
- onScroll: this.handleScroll,
403
- children: [tabs, withScrollFade && startScrollOverlay, withScrollFade && endScrollOverlay]
404
- })
508
+ role: "tablist",
509
+ css: styles?.tabList,
510
+ "aria-label": screenReaderLabel,
511
+ elementRef: this.handleTabListRef,
512
+ onScroll: this.handleScroll,
513
+ onBlur: this.handleTabListBlur,
514
+ children: [tabs, withScrollFade && startScrollOverlay, withScrollFade && endScrollOverlay]
405
515
  })
406
516
  }), (0, _jsxRuntime.jsx)("div", {
407
517
  css: styles?.panelsContainer,
@@ -409,5 +519,5 @@ let Tabs = exports.Tabs = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec
409
519
  })]
410
520
  });
411
521
  }
412
- }) || _class) || _class);
522
+ }) || _class) || _class) || _class);
413
523
  var _default = exports.default = Tabs;
@@ -28,4 +28,4 @@ exports.allowedProps = void 0;
28
28
  * SOFTWARE.
29
29
  */
30
30
 
31
- const allowedProps = exports.allowedProps = ['children', 'variant', 'screenReaderLabel', 'onRequestTabChange', 'maxWidth', 'maxHeight', 'minHeight', 'fixHeight', 'margin', 'padding', 'textAlign', 'elementRef', 'tabOverflow', 'shouldFocusOnRender'];
31
+ const allowedProps = exports.allowedProps = ['children', 'variant', 'screenReaderLabel', 'onRequestTabChange', 'maxWidth', 'maxHeight', 'minHeight', 'fixHeight', 'margin', 'padding', 'textAlign', 'elementRef', 'tabOverflow', 'shouldFocusOnRender', 'activationMode'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-tabs",
3
- "version": "11.7.10",
3
+ "version": "11.7.11-snapshot-4",
4
4
  "description": "A UI component library made by Instructure Inc.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,24 +15,24 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.29.7",
18
- "@instructure/console": "11.7.10",
19
- "@instructure/debounce": "11.7.10",
20
- "@instructure/emotion": "11.7.10",
21
- "@instructure/shared-types": "11.7.10",
22
- "@instructure/ui-dom-utils": "11.7.10",
23
- "@instructure/ui-focusable": "11.7.10",
24
- "@instructure/ui-i18n": "11.7.10",
25
- "@instructure/ui-motion": "11.7.10",
26
- "@instructure/ui-react-utils": "11.7.10",
27
- "@instructure/ui-themes": "11.7.10",
28
- "@instructure/ui-utils": "11.7.10",
29
- "@instructure/ui-view": "11.7.10",
18
+ "@instructure/console": "11.7.11-snapshot-4",
19
+ "@instructure/debounce": "11.7.11-snapshot-4",
20
+ "@instructure/emotion": "11.7.11-snapshot-4",
21
+ "@instructure/shared-types": "11.7.11-snapshot-4",
22
+ "@instructure/ui-dom-utils": "11.7.11-snapshot-4",
23
+ "@instructure/ui-focusable": "11.7.11-snapshot-4",
24
+ "@instructure/ui-i18n": "11.7.11-snapshot-4",
25
+ "@instructure/ui-motion": "11.7.11-snapshot-4",
26
+ "@instructure/ui-react-utils": "11.7.11-snapshot-4",
27
+ "@instructure/ui-themes": "11.7.11-snapshot-4",
28
+ "@instructure/ui-utils": "11.7.11-snapshot-4",
29
+ "@instructure/ui-view": "11.7.11-snapshot-4",
30
30
  "keycode": "^2"
31
31
  },
32
32
  "devDependencies": {
33
- "@instructure/ui-axe-check": "11.7.10",
34
- "@instructure/ui-babel-preset": "11.7.10",
35
- "@instructure/ui-color-utils": "11.7.10",
33
+ "@instructure/ui-axe-check": "11.7.11-snapshot-4",
34
+ "@instructure/ui-babel-preset": "11.7.11-snapshot-4",
35
+ "@instructure/ui-color-utils": "11.7.11-snapshot-4",
36
36
  "vitest": "^5.0.0"
37
37
  },
38
38
  "peerDependencies": {
@@ -2,7 +2,7 @@
2
2
  describes: Tabs
3
3
  ---
4
4
 
5
- `<Tabs />` is an accessible tabbed navigation component. Use the TAB key to focus the component and arrow keys to navigate between panels of content. To set a default panel that should be selected on initial render, set the `selected` prop on that `<Tabs.Panel>`.
5
+ `<Tabs />` is an accessible tabbed navigation component. Use the TAB key to focus the component and arrow keys to navigate between panels of content. Home and End jump to the first and last tab, and the arrow keys are mirrored in right-to-left text direction. By default a tab is selected as soon as an arrow key reaches it; see the Manual activation section below to require a key press instead. To set a default panel that should be selected on initial render, set the `selected` prop on that `<Tabs.Panel>`.
6
6
 
7
7
  ```js
8
8
  ---
@@ -533,6 +533,62 @@ const Example = () => {
533
533
  render(<Example />)
534
534
  ```
535
535
 
536
+ ### Manual activation
537
+
538
+ Set `activationMode="manual"` to separate the two: the arrow keys, Home
539
+ and End only move focus, and Enter or Space selects the focused tab.
540
+
541
+ Use manual activation for expensive panels. This is also important for VoiceOver, which reserves arrow keys for its cursor, while Enter and Space work across screen readers.
542
+
543
+ ```js
544
+ ---
545
+ type: example
546
+ ---
547
+ const Example = () => {
548
+ const [selectedIndex, setSelectedIndex] = useState(0)
549
+
550
+ const handleTabChange = (event, { index }) => {
551
+ setSelectedIndex(index)
552
+ }
553
+
554
+ return (
555
+ <Tabs
556
+ activationMode="manual"
557
+ margin="general.space2xl auto"
558
+ padding="general.spaceXl"
559
+ onRequestTabChange={handleTabChange}
560
+ >
561
+ <Tabs.Panel
562
+ id="manualA"
563
+ renderTitle="First Tab"
564
+ isSelected={selectedIndex === 0}
565
+ tabIndex={0}
566
+ >
567
+ Arrow to another tab, then press Enter or Space to open it.
568
+ </Tabs.Panel>
569
+ <Tabs.Panel
570
+ id="manualB"
571
+ renderTitle="Second Tab"
572
+ isSelected={selectedIndex === 1}
573
+ tabIndex={0}
574
+ >
575
+ Second panel
576
+ </Tabs.Panel>
577
+ <Tabs.Panel
578
+ id="manualC"
579
+ renderTitle="Third Tab"
580
+ isSelected={selectedIndex === 2}
581
+ tabIndex={0}
582
+ >
583
+ Third panel
584
+ </Tabs.Panel>
585
+ </Tabs>
586
+ )
587
+ }
588
+
589
+ render(<Example />)
590
+ ```
591
+
536
592
  ### Guidelines
537
593
 
538
594
  ```js
@@ -86,12 +86,25 @@ class Tab extends Component<TabsTabProps> {
86
86
  }
87
87
  }
88
88
 
89
+ handleFocus = (event: React.FocusEvent<ViewOwnProps>) => {
90
+ const { onFocus, index, id, isDisabled } = this.props
91
+
92
+ if (isDisabled) {
93
+ return
94
+ }
95
+
96
+ if (typeof onFocus === 'function') {
97
+ onFocus(event, { index, id })
98
+ }
99
+ }
100
+
89
101
  render() {
90
102
  const {
91
103
  id,
92
104
  variant,
93
105
  isSelected,
94
106
  isDisabled,
107
+ isTabbable = isSelected,
95
108
  controls,
96
109
  children,
97
110
  styles,
@@ -106,11 +119,12 @@ class Tab extends Component<TabsTabProps> {
106
119
  id={id}
107
120
  onClick={this.handleClick}
108
121
  onKeyDown={this.handleKeyDown}
122
+ onFocus={this.handleFocus}
109
123
  css={styles?.tab}
110
- aria-selected={isSelected ? 'true' : undefined}
124
+ aria-selected={isSelected ? 'true' : 'false'}
111
125
  aria-disabled={isDisabled ? 'true' : undefined}
112
126
  aria-controls={controls}
113
- tabIndex={isSelected && !isDisabled ? 0 : undefined}
127
+ tabIndex={isDisabled ? undefined : isTabbable ? 0 : -1}
114
128
  position="relative"
115
129
  focusPosition="offset"
116
130
  >
@@ -35,6 +35,7 @@ type TabsTabOwnProps = {
35
35
  controls: string
36
36
  isDisabled?: boolean
37
37
  isSelected?: boolean
38
+ isTabbable?: boolean
38
39
  onClick?: (
39
40
  event: React.MouseEvent<ViewOwnProps>,
40
41
  tabData: { index: number; id: string }
@@ -43,6 +44,10 @@ type TabsTabOwnProps = {
43
44
  event: React.KeyboardEvent<ViewOwnProps>,
44
45
  tabData: { index: number; id: string }
45
46
  ) => void
47
+ onFocus?: (
48
+ event: React.FocusEvent<ViewOwnProps>,
49
+ tabData: { index: number; id: string }
50
+ ) => void
46
51
  children?: Renderable
47
52
  /**
48
53
  * Whether tabOverflow prop in Tabs is set to 'scroll'.
@@ -67,8 +72,10 @@ const allowedProps: AllowedPropKeys = [
67
72
  'controls',
68
73
  'isDisabled',
69
74
  'isSelected',
75
+ 'isTabbable',
70
76
  'onClick',
71
77
  'onKeyDown',
78
+ 'onFocus',
72
79
  'children',
73
80
  'isOverflowScroll'
74
81
  ]