Skip to content

Commit

Permalink
Fix supertab
Browse files Browse the repository at this point in the history
Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
  • Loading branch information
automated-signal and jamiebuilds-signal committed Mar 4, 2024
1 parent ddecf0a commit 578d6f4
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 27 deletions.
2 changes: 1 addition & 1 deletion ts/components/NavTabs.tsx
Expand Up @@ -234,6 +234,7 @@ export function NavTabs({
return (
<Tabs orientation="vertical" className="NavTabs__Container">
<nav
data-supertab
className={classNames('NavTabs', {
'NavTabs--collapsed': navTabsCollapsed,
})}
Expand Down Expand Up @@ -356,7 +357,6 @@ export function NavTabs({
<button
type="button"
className="NavTabs__Item NavTabs__Item--Profile"
data-supertab
onClick={() => {
onToggleProfileEditor();
}}
Expand Down
7 changes: 3 additions & 4 deletions ts/components/Preferences.tsx
Expand Up @@ -61,7 +61,7 @@ import {
import { DurationInSeconds } from '../util/durations';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useUniqueId } from '../hooks/useUniqueId';
import { focusableSelectors } from '../util/focusableSelectors';
import { focusableSelector } from '../util/focusableSelectors';
import { Modal } from './Modal';
import { SearchInput } from './SearchInput';
import { removeDiacritics } from '../util/removeDiacritics';
Expand Down Expand Up @@ -400,7 +400,6 @@ export function Preferences({
[onSelectedMicrophoneChange, availableMicrophones]
);

const selectors = useMemo(() => focusableSelectors.join(','), []);
const settingsPaneRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const settingsPane = settingsPaneRef.current;
Expand All @@ -414,12 +413,12 @@ export function Preferences({
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement
>(selectors);
>(focusableSelector);
if (!elements.length) {
return;
}
elements[0]?.focus();
}, [page, selectors]);
}, [page]);

const onAudioOutputSelectChange = useCallback(
(value: string) => {
Expand Down
39 changes: 22 additions & 17 deletions ts/services/addGlobalKeyboardShortcuts.ts
Expand Up @@ -6,7 +6,7 @@ import * as log from '../logging/log';
import { PanelType } from '../types/Panels';
import { clearConversationDraftAttachments } from '../util/clearConversationDraftAttachments';
import { drop } from '../util/drop';
import { focusableSelectors } from '../util/focusableSelectors';
import { matchOrQueryFocusable } from '../util/focusableSelectors';
import { getQuotedMessageSelector } from '../state/selectors/composer';
import { removeLinkPreview } from './LinkPreview';

Expand Down Expand Up @@ -48,24 +48,31 @@ export function addGlobalKeyboardShortcuts(): void {
) {
window.enterKeyboardMode();
const focusedElement = document.activeElement;
const targets: Array<HTMLElement> = Array.from(
document.querySelectorAll('[data-supertab="true"]')
const targets = Array.from(
document.querySelectorAll<HTMLElement>('[data-supertab="true"]')
);
const focusedIndex = targets.findIndex(target => {
if (!target || !focusedElement) {
return false;
}
const focusedIndexes: Array<number> = [];

if (target === focusedElement) {
return true;
targets.forEach((target, index) => {
if (
(focusedElement != null && target === focusedElement) ||
target.contains(focusedElement)
) {
focusedIndexes.push(index);
}
});

if (target.contains(focusedElement)) {
return true;
}
if (focusedIndexes.length > 1) {
log.error(
`supertab: found multiple supertab elements containing the current active element: ${focusedIndexes.join(
', '
)}`
);
}

return false;
});
// 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 increment = shiftKey ? -1 : 1;
Expand All @@ -85,9 +92,7 @@ export function addGlobalKeyboardShortcuts(): void {
}

const node = targets[index];
const firstFocusableElement = node.querySelectorAll<HTMLElement>(
focusableSelectors.join(',')
)[0];
const firstFocusableElement = matchOrQueryFocusable(node);

if (firstFocusableElement) {
firstFocusableElement.focus();
Expand Down
8 changes: 3 additions & 5 deletions ts/state/smart/ConversationPanel.tsx
Expand Up @@ -6,7 +6,6 @@ import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
Expand All @@ -32,7 +31,7 @@ import {
getPanelInformation,
getWasPanelAnimated,
} from '../selectors/conversations';
import { focusableSelectors } from '../../util/focusableSelectors';
import { focusableSelector } from '../../util/focusableSelectors';
import { missingCaseError } from '../../util/missingCaseError';
import { useConversationsActions } from '../ducks/conversations';
import { useReducedMotion } from '../../hooks/useReducedMotion';
Expand Down Expand Up @@ -269,7 +268,6 @@ const PanelContainer = forwardRef<
const { popPanelForConversation } = useConversationsActions();
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);

const selectors = useMemo(() => focusableSelectors.join(','), []);
const focusRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!isActive) {
Expand All @@ -281,12 +279,12 @@ const PanelContainer = forwardRef<
return;
}

const elements = focusNode.querySelectorAll<HTMLElement>(selectors);
const elements = focusNode.querySelectorAll<HTMLElement>(focusableSelector);
if (!elements.length) {
return;
}
elements[0]?.focus();
}, [isActive, panel, selectors]);
}, [isActive, panel]);

return (
<div className="ConversationPanel" ref={ref}>
Expand Down
14 changes: 14 additions & 0 deletions ts/util/focusableSelectors.ts
Expand Up @@ -28,3 +28,17 @@ export const focusableSelectors = [
`[contenteditable]${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);
}

0 comments on commit 578d6f4

Please sign in to comment.