2022-03-04 21:14:52 +00:00
|
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import type { KeyboardEvent } from 'react';
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
import classNames from 'classnames';
|
2022-09-15 19:17:15 +00:00
|
|
|
import { assertDev } from '../util/assert';
|
2022-03-04 21:14:52 +00:00
|
|
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
|
|
|
|
|
|
|
type Tab = {
|
|
|
|
id: string;
|
|
|
|
label: string;
|
|
|
|
};
|
|
|
|
|
2022-10-11 17:59:02 +00:00
|
|
|
export type BaseTabsOptionsType = {
|
2022-03-04 21:14:52 +00:00
|
|
|
moduleClassName?: string;
|
|
|
|
tabs: Array<Tab>;
|
|
|
|
};
|
|
|
|
|
2022-10-11 17:59:02 +00:00
|
|
|
export type ControlledTabsOptionsType = BaseTabsOptionsType & {
|
|
|
|
selectedTab: string;
|
|
|
|
onTabChange: (selectedTab: string) => unknown;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type UncontrolledTabsOptionsType = BaseTabsOptionsType & {
|
|
|
|
initialSelectedTab?: string;
|
|
|
|
onTabChange?: (selectedTab: string) => unknown;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type TabsOptionsType =
|
|
|
|
| ControlledTabsOptionsType
|
|
|
|
| UncontrolledTabsOptionsType;
|
|
|
|
|
|
|
|
type TabsProps = {
|
2022-03-04 21:14:52 +00:00
|
|
|
selectedTab: string;
|
|
|
|
tabsHeaderElement: JSX.Element;
|
2022-10-11 17:59:02 +00:00
|
|
|
};
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-10-11 17:59:02 +00:00
|
|
|
export function useTabs(options: TabsOptionsType): TabsProps {
|
|
|
|
assertDev(options.tabs.length, 'Tabs needs more than 1 tab present');
|
2022-03-04 21:14:52 +00:00
|
|
|
|
2022-10-11 17:59:02 +00:00
|
|
|
const getClassName = getClassNamesFor('Tabs', options.moduleClassName);
|
|
|
|
|
|
|
|
let selectedTab: string;
|
|
|
|
let onChange: (selectedTab: string) => void;
|
|
|
|
|
|
|
|
if ('selectedTab' in options) {
|
|
|
|
selectedTab = options.selectedTab;
|
|
|
|
onChange = options.onTabChange;
|
|
|
|
} else {
|
|
|
|
// useTabs should always be either controlled or uncontrolled.
|
|
|
|
// This is enforced by the type system.
|
|
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
const [tabState, setTabState] = useState<string>(
|
|
|
|
options.initialSelectedTab || options.tabs[0].id
|
|
|
|
);
|
|
|
|
|
|
|
|
selectedTab = tabState;
|
|
|
|
onChange = (newTab: string) => {
|
|
|
|
setTabState(newTab);
|
|
|
|
options.onTabChange?.(newTab);
|
|
|
|
};
|
|
|
|
}
|
2022-03-04 21:14:52 +00:00
|
|
|
|
|
|
|
const tabsHeaderElement = (
|
2023-05-04 19:34:52 +00:00
|
|
|
<div className={getClassName('')} data-supertab>
|
2022-10-11 17:59:02 +00:00
|
|
|
{options.tabs.map(({ id, label }) => (
|
2022-03-04 21:14:52 +00:00
|
|
|
<div
|
2023-05-04 23:33:36 +00:00
|
|
|
aria-selected={selectedTab === id}
|
2022-03-04 21:14:52 +00:00
|
|
|
className={classNames(
|
|
|
|
getClassName('__tab'),
|
|
|
|
selectedTab === id && getClassName('__tab--selected')
|
|
|
|
)}
|
|
|
|
key={id}
|
|
|
|
onClick={() => {
|
2022-10-11 17:59:02 +00:00
|
|
|
onChange(id);
|
2022-03-04 21:14:52 +00:00
|
|
|
}}
|
|
|
|
onKeyUp={(e: KeyboardEvent) => {
|
|
|
|
if (e.target === e.currentTarget && e.keyCode === 13) {
|
2022-10-11 17:59:02 +00:00
|
|
|
onChange(id);
|
2022-03-04 21:14:52 +00:00
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
role="tab"
|
|
|
|
tabIndex={0}
|
|
|
|
>
|
|
|
|
{label}
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
selectedTab,
|
|
|
|
tabsHeaderElement,
|
|
|
|
};
|
|
|
|
}
|