Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/middle/composer/FormattedDateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ const FormattedDateModal = ({
className={buildClassName(styles.tabList, areOtherDateOptionsDisabled && styles.tabListDisabled)}
tabs={formatTabs}
activeTab={activeDateTab}
isDisabled={areOtherDateOptionsDisabled}
onSwitchTab={handleDateStyleChange}
/>
</div>
Expand All @@ -200,6 +201,7 @@ const FormattedDateModal = ({
className={buildClassName(styles.tabList, areOtherDateOptionsDisabled && styles.tabListDisabled)}
tabs={formatTabs}
activeTab={activeTimeTab}
isDisabled={areOtherDateOptionsDisabled}
onSwitchTab={handleTimeStyleChange}
/>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/components/ui/TabList.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
opacity: 0.85;
}

&:focus-visible {
outline: 0.125rem solid var(--color-primary);
outline-offset: -0.125rem;
}

.activeIndicator & {
color: var(--color-primary);
}
Expand Down
40 changes: 37 additions & 3 deletions src/components/ui/TabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TabWithProperties } from './SquareTabList';
export type { TabWithProperties };

import buildClassName from '../../util/buildClassName';
import focusNoScroll from '../../util/focusNoScroll';
import renderText from '../common/helpers/renderText';

import useFlag from '../../hooks/useFlag';
Expand Down Expand Up @@ -37,6 +38,7 @@ type OwnProps = {
itemAlignment?: 'vertical' | 'horizontal';
withFadeMask?: boolean;
fadeMaskClassName?: string;
isDisabled?: boolean;
onSwitchTab: (index: number) => void;
renderExtra?: (tab: TabWithProperties, index: number) => TeactNode;
};
Expand All @@ -52,6 +54,7 @@ const TabList = ({
itemAlignment,
withFadeMask,
fadeMaskClassName,
isDisabled,
renderExtra,
onSwitchTab,
}: OwnProps) => {
Expand Down Expand Up @@ -89,10 +92,34 @@ const TabList = ({

useScrollToActiveTab(containerRef, activeTab);

// A tab list holds a single tab stop, which falls back to the first tab while `activeTab` points outside the list
const focusableTab = activeTab >= 0 && activeTab < tabs.length ? activeTab : 0;

const handleTabClick = useLastCallback((index: number) => {
onSwitchTab(index);
});

const handleKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
if (isDisabled) return;

const isNext = e.key === 'ArrowRight';
if (!isNext && e.key !== 'ArrowLeft') return;

const container = containerRef.current;
if (!container) return;

e.preventDefault();

// Focus leads activation, so the focused tab is the reliable starting point while `activeTab` catches up
const tabElements = Array.from(container.children);
const focusedIndex = tabElements.indexOf(e.target as Element);
const currentIndex = focusedIndex >= 0 && focusedIndex < tabs.length ? focusedIndex : focusableTab;
const newIndex = (currentIndex + (isNext ? 1 : -1) + tabs.length) % tabs.length;

onSwitchTab(newIndex);
focusNoScroll(tabElements[newIndex] as HTMLElement | undefined);
});

const handleContextMenu = useLastCallback((index: number, e: React.MouseEvent) => {
const actions = tabs[index]?.contextActions;
if (!actions?.length) return;
Expand Down Expand Up @@ -123,7 +150,8 @@ const TabList = ({

const hasContextActions = tabs.some((tab) => tab.contextActions?.length);

const renderTab = (tab: TabWithProperties, index: number) => {
// The active indicator renders a mirrored copy of every tab, so those copies stay out of the accessibility tree
const renderTab = (tab: TabWithProperties, index: number, noInteractive?: boolean) => {
const customEmojiId = tab.customEmojiDocumentId
|| (typeof tab.emoticon === 'object' ? tab.emoticon.documentId : undefined);
const stringEmoticon = typeof tab.emoticon === 'string' ? tab.emoticon : undefined;
Expand All @@ -137,6 +165,10 @@ const TabList = ({
itemAlignment === 'vertical' && styles.vertical,
stretched && styles.stretched,
)}
role={noInteractive ? undefined : 'tab'}
tabIndex={noInteractive ? undefined : (index === focusableTab && !isDisabled ? 0 : -1)}
aria-selected={noInteractive ? undefined : index === activeTab}
aria-disabled={noInteractive ? undefined : isDisabled}
onClick={() => handleTabClick(index)}
onContextMenu={hasContextActions ? (e) => handleContextMenu(index, e) : undefined}
>
Expand All @@ -162,6 +194,7 @@ const TabList = ({
const tabListElement = (
<div
ref={containerRef}
role="tablist"
className={buildClassName(
'TabList',
styles.container,
Expand All @@ -171,8 +204,9 @@ const TabList = ({
className,
clipPath && styles.ready,
)}
onKeyDown={handleKeyDown}
>
{tabs.map(renderTab)}
{tabs.map((tab, index) => renderTab(tab, index))}

<div
ref={clipPathContainerRef}
Expand All @@ -183,7 +217,7 @@ const TabList = ({
style={clipPath ? `clip-path: ${clipPath}` : undefined}
aria-hidden
>
{tabs.map(renderTab)}
{tabs.map((tab, index) => renderTab(tab, index, true))}
</div>
</div>
);
Expand Down