diff --git a/package-lock.json b/package-lock.json index f376da29ad..b10b72804d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55791,7 +55791,9 @@ "@instructure/ui-color-utils": "8.46.1", "@instructure/ui-test-locator": "8.46.1", "@instructure/ui-test-utils": "8.46.1", - "@instructure/ui-themes": "8.46.1" + "@instructure/ui-themes": "8.46.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0" }, "peerDependencies": { "react": ">=16.8 <=18" @@ -61043,6 +61045,8 @@ "@instructure/ui-utils": "8.46.1", "@instructure/ui-view": "8.46.1", "@instructure/uid": "8.46.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0", "keycode": "^2.2.1", "prop-types": "^15.8.1" } diff --git a/packages/ui-tabs/package.json b/packages/ui-tabs/package.json index 9917143c65..4f16391180 100644 --- a/packages/ui-tabs/package.json +++ b/packages/ui-tabs/package.json @@ -27,7 +27,9 @@ "@instructure/ui-color-utils": "8.46.1", "@instructure/ui-test-locator": "8.46.1", "@instructure/ui-test-utils": "8.46.1", - "@instructure/ui-themes": "8.46.1" + "@instructure/ui-themes": "8.46.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0" }, "dependencies": { "@babel/runtime": "^7.22.15", diff --git a/packages/ui-tabs/src/Tabs/Panel/index.tsx b/packages/ui-tabs/src/Tabs/Panel/index.tsx index 985cba5800..23cbf1368c 100644 --- a/packages/ui-tabs/src/Tabs/Panel/index.tsx +++ b/packages/ui-tabs/src/Tabs/Panel/index.tsx @@ -56,7 +56,7 @@ class Panel extends Component { variant: 'default', isSelected: false, padding: 'small', - active: 'false' + active: false } componentDidMount() { diff --git a/packages/ui-tabs/src/Tabs/__new-tests__/Tabs.test.tsx b/packages/ui-tabs/src/Tabs/__new-tests__/Tabs.test.tsx new file mode 100644 index 0000000000..7eb26421ba --- /dev/null +++ b/packages/ui-tabs/src/Tabs/__new-tests__/Tabs.test.tsx @@ -0,0 +1,85 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from 'react' + +import { Tabs } from '../index' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' + +describe('', () => { + it('should render the correct number of panels', () => { + const { container } = render( + + Tab 1 content + Tab 2 content + + Tab 3 content + + + ) + + expect(container.firstChild).toBeInTheDocument() + }) + + it('should render same content for other tabs as for the active one', () => { + const { container } = render( + + + CONTENT + + + + + ) + + const tabPanel = screen.getByText('CONTENT') + + expect(container).toBeInTheDocument() + expect(tabPanel).toBeInTheDocument() + }) + + it('should warn if multiple active tabs exist', () => { + const consoleMock = jest.spyOn(console, 'error').mockImplementation() + const { container } = render( + + + Tab 1 content + + + Tab 2 content + + + Tab 3 content + + + ) + + expect(container.firstChild).toBeInTheDocument() + + expect(consoleMock.mock.calls[0][0]).toEqual( + 'Warning: [Tabs] Only one Panel can be marked as active.' + ) + }) +}) diff --git a/packages/ui-tabs/src/Tabs/index.tsx b/packages/ui-tabs/src/Tabs/index.tsx index ecec90277e..6fbf02ae01 100644 --- a/packages/ui-tabs/src/Tabs/index.tsx +++ b/packages/ui-tabs/src/Tabs/index.tsx @@ -27,7 +27,8 @@ import React, { Component, ComponentClass, ComponentElement, - createElement + createElement, + ReactElement } from 'react' import keycode from 'keycode' @@ -359,24 +360,44 @@ class Tabs extends Component { _index: number, generatedId: string, selected: boolean, - panel: PanelChild + panel: PanelChild, + activePanel?: PanelChild ) { const id = panel.props.id || generatedId // fixHeight can be 0, so simply `fixheight` could return falsy value const hasFixedHeight = typeof this.props.fixHeight !== 'undefined' - return safeCloneElement(panel, { + const commonProps = { id: panel.props.id || `panel-${id}`, labelledBy: `tab-${id}`, isSelected: selected, - key: panel.props.id || `panel-${id}`, variant: this.props.variant, - padding: panel.props.padding || this.props.padding, - textAlign: panel.props.textAlign || this.props.textAlign, maxHeight: !hasFixedHeight ? this.props.maxHeight : undefined, minHeight: !hasFixedHeight ? this.props.minHeight : '100%' - } as TabsPanelProps & { key: string }) as PanelChild + } + + let activePanelClone = null + if (activePanel !== undefined) { + // cloning active panel with a proper custom key as a workaround because + // safeCloneElement overwrites it with the key from the original element + activePanelClone = React.cloneElement(activePanel as ReactElement, { + key: panel.props.id || `panel-${id}` + }) + + return safeCloneElement(activePanelClone, { + padding: activePanelClone.props.padding || this.props.padding, + textAlign: activePanelClone.props.textAlign || this.props.textAlign, + ...commonProps + } as TabsPanelProps & { key: string }) as PanelChild + } else { + return safeCloneElement(panel, { + key: panel.props.id || `panel-${id}`, + padding: panel.props.padding || this.props.padding, + textAlign: panel.props.textAlign || this.props.textAlign, + ...commonProps + } as TabsPanelProps & { key: string }) as PanelChild + } } handleFocusableRef = (el: Focusable | null) => { @@ -456,7 +477,9 @@ class Tabs extends Component { tabs.push(this.createTab(index, id, selected, child)) if (activePanels.length === 1) { - panels.push(this.clonePanel(index, id, selected, activePanels[0])) + panels.push( + this.clonePanel(index, id, selected, child, activePanels[0]) + ) } else { panels.push(this.clonePanel(index, id, selected, child)) }