diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index a55e514073..9a5f36ea0b 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -60,21 +60,16 @@ interface IProps { } interface IState { - activeTabIndex: number; + activeTabId: string; } export default class TabbedView extends React.Component { constructor(props: IProps) { super(props); - let activeTabIndex = 0; - if (props.initialTabId) { - const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId); - if (tabIndex >= 0) activeTabIndex = tabIndex; - } - + const initialTabIdIsValid = props.tabs.find(tab => tab.id === props.initialTabId); this.state = { - activeTabIndex, + activeTabId: initialTabIdIsValid ? props.initialTabId : props.tabs[0]?.id, }; } @@ -82,9 +77,8 @@ export default class TabbedView extends React.Component { tabLocation: TabLocation.LEFT, }; - private getActiveTabIndex() { - if (!this.state || !this.state.activeTabIndex) return 0; - return this.state.activeTabIndex; + private getTabById(id: string): Tab | undefined { + return this.props.tabs.find(tab => tab.id === id); } /** @@ -93,10 +87,10 @@ export default class TabbedView extends React.Component { * @private */ private setActiveTab(tab: Tab) { - const idx = this.props.tabs.indexOf(tab); - if (idx !== -1) { + // make sure this tab is still in available tabs + if (!!this.getTabById(tab.id)) { if (this.props.onChange) this.props.onChange(tab.id); - this.setState({ activeTabIndex: idx }); + this.setState({ activeTabId: tab.id }); } else { logger.error("Could not find tab " + tab.label + " in tabs"); } @@ -105,8 +99,7 @@ export default class TabbedView extends React.Component { private renderTabLabel(tab: Tab) { let classes = "mx_TabbedView_tabLabel "; - const idx = this.props.tabs.indexOf(tab); - if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active"; + if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active"; let tabIcon = null; if (tab.icon) { @@ -143,8 +136,8 @@ export default class TabbedView extends React.Component { public render(): React.ReactNode { const labels = this.props.tabs.map(tab => this.renderTabLabel(tab)); - const tab = this.props.tabs[this.getActiveTabIndex()]; - const panel = this.renderTabPanel(tab); + const tab = this.getTabById(this.state.activeTabId); + const panel = tab ? this.renderTabPanel(tab) : null; const tabbedViewClasses = classNames({ 'mx_TabbedView': true, diff --git a/test/components/structures/TabbedView-test.tsx b/test/components/structures/TabbedView-test.tsx new file mode 100644 index 0000000000..e690a10ac3 --- /dev/null +++ b/test/components/structures/TabbedView-test.tsx @@ -0,0 +1,133 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { fireEvent, render } from "@testing-library/react"; +import { act } from 'react-dom/test-utils'; + +import TabbedView, { Tab, TabLocation } from "../../../src/components/structures/TabbedView"; + +describe('', () => { + const generalTab = new Tab( + 'GENERAL', + 'General', + 'general', +
general
, + ); + const labsTab = new Tab( + 'LABS', + 'Labs', + 'labs', +
labs
, + ); + const securityTab = new Tab( + 'SECURITY', + 'Security', + 'security', +
security
, + ); + const defaultProps = { + tabLocation: TabLocation.LEFT, + tabs: [generalTab, labsTab, securityTab], + }; + const getComponent = (props = {}): React.ReactElement => ; + + const getTabTestId = (tab: Tab): string => `settings-tab-${tab.id}`; + const getActiveTab = (container: HTMLElement): Element | undefined => + container.getElementsByClassName('mx_TabbedView_tabLabel_active')[0]; + const getActiveTabBody = (container: HTMLElement): Element | undefined => + container.getElementsByClassName('mx_TabbedView_tabPanel')[0]; + + it('renders tabs', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders first tab as active tab when no initialTabId', () => { + const { container } = render(getComponent()); + expect(getActiveTab(container).textContent).toEqual(generalTab.label); + expect(getActiveTabBody(container).textContent).toEqual('general'); + }); + + it('renders first tab as active tab when initialTabId is not valid', () => { + const { container } = render(getComponent({ initialTabId: 'bad-tab-id' })); + expect(getActiveTab(container).textContent).toEqual(generalTab.label); + expect(getActiveTabBody(container).textContent).toEqual('general'); + }); + + it('renders initialTabId tab as active when valid', () => { + const { container } = render(getComponent({ initialTabId: securityTab.id })); + expect(getActiveTab(container).textContent).toEqual(securityTab.label); + expect(getActiveTabBody(container).textContent).toEqual('security'); + }); + + it('renders without error when there are no tabs', () => { + const { container } = render(getComponent({ tabs: [] })); + expect(container).toMatchSnapshot(); + }); + + it('sets active tab on tab click', () => { + const { container, getByTestId } = render(getComponent()); + + act(() => { + fireEvent.click(getByTestId(getTabTestId(securityTab))); + }); + + expect(getActiveTab(container).textContent).toEqual(securityTab.label); + expect(getActiveTabBody(container).textContent).toEqual('security'); + }); + + it('calls onchange on on tab click', () => { + const onChange = jest.fn(); + const { getByTestId } = render(getComponent({ onChange })); + + act(() => { + fireEvent.click(getByTestId(getTabTestId(securityTab))); + }); + + expect(onChange).toHaveBeenCalledWith(securityTab.id); + }); + + it('keeps same tab active when order of tabs changes', () => { + // start with middle tab active + const { container, rerender } = render(getComponent({ initialTabId: labsTab.id })); + + expect(getActiveTab(container).textContent).toEqual(labsTab.label); + + rerender(getComponent({ tabs: [labsTab, generalTab, securityTab] })); + + // labs tab still active + expect(getActiveTab(container).textContent).toEqual(labsTab.label); + }); + + it('does not reactivate inititalTabId on rerender', () => { + const { container, getByTestId, rerender } = render(getComponent()); + + expect(getActiveTab(container).textContent).toEqual(generalTab.label); + + // make security tab active + act(() => { + fireEvent.click(getByTestId(getTabTestId(securityTab))); + }); + expect(getActiveTab(container).textContent).toEqual(securityTab.label); + + // rerender with new tab location + rerender(getComponent({ tabLocation: TabLocation.TOP })); + + // still security tab + expect(getActiveTab(container).textContent).toEqual(securityTab.label); + }); +}); diff --git a/test/components/structures/__snapshots__/TabbedView-test.tsx.snap b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap new file mode 100644 index 0000000000..bee195c1a0 --- /dev/null +++ b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders tabs 1`] = ` +
+
+
+
+ + + General + +
+
+ + + Labs + +
+
+ + + Security + +
+
+
+
+
+ general +
+
+
+
+
+`; + +exports[` renders without error when there are no tabs 1`] = ` +
+
+
+
+
+`;