diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 61e34d2d0d..d6b88b1d4f 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020, 2024 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. @@ -52,133 +52,148 @@ export enum TabLocation { TOP = "top", } +interface ITabPanelProps { + tab: Tab; +} + +function domIDForTabID(tabId: string): string { + return `mx_tabpanel_${tabId}`; +} + +function TabPanel({ tab }: ITabPanelProps): JSX.Element { + return ( +
+ {tab.body} +
+ ); +} + +interface ITabLabelProps { + tab: Tab; + isActive: boolean; + onClick: () => void; +} + +function TabLabel({ tab, isActive, onClick }: ITabLabelProps): JSX.Element { + const classes = classNames("mx_TabbedView_tabLabel", { + mx_TabbedView_tabLabel_active: isActive, + }); + + let tabIcon: JSX.Element | undefined; + if (tab.icon) { + tabIcon = ; + } + + const id = domIDForTabID(tab.id); + + const label = _t(tab.label); + return ( + + {tabIcon} + + {label} + + + ); +} + interface IProps { + // An array of objects representign tabs that the tabbed view will display. tabs: NonEmptyArray>; + // The ID of the tab to display initially. initialTabId?: T; - tabLocation: TabLocation; + // The location of the tabs, dictating the layout of the TabbedView. + tabLocation?: TabLocation; + // A callback that is called when the active tab changes. onChange?: (tabId: T) => void; + // The screen name to report to Posthog. screenName?: ScreenName; } -interface IState { - activeTabId: T; -} - -export default class TabbedView extends React.Component, IState> { - public constructor(props: IProps) { - super(props); +/** + * A tabbed view component. Given objects representing content with titles, displays + * them in a tabbed view where the user can select which one of the items to view at once. + */ +export default function TabbedView(props: IProps): JSX.Element { + const tabLocation = props.tabLocation ?? TabLocation.LEFT; + const [activeTabId, setActiveTabId] = React.useState((): T => { const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); - this.state = { - activeTabId: initialTabIdIsValid ? props.initialTabId! : props.tabs[0].id, - }; - } + // unfortunately typescript doesn't infer the types coorectly if the null check is included above + return initialTabIdIsValid && props.initialTabId ? props.initialTabId : props.tabs[0].id; + }); - public static defaultProps = { - tabLocation: TabLocation.LEFT, + const getTabById = (id: T): Tab | undefined => { + return props.tabs.find((tab) => tab.id === id); }; - private getTabById(id: T): Tab | undefined { - return this.props.tabs.find((tab) => tab.id === id); - } - /** * Shows the given tab * @param {Tab} tab the tab to show - * @private */ - private setActiveTab(tab: Tab): void { + const setActiveTab = (tab: Tab): void => { // 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({ activeTabId: tab.id }); + if (!!getTabById(tab.id)) { + props.onChange?.(tab.id); + setActiveTabId(tab.id); } else { logger.error("Could not find tab " + tab.label + " in tabs"); } - } + }; - private renderTabLabel(tab: Tab): JSX.Element { - const isActive = this.state.activeTabId === tab.id; - const classes = classNames("mx_TabbedView_tabLabel", { - mx_TabbedView_tabLabel_active: isActive, - }); + const labels = props.tabs.map((tab) => ( + setActiveTab(tab)} + /> + )); + const tab = getTabById(activeTabId); + const panel = tab ? : null; - let tabIcon: JSX.Element | undefined; - if (tab.icon) { - tabIcon = ; - } + const tabbedViewClasses = classNames({ + mx_TabbedView: true, + mx_TabbedView_tabsOnLeft: tabLocation == TabLocation.LEFT, + mx_TabbedView_tabsOnTop: tabLocation == TabLocation.TOP, + }); - const onClickHandler = (): void => this.setActiveTab(tab); - const id = this.getTabId(tab); + const screenName = tab?.screenName ?? props.screenName; - const label = _t(tab.label); - return ( - + {screenName && } + - {tabIcon} - - {label} - - - ); - } - - private getTabId(tab: Tab): string { - return `mx_tabpanel_${tab.id}`; - } - - private renderTabPanel(tab: Tab): React.ReactNode { - const id = this.getTabId(tab); - return ( -
- {tab.body} -
- ); - } - - public render(): React.ReactNode { - const labels = this.props.tabs.map((tab) => this.renderTabLabel(tab)); - const tab = this.getTabById(this.state.activeTabId); - const panel = tab ? this.renderTabPanel(tab) : null; - - const tabbedViewClasses = classNames({ - mx_TabbedView: true, - mx_TabbedView_tabsOnLeft: this.props.tabLocation == TabLocation.LEFT, - mx_TabbedView_tabsOnTop: this.props.tabLocation == TabLocation.TOP, - }); - - const screenName = tab?.screenName ?? this.props.screenName; - - return ( -
- {screenName && } - - {({ onKeyDownHandler }) => ( -
    - {labels} -
- )} -
- {panel} -
- ); - } + {({ onKeyDownHandler }) => ( +
    + {labels} +
+ )} + + {panel} + + ); }