@instructure/ui-tabs 8.46.2-snapshot-7 → 8.46.2-snapshot-8

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/CHANGELOG.md CHANGED
@@ -3,9 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [8.46.2-snapshot-7](https://github.com/instructure/instructure-ui/compare/v8.46.1...v8.46.2-snapshot-7) (2023-10-20)
6
+ ## [8.46.2-snapshot-8](https://github.com/instructure/instructure-ui/compare/v8.46.1...v8.46.2-snapshot-8) (2023-10-23)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-tabs
8
+
9
+ ### Features
10
+
11
+ * **ui-tabs:** add active property to tabs ([5037855](https://github.com/instructure/instructure-ui/commit/5037855e67b322ce07c2ca1c3113ddeb0229f003))
9
12
 
10
13
 
11
14
 
@@ -1,5 +1,5 @@
1
1
  import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
2
- const _excluded = ["labelledBy", "variant", "id", "maxHeight", "minHeight", "padding", "textAlign", "children", "elementRef", "isDisabled", "isSelected", "styles"];
2
+ const _excluded = ["labelledBy", "variant", "id", "maxHeight", "minHeight", "padding", "textAlign", "children", "elementRef", "isDisabled", "isSelected", "styles", "active"];
3
3
  var _dec, _class, _class2;
4
4
  /*
5
5
  * The MIT License (MIT)
@@ -76,6 +76,7 @@ let Panel = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
76
76
  isDisabled = _this$props3.isDisabled,
77
77
  isSelected = _this$props3.isSelected,
78
78
  styles = _this$props3.styles,
79
+ active = _this$props3.active,
79
80
  props = _objectWithoutProperties(_this$props3, _excluded);
80
81
  const isHidden = !isSelected || !!isDisabled;
81
82
  return jsx("div", Object.assign({}, passthroughProps(props), {
@@ -104,7 +105,8 @@ let Panel = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
104
105
  textAlign: 'start',
105
106
  variant: 'default',
106
107
  isSelected: false,
107
- padding: 'small'
108
+ padding: 'small',
109
+ active: false
108
110
  }, _class2)) || _class);
109
111
  export default Panel;
110
112
  export { Panel };
@@ -36,7 +36,8 @@ const propTypes = {
36
36
  labelledBy: PropTypes.string,
37
37
  padding: ThemeablePropTypes.spacing,
38
38
  textAlign: PropTypes.oneOf(['start', 'center', 'end']),
39
- elementRef: PropTypes.func
39
+ elementRef: PropTypes.func,
40
+ active: PropTypes.bool
40
41
  };
41
- const allowedProps = ['renderTitle', 'children', 'variant', 'isSelected', 'isDisabled', 'maxHeight', 'minHeight', 'id', 'labelledBy', 'padding', 'textAlign', 'elementRef'];
42
+ const allowedProps = ['renderTitle', 'children', 'variant', 'isSelected', 'isDisabled', 'maxHeight', 'minHeight', 'id', 'labelledBy', 'padding', 'textAlign', 'elementRef', 'active'];
42
43
  export { propTypes, allowedProps };
@@ -0,0 +1,120 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
2
+ var _p, _Tabs, _Tabs2, _Tabs3;
3
+ /*
4
+ * The MIT License (MIT)
5
+ *
6
+ * Copyright (c) 2015 - present Instructure, Inc.
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ */
26
+
27
+ import React from 'react';
28
+ import { Tabs } from '../index';
29
+ import { fireEvent, render, screen } from '@testing-library/react';
30
+ import '@testing-library/jest-dom';
31
+ const TabExample = props => {
32
+ const _React$useState = React.useState(0),
33
+ _React$useState2 = _slicedToArray(_React$useState, 2),
34
+ selectedIndex = _React$useState2[0],
35
+ setSelectedIndex = _React$useState2[1];
36
+ return /*#__PURE__*/React.createElement(Tabs, {
37
+ onRequestTabChange: (_event, {
38
+ index
39
+ }) => {
40
+ setSelectedIndex(index);
41
+ props.onIndexChange(index);
42
+ },
43
+ variant: "default",
44
+ margin: "medium"
45
+ }, /*#__PURE__*/React.createElement(Tabs.Panel, {
46
+ renderTitle: "First Tab",
47
+ id: "first",
48
+ isSelected: selectedIndex === 0,
49
+ active: true
50
+ }, _p || (_p = /*#__PURE__*/React.createElement("p", null, "CONTENT"))), /*#__PURE__*/React.createElement(Tabs.Panel, {
51
+ renderTitle: "Second Tab",
52
+ id: "second",
53
+ isSelected: selectedIndex === 1
54
+ }), /*#__PURE__*/React.createElement(Tabs.Panel, {
55
+ renderTitle: "Third Tab",
56
+ id: "third",
57
+ isSelected: selectedIndex === 2
58
+ }));
59
+ };
60
+ describe('<Tabs />', () => {
61
+ it('should render the correct number of panels', () => {
62
+ const _render = render(_Tabs || (_Tabs = /*#__PURE__*/React.createElement(Tabs, null, /*#__PURE__*/React.createElement(Tabs.Panel, {
63
+ renderTitle: "First Tab"
64
+ }, "Tab 1 content"), /*#__PURE__*/React.createElement(Tabs.Panel, {
65
+ renderTitle: "Second Tab"
66
+ }, "Tab 2 content"), /*#__PURE__*/React.createElement(Tabs.Panel, {
67
+ renderTitle: "Third Tab",
68
+ isDisabled: true
69
+ }, "Tab 3 content")))),
70
+ container = _render.container;
71
+ expect(container.firstChild).toBeInTheDocument();
72
+ });
73
+ it('should render same content for other tabs as for the active one', () => {
74
+ const _render2 = render(_Tabs2 || (_Tabs2 = /*#__PURE__*/React.createElement(Tabs, null, /*#__PURE__*/React.createElement(Tabs.Panel, {
75
+ renderTitle: "First Tab",
76
+ active: true
77
+ }, "CONTENT"), /*#__PURE__*/React.createElement(Tabs.Panel, {
78
+ id: "secondTab",
79
+ renderTitle: "Second Tab",
80
+ isSelected: true
81
+ }, "Child"), /*#__PURE__*/React.createElement(Tabs.Panel, {
82
+ renderTitle: "Third Tab"
83
+ }, "Child")))),
84
+ container = _render2.container;
85
+ const tabContent = screen.getByText('CONTENT');
86
+ expect(container).toBeInTheDocument();
87
+ expect(tabContent).toBeInTheDocument();
88
+ const childContent = screen.queryByText('Child');
89
+ expect(childContent).toBeNull();
90
+ });
91
+ it('should render the same content in second tab when selected', () => {
92
+ const onIndexChange = jest.fn();
93
+ const _render3 = render( /*#__PURE__*/React.createElement(TabExample, {
94
+ onIndexChange: onIndexChange
95
+ })),
96
+ container = _render3.container;
97
+ expect(container).toBeInTheDocument();
98
+ const secondTab = screen.getAllByRole('tab')[1];
99
+ fireEvent.click(secondTab);
100
+ expect(onIndexChange).toHaveBeenCalledWith(1);
101
+ const panelContent = screen.queryByText('CONTENT');
102
+ expect(panelContent).toBeInTheDocument();
103
+ });
104
+ it('should warn if multiple active tabs exist', () => {
105
+ const consoleMock = jest.spyOn(console, 'error').mockImplementation();
106
+ const _render4 = render(_Tabs3 || (_Tabs3 = /*#__PURE__*/React.createElement(Tabs, null, /*#__PURE__*/React.createElement(Tabs.Panel, {
107
+ renderTitle: "First Tab",
108
+ active: true
109
+ }, "Tab 1 content"), /*#__PURE__*/React.createElement(Tabs.Panel, {
110
+ renderTitle: "Second Tab",
111
+ active: true
112
+ }, "Tab 2 content"), /*#__PURE__*/React.createElement(Tabs.Panel, {
113
+ renderTitle: "Third Tab",
114
+ isDisabled: true
115
+ }, "Tab 3 content")))),
116
+ container = _render4.container;
117
+ expect(container.firstChild).toBeInTheDocument();
118
+ expect(consoleMock.mock.calls[0][0]).toEqual('Warning: [Tabs] Only one Panel can be marked as active.');
119
+ });
120
+ });
package/es/Tabs/index.js CHANGED
@@ -270,22 +270,39 @@ let Tabs = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 = tes
270
270
  onKeyDown: this.handleTabKeyDown
271
271
  });
272
272
  }
