Use generics to better type TabbedView (#10726)

This commit is contained in:
Michael Telatynski 2023-04-27 12:55:29 +01:00 committed by GitHub
parent fcf2fe2c1d
commit a629ce3a53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 56 additions and 52 deletions

View file

@ -32,7 +32,7 @@ import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore";
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
import defaultDispatcher from "./dispatcher/dispatcher"; import defaultDispatcher from "./dispatcher/dispatcher";
import { MatrixClientPeg } from "./MatrixClientPeg"; import { MatrixClientPeg } from "./MatrixClientPeg";
import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; import { RoomSettingsTab } from "./components/views/dialogs/RoomSettingsDialog";
import AccessibleButton, { ButtonEvent } from "./components/views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore"; import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
@ -236,7 +236,7 @@ function textForTombstoneEvent(ev: MatrixEvent): (() => string) | null {
const onViewJoinRuleSettingsClick = (): void => { const onViewJoinRuleSettingsClick = (): void => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "open_room_settings", action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB, initial_tab_id: RoomSettingsTab.Security,
}); });
}; };

View file

@ -29,7 +29,7 @@ import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibil
/** /**
* Represents a tab for the TabbedView. * Represents a tab for the TabbedView.
*/ */
export class Tab { export class Tab<T extends string> {
/** /**
* Creates a new tab. * Creates a new tab.
* @param {string} id The tab's ID. * @param {string} id The tab's ID.
@ -39,7 +39,7 @@ export class Tab {
* @param {string} screenName The screen name to report to Posthog. * @param {string} screenName The screen name to report to Posthog.
*/ */
public constructor( public constructor(
public readonly id: string, public readonly id: T,
public readonly label: string, public readonly label: string,
public readonly icon: string | null, public readonly icon: string | null,
public readonly body: React.ReactNode, public readonly body: React.ReactNode,
@ -52,20 +52,20 @@ export enum TabLocation {
TOP = "top", TOP = "top",
} }
interface IProps { interface IProps<T extends string> {
tabs: NonEmptyArray<Tab>; tabs: NonEmptyArray<Tab<T>>;
initialTabId?: string; initialTabId?: T;
tabLocation: TabLocation; tabLocation: TabLocation;
onChange?: (tabId: string) => void; onChange?: (tabId: T) => void;
screenName?: ScreenName; screenName?: ScreenName;
} }
interface IState { interface IState<T extends string> {
activeTabId: string; activeTabId: T;
} }
export default class TabbedView extends React.Component<IProps, IState> { export default class TabbedView<T extends string> extends React.Component<IProps<T>, IState<T>> {
public constructor(props: IProps) { public constructor(props: IProps<T>) {
super(props); super(props);
const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId);
@ -78,7 +78,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
tabLocation: TabLocation.LEFT, tabLocation: TabLocation.LEFT,
}; };
private getTabById(id: string): Tab | undefined { private getTabById(id: T): Tab<T> | undefined {
return this.props.tabs.find((tab) => tab.id === id); return this.props.tabs.find((tab) => tab.id === id);
} }
@ -87,7 +87,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
* @param {Tab} tab the tab to show * @param {Tab} tab the tab to show
* @private * @private
*/ */
private setActiveTab(tab: Tab): void { private setActiveTab(tab: Tab<T>): void {
// make sure this tab is still in available tabs // make sure this tab is still in available tabs
if (!!this.getTabById(tab.id)) { if (!!this.getTabById(tab.id)) {
if (this.props.onChange) this.props.onChange(tab.id); if (this.props.onChange) this.props.onChange(tab.id);
@ -97,7 +97,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
} }
} }
private renderTabLabel(tab: Tab): JSX.Element { private renderTabLabel(tab: Tab<T>): JSX.Element {
const isActive = this.state.activeTabId === tab.id; const isActive = this.state.activeTabId === tab.id;
const classes = classNames("mx_TabbedView_tabLabel", { const classes = classNames("mx_TabbedView_tabLabel", {
mx_TabbedView_tabLabel_active: isActive, mx_TabbedView_tabLabel_active: isActive,
@ -130,11 +130,11 @@ export default class TabbedView extends React.Component<IProps, IState> {
); );
} }
private getTabId(tab: Tab): string { private getTabId(tab: Tab<T>): string {
return `mx_tabpanel_${tab.id}`; return `mx_tabpanel_${tab.id}`;
} }
private renderTabPanel(tab: Tab): React.ReactNode { private renderTabPanel(tab: Tab<T>): React.ReactNode {
const id = this.getTabId(tab); const id = this.getTabId(tab);
return ( return (
<div className="mx_TabbedView_tabPanel" key={id} id={id} aria-labelledby={`${id}_label`}> <div className="mx_TabbedView_tabPanel" key={id} id={id} aria-labelledby={`${id}_label`}>

View file

@ -38,7 +38,7 @@ import ExportDialog from "../dialogs/ExportDialog";
import { useFeatureEnabled } from "../../../hooks/useSettings"; import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard"; import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
@ -199,7 +199,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
dis.dispatch({ dis.dispatch({
action: "open_room_settings", action: "open_room_settings",
room_id: room.roomId, room_id: room.roomId,
initial_tab_id: ROOM_NOTIFICATIONS_TAB, initial_tab_id: RoomSettingsTab.Notifications,
}); });
onFinished(); onFinished();

View file

@ -1494,7 +1494,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
let dialogContent; let dialogContent;
if (this.props.kind === InviteKind.CallTransfer) { if (this.props.kind === InviteKind.CallTransfer) {
const tabs: NonEmptyArray<Tab> = [ const tabs: NonEmptyArray<Tab<TabId>> = [
new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection), new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection),
]; ];

View file

@ -40,14 +40,16 @@ import { NonEmptyArray } from "../../../@types/common";
import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab";
import ErrorBoundary from "../elements/ErrorBoundary"; import ErrorBoundary from "../elements/ErrorBoundary";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; export const enum RoomSettingsTab {
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB"; General = "ROOM_GENERAL_TAB",
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; Voip = "ROOM_VOIP_TAB",
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; Security = "ROOM_SECURITY_TAB",
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; Roles = "ROOM_ROLES_TAB",
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; Notifications = "ROOM_NOTIFICATIONS_TAB",
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; Bridges = "ROOM_BRIDGES_TAB",
export const ROOM_POLL_HISTORY_TAB = "ROOM_POLL_HISTORY_TAB"; Advanced = "ROOM_ADVANCED_TAB",
PollHistory = "ROOM_POLL_HISTORY_TAB",
}
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -118,12 +120,12 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
this.forceUpdate(); this.forceUpdate();
}; };
private getTabs(): NonEmptyArray<Tab> { private getTabs(): NonEmptyArray<Tab<RoomSettingsTab>> {
const tabs: Tab[] = []; const tabs: Tab<RoomSettingsTab>[] = [];
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_GENERAL_TAB, RoomSettingsTab.General,
_td("General"), _td("General"),
"mx_RoomSettingsDialog_settingsIcon", "mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab room={this.state.room} />, <GeneralRoomSettingsTab room={this.state.room} />,
@ -133,7 +135,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_group_calls")) { if (SettingsStore.getValue("feature_group_calls")) {
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_VOIP_TAB, RoomSettingsTab.Voip,
_td("Voice & Video"), _td("Voice & Video"),
"mx_RoomSettingsDialog_voiceIcon", "mx_RoomSettingsDialog_voiceIcon",
<VoipRoomSettingsTab room={this.state.room} />, <VoipRoomSettingsTab room={this.state.room} />,
@ -142,7 +144,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
} }
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_SECURITY_TAB, RoomSettingsTab.Security,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon", "mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />, <SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />,
@ -151,7 +153,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
); );
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_ROLES_TAB, RoomSettingsTab.Roles,
_td("Roles & Permissions"), _td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon", "mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab room={this.state.room} />, <RolesRoomSettingsTab room={this.state.room} />,
@ -160,7 +162,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
); );
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_NOTIFICATIONS_TAB, RoomSettingsTab.Notifications,
_td("Notifications"), _td("Notifications"),
"mx_RoomSettingsDialog_notificationsIcon", "mx_RoomSettingsDialog_notificationsIcon",
( (
@ -176,7 +178,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
if (SettingsStore.getValue("feature_bridge_state")) { if (SettingsStore.getValue("feature_bridge_state")) {
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_BRIDGES_TAB, RoomSettingsTab.Bridges,
_td("Bridges"), _td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon", "mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab room={this.state.room} />, <BridgeSettingsTab room={this.state.room} />,
@ -187,7 +189,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_POLL_HISTORY_TAB, RoomSettingsTab.PollHistory,
_td("Poll history"), _td("Poll history"),
"mx_RoomSettingsDialog_pollsIcon", "mx_RoomSettingsDialog_pollsIcon",
<PollHistoryTab room={this.state.room} onFinished={() => this.props.onFinished(true)} />, <PollHistoryTab room={this.state.room} onFinished={() => this.props.onFinished(true)} />,
@ -197,7 +199,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
tabs.push( tabs.push(
new Tab( new Tab(
ROOM_ADVANCED_TAB, RoomSettingsTab.Advanced,
_td("Advanced"), _td("Advanced"),
"mx_RoomSettingsDialog_warningIcon", "mx_RoomSettingsDialog_warningIcon",
( (
@ -211,7 +213,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
); );
} }
return tabs as NonEmptyArray<Tab>; return tabs as NonEmptyArray<Tab<RoomSettingsTab>>;
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -70,7 +70,7 @@ const SpacePreferencesAppearanceTab: React.FC<Pick<IProps, "space">> = ({ space
}; };
const SpacePreferencesDialog: React.FC<IProps> = ({ space, initialTabId, onFinished }) => { const SpacePreferencesDialog: React.FC<IProps> = ({ space, initialTabId, onFinished }) => {
const tabs: NonEmptyArray<Tab> = [ const tabs: NonEmptyArray<Tab<SpacePreferenceTab>> = [
new Tab( new Tab(
SpacePreferenceTab.Appearance, SpacePreferenceTab.Appearance,
_td("Appearance"), _td("Appearance"),

View file

@ -80,7 +80,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<AdvancedRoomSettingsTab room={space} closeSettingsFn={onFinished} />, <AdvancedRoomSettingsTab room={space} closeSettingsFn={onFinished} />,
) )
: null, : null,
].filter(Boolean) as NonEmptyArray<Tab>; ].filter(Boolean) as NonEmptyArray<Tab<SpaceSettingsTab>>;
}, [cli, space, onFinished]); }, [cli, space, onFinished]);
return ( return (

View file

@ -81,8 +81,8 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
this.setState({ newSessionManagerEnabled: newValue }); this.setState({ newSessionManagerEnabled: newValue });
}; };
private getTabs(): NonEmptyArray<Tab> { private getTabs(): NonEmptyArray<Tab<UserTab>> {
const tabs: Tab[] = []; const tabs: Tab<UserTab>[] = [];
tabs.push( tabs.push(
new Tab( new Tab(
@ -208,7 +208,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
), ),
); );
return tabs as NonEmptyArray<Tab>; return tabs as NonEmptyArray<Tab<UserTab>>;
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -85,6 +85,8 @@ export interface PickerIProps {
onFinished(sourceId?: string): void; onFinished(sourceId?: string): void;
} }
type TabId = "screen" | "window";
export default class DesktopCapturerSourcePicker extends React.Component<PickerIProps, PickerIState> { export default class DesktopCapturerSourcePicker extends React.Component<PickerIProps, PickerIState> {
public interval: number; public interval: number;
@ -134,7 +136,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<PickerI
this.props.onFinished(); this.props.onFinished();
}; };
private getTab(type: "screen" | "window", label: string): Tab { private getTab(type: TabId, label: string): Tab<TabId> {
const sources = this.state.sources const sources = this.state.sources
.filter((source) => source.id.startsWith(type)) .filter((source) => source.id.startsWith(type))
.map((source) => { .map((source) => {
@ -152,7 +154,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<PickerI
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const tabs: NonEmptyArray<Tab> = [ const tabs: NonEmptyArray<Tab<TabId>> = [
this.getTab("screen", _t("Share entire screen")), this.getTab("screen", _t("Share entire screen")),
this.getTab("window", _t("Application window")), this.getTab("window", _t("Application window")),
]; ];

View file

@ -33,7 +33,7 @@ import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space"; import { showSpaceInvite } from "../../../utils/space";
import EventTileBubble from "../messages/EventTileBubble"; import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature"; import { UIComponent } from "../../../settings/UIFeature";
@ -268,7 +268,7 @@ const NewRoomIntro: React.FC = () => {
event.preventDefault(); event.preventDefault();
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "open_room_settings", action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB, initial_tab_id: RoomSettingsTab.Security,
}); });
} }

View file

@ -31,7 +31,7 @@ import { upgradeRoom } from "../../../utils/RoomUpgrade";
import { arrayHasDiff } from "../../../utils/arrays"; import { arrayHasDiff } from "../../../utils/arrays";
import { useLocalEcho } from "../../../hooks/useLocalEcho"; import { useLocalEcho } from "../../../hooks/useLocalEcho";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions"; import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions";
@ -320,7 +320,7 @@ const JoinRuleSettings: React.FC<IProps> = ({
// open new settings on this tab // open new settings on this tab
dis.dispatch({ dis.dispatch({
action: "open_room_settings", action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB, initial_tab_id: RoomSettingsTab.Security,
}); });
}, },
}); });

View file

@ -26,11 +26,11 @@ describe("<TabbedView />", () => {
const securityTab = new Tab("SECURITY", "Security", "security", <div>security</div>); const securityTab = new Tab("SECURITY", "Security", "security", <div>security</div>);
const defaultProps = { const defaultProps = {
tabLocation: TabLocation.LEFT, tabLocation: TabLocation.LEFT,
tabs: [generalTab, labsTab, securityTab] as NonEmptyArray<Tab>, tabs: [generalTab, labsTab, securityTab] as NonEmptyArray<Tab<any>>,
}; };
const getComponent = (props = {}): React.ReactElement => <TabbedView {...defaultProps} {...props} />; const getComponent = (props = {}): React.ReactElement => <TabbedView {...defaultProps} {...props} />;
const getTabTestId = (tab: Tab): string => `settings-tab-${tab.id}`; const getTabTestId = (tab: Tab<string>): string => `settings-tab-${tab.id}`;
const getActiveTab = (container: HTMLElement): Element | undefined => const getActiveTab = (container: HTMLElement): Element | undefined =>
container.getElementsByClassName("mx_TabbedView_tabLabel_active")[0]; container.getElementsByClassName("mx_TabbedView_tabLabel_active")[0];
const getActiveTabBody = (container: HTMLElement): Element | undefined => const getActiveTabBody = (container: HTMLElement): Element | undefined =>