A super tab idea
This commit is contained in:
@@ -191,6 +191,7 @@ import { RetryPlaceholders } from './util/retryPlaceholders';
|
||||
import { setBatchingStrategy } from './util/messageBatcher';
|
||||
import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration';
|
||||
import { makeLookup } from './util/makeLookup';
|
||||
import { focusableSelectors } from './util/focusableSelectors';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
const HOUR = 1000 * 60 * 60;
|
||||
@@ -1352,6 +1353,52 @@ export async function startApp(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Super tab :)
|
||||
if (commandOrCtrl && key === 'F6') {
|
||||
window.enterKeyboardMode();
|
||||
const focusedElement = document.activeElement;
|
||||
const targets: Array<HTMLElement> = Array.from(
|
||||
document.querySelectorAll('[data-supertab="true"]')
|
||||
);
|
||||
|
||||
const focusedIndex = targets.findIndex(target => {
|
||||
if (!target || !focusedElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target === focusedElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.contains(focusedElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const lastIndex = targets.length - 1;
|
||||
const increment = shiftKey ? -1 : 1;
|
||||
|
||||
let index;
|
||||
if (focusedIndex < 0 || focusedIndex >= lastIndex) {
|
||||
index = 0;
|
||||
} else {
|
||||
index = focusedIndex + increment;
|
||||
}
|
||||
|
||||
while (!targets[index]) {
|
||||
index += increment;
|
||||
if (index > lastIndex || index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
targets[index]
|
||||
.querySelectorAll<HTMLElement>(focusableSelectors.join(','))[0]
|
||||
?.focus();
|
||||
}
|
||||
|
||||
// Navigate by section
|
||||
if (commandOrCtrl && !shiftKey && (key === 't' || key === 'T')) {
|
||||
window.enterKeyboardMode();
|
||||
|
@@ -96,76 +96,82 @@ export function CustomColorEditor({
|
||||
includeAnotherBubble
|
||||
/>
|
||||
{selectedTab === TabViews.Gradient && (
|
||||
<GradientDial
|
||||
deg={color.deg}
|
||||
knob1Style={{ backgroundColor: getHSL(color.start) }}
|
||||
knob2Style={{
|
||||
backgroundColor: getHSL(color.end || ULTRAMARINE_ISH_VALUES),
|
||||
}}
|
||||
onChange={deg => {
|
||||
setColor({
|
||||
...color,
|
||||
deg,
|
||||
});
|
||||
}}
|
||||
onClick={knob => setSelectedColorKnob(knob)}
|
||||
selectedKnob={selectedColorKnob}
|
||||
/>
|
||||
<div data-supertab>
|
||||
<GradientDial
|
||||
deg={color.deg}
|
||||
knob1Style={{ backgroundColor: getHSL(color.start) }}
|
||||
knob2Style={{
|
||||
backgroundColor: getHSL(
|
||||
color.end || ULTRAMARINE_ISH_VALUES
|
||||
),
|
||||
}}
|
||||
onChange={deg => {
|
||||
setColor({
|
||||
...color,
|
||||
deg,
|
||||
});
|
||||
}}
|
||||
onClick={knob => setSelectedColorKnob(knob)}
|
||||
selectedKnob={selectedColorKnob}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="CustomColorEditor__slider-container">
|
||||
{i18n('icu:CustomColorEditor__hue')}
|
||||
<Slider
|
||||
handleStyle={{
|
||||
backgroundColor: getHSL({
|
||||
hue,
|
||||
saturation: 100,
|
||||
}),
|
||||
}}
|
||||
label={i18n('icu:CustomColorEditor__hue')}
|
||||
moduleClassName="CustomColorEditor__hue-slider"
|
||||
onChange={(percentage: number) => {
|
||||
setColor({
|
||||
...color,
|
||||
[selectedColorKnob]: {
|
||||
...ULTRAMARINE_ISH_VALUES,
|
||||
...color[selectedColorKnob],
|
||||
hue: getValue(percentage, MAX_HUE),
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={getPercentage(hue, MAX_HUE)}
|
||||
/>
|
||||
<div data-supertab>
|
||||
<div className="CustomColorEditor__slider-container">
|
||||
{i18n('icu:CustomColorEditor__hue')}
|
||||
<Slider
|
||||
handleStyle={{
|
||||
backgroundColor: getHSL({
|
||||
hue,
|
||||
saturation: 100,
|
||||
}),
|
||||
}}
|
||||
label={i18n('icu:CustomColorEditor__hue')}
|
||||
moduleClassName="CustomColorEditor__hue-slider"
|
||||
onChange={(percentage: number) => {
|
||||
setColor({
|
||||
...color,
|
||||
[selectedColorKnob]: {
|
||||
...ULTRAMARINE_ISH_VALUES,
|
||||
...color[selectedColorKnob],
|
||||
hue: getValue(percentage, MAX_HUE),
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={getPercentage(hue, MAX_HUE)}
|
||||
/>
|
||||
</div>
|
||||
<div className="CustomColorEditor__slider-container">
|
||||
{i18n('icu:CustomColorEditor__saturation')}
|
||||
<Slider
|
||||
containerStyle={getCustomColorStyle({
|
||||
deg: 180,
|
||||
start: { hue, saturation: 0 },
|
||||
end: { hue, saturation: 100 },
|
||||
})}
|
||||
handleStyle={{
|
||||
backgroundColor: getHSL(
|
||||
color[selectedColorKnob] || ULTRAMARINE_ISH_VALUES
|
||||
),
|
||||
}}
|
||||
label={i18n('icu:CustomColorEditor__saturation')}
|
||||
moduleClassName="CustomColorEditor__saturation-slider"
|
||||
onChange={(value: number) => {
|
||||
setColor({
|
||||
...color,
|
||||
[selectedColorKnob]: {
|
||||
...ULTRAMARINE_ISH_VALUES,
|
||||
...color[selectedColorKnob],
|
||||
saturation: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={saturation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="CustomColorEditor__slider-container">
|
||||
{i18n('icu:CustomColorEditor__saturation')}
|
||||
<Slider
|
||||
containerStyle={getCustomColorStyle({
|
||||
deg: 180,
|
||||
start: { hue, saturation: 0 },
|
||||
end: { hue, saturation: 100 },
|
||||
})}
|
||||
handleStyle={{
|
||||
backgroundColor: getHSL(
|
||||
color[selectedColorKnob] || ULTRAMARINE_ISH_VALUES
|
||||
),
|
||||
}}
|
||||
label={i18n('icu:CustomColorEditor__saturation')}
|
||||
moduleClassName="CustomColorEditor__saturation-slider"
|
||||
onChange={(value: number) => {
|
||||
setColor({
|
||||
...color,
|
||||
[selectedColorKnob]: {
|
||||
...ULTRAMARINE_ISH_VALUES,
|
||||
...color[selectedColorKnob],
|
||||
saturation: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={saturation}
|
||||
/>
|
||||
</div>
|
||||
<div className="CustomColorEditor__footer">
|
||||
<div className="CustomColorEditor__footer" data-supertab>
|
||||
<Button variant={ButtonVariant.Secondary} onClick={onClose}>
|
||||
{i18n('icu:cancel')}
|
||||
</Button>
|
||||
|
@@ -3,7 +3,6 @@
|
||||
|
||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
||||
import type { ReactNode } from 'react';
|
||||
import focusableSelectors from 'focusable-selectors';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -59,6 +58,7 @@ import { DurationInSeconds } from '../util/durations';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { useUniqueId } from '../hooks/useUniqueId';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { focusableSelectors } from '../util/focusableSelectors';
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||
|
@@ -63,7 +63,7 @@ export function useTabs(options: TabsOptionsType): TabsProps {
|
||||
}
|
||||
|
||||
const tabsHeaderElement = (
|
||||
<div className={getClassName('')}>
|
||||
<div className={getClassName('')} data-supertab>
|
||||
{options.tabs.map(({ id, label }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
|
30
ts/util/focusableSelectors.ts
Normal file
30
ts/util/focusableSelectors.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// https://www.npmjs.com/package/focusable-selectors
|
||||
// https://github.com/KittyGiraudel/focusable-selectors/issues/15
|
||||
|
||||
const not = {
|
||||
inert: ':not([inert]):not([inert] *)',
|
||||
negTabIndex: ':not([tabindex^="-"])',
|
||||
disabled: ':not(:disabled)',
|
||||
};
|
||||
|
||||
export const focusableSelectors = [
|
||||
`a[href]${not.inert}${not.negTabIndex}`,
|
||||
`area[href]${not.inert}${not.negTabIndex}`,
|
||||
`input:not([type="hidden"]):not([type="radio"])${not.inert}${not.negTabIndex}${not.disabled}`,
|
||||
`input[type="radio"]${not.inert}${not.negTabIndex}${not.disabled}`,
|
||||
`select${not.inert}${not.negTabIndex}${not.disabled}`,
|
||||
`textarea${not.inert}${not.negTabIndex}${not.disabled}`,
|
||||
`button${not.inert}${not.negTabIndex}${not.disabled}`,
|
||||
`details${not.inert} > summary:first-of-type${not.negTabIndex}`,
|
||||
// Discard until Firefox supports `:has()`
|
||||
// See: https://github.com/KittyGiraudel/focusable-selectors/issues/12
|
||||
// `details:not(:has(> summary))${not.inert}${not.negTabIndex}`,
|
||||
`iframe${not.inert}${not.negTabIndex}`,
|
||||
`audio[controls]${not.inert}${not.negTabIndex}`,
|
||||
`video[controls]${not.inert}${not.negTabIndex}`,
|
||||
`[contenteditable]${not.inert}${not.negTabIndex}`,
|
||||
`[tabindex]${not.inert}${not.negTabIndex}`,
|
||||
];
|
Reference in New Issue
Block a user