mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
toolbar: use roving focus
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
@@ -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())}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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())}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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())}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user