2019-01-18 13:43:17 -07:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 , 2024 The Matrix.org Foundation C.I.C.
2019-01-18 13:43:17 -07:00
Copyright 2019 New Vector Ltd
2024-09-09 14:57:16 +01:00
Copyright 2017 Travis Ralston
2019-01-18 13:43:17 -07:00
2025-01-06 11:18:54 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2019-01-18 13:43:17 -07:00
*/
import * as React from "react" ;
2021-10-22 17:23:32 -05:00
import classNames from "classnames" ;
2025-02-05 13:25:06 +00:00
import { _t , type TranslationKey } from "../../languageHandler" ;
2020-03-28 00:51:01 +00:00
import AutoHideScrollbar from "./AutoHideScrollbar" ;
2025-02-05 13:25:06 +00:00
import { PosthogScreenTracker , type ScreenName } from "../../PosthogTrackers" ;
import { type NonEmptyArray } from "../../@types/common" ;
2023-04-25 09:31:54 +01:00
import { RovingAccessibleButton , RovingTabIndexProvider } from "../../accessibility/RovingTabIndex" ;
2024-06-14 15:17:46 +01:00
import { useWindowWidth } from "../../hooks/useWindowWidth" ;
2019-01-18 13:43:17 -07:00
/**
2019-01-18 15:07:11 -07:00
* Represents a tab for the TabbedView.
2019-01-18 13:43:17 -07:00
*/
2023-04-27 12:55:29 +01:00
export class Tab < T extends string > {
2019-01-18 13:43:17 -07:00
/**
2019-01-18 15:07:11 -07:00
* Creates a new tab.
2020-06-08 08:20:15 -06:00
* @param {string} id The tab's ID.
* @param {string} label The untranslated tab label.
2024-08-06 17:08:45 +01:00
* @param {string|JSX.Element} icon An SVG element to use for the tab icon. Can also be a string for legacy icons, in which case it is the class for the tab icon. This should be a simple mask.
2020-06-08 08:20:15 -06:00
* @param {React.ReactNode} body The JSX for the tab container.
2022-02-09 14:25:58 +00:00
* @param {string} screenName The screen name to report to Posthog.
2019-01-18 13:43:17 -07:00
*/
2022-12-16 12:29:59 +00:00
public constructor (
2023-04-27 12:55:29 +01:00
public readonly id : T ,
2023-08-22 16:32:05 +01:00
public readonly label : TranslationKey ,
2024-08-06 17:08:45 +01:00
public readonly icon : string | JSX . Element | null ,
2022-02-09 14:25:58 +00:00
public readonly body : React.ReactNode ,
public readonly screenName? : ScreenName ,
) {}
2019-01-18 13:43:17 -07:00
}
2024-05-03 16:01:01 +01:00
export function useActiveTabWithDefault < T extends string >(
tabs : NonEmptyArray < Tab < string >>,
defaultTabID : T ,
initialTabID? : T ,
) : [ T , ( tabId : T ) => void ] {
const [ activeTabId , setActiveTabId ] = React . useState (
initialTabID && tabs . some (( t ) => t . id === initialTabID ) ? initialTabID : defaultTabID ,
);
return [ activeTabId , setActiveTabId ];
}
2021-07-15 09:55:58 +01:00
export enum TabLocation {
LEFT = "left" ,
TOP = "top" ,
}
2024-05-03 13:59:56 +01:00
interface ITabPanelProps < T extends string > {
tab : Tab < T >;
}
function domIDForTabID ( tabId : string ) : string {
return `mx_tabpanel_ ${ tabId } ` ;
}
function TabPanel < T extends string >({ tab } : ITabPanelProps < T >) : JSX . Element {
return (
< div
className = "mx_TabbedView_tabPanel"
key = { tab . id }
id = { domIDForTabID ( tab . id )}
aria-labelledby = { ` ${ domIDForTabID ( tab . id ) } _label` }
>
< AutoHideScrollbar className = "mx_TabbedView_tabPanelContent" >{ tab . body }</ AutoHideScrollbar >
</ div >
);
}
interface ITabLabelProps < T extends string > {
tab : Tab < T >;
isActive : boolean ;
2024-06-14 15:17:46 +01:00
showToolip : boolean ;
2024-05-03 13:59:56 +01:00
onClick : () => void ;
}
2024-06-14 15:17:46 +01:00
function TabLabel < T extends string >({ tab , isActive , showToolip , onClick } : ITabLabelProps < T >) : JSX . Element {
2024-05-03 13:59:56 +01:00
const classes = classNames ( "mx_TabbedView_tabLabel" , {
mx_TabbedView_tabLabel_active : isActive ,
});
let tabIcon : JSX.Element | undefined ;
if ( tab . icon ) {
2024-08-06 17:08:45 +01:00
if ( typeof tab . icon === "object" ) {
tabIcon = tab . icon ;
} else if ( typeof tab . icon === "string" ) {
tabIcon = < span className = { `mx_TabbedView_maskedIcon ${ tab . icon } ` } />;
}
2024-05-03 13:59:56 +01:00
}
const id = domIDForTabID ( tab . id );
const label = _t ( tab . label );
return (
< RovingAccessibleButton
className = { classes }
onClick = { onClick }
data-testid = { `settings-tab- ${ tab . id } ` }
role = "tab"
aria-selected = { isActive }
aria-controls = { id }
element = "li"
2024-06-14 15:17:46 +01:00
title = { showToolip ? label : undefined }
2024-05-03 13:59:56 +01:00
>
{ tabIcon }
< span className = "mx_TabbedView_tabLabel_text" id = { ` ${ id } _label` }>
{ label }
</ span >
</ RovingAccessibleButton >
);
}
2023-04-27 12:55:29 +01:00
interface IProps < T extends string > {
2024-05-03 13:59:56 +01:00
// An array of objects representign tabs that the tabbed view will display.
2023-04-27 12:55:29 +01:00
tabs : NonEmptyArray < Tab < T >>;
2024-05-03 16:01:01 +01:00
// The ID of the tab to show
activeTabId : T ;
2024-05-03 13:59:56 +01:00
// The location of the tabs, dictating the layout of the TabbedView.
tabLocation? : TabLocation ;
2024-05-03 16:01:01 +01:00
// A callback that is called when the active tab should change
onChange : ( tabId : T ) => void ;
2024-05-03 13:59:56 +01:00
// The screen name to report to Posthog.
2022-02-09 14:25:58 +00:00
screenName? : ScreenName ;
2024-05-10 17:29:50 +01:00
/**
* If true, the layout of the tabbed view will be responsive to the viewport size (eg, just showing icons
* instead of names of tabs).
* Only applies if `tabLocation === TabLocation.LEFT`.
* Default: false.
*/
responsive? : boolean ;
2020-03-11 18:02:17 -06:00
}
2024-05-03 13:59:56 +01:00
/**
* 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 < T extends string >( props : IProps < T >) : JSX . Element {
const tabLocation = props . tabLocation ?? TabLocation . LEFT ;
2019-01-18 13:43:17 -07:00
2024-05-03 13:59:56 +01:00
const getTabById = ( id : T ) : Tab < T > | undefined => {
return props . tabs . find (( tab ) => tab . id === id );
2021-07-15 09:55:58 +01:00
};
2024-06-14 15:17:46 +01:00
const windowWidth = useWindowWidth ();
2024-05-03 13:59:56 +01:00
const labels = props . tabs . map (( tab ) => (
< TabLabel
key = { "tab_label_" + tab . id }
tab = { tab }
2024-05-03 16:01:01 +01:00
isActive = { tab . id === props . activeTabId }
onClick = {() => props . onChange ( tab . id )}
2024-06-14 15:17:46 +01:00
// This should be the same as the the CSS breakpoint at which the tab labels are hidden
showToolip = { windowWidth < 1024 && tabLocation == TabLocation . LEFT }
2024-05-03 13:59:56 +01:00
/>
));
2024-05-03 16:01:01 +01:00
const tab = getTabById ( props . activeTabId );
2024-05-03 13:59:56 +01:00
const panel = tab ? < TabPanel tab = { tab } /> : null ;
2019-01-18 13:43:17 -07:00
2024-05-03 13:59:56 +01:00
const tabbedViewClasses = classNames ({
mx_TabbedView : true ,
mx_TabbedView_tabsOnLeft : tabLocation == TabLocation . LEFT ,
mx_TabbedView_tabsOnTop : tabLocation == TabLocation . TOP ,
2024-05-10 17:29:50 +01:00
mx_TabbedView_responsive : props.responsive ,
2024-05-03 13:59:56 +01:00
});
2019-01-18 19:40:21 -07:00
2024-05-03 13:59:56 +01:00
const screenName = tab ? . screenName ?? props . screenName ;
2019-01-21 17:49:48 -07:00
2024-05-03 13:59:56 +01:00
return (
< div className = { tabbedViewClasses }>
{ screenName && < PosthogScreenTracker screenName = { screenName } />}
< RovingTabIndexProvider
handleLoop
handleHomeEnd
handleLeftRight = { tabLocation == TabLocation . TOP }
handleUpDown = { tabLocation == TabLocation . LEFT }
2022-08-01 08:47:13 +02:00
>
2024-05-03 13:59:56 +01:00
{({ onKeyDownHandler }) => (
< ul
className = "mx_TabbedView_tabLabels"
role = "tablist"
aria-orientation = { tabLocation == TabLocation . LEFT ? "vertical" : "horizontal" }
onKeyDown = { onKeyDownHandler }
>
{ labels }
</ ul >
)}
</ RovingTabIndexProvider >
{ panel }
</ div >
);
2019-01-21 17:49:48 -07:00
}