273
- clonePanel(_index, generatedId, selected, panel) {
273
+ clonePanel(_index, generatedId, selected, panel, activePanel) {
274
274
  const id = panel.props.id || generatedId;
275
275
 
276
276
  // fixHeight can be 0, so simply `fixheight` could return falsy value
277
277
  const hasFixedHeight = typeof this.props.fixHeight !== 'undefined';
278
- return safeCloneElement(panel, {
278
+ const commonProps = {
279
279
  id: panel.props.id || `panel-${id}`,
280
280
  labelledBy: `tab-${id}`,
281
281
  isSelected: selected,
282
- key: panel.props.id || `panel-${id}`,
283
282
  variant: this.props.variant,
284
- padding: panel.props.padding || this.props.padding,
285
- textAlign: panel.props.textAlign || this.props.textAlign,
286
283
  maxHeight: !hasFixedHeight ? this.props.maxHeight : void 0,
287
284
  minHeight: !hasFixedHeight ? this.props.minHeight : '100%'
288
- });
285
+ };
286
+ let activePanelClone = null;
287
+ if (activePanel !== void 0) {
288
+ // cloning active panel with a proper custom key as a workaround because
289
+ // safeCloneElement overwrites it with the key from the original element
290
+ activePanelClone = /*#__PURE__*/React.cloneElement(activePanel, {
291
+ key: panel.props.id || `panel-${id}`
292
+ });
293
+ return safeCloneElement(activePanelClone, {
294
+ padding: activePanelClone.props.padding || this.props.padding,
295
+ textAlign: activePanelClone.props.textAlign || this.props.textAlign,
296
+ ...commonProps
297
+ });
298
+ } else {
299
+ return safeCloneElement(panel, {
300
+ key: panel.props.id || `panel-${id}`,
301
+ padding: panel.props.padding || this.props.padding,
302
+ textAlign: panel.props.textAlign || this.props.textAlign,
303
+ ...commonProps
304
+ });
305
+ }
289
306
  }
290
307
  focus() {
291
308
  this._focusable && typeof this._focusable.focus === 'function' && this._focusable.focus();
@@ -304,6 +321,10 @@ let Tabs = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 = tes
304
321
  tabOverflow = _this$props4.tabOverflow,
305
322
  styles = _this$props4.styles,
306
323
  props = _objectWithoutProperties(_this$props4, _excluded);
324
+ const activePanels = React.Children.toArray(children).filter(child => matchComponentTypes(child, [Panel])).filter(child => child.props.active);
325
+ if (activePanels.length > 1) {
326
+ error(false, `[Tabs] Only one Panel can be marked as active.`);
327
+ }
307
328
  const selectedChildIndex = React.Children.toArray(children).filter(child => matchComponentTypes(child, [Panel])).findIndex(child => child.props.isSelected && !child.props.isDisabled);
308
329
  let index = 0;
309
330
  const selectedIndex = selectedChildIndex >= 0 ? selectedChildIndex : 0;
@@ -312,7 +333,11 @@ let Tabs = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 = tes
312
333
  const selected = !child.props.isDisabled && (child.props.isSelected || selectedIndex === index);
313
334
  const id = uid();
314
335
  tabs.push(this.createTab(index, id, selected, child));
315
- panels.push(this.clonePanel(index, id, selected, child));
336
+ if (activePanels.length === 1) {
337
+ panels.push(this.clonePanel(index, id, selected, child, activePanels[0]));
338
+ } else {
339
+ panels.push(this.clonePanel(index, id, selected, child));
340
+ }
316
341
  index++;
317
342
  } else {
318
343
  panels.push(child);
@@ -14,7 +14,7 @@ var _emotion = require("@instructure/emotion");
14
14
  var _styles = _interopRequireDefault(require("./styles"));
15
15
  var _theme = _interopRequireDefault(require("./theme"));
16
16
  var _props = require("./props");
17
- const _excluded = ["labelledBy", "variant", "id", "maxHeight", "minHeight", "padding", "textAlign", "children", "elementRef", "isDisabled", "isSelected", "styles"];
17
+ const _excluded = ["labelledBy", "variant", "id", "maxHeight", "minHeight", "padding", "textAlign", "children", "elementRef", "isDisabled", "isSelected", "styles", "active"];
18
18
  var _dec, _class, _class2;
19
19
  /*
20
20
  * The MIT License (MIT)
@@ -81,6 +81,7 @@ let Panel = exports.Panel = (_dec = (0, _emotion.withStyle)(_styles.default, _th
81
81
  isDisabled = _this$props3.isDisabled,
82
82
  isSelected = _this$props3.isSelected,
83
83
  styles = _this$props3.styles,
84
+ active = _this$props3.active,
84
85
  props = (0, _objectWithoutProperties2.default)(_this$props3, _excluded);
85
86
  const isHidden = !isSelected || !!isDisabled;
86
87
  return (0, _emotion.jsx)("div", Object.assign({}, (0, _passthroughProps.passthroughProps)(props), {
@@ -109,6 +110,7 @@ let Panel = exports.Panel = (_dec = (0, _emotion.withStyle)(_styles.default, _th
109
110
  textAlign: 'start',
110
111
  variant: 'default',
111
112
  isSelected: false,
112
- padding: 'small'
113
+ padding: 'small',
114
+ active: false
113
115
  }, _class2)) || _class);
114
116
  var _default = exports.default = Panel;
@@ -43,6 +43,7 @@ const propTypes = exports.propTypes = {
43
43
  labelledBy: _propTypes.default.string,
44
44
  padding: _emotion.ThemeablePropTypes.spacing,
45
45
  textAlign: _propTypes.default.oneOf(['start', 'center', 'end']),
46
- elementRef: _propTypes.default.func
46
+ elementRef: _propTypes.default.func,
47
+ active: _propTypes.default.bool
47
48
  };
48
- const allowedProps = exports.allowedProps = ['renderTitle', 'children', 'variant', 'isSelected', 'isDisabled', 'maxHeight', 'minHeight', 'id', 'labelledBy', 'padding', 'textAlign', 'elementRef'];
49
+ const allowedProps = exports.allowedProps = ['renderTitle', 'children', 'variant', 'isSelected', 'isDisabled', 'maxHeight', 'minHeight', 'id', 'labelledBy', 'padding', 'textAlign', 'elementRef', 'active'];
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
5
+ var _react = _interopRequireDefault(require("react"));
6
+ var _index = require("../index");
7
+ var _react2 = require("@testing-library/react");
8
+ require("@testing-library/jest-dom");
9
+ var _p, _Tabs, _Tabs2, _Tabs3;
10
+ /*
11
+ * The MIT License (MIT)
12
+ *
13
+ * Copyright (c) 2015 - present Instructure, Inc.
14
+ *
15
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ * of this software and associated documentation files (the "Software"), to deal
17
+ * in the Software without restriction, including without limitation the rights
18
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ * copies of the Software, and to permit persons to whom the Software is
20
+ * furnished to do so, subject to the following conditions:
21
+ *
22
+ * The above copyright notice and this permission notice shall be included in all
23
+ * copies or substantial portions of the Software.
24
+ *
25
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ * SOFTWARE.
32
+ */
33
+ const TabExample = props => {
34
+ const _React$useState = _react.default.useState(0),
35
+ _React$useState2 = (0, _slicedToArray2.default)(_React$useState, 2),
36
+ selectedIndex = _React$useState2[0],
37
+ setSelectedIndex = _React$useState2[1];
38
+ return /*#__PURE__*/_react.default.createElement(_index.Tabs, {
39
+ onRequestTabChange: (_event, {
40
+ index
41
+ }) => {
42
+ setSelectedIndex(index);
43
+ props.onIndexChange(index);
44
+ },
45
+ variant: "default",
46
+ margin: "medium"
47
+ }, /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
48
+ renderTitle: "First Tab",
49
+ id: "first",
50
+ isSelected: selectedIndex === 0,
51
+ active: true
52
+ }, _p || (_p = /*#__PURE__*/_react.default.createElement("p", null, "CONTENT"))), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
53
+ renderTitle: "Second Tab",
54
+ id: "second",
55
+ isSelected: selectedIndex === 1
56
+ }), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
57
+ renderTitle: "Third Tab",
58
+ id: "third",
59
+ isSelected: selectedIndex === 2
60
+ }));
61
+ };
62
+ describe('<Tabs />', () => {
63
+ it('should render the correct number of panels', () => {
64
+ const _render = (0, _react2.render)(_Tabs || (_Tabs = /*#__PURE__*/_react.default.createElement(_index.Tabs, null, /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
65
+ renderTitle: "First Tab"
66
+ }, "Tab 1 content"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
67
+ renderTitle: "Second Tab"
68
+ }, "Tab 2 content"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
69
+ renderTitle: "Third Tab",
70
+ isDisabled: true
71
+ }, "Tab 3 content")))),
72
+ container = _render.container;
73
+ expect(container.firstChild).toBeInTheDocument();
74
+ });
75
+ it('should render same content for other tabs as for the active one', () => {
76
+ const _render2 = (0, _react2.render)(_Tabs2 || (_Tabs2 = /*#__PURE__*/_react.default.createElement(_index.Tabs, null, /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
77
+ renderTitle: "First Tab",
78
+ active: true
79
+ }, "CONTENT"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
80
+ id: "secondTab",
81
+ renderTitle: "Second Tab",
82
+ isSelected: true
83
+ }, "Child"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
84
+ renderTitle: "Third Tab"
85
+ }, "Child")))),
86
+ container = _render2.container;
87
+ const tabContent = _react2.screen.getByText('CONTENT');
88
+ expect(container).toBeInTheDocument();
89
+ expect(tabContent).toBeInTheDocument();
90
+ const childContent = _react2.screen.queryByText('Child');
91
+ expect(childContent).toBeNull();
92
+ });
93
+ it('should render the same content in second tab when selected', () => {
94
+ const onIndexChange = jest.fn();
95
+ const _render3 = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(TabExample, {
96
+ onIndexChange: onIndexChange
97
+ })),
98
+ container = _render3.container;
99
+ expect(container).toBeInTheDocument();
100
+ const secondTab = _react2.screen.getAllByRole('tab')[1];
101
+ _react2.fireEvent.click(secondTab);
102
+ expect(onIndexChange).toHaveBeenCalledWith(1);
103
+ const panelContent = _react2.screen.queryByText('CONTENT');
104
+ expect(panelContent).toBeInTheDocument();
105
+ });
106
+ it('should warn if multiple active tabs exist', () => {
107
+ const consoleMock = jest.spyOn(console, 'error').mockImplementation();
108
+ const _render4 = (0, _react2.render)(_Tabs3 || (_Tabs3 = /*#__PURE__*/_react.default.createElement(_index.Tabs, null, /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
109
+ renderTitle: "First Tab",
110
+ active: true
111
+ }, "Tab 1 content"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
112
+ renderTitle: "Second Tab",
113
+ active: true
114
+ }, "Tab 2 content"), /*#__PURE__*/_react.default.createElement(_index.Tabs.Panel, {
115
+ renderTitle: "Third Tab",
116
+ isDisabled: true
117
+ }, "Tab 3 content")))),
118
+ container = _render4.container;
119
+ expect(container.firstChild).toBeInTheDocument();
120
+ expect(consoleMock.mock.calls[0][0]).toEqual('Warning: [Tabs] Only one Panel can be marked as active.');
121
+ });
122
+ });
package/lib/Tabs/index.js CHANGED
@@ -285,22 +285,39 @@ let Tabs = exports.Tabs = (_dec = (0, _emotion.withStyle)(_styles.default, _them
285
285
  onKeyDown: this.handleTabKeyDown
286
286
  });
