Fix supertab
This commit is contained in:
parent
698cd59693
commit
e5333546db
5 changed files with 44 additions and 28 deletions
|
@ -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();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue