@instructure/ui-tabs 8.46.2-snapshot-7 → 8.46.2-snapshot-9
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 +5 -2
- package/es/Tabs/Panel/index.js +4 -2
- package/es/Tabs/Panel/props.js +3 -2
- package/es/Tabs/__new-tests__/Tabs.test.js +120 -0
- package/es/Tabs/index.js +32 -7
- package/lib/Tabs/Panel/index.js +4 -2
- package/lib/Tabs/Panel/props.js +3 -2
- package/lib/Tabs/__new-tests__/Tabs.test.js +122 -0
- package/lib/Tabs/index.js +32 -7
- package/package.json +22 -20
- package/src/Tabs/Panel/index.tsx +3 -1
- package/src/Tabs/Panel/props.ts +9 -2
- package/src/Tabs/README.md +71 -0
- package/src/Tabs/__new-tests__/Tabs.test.tsx +128 -0
- package/src/Tabs/index.tsx +43 -8
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/Tabs/Panel/index.d.ts +3 -0
- package/types/Tabs/Panel/index.d.ts.map +1 -1
- package/types/Tabs/Panel/props.d.ts +5 -0
- package/types/Tabs/Panel/props.d.ts.map +1 -1
- package/types/Tabs/__new-tests__/Tabs.test.d.ts +2 -0
- package/types/Tabs/__new-tests__/Tabs.test.d.ts.map +1 -0
- package/types/Tabs/index.d.ts +1 -1
- package/types/Tabs/index.d.ts.map +1 -1
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-
|
|
6
|
+
## [8.46.2-snapshot-9](https://github.com/instructure/instructure-ui/compare/v8.46.1...v8.46.2-snapshot-9) (2023-10-24)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
package/es/Tabs/Panel/index.js
CHANGED
|
@@ -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 };
|
package/es/Tabs/Panel/props.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/lib/Tabs/Panel/index.js
CHANGED
|
@@ -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;
|
package/lib/Tabs/Panel/props.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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-
|
|
3
|
+
"version": "8.46.2-snapshot-9",
|
|
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-
|
|
27
|
-
"@instructure/ui-color-utils": "8.46.2-snapshot-
|
|
28
|
-
"@instructure/ui-test-locator": "8.46.2-snapshot-
|
|
29
|
-
"@instructure/ui-test-utils": "8.46.2-snapshot-
|
|
30
|
-
"@instructure/ui-themes": "8.46.2-snapshot-
|
|
26
|
+
"@instructure/ui-babel-preset": "8.46.2-snapshot-9",
|
|
27
|
+
"@instructure/ui-color-utils": "8.46.2-snapshot-9",
|
|
28
|
+
"@instructure/ui-test-locator": "8.46.2-snapshot-9",
|
|
29
|
+
"@instructure/ui-test-utils": "8.46.2-snapshot-9",
|
|
30
|
+
"@instructure/ui-themes": "8.46.2-snapshot-9",
|
|
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-
|
|
35
|
-
"@instructure/debounce": "8.46.2-snapshot-
|
|
36
|
-
"@instructure/emotion": "8.46.2-snapshot-
|
|
37
|
-
"@instructure/shared-types": "8.46.2-snapshot-
|
|
38
|
-
"@instructure/ui-dom-utils": "8.46.2-snapshot-
|
|
39
|
-
"@instructure/ui-focusable": "8.46.2-snapshot-
|
|
40
|
-
"@instructure/ui-i18n": "8.46.2-snapshot-
|
|
41
|
-
"@instructure/ui-motion": "8.46.2-snapshot-
|
|
42
|
-
"@instructure/ui-prop-types": "8.46.2-snapshot-
|
|
43
|
-
"@instructure/ui-react-utils": "8.46.2-snapshot-
|
|
44
|
-
"@instructure/ui-testable": "8.46.2-snapshot-
|
|
45
|
-
"@instructure/ui-utils": "8.46.2-snapshot-
|
|
46
|
-
"@instructure/ui-view": "8.46.2-snapshot-
|
|
47
|
-
"@instructure/uid": "8.46.2-snapshot-
|
|
36
|
+
"@instructure/console": "8.46.2-snapshot-9",
|
|
37
|
+
"@instructure/debounce": "8.46.2-snapshot-9",
|
|
38
|
+
"@instructure/emotion": "8.46.2-snapshot-9",
|
|
39
|
+
"@instructure/shared-types": "8.46.2-snapshot-9",
|
|
40
|
+
"@instructure/ui-dom-utils": "8.46.2-snapshot-9",
|
|
41
|
+
"@instructure/ui-focusable": "8.46.2-snapshot-9",
|
|
42
|
+
"@instructure/ui-i18n": "8.46.2-snapshot-9",
|
|
43
|
+
"@instructure/ui-motion": "8.46.2-snapshot-9",
|
|
44
|
+
"@instructure/ui-prop-types": "8.46.2-snapshot-9",
|
|
45
|
+
"@instructure/ui-react-utils": "8.46.2-snapshot-9",
|
|
46
|
+
"@instructure/ui-testable": "8.46.2-snapshot-9",
|
|
47
|
+
"@instructure/ui-utils": "8.46.2-snapshot-9",
|
|
48
|
+
"@instructure/ui-view": "8.46.2-snapshot-9",
|
|
49
|
+
"@instructure/uid": "8.46.2-snapshot-9",
|
|
48
50
|
"keycode": "^2.2.1",
|
|
49
51
|
"prop-types": "^15.8.1"
|
|
50
52
|
},
|
package/src/Tabs/Panel/index.tsx
CHANGED
|
@@ -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
|
package/src/Tabs/Panel/props.ts
CHANGED
|
@@ -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 }
|
package/src/Tabs/README.md
CHANGED
|
@@ -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
|