287
287
  }
288
- clonePanel(_index, generatedId, selected, panel) {
288
+ clonePanel(_index, generatedId, selected, panel, activePanel) {
289
289
  const id = panel.props.id || generatedId;
290
290
 
291
291
  // fixHeight can be 0, so simply `fixheight` could return falsy value
292
292
  const hasFixedHeight = typeof this.props.fixHeight !== 'undefined';
293
- return (0, _safeCloneElement.safeCloneElement)(panel, {
293
+ const commonProps = {
294
294
  id: panel.props.id || `panel-${id}`,
295
295
  labelledBy: `tab-${id}`,
296
296
  isSelected: selected,
297
- key: panel.props.id || `panel-${id}`,
298
297
  variant: this.props.variant,
299
- padding: panel.props.padding || this.props.padding,
300
- textAlign: panel.props.textAlign || this.props.textAlign,
301
298
  maxHeight: !hasFixedHeight ? this.props.maxHeight : void 0,
302
299
  minHeight: !hasFixedHeight ? this.props.minHeight : '100%'
303
- });
300
+ };
301
+ let activePanelClone = null;
302
+ if (activePanel !== void 0) {
303
+ // cloning active panel with a proper custom key as a workaround because
304
+ // safeCloneElement overwrites it with the key from the original element
305
+ activePanelClone = /*#__PURE__*/_react.default.cloneElement(activePanel, {
306
+ key: panel.props.id || `panel-${id}`
307
+ });
308
+ return (0, _safeCloneElement.safeCloneElement)(activePanelClone, {
309
+ padding: activePanelClone.props.padding || this.props.padding,
310
+ textAlign: activePanelClone.props.textAlign || this.props.textAlign,
311
+ ...commonProps
312
+ });
313
+ } else {
314
+ return (0, _safeCloneElement.safeCloneElement)(panel, {
315
+ key: panel.props.id || `panel-${id}`,
316
+ padding: panel.props.padding || this.props.padding,
317
+ textAlign: panel.props.textAlign || this.props.textAlign,
318
+ ...commonProps
319
+ });
320
+ }
304
321
  }
305
322
  focus() {
306
323
  this._focusable && typeof this._focusable.focus === 'function' && this._focusable.focus();
@@ -319,6 +336,10 @@ let Tabs = exports.Tabs = (_dec = (0, _emotion.withStyle)(_styles.default, _them
319
336
  tabOverflow = _this$props4.tabOverflow,
320
337
  styles = _this$props4.styles,
321
338
  props = (0, _objectWithoutProperties2.default)(_this$props4, _excluded);
339
+ const activePanels = _react.default.Children.toArray(children).filter(child => (0, _matchComponentTypes.matchComponentTypes)(child, [_Panel.Panel])).filter(child => child.props.active);
340
+ if (activePanels.length > 1) {
341
+ (0, _console.logError)(false, `[Tabs] Only one Panel can be marked as active.`);
342
+ }
322
343
  const selectedChildIndex = _react.default.Children.toArray(children).filter(child => (0, _matchComponentTypes.matchComponentTypes)(child, [_Panel.Panel])).findIndex(child => child.props.isSelected && !child.props.isDisabled);
323
344
  let index = 0;
324
345
  const selectedIndex = selectedChildIndex >= 0 ? selectedChildIndex : 0;
@@ -327,7 +348,11 @@ let Tabs = exports.Tabs = (_dec = (0, _emotion.withStyle)(_styles.default, _them
327
348
  const selected = !child.props.isDisabled && (child.props.isSelected || selectedIndex === index);
328
349
  const id = (0, _uid.uid)();
329
350
  tabs.push(this.createTab(index, id, selected, child));
330
- panels.push(this.clonePanel(index, id, selected, child));
351
+ if (activePanels.length === 1) {
352
+ panels.push(this.clonePanel(index, id, selected, child, activePanels[0]));
353
+ } else {
354
+ panels.push(this.clonePanel(index, id, selected, child));
355
+ }
331
356
  index++;
332
357
  } else {
333
358
  panels.push(child);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-tabs",
3
- "version": "8.46.2-snapshot-7",
3
+ "version": "8.46.2-snapshot-8",
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",
@@ -23,28 +23,30 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-babel-preset": "8.46.2-snapshot-7",
27
- "@instructure/ui-color-utils": "8.46.2-snapshot-7",
28
- "@instructure/ui-test-locator": "8.46.2-snapshot-7",
29
- "@instructure/ui-test-utils": "8.46.2-snapshot-7",
30
- "@instructure/ui-themes": "8.46.2-snapshot-7"
26
+ "@instructure/ui-babel-preset": "8.46.2-snapshot-8",
27
+ "@instructure/ui-color-utils": "8.46.2-snapshot-8",
28
+ "@instructure/ui-test-locator": "8.46.2-snapshot-8",
29
+ "@instructure/ui-test-utils": "8.46.2-snapshot-8",
30
+ "@instructure/ui-themes": "8.46.2-snapshot-8",
31
+ "@testing-library/jest-dom": "^6.1.4",
32
+ "@testing-library/react": "^14.0.0"
31
33
  },
32
34
  "dependencies": {
33
35
  "@babel/runtime": "^7.23.2",
34
- "@instructure/console": "8.46.2-snapshot-7",
35
- "@instructure/debounce": "8.46.2-snapshot-7",
36
- "@instructure/emotion": "8.46.2-snapshot-7",
37
- "@instructure/shared-types": "8.46.2-snapshot-7",
38
- "@instructure/ui-dom-utils": "8.46.2-snapshot-7",
39
- "@instructure/ui-focusable": "8.46.2-snapshot-7",
40
- "@instructure/ui-i18n": "8.46.2-snapshot-7",
41
- "@instructure/ui-motion": "8.46.2-snapshot-7",
42
- "@instructure/ui-prop-types": "8.46.2-snapshot-7",
43
- "@instructure/ui-react-utils": "8.46.2-snapshot-7",
44
- "@instructure/ui-testable": "8.46.2-snapshot-7",
45
- "@instructure/ui-utils": "8.46.2-snapshot-7",
46
- "@instructure/ui-view": "8.46.2-snapshot-7",
47
- "@instructure/uid": "8.46.2-snapshot-7",
36
+ "@instructure/console": "8.46.2-snapshot-8",
37
+ "@instructure/debounce": "8.46.2-snapshot-8",
38
+ "@instructure/emotion": "8.46.2-snapshot-8",
39
+ "@instructure/shared-types": "8.46.2-snapshot-8",
40
+ "@instructure/ui-dom-utils": "8.46.2-snapshot-8",
41
+ "@instructure/ui-focusable": "8.46.2-snapshot-8",
42
+ "@instructure/ui-i18n": "8.46.2-snapshot-8",
43
+ "@instructure/ui-motion": "8.46.2-snapshot-8",
44
+ "@instructure/ui-prop-types": "8.46.2-snapshot-8",
45
+ "@instructure/ui-react-utils": "8.46.2-snapshot-8",
46
+ "@instructure/ui-testable": "8.46.2-snapshot-8",
47
+ "@instructure/ui-utils": "8.46.2-snapshot-8",
48
+ "@instructure/ui-view": "8.46.2-snapshot-8",
49
+ "@instructure/uid": "8.46.2-snapshot-8",
48
50
  "keycode": "^2.2.1",
49
51
  "prop-types": "^15.8.1"
50
52
  },
@@ -55,7 +55,8 @@ class Panel extends Component<TabsPanelProps> {
55
55
  textAlign: 'start',
56
56
  variant: 'default',
57
57
  isSelected: false,
58
- padding: 'small'
58
+ padding: 'small',
59
+ active: false
59
60
  }
60
61
 
61
62
  componentDidMount() {
@@ -92,6 +93,7 @@ class Panel extends Component<TabsPanelProps> {
92
93
  isDisabled,
93
94
  isSelected,
94
95
  styles,
96
+ active,
95
97
  ...props
96
98
  } = this.props
97
99
  const isHidden = !isSelected || !!isDisabled
@@ -58,6 +58,11 @@ type TabsPanelOwnProps = {
58
58
  * provides a reference to the underlying html root element
59
59
  */
60
60
  elementRef?: (element: HTMLDivElement | null) => void
61
+ /**
62
+ * Only one `<Tabs.Panel />` can be marked as active. The marked panel's content is rendered
63
+ * for all the `<Tabs.Panel />`s.
64
+ */
65
+ active?: boolean
61
66
  }
62
67
 
63
68
  type PropKeys = keyof TabsPanelOwnProps
@@ -82,7 +87,8 @@ const propTypes: PropValidators<PropKeys> = {
82
87
  labelledBy: PropTypes.string,
83
88
  padding: ThemeablePropTypes.spacing,
84
89
  textAlign: PropTypes.oneOf(['start', 'center', 'end']),
85
- elementRef: PropTypes.func
90
+ elementRef: PropTypes.func,
91
+ active: PropTypes.bool
86
92
  }
87
93
 
88
94
  const allowedProps: AllowedPropKeys = [
@@ -97,7 +103,8 @@ const allowedProps: AllowedPropKeys = [
97
103
  'labelledBy',
98
104
  'padding',
99
105
  'textAlign',
100
- 'elementRef'
106
+ 'elementRef',
107
+ 'active'
101
108
  ]
102
109
 
103
110
  export type { TabsPanelProps, TabsPanelStyle }
@@ -256,6 +256,77 @@ class Example extends React.Component {
256
256
  render(<Example />)
257
257
  ```
258
258
 
259
+ ### Support for dynamic content with active panel
260
+
261
+ Marking one of the `<Tabs.Panel>` as `active` will render that panel's content in all the panels. This is useful for dynamic content rendering: the panel area can be used as a container, what routing libraries, such as React Router, can use to render their children elements into.
262
+
263
+ ```js
264
+ ---
265
+ example: true
266
+ render: false
267
+ ---
268
+ class Outlet extends React.Component {
269
+ state = {
270
+ show: false
271
+ }
272
+
273
+ componentDidMount() {
274
+ setTimeout(() => this.setState({ show: true }), 2000)
275
+ }
276
+
277
+ render() {
278
+ return (
279
+ <div>
280
+ <Heading level='h1' as='h1' margin='0 0 x-small'>
281
+ {this.state.show ? 'Hello Developer' : 'Simulating network call...'}
282
+ </Heading>
283
+ {this.state.show ? lorem.paragraphs() : <Spinner renderTitle='Loading' size='medium' />
284
+ }
285
+ </div>
286
+ )
287
+ }
288
+ }
289
+
290
+
291
+ class Example extends React.Component {
292
+ state = {
293
+ selectedIndex: 0
294
+ }
295
+ handleTabChange = (event, { index, id }) => {
296
+ this.setState({
297
+ selectedIndex: index
298
+ })
299
+ }
300
+
301
+ render() {
302
+ const { selectedIndex } = this.state
303
+ return (
304
+ <Tabs
305
+ margin='large auto'
306
+ padding='medium'
307
+ onRequestTabChange={this.handleTabChange}
308
+ >
309
+ <Tabs.Panel
310
+ id='tabA'
311
+ renderTitle='Tab A'
312
+ textAlign='center'
313
+ padding='large'
314
+ isSelected={selectedIndex === 0}
315
+ active
316
+ >
317
+ <Outlet />
318
+ </Tabs.Panel>
319
+ <Tabs.Panel id='tabB' renderTitle='Disabled Tab' isDisabled />
320
+ <Tabs.Panel id='tabC' renderTitle='Tab C' isSelected={selectedIndex === 2} />
321
+ <Tabs.Panel id='tabD' renderTitle='Tab D' isSelected={selectedIndex === 3} />
322
+ </Tabs>
323
+ )
324
+ }
325
+ }
326
+
327
+ render(<Example />)
328
+ ```
329
+
259
330
  ### Guidelines
260
331
 
261
332
  ```js