Fix supertab

This commit is contained in:
Jamie Kyle 2024-03-04 12:32:51 -08:00 committed by GitHub
parent 698cd59693
commit e5333546db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 44 additions and 28 deletions

View file

@ -234,6 +234,7 @@ export function NavTabs({
return ( return (
<Tabs orientation="vertical" className="NavTabs__Container"> <Tabs orientation="vertical" className="NavTabs__Container">
<nav <nav
data-supertab
className={classNames('NavTabs', { className={classNames('NavTabs', {
'NavTabs--collapsed': navTabsCollapsed, 'NavTabs--collapsed': navTabsCollapsed,
})} })}
@ -356,7 +357,6 @@ export function NavTabs({
<button <button
type="button" type="button"
className="NavTabs__Item NavTabs__Item--Profile" className="NavTabs__Item NavTabs__Item--Profile"
data-supertab
onClick={() => { onClick={() => {
onToggleProfileEditor(); onToggleProfileEditor();
}} }}

View file

@ -61,7 +61,7 @@ import {
import { DurationInSeconds } from '../util/durations'; import { DurationInSeconds } from '../util/durations';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useUniqueId } from '../hooks/useUniqueId'; import { useUniqueId } from '../hooks/useUniqueId';
import { focusableSelectors } from '../util/focusableSelectors'; import { focusableSelector } from '../util/focusableSelectors';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { removeDiacritics } from '../util/removeDiacritics'; import { removeDiacritics } from '../util/removeDiacritics';
@ -400,7 +400,6 @@ export function Preferences({
[onSelectedMicrophoneChange, availableMicrophones] [onSelectedMicrophoneChange, availableMicrophones]
); );
const selectors = useMemo(() => focusableSelectors.join(','), []);
const settingsPaneRef = useRef<HTMLDivElement | null>(null); const settingsPaneRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
const settingsPane = settingsPaneRef.current; const settingsPane = settingsPaneRef.current;
@ -414,12 +413,12 @@ export function Preferences({
| HTMLInputElement | HTMLInputElement
| HTMLSelectElement | HTMLSelectElement
| HTMLTextAreaElement | HTMLTextAreaElement
>(selectors); >(focusableSelector);
if (!elements.length) { if (!elements.length) {
return; return;
} }
elements[0]?.focus(); elements[0]?.focus();
}, [page, selectors]); }, [page]);
const onAudioOutputSelectChange = useCallback( const onAudioOutputSelectChange = useCallback(
(value: string) => { (value: string) => {

View file

@ -6,7 +6,7 @@ import * as log from '../logging/log';
import { PanelType } from '../types/Panels'; import { PanelType } from '../types/Panels';
import { clearConversationDraftAttachments } from '../util/clearConversationDraftAttachments'; import { clearConversationDraftAttachments } from '../util/clearConversationDraftAttachments';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { focusableSelectors } from '../util/focusableSelectors'; import { matchOrQueryFocusable } from '../util/focusableSelectors';
import { getQuotedMessageSelector } from '../state/selectors/composer'; import { getQuotedMessageSelector } from '../state/selectors/composer';
import { removeLinkPreview } from './LinkPreview'; import { removeLinkPreview } from './LinkPreview';
@ -48,25 +48,32 @@ export function addGlobalKeyboardShortcuts(): void {
) { ) {
window.enterKeyboardMode(); window.enterKeyboardMode();
const focusedElement = document.activeElement; const focusedElement = document.activeElement;
const targets: Array<HTMLElement> = Array.from( const targets = Array.from(
document.querySelectorAll('[data-supertab="true"]') document.querySelectorAll<HTMLElement>('[data-supertab="true"]')
); );
const focusedIndex = targets.findIndex(target => { const focusedIndexes: Array<number> = [];
if (!target || !focusedElement) {
return false;
}
if (target === focusedElement) { targets.forEach((target, index) => {
return true; if (
(focusedElement != null && target === focusedElement) ||
target.contains(focusedElement)
) {
focusedIndexes.push(index);
} }
if (target.contains(focusedElement)) {
return true;
}
return false;
}); });
if (focusedIndexes.length > 1) {
log.error(
`supertab: found multiple supertab elements containing the current active element: ${focusedIndexes.join(
', '
)}`
);
}
// Default to the last focusable element to avoid cycles when multiple
// elements match (generally going to be a parent element)
const focusedIndex = focusedIndexes.at(-1) ?? -1;
const lastIndex = targets.length - 1; const lastIndex = targets.length - 1;
const increment = shiftKey ? -1 : 1; const increment = shiftKey ? -1 : 1;
@ -85,9 +92,7 @@ export function addGlobalKeyboardShortcuts(): void {
} }
const node = targets[index]; const node = targets[index];
const firstFocusableElement = node.querySelectorAll<HTMLElement>( const firstFocusableElement = matchOrQueryFocusable(node);
focusableSelectors.join(',')
)[0];
if (firstFocusableElement) { if (firstFocusableElement) {
firstFocusableElement.focus(); firstFocusableElement.focus();

View file

@ -6,7 +6,6 @@ import React, {
forwardRef, forwardRef,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
@ -32,7 +31,7 @@ import {
getPanelInformation, getPanelInformation,
getWasPanelAnimated, getWasPanelAnimated,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { focusableSelectors } from '../../util/focusableSelectors'; import { focusableSelector } from '../../util/focusableSelectors';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useReducedMotion } from '../../hooks/useReducedMotion'; import { useReducedMotion } from '../../hooks/useReducedMotion';
@ -269,7 +268,6 @@ const PanelContainer = forwardRef<
const { popPanelForConversation } = useConversationsActions(); const { popPanelForConversation } = useConversationsActions();
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type); const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);
const selectors = useMemo(() => focusableSelectors.join(','), []);
const focusRef = useRef<HTMLDivElement | null>(null); const focusRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
if (!isActive) { if (!isActive) {
@ -281,12 +279,12 @@ const PanelContainer = forwardRef<
return; return;
} }
const elements = focusNode.querySelectorAll<HTMLElement>(selectors); const elements = focusNode.querySelectorAll<HTMLElement>(focusableSelector);
if (!elements.length) { if (!elements.length) {
return; return;
} }
elements[0]?.focus(); elements[0]?.focus();
}, [isActive, panel, selectors]); }, [isActive, panel]);
return ( return (
<div className="ConversationPanel" ref={ref}> <div className="ConversationPanel" ref={ref}>

View file

@ -28,3 +28,17 @@ export const focusableSelectors = [
`[contenteditable]${not.inert}${not.negTabIndex}`, `[contenteditable]${not.inert}${not.negTabIndex}`,
`[tabindex]${not.inert}${not.negTabIndex}`, `[tabindex]${not.inert}${not.negTabIndex}`,
]; ];
export const focusableSelector = focusableSelectors.join(', ');
/**
* Matches the first focusable element within the given element or itself if it
* is focusable.
*/
export function matchOrQueryFocusable(
element: HTMLElement
): HTMLElement | null {
return element.matches(focusableSelector)
? element
: element.querySelector(focusableSelector);
}