src: use prop unpacking pattern

This makes the code a bit shorter by not having to use `props.` all of
the time. Also make everything VoidFunctionComponent while we are
touching this.
This commit is contained in:
David Lechner
2022-03-12 16:07:31 -06:00
parent 228ba4ebb8
commit 8e4045449c
18 changed files with 193 additions and 131 deletions
+6 -3
View File
@@ -24,7 +24,10 @@ import './about.scss';
type AboutDialogProps = { isOpen: boolean; onClose: () => void };
const AboutDialog: React.FunctionComponent<AboutDialogProps> = (props) => {
const AboutDialog: React.VoidFunctionComponent<AboutDialogProps> = ({
isOpen,
onClose,
}) => {
const [isLicenseDialogOpen, setIsLicenseDialogOpen] = useState(false);
const [i18n] = useI18n({ id: 'about', translations: { en }, fallback: en });
@@ -32,8 +35,8 @@ const AboutDialog: React.FunctionComponent<AboutDialogProps> = (props) => {
return (
<Dialog
title={`Pybricks v${firmwareVersion} (${appName} v${appVersion})`}
isOpen={props.isOpen}
onClose={props.onClose}
isOpen={isOpen}
onClose={onClose}
>
<div className={Classes.DIALOG_BODY}>
<div className="pb-about-icon">
+3 -3
View File
@@ -127,7 +127,7 @@ type AppProps = {
onEditorChanged?: (editor: EditorType) => void;
};
const App: React.VoidFunctionComponent<AppProps> = (props) => {
const App: React.VoidFunctionComponent<AppProps> = ({ onEditorChanged }) => {
const darkMode = useSelector((s): boolean => s.settings.darkMode);
const showDocs = useSelector((s): boolean => s.settings.showDocs);
const [isDragging, setIsDragging] = useState(false);
@@ -139,8 +139,8 @@ const App: React.VoidFunctionComponent<AppProps> = (props) => {
setEditor: (editor) => {
setEditor(editor);
if (props.onEditorChanged) {
props.onEditorChanged(editor);
if (onEditorChanged) {
onEditorChanged(editor);
}
},
}),
+20 -14
View File
@@ -46,16 +46,22 @@ type ActionButtonProps = {
onClick: () => void;
};
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = (props) => {
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
icon,
toolTipId,
toolTipReplacements,
disabled,
onClick,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
return (
<Button
icon={props.icon}
title={i18n.translate(props.toolTipId, props.toolTipReplacements)}
disabled={props.disabled}
icon={icon}
title={i18n.translate(toolTipId, toolTipReplacements)}
disabled={disabled}
onMouseDown={preventFocusOnClick}
onClick={props.onClick}
onClick={onClick}
/>
);
};
@@ -73,7 +79,7 @@ type ActionButtonGroupProps = {
const FileActionButtonGroup = forwardRef<
FileActionButtonGroupRef,
ActionButtonGroupProps
>((props, ref) => {
>(({ fileName }, ref) => {
const dispatch = useDispatch();
const [visible, setVisible] = useState(false);
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
@@ -85,10 +91,10 @@ const FileActionButtonGroup = forwardRef<
// received.
const fileNames = useSelector((s) => s.fileStorage.fileNames);
useEffect(() => {
if (!fileNames.includes(props.fileName)) {
if (!fileNames.includes(fileName)) {
setVisible(false);
}
}, [fileNames, props.fileName, setVisible]);
}, [fileNames, fileName, setVisible]);
useImperativeHandle(ref, () => ({ setVisible }), [setVisible]);
@@ -97,11 +103,11 @@ const FileActionButtonGroup = forwardRef<
<ActionButton
icon="edit"
toolTipId={ExplorerStringId.TreeItemRenameTooltip}
toolTipReplacements={{ fileName: props.fileName }}
toolTipReplacements={{ fileName: fileName }}
onClick={() => setIsRenameDialogOpen(true)}
/>
<RenameFileDialog
oldName={props.fileName}
oldName={fileName}
isOpen={isRenameDialogOpen}
onClose={() => setIsRenameDialogOpen(false)}
/>
@@ -113,14 +119,14 @@ const FileActionButtonGroup = forwardRef<
// download operation
icon="import"
toolTipId={ExplorerStringId.TreeItemExportTooltip}
toolTipReplacements={{ fileName: props.fileName }}
onClick={() => dispatch(fileStorageExportFile(props.fileName))}
toolTipReplacements={{ fileName: fileName }}
onClick={() => dispatch(fileStorageExportFile(fileName))}
/>
<ActionButton
icon="trash"
toolTipId={ExplorerStringId.TreeItemDeleteTooltip}
toolTipReplacements={{ fileName: props.fileName }}
onClick={() => dispatch(explorerDeleteFile(props.fileName))}
toolTipReplacements={{ fileName: fileName }}
onClick={() => dispatch(explorerDeleteFile(fileName))}
/>
</ButtonGroup>
);
+18 -14
View File
@@ -17,12 +17,12 @@ type FileNameHelpTextProps = {
/**
* Component that maps FileNameValidationResult to help message to display to user.
*/
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = (
props,
) => {
const FileNameHelpText: React.VoidFunctionComponent<FileNameHelpTextProps> = ({
validation,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
switch (props.validation) {
switch (validation) {
case FileNameValidationResult.IsOk:
return <>{i18n.translate(NewFileWizardStringId.FileNameHelpTextIsOk)}</>;
case FileNameValidationResult.IsEmpty:
@@ -92,23 +92,27 @@ type FileNameFormGroupProps = {
/**
* Component used to get a valid new file name.
*/
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
props,
) => {
const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = ({
fileName,
fileExtension,
inputRef,
onChange,
onValidation,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
const fileNames = useSelector((s) => s.fileStorage.fileNames);
const [fileNameValidation, fileNameIntent] = useMemo(() => {
const result = validateFileName(props.fileName, props.fileExtension, fileNames);
const result = validateFileName(fileName, fileExtension, fileNames);
// can't call callback now because it would break react, so defer it
setTimeout(() => props.onValidation(result), 0);
setTimeout(() => onValidation(result), 0);
return [
result,
result === FileNameValidationResult.IsOk ? Intent.NONE : Intent.DANGER,
];
}, [props.fileName, props.fileExtension, fileNames]);
}, [fileName, fileExtension, fileNames]);
return (
<FormGroup
@@ -118,11 +122,11 @@ const FileNameFormGroup: React.VoidFunctionComponent<FileNameFormGroupProps> = (
>
<InputGroup
aria-label="File name"
value={props.fileName}
inputRef={props.inputRef}
value={fileName}
inputRef={inputRef}
intent={fileNameIntent}
rightElement={<Tag>{props.fileExtension}</Tag>}
onChange={(e) => props.onChange(e.target.value)}
rightElement={<Tag>{fileExtension}</Tag>}
onChange={(e) => onChange(e.target.value)}
/>
</FormGroup>
);
+7 -4
View File
@@ -32,7 +32,10 @@ type NewFileWizardProps = {
readonly onClose: () => void;
};
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) => {
const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
isOpen,
onClose,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
const dispatch = useDispatch();
@@ -48,10 +51,10 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
<Dialog
icon="plus"
title={i18n.translate(NewFileWizardStringId.Title)}
isOpen={props.isOpen}
isOpen={isOpen}
onOpening={() => setFileName('')}
onOpened={() => fileNameInputRef.current?.focus()}
onClose={props.onClose}
onClose={onClose}
>
<div className={Classes.DIALOG_BODY}>
<FileNameFormGroup
@@ -83,7 +86,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
disabled={fileNameValidation !== FileNameValidationResult.IsOk}
onMouseDown={preventFocusOnClick}
onClick={() => {
props.onClose();
onClose();
dispatch(
explorerCreateNewFile(
fileName,
+11 -9
View File
@@ -21,13 +21,15 @@ type RenameFileDialogProps = {
onClose: () => void;
};
const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
props,
) => {
const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = ({
oldName,
isOpen,
onClose,
}) => {
const [i18n] = useI18n({ id: 'explorer', translations: { en }, fallback: en });
const dispatch = useDispatch();
const [baseName, extension] = props.oldName.split(/(\.\w+)$/);
const [baseName, extension] = oldName.split(/(\.\w+)$/);
const [newName, setNewName] = useState(baseName);
const [result, setResult] = useState(FileNameValidationResult.Unknown);
@@ -37,15 +39,15 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
return (
<Dialog
title={i18n.translate(RenameFileStringId.Title, {
fileName: props.oldName,
fileName: oldName,
})}
isOpen={props.isOpen}
isOpen={isOpen}
onOpening={() => setNewName(baseName)}
onOpened={() => {
inputRef.current?.select();
inputRef.current?.focus();
}}
onClose={() => props.onClose()}
onClose={() => onClose()}
>
<div className={Classes.DIALOG_BODY}>
<FileNameFormGroup
@@ -64,10 +66,10 @@ const RenameFileDialog: React.VoidFunctionComponent<RenameFileDialogProps> = (
disabled={result !== FileNameValidationResult.IsOk}
onMouseDown={preventFocusOnClick}
onClick={() => {
props.onClose();
onClose();
dispatch(
fileStorageRenameFile(
props.oldName,
oldName,
`${newName}${extension}`,
),
);
+3 -3
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
@@ -14,7 +14,7 @@ import firmwareIcon from './firmware.svg';
type FlashButtonProps = Pick<OpenFileButtonProps, 'id'>;
const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
const FlashButton: React.VoidFunctionComponent<FlashButtonProps> = ({ id }) => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const flashing = useSelector((s) => s.firmware.flashing);
@@ -24,6 +24,7 @@ const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
return (
<OpenFileButton
id={id}
fileExtension=".zip"
icon={firmwareIcon}
tooltip={flashing ? TooltipId.FlashProgress : TooltipId.Flash}
@@ -43,7 +44,6 @@ const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
)
}
onClick={() => dispatch(flashFirmware(null))}
{...props}
/>
);
};
+3 -3
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
@@ -14,7 +14,7 @@ import btDisconnectedIcon from './bt-disconnected.svg';
type BluetoothButtonProps = Pick<ActionButtonProps, 'id'>;
const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) => {
const BluetoothButton: React.VoidFunctionComponent<BluetoothButtonProps> = ({ id }) => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
@@ -26,6 +26,7 @@ const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) =
return (
<ActionButton
id={id}
tooltip={
isDisconnected
? TooltipId.BluetoothConnect
@@ -35,7 +36,6 @@ const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) =
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
showProgress={bleConnection === BleConnectionState.Connecting}
onAction={() => dispatch(toggleBluetooth())}
{...props}
/>
);
};
+7 -4
View File
@@ -11,10 +11,12 @@ import { downloadAndRun } from './actions';
import { HubRuntimeState } from './reducers';
import runIcon from './run.svg';
type RunButtonProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
type RunButtonProps = Pick<ActionButtonProps, 'id' | 'keyboardShortcut'>;
const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
const RunButton: React.VoidFunctionComponent<RunButtonProps> = ({
id,
keyboardShortcut,
}) => {
const { editor } = useContext(EditorContext);
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
@@ -23,6 +25,8 @@ const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
return (
<ActionButton
id={id}
keyboardShortcut={keyboardShortcut}
tooltip={TooltipId.Run}
progressTooltip={TooltipId.RunProgress}
icon={runIcon}
@@ -30,7 +34,6 @@ const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
showProgress={runtime === HubRuntimeState.Loading}
progress={downloadProgress === null ? undefined : downloadProgress}
onAction={() => dispatch(downloadAndRun())}
{...props}
/>
);
};
+8 -5
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import React from 'react';
import { useDispatch } from 'react-redux';
@@ -10,21 +10,24 @@ import { stop } from './actions';
import { HubRuntimeState } from './reducers';
import stopIcon from './stop.svg';
type StopButtonProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
type StopButtonProps = Pick<ActionButtonProps, 'id' | 'keyboardShortcut'>;
const StopButton: React.FunctionComponent<StopButtonProps> = (props) => {
const StopButton: React.VoidFunctionComponent<StopButtonProps> = ({
id,
keyboardShortcut,
}) => {
const runtime = useSelector((s) => s.hub.runtime);
const dispatch = useDispatch();
return (
<ActionButton
id={id}
keyboardShortcut={keyboardShortcut}
tooltip={TooltipId.Stop}
icon={stopIcon}
enabled={runtime === HubRuntimeState.Running}
onAction={() => dispatch(stop())}
{...props}
/>
);
};
+13 -9
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
// The license dialog
@@ -28,9 +28,9 @@ type LicenseListPanelProps = {
onItemClick(info: LicenseInfo): void;
};
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
props,
) => {
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = ({
onItemClick,
}) => {
const licenseList = useSelector((s) => s.licenses.list);
return (
@@ -43,7 +43,7 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
) : (
<ButtonGroup minimal={true} vertical={true} alignText="left">
{licenseList.map((info, i) => (
<Button key={i} onClick={() => props.onItemClick(info)}>
<Button key={i} onClick={() => onItemClick(info)}>
{info.name}
</Button>
))}
@@ -107,7 +107,10 @@ type LicenseDialogProps = {
onClose(): void;
};
const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = (props) => {
const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = ({
isOpen,
onClose,
}) => {
const infoDiv = React.useRef<HTMLDivElement>(null);
const dispatch = useDispatch();
@@ -116,10 +119,11 @@ const LicenseDialog: React.VoidFunctionComponent<LicenseDialogProps> = (props) =
return (
<Dialog
title={i18n.translate(LicenseStringId.Title)}
onOpening={() => dispatch(fetchList())}
className="pb-license-dialog"
{...props}
title={i18n.translate(LicenseStringId.Title)}
isOpen={isOpen}
onOpening={() => dispatch(fetchList())}
onClose={onClose}
>
<div className={Classes.DIALOG_BODY}>
<Callout className={Classes.INTENT_PRIMARY} icon="info-sign">
+6 -3
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
// provides translation for notification text
@@ -13,14 +13,17 @@ type NotificationActionProps = {
replacements?: Replacements;
};
const NotificationAction: React.FC<NotificationActionProps> = (props) => {
const NotificationAction: React.VoidFunctionComponent<NotificationActionProps> = ({
messageId,
replacements,
}) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
fallback: en,
});
return <>{i18n.translate(props.messageId, props.replacements)}</>;
return <>{i18n.translate(messageId, replacements)}</>;
};
export default NotificationAction;
+6 -3
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
// provides translation for notification text
@@ -13,14 +13,17 @@ type NotificationMessageProps = {
replacements?: Replacements;
};
const NotificationMessage: React.FC<NotificationMessageProps> = (props) => {
const NotificationMessage: React.VoidFunctionComponent<NotificationMessageProps> = ({
messageId,
replacements,
}) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
fallback: en,
});
let message = i18n.translate(props.messageId, props.replacements) as
let message = i18n.translate(messageId, replacements) as
| React.ReactElement
| string;
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
// Provides special notification contents for unexpected errors.
@@ -14,9 +14,9 @@ type UnexpectedErrorNotificationProps = {
err: Error;
};
const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> = (
props,
) => {
const UnexpectedErrorNotification: React.VoidFunctionComponent<
UnexpectedErrorNotificationProps
> = ({ messageId, err }) => {
const [i18n] = useI18n({
id: 'notification',
translations: { en },
@@ -25,9 +25,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
return (
<>
<p>
{i18n.translate(props.messageId, { errorMessage: props.err.message })}
</p>
<p>{i18n.translate(messageId, { errorMessage: err.message })}</p>
<div>
<ButtonGroup minimal={true} fill={true}>
<Button
@@ -35,9 +33,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
icon="duplicate"
onClick={() =>
navigator.clipboard.writeText(
`\`\`\`\n${
props.err.stack || props.err.message
}\n\`\`\``,
`\`\`\`\n${err.stack || err.message}\n\`\`\``,
)
}
>
@@ -48,7 +44,7 @@ const UnexpectedErrorNotification: React.FC<UnexpectedErrorNotificationProps> =
icon="virus"
href={`https://github.com/pybricks/support/issues?q=${encodeURIComponent(
'is:issue',
)}+${encodeURIComponent(props.err.message)}`}
)}+${encodeURIComponent(err.message)}`}
target="_blank"
>
{i18n.translate(MessageId.ReportBug)}
+13 -3
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Copyright (c) 2021-2022 The Pybricks Authors
import React from 'react';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
@@ -8,8 +8,18 @@ import settingsIcon from './settings.svg';
type SettingsButtonProps = Pick<ActionButtonProps, 'id' | 'onAction'>;
const SettingsButton: React.FunctionComponent<SettingsButtonProps> = (props) => {
return <ActionButton tooltip={TooltipId.Settings} icon={settingsIcon} {...props} />;
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
id,
onAction,
}) => {
return (
<ActionButton
id={id}
tooltip={TooltipId.Settings}
icon={settingsIcon}
onAction={onAction}
/>
);
};
export default SettingsButton;
+6 -3
View File
@@ -45,7 +45,10 @@ type SettingsProps = {
onClose(): void;
};
const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
isOpen,
onClose,
}) => {
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
const showDocs = useSelector((s) => s.settings.showDocs);
@@ -89,11 +92,11 @@ const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
return (
<Drawer
isOpen={props.isOpen}
isOpen={isOpen}
icon="cog"
size={DrawerSize.SMALL}
title={i18n.translate(SettingsStringId.Title)}
onClose={props.onClose}
onClose={onClose}
>
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
+29 -21
View File
@@ -40,7 +40,17 @@ export interface ActionButtonProps {
readonly onAction: () => void;
}
const ActionButton: React.FC<ActionButtonProps> = (props) => {
const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
id,
keyboardShortcut,
tooltip,
progressTooltip,
icon,
enabled,
showProgress,
progress,
onAction,
}) => {
const [i18n] = useI18n({ id: 'actionButton', translations: { en }, fallback: en });
const [isSmallScreen, setIsSmallScreen] = useState(
@@ -58,18 +68,16 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
const tooltipText =
props.showProgress && props.progressTooltip
? i18n.translate(props.progressTooltip, {
showProgress && progressTooltip
? i18n.translate(progressTooltip, {
percent:
props.progress === undefined
? ''
: i18n.formatPercentage(props.progress),
progress === undefined ? '' : i18n.formatPercentage(progress),
})
: i18n.translate(props.tooltip) +
(props.keyboardShortcut ? ` (${props.keyboardShortcut})` : '');
: i18n.translate(tooltip) +
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
const hotkeys = useMemo(() => {
if (!props.keyboardShortcut) {
if (!keyboardShortcut) {
return [];
}
@@ -78,16 +86,16 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
global: true,
allowInInput: true,
preventDefault: true,
combo: props.keyboardShortcut.replaceAll('-', '+'),
label: i18n.translate(props.tooltip),
combo: keyboardShortcut.replaceAll('-', '+'),
label: i18n.translate(tooltip),
onKeyDown: () => {
if (props.enabled) {
props.onAction();
if (enabled) {
onAction();
}
},
},
];
}, [props.keyboardShortcut, props.tooltip, props.enabled, props.onAction, i18n]);
}, [keyboardShortcut, tooltip, enabled, onAction, i18n]);
useHotkeys(hotkeys);
@@ -106,13 +114,13 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
{...tooltipTargetProps}
intent={Intent.PRIMARY}
onMouseDown={preventFocusOnClick}
onClick={props.onAction}
disabled={props.enabled === false}
style={props.enabled === false ? pointerEventsNone : undefined}
onClick={onAction}
disabled={enabled === false}
style={enabled === false ? pointerEventsNone : undefined}
>
{props.showProgress ? (
{showProgress ? (
<Spinner
value={props.progress}
value={progress}
intent={Intent.PRIMARY}
size={buttonSize}
/>
@@ -120,8 +128,8 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
<img
width={`${buttonSize}px`}
height={`${buttonSize}px`}
src={props.icon}
alt={props.id}
src={icon}
alt={id}
style={pointerEventsNone}
/>
)}
+27 -16
View File
@@ -38,7 +38,18 @@ export interface OpenFileButtonProps {
/**
* Button that opens a file chooser dialog or accepts files dropped on it.
*/
const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
id,
fileExtension,
tooltip,
icon,
enabled,
showProgress,
progress,
onFile,
onReject,
onClick,
}) => {
const [i18n] = useI18n({
id: 'openFileButton',
translations: { en },
@@ -60,11 +71,11 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
const { getRootProps, getInputProps } = useDropzone({
accept: props.fileExtension,
accept: fileExtension,
// using File System Access API is blocked by https://github.com/react-dropzone/react-dropzone/issues/1141
useFsAccessApi: false,
multiple: false,
noClick: props.onClick !== undefined,
noClick: onClick !== undefined,
onDropAccepted: (acceptedFiles) => {
// should only be one file since multiple={false}
acceptedFiles.forEach((f) => {
@@ -80,7 +91,7 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
if (typeof binaryStr === 'string') {
throw Error('Unexpected string binaryStr');
}
props.onFile(binaryStr);
onFile(binaryStr);
};
reader.readAsArrayBuffer(f);
});
@@ -88,7 +99,7 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
onDropRejected: (fileRejections) => {
// should only be one file since multiple={false}
fileRejections.forEach((r) => {
props.onReject(r.file);
onReject(r.file);
});
},
});
@@ -96,13 +107,13 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
return (
<Tooltip2
content={i18n.translate(
props.tooltip,
props.tooltip === TooltipId.FlashProgress
tooltip,
tooltip === TooltipId.FlashProgress
? {
percent:
props.progress === undefined
progress === undefined
? ''
: i18n.formatPercentage(props.progress),
: i18n.formatPercentage(progress),
}
: undefined,
)}
@@ -119,16 +130,16 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
elementRef: tooltipTargetRef as IRef<HTMLButtonElement>,
...tooltipTargetProps,
intent: Intent.PRIMARY,
disabled: props.enabled === false,
style: props.enabled === false ? pointerEventsNone : undefined,
disabled: enabled === false,
style: enabled === false ? pointerEventsNone : undefined,
onMouseDown: preventFocusOnClick,
onClick: props.onClick,
onClick: onClick,
})}
>
<input {...getInputProps()} />
{props.showProgress ? (
{showProgress ? (
<Spinner
value={props.progress}
value={progress}
intent={Intent.PRIMARY}
size={buttonSize}
/>
@@ -136,8 +147,8 @@ const OpenFileButton: React.FC<OpenFileButtonProps> = (props) => {
<img
width={`${buttonSize}px`}
height={`${buttonSize}px`}
src={props.icon}
alt={props.id}
src={icon}
alt={id}
style={pointerEventsNone}
/>
)}