toolbar: use roving focus

This commit is contained in:
David Lechner
2022-05-13 23:28:12 -05:00
parent f0dcb281de
commit 404818bcdf
8 changed files with 70 additions and 25 deletions
+9 -3
View File
@@ -3,14 +3,14 @@
import {
Button,
IRef,
Intent,
Spinner,
SpinnerSize,
mergeRefs,
useHotkeys,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import React, { useEffect, useMemo, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useState } from 'react';
import { tooltipDelay } from '../app/constants';
const smallScreenThreshold = 700;
@@ -30,6 +30,8 @@ export interface ActionButtonProps {
readonly showProgress?: boolean;
/** The progress value (0 to 1) or undefined for indeterminate progress. */
readonly progress?: number;
/** Reference to the <button> HTML element. */
readonly elementRef?: RefObject<HTMLButtonElement>;
/** Callback that is called when the button is activated (clicked). */
readonly onAction: () => void;
}
@@ -42,6 +44,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
enabled,
showProgress,
progress,
elementRef,
onAction,
}) => {
const [isSmallScreen, setIsSmallScreen] = useState(
@@ -93,7 +96,10 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
}) => (
<Button
aria-label={label}
elementRef={tooltipTargetRef as IRef<HTMLButtonElement>}
elementRef={mergeRefs<HTMLButtonElement>(
elementRef ?? null,
tooltipTargetRef,
)}
{...tooltipTargetProps}
// https://github.com/palantir/blueprint/pull/5300
aria-haspopup={undefined}
+9 -3
View File
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { Button, IRef, Intent, Spinner, SpinnerSize } from '@blueprintjs/core';
import { Button, Intent, Spinner, SpinnerSize, mergeRefs } from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import React, { useEffect, useState } from 'react';
import React, { RefObject, useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { tooltipDelay } from '../app/constants';
@@ -23,6 +23,8 @@ export interface OpenFileButtonProps {
readonly showProgress?: boolean;
/** The progress value (0 to 1) for the progress spinner. */
readonly progress?: number;
/** Reference to the <button> HTML element. */
readonly elementRef?: RefObject<HTMLButtonElement>;
/** Callback that is called when a file has been selected and opened for reading. */
readonly onFile: (data: ArrayBuffer) => void;
/** Callback that is called when a file has been rejected (e.g. bad file extension). */
@@ -42,6 +44,7 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
enabled,
showProgress,
progress,
elementRef,
onFile,
onReject,
onClick,
@@ -108,7 +111,10 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
{...getRootProps({
'aria-label': label,
refKey: 'elementRef',
elementRef: tooltipTargetRef as IRef<HTMLButtonElement>,
elementRef: mergeRefs<HTMLButtonElement>(
elementRef ?? null,
tooltipTargetRef,
),
...tooltipTargetProps,
// https://github.com/palantir/blueprint/pull/5300
'aria-haspopup': undefined,
+25 -9
View File
@@ -2,7 +2,9 @@
// Copyright (c) 2020-2022 The Pybricks Authors
import { ButtonGroup } from '@blueprintjs/core';
import React from 'react';
import React, { useRef } from 'react';
import UtilsToolbar from '../utils/Toolbar';
import { useRovingTabIndex } from '../utils/react';
import BluetoothButton from './buttons/bluetooth/BluetoothButton';
import FlashButton from './buttons/flash/FlashButton';
import ReplButton from './buttons/repl/ReplButton';
@@ -11,19 +13,33 @@ import StopButton from './buttons/stop/StopButton';
import './toolbar.scss';
const Toolbar: React.VFC = (_props) => {
const Toolbar: React.VFC = () => {
const flashButtonRef = useRef<HTMLButtonElement>(null);
const bluetoothButtonRef = useRef<HTMLButtonElement>(null);
const runButtonRef = useRef<HTMLButtonElement>(null);
const stopButtonRef = useRef<HTMLButtonElement>(null);
const replButtonRef = useRef<HTMLButtonElement>(null);
const moveFocus = useRovingTabIndex(
flashButtonRef,
bluetoothButtonRef,
runButtonRef,
stopButtonRef,
replButtonRef,
);
return (
<div role="toolbar" className="pb-toolbar">
<UtilsToolbar className="pb-toolbar" onKeyboard={moveFocus}>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<FlashButton />
<BluetoothButton />
<FlashButton elementRef={flashButtonRef} />
<BluetoothButton elementRef={bluetoothButtonRef} />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<RunButton />
<StopButton />
<ReplButton />
<RunButton elementRef={runButtonRef} />
<StopButton elementRef={stopButtonRef} />
<ReplButton elementRef={replButtonRef} />
</ButtonGroup>
</div>
</UtilsToolbar>
);
};
@@ -8,12 +8,16 @@ import { toggleBluetooth } from '../../../ble/actions';
import { BleConnectionState } from '../../../ble/reducers';
import { BootloaderConnectionState } from '../../../lwp3-bootloader/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import connectedIcon from './connected.svg';
import disconnectedIcon from './disconnected.svg';
import { I18nId } from './i18n';
const BluetoothButton: React.VFC = () => {
type BluetoothButtonProps = Pick<ActionButtonProps, 'elementRef'>;
const BluetoothButton: React.VoidFunctionComponent<BluetoothButtonProps> = ({
elementRef,
}) => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
@@ -34,6 +38,7 @@ const BluetoothButton: React.VFC = () => {
icon={isDisconnected ? disconnectedIcon : connectedIcon}
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
showProgress={bleConnection === BleConnectionState.Connecting}
elementRef={elementRef}
onAction={() => dispatch(toggleBluetooth())}
/>
);
+5 -2
View File
@@ -13,11 +13,13 @@ import {
useSettingFlashCurrentProgram,
useSettingHubName,
} from '../../../settings/hooks';
import OpenFileButton from '../../../toolbar/OpenFileButton';
import OpenFileButton, { OpenFileButtonProps } from '../../../toolbar/OpenFileButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const FlashButton: React.VFC = () => {
type FlashButtonProps = Pick<OpenFileButtonProps, 'elementRef'>;
const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ elementRef }) => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const flashing = useSelector((s) => s.firmware.flashing);
@@ -47,6 +49,7 @@ const FlashButton: React.VFC = () => {
}
showProgress={flashing}
progress={progress === null ? undefined : progress}
elementRef={elementRef}
onFile={(data) =>
dispatch(
flashFirmware(data, isSettingFlashCurrentProgramEnabled, hubName),
+5 -2
View File
@@ -7,11 +7,13 @@ import { useDispatch } from 'react-redux';
import { repl } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const ReplButton: React.VFC = () => {
type ReplButtonProps = Pick<ActionButtonProps, 'elementRef'>;
const ReplButton: React.VoidFunctionComponent<ReplButtonProps> = ({ elementRef }) => {
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useI18n();
const dispatch = useDispatch();
@@ -25,6 +27,7 @@ const ReplButton: React.VFC = () => {
tooltip={i18n.translate(I18nId.Tooltip)}
icon={icon}
enabled={enabled}
elementRef={elementRef}
onAction={action}
/>
);
+5 -2
View File
@@ -7,11 +7,13 @@ import { useDispatch } from 'react-redux';
import { downloadAndRun } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const RunButton: React.VFC = () => {
type RunButtonProps = Pick<ActionButtonProps, 'elementRef'>;
const RunButton: React.VoidFunctionComponent<RunButtonProps> = ({ elementRef }) => {
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
const isEditorReady = useSelector((s) => s.editor.isReady);
@@ -36,6 +38,7 @@ const RunButton: React.VFC = () => {
enabled={isEditorReady && runtime === HubRuntimeState.Idle}
showProgress={runtime === HubRuntimeState.Loading}
progress={downloadProgress === null ? undefined : downloadProgress}
elementRef={elementRef}
onAction={() => dispatch(downloadAndRun())}
/>
);
+5 -2
View File
@@ -7,11 +7,13 @@ import { useDispatch } from 'react-redux';
import { stop } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const StopButton: React.VFC = () => {
type StopButtonProps = Pick<ActionButtonProps, 'elementRef'>;
const StopButton: React.VoidFunctionComponent<StopButtonProps> = ({ elementRef }) => {
const runtime = useSelector((s) => s.hub.runtime);
const keyboardShortcut = 'F6';
@@ -26,6 +28,7 @@ const StopButton: React.VFC = () => {
tooltip={i18n.translate(I18nId.Tooltip, { key: keyboardShortcut })}
icon={icon}
enabled={runtime === HubRuntimeState.Running}
elementRef={elementRef}
onAction={() => dispatch(stop())}
/>
);