mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
components: add new component to fix a11y issues
This commit is contained in:
@@ -80,11 +80,13 @@
|
|||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
|
"react-aria": "^3.15.0",
|
||||||
"react-complex-tree": "^1.1.5",
|
"react-complex-tree": "^1.1.5",
|
||||||
"react-dev-utils": "^12.0.1",
|
"react-dev-utils": "^12.0.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-dropzone": "^12.0.4",
|
"react-dropzone": "^12.0.4",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
|
"react-popper": "^2.3.0",
|
||||||
"react-redux": "^7.2.6",
|
"react-redux": "^7.2.6",
|
||||||
"react-refresh": "^0.11.0",
|
"react-refresh": "^0.11.0",
|
||||||
"react-splitter-layout": "^4.0.0",
|
"react-splitter-layout": "^4.0.0",
|
||||||
|
|||||||
+15
-1
@@ -2,7 +2,7 @@
|
|||||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||||
|
|
||||||
import { eventChannel } from 'redux-saga';
|
import { eventChannel } from 'redux-saga';
|
||||||
import { call, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
|
import { call, delay, fork, put, take, takeEvery } from 'typed-redux-saga/macro';
|
||||||
import {
|
import {
|
||||||
serviceWorkerDidSucceed,
|
serviceWorkerDidSucceed,
|
||||||
serviceWorkerDidUpdate,
|
serviceWorkerDidUpdate,
|
||||||
@@ -121,8 +121,22 @@ function* monitorBeforeInstallPrompt(): Generator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// istabul ignore next: for dev envionemnt only
|
||||||
|
function* fakeCheckForUpdate(): Generator {
|
||||||
|
console.warn('checking for updates is not supported in developemnt mode');
|
||||||
|
// let this "busy" indication spin for a it
|
||||||
|
yield* delay(3000);
|
||||||
|
// then indicate we are already up-to-date
|
||||||
|
yield* put(appDidCheckForUpdate(false));
|
||||||
|
}
|
||||||
|
|
||||||
export default function* app(): Generator {
|
export default function* app(): Generator {
|
||||||
yield* fork(monitorServiceWorkerRegistration);
|
yield* fork(monitorServiceWorkerRegistration);
|
||||||
yield* fork(monitorAppInstalled);
|
yield* fork(monitorAppInstalled);
|
||||||
yield* fork(monitorBeforeInstallPrompt);
|
yield* fork(monitorBeforeInstallPrompt);
|
||||||
|
|
||||||
|
// istabul ignore if: for dev envionemnt only
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
yield* takeEvery(appCheckForUpdate, fakeCheckForUpdate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
import { Classes, Icon, IconName, IconSize, Spinner } from '@blueprintjs/core';
|
||||||
|
import { mergeRefs, useId } from '@react-aria/utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { FocusRing, useButton } from 'react-aria';
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
/** The label for the button. */
|
||||||
|
label: string;
|
||||||
|
/** If true, the label will not be visible (will use aria-label instead). */
|
||||||
|
hideLabel?: boolean;
|
||||||
|
/** A description of what the button does (not displayed - read by screen reader). */
|
||||||
|
description?: string;
|
||||||
|
/** When true, the contents of the button will be replaced with a {@link Spinner}. */
|
||||||
|
loading?: boolean;
|
||||||
|
/** When true, the button will use the {@link Classes.MINIMAL} style. */
|
||||||
|
minimal?: boolean;
|
||||||
|
/** Icon that will be displayed to the left of the button content. */
|
||||||
|
icon: IconName;
|
||||||
|
/** A refernece to the underlying <button> HTML element. */
|
||||||
|
elementRef?: React.ForwardedRef<HTMLButtonElement>;
|
||||||
|
/** Called when the button is pressed. */
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Similar to Blueprint.js button with better accessibility. */
|
||||||
|
export const Button: React.VoidFunctionComponent<ButtonProps> = ({
|
||||||
|
label,
|
||||||
|
hideLabel,
|
||||||
|
description,
|
||||||
|
loading,
|
||||||
|
minimal,
|
||||||
|
icon,
|
||||||
|
elementRef,
|
||||||
|
onPress,
|
||||||
|
}) => {
|
||||||
|
const isLabelVisible = !hideLabel && !loading;
|
||||||
|
|
||||||
|
const labelId = useId();
|
||||||
|
const descriptionId = useId();
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const { buttonProps, isPressed } = useButton(
|
||||||
|
{
|
||||||
|
onPress,
|
||||||
|
'aria-label': isLabelVisible ? undefined : label,
|
||||||
|
'aria-labelledby': isLabelVisible ? labelId : undefined,
|
||||||
|
// REVISIT: aria-description is not widley supported yet
|
||||||
|
'aria-describedby': description ? descriptionId : undefined,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* The blueprint focus manage doesn't always get it right, so we ignore it. */}
|
||||||
|
<FocusRing focusRingClass={Classes.FOCUS_STYLE_MANAGER_IGNORE}>
|
||||||
|
<button
|
||||||
|
className={classNames(Classes.BUTTON, {
|
||||||
|
[Classes.ACTIVE]: isPressed,
|
||||||
|
[Classes.LOADING]: loading,
|
||||||
|
[Classes.MINIMAL]: minimal,
|
||||||
|
})}
|
||||||
|
ref={mergeRefs(ref, elementRef ?? null)}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Spinner
|
||||||
|
className={Classes.BUTTON_SPINNER}
|
||||||
|
size={IconSize.LARGE}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon icon={icon} aria-hidden />
|
||||||
|
{isLabelVisible && (
|
||||||
|
<span id={labelId} className={Classes.BUTTON_TEXT}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</FocusRing>
|
||||||
|
{/* This can't be inside the buttton element, otherwise it messes up the styling */}
|
||||||
|
{description && (
|
||||||
|
<div id={descriptionId} hidden>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
import { Classes } from '@blueprintjs/core';
|
||||||
|
import { useI18n } from '@shopify/react-i18n';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { OverlayContainer } from 'react-aria';
|
||||||
|
import { useBoolean } from 'usehooks-ts';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import HelpDialog from './HelpDialog';
|
||||||
|
import { I18nId } from './i18n';
|
||||||
|
|
||||||
|
type HelpButtonProps = {
|
||||||
|
/** The label of the control this button provides help for. */
|
||||||
|
helpForLabel: string;
|
||||||
|
/** The help dialog content. */
|
||||||
|
content: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HelpButton: React.VoidFunctionComponent<HelpButtonProps> = ({
|
||||||
|
helpForLabel,
|
||||||
|
content,
|
||||||
|
}) => {
|
||||||
|
// istanbul ignore next: babel-loader rewrites this line
|
||||||
|
const [i18n] = useI18n();
|
||||||
|
|
||||||
|
const {
|
||||||
|
value: isDialogOpen,
|
||||||
|
setTrue: setIsDialogOpenTrue,
|
||||||
|
setFalse: setIsDialogOpenFalse,
|
||||||
|
} = useBoolean(false);
|
||||||
|
|
||||||
|
const [isDialogMounted, setIsDialogMounted] = useState(false);
|
||||||
|
|
||||||
|
// isDialogMounted=true is triggered on isDialogOpen==true rising edge
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDialogOpen) {
|
||||||
|
setIsDialogMounted(true);
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, setIsDialogMounted]);
|
||||||
|
|
||||||
|
// isDialogMounted=false is triggered when isDialogOpen==false and the animation ends
|
||||||
|
const handleAnimationEnd = useCallback(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setIsDialogMounted(false);
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, setIsDialogMounted]);
|
||||||
|
|
||||||
|
const [openButton, setOpenButton] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label={i18n.translate(I18nId.HelpButtonLabel)}
|
||||||
|
hideLabel
|
||||||
|
description={i18n.translate(I18nId.HelpButtonDescription, {
|
||||||
|
helpForLabel,
|
||||||
|
})}
|
||||||
|
minimal
|
||||||
|
icon="help"
|
||||||
|
elementRef={setOpenButton}
|
||||||
|
onPress={setIsDialogOpenTrue}
|
||||||
|
/>
|
||||||
|
{isDialogMounted && (
|
||||||
|
<OverlayContainer className={Classes.PORTAL}>
|
||||||
|
<HelpDialog
|
||||||
|
title={i18n.translate(I18nId.HelpDialogTitle)}
|
||||||
|
isOpen={isDialogOpen}
|
||||||
|
openButton={openButton}
|
||||||
|
onClose={setIsDialogOpenFalse}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</HelpDialog>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HelpButton;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
@use '@blueprintjs/core/lib/scss/variables' as bp;
|
||||||
|
|
||||||
|
.#{bp.$ns}-popover2-open.#{bp.$ns}-popover2-placement-right {
|
||||||
|
animation: open-right 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{bp.$ns}-popover2-close.#{bp.$ns}-popover2-placement-right {
|
||||||
|
animation: close-right 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin start-right {
|
||||||
|
// starts with 0 size aligned to the left edge
|
||||||
|
transform: translateX(-50%) scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin end {
|
||||||
|
transform: translateX(0%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes open-right {
|
||||||
|
from {
|
||||||
|
@include start-right;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
@include end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes close-right {
|
||||||
|
from {
|
||||||
|
@include end;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
@include start-right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
import './HelpDialog.scss';
|
||||||
|
import { Classes } from '@blueprintjs/core';
|
||||||
|
import { Classes as Classes2 } from '@blueprintjs/popover2';
|
||||||
|
import {
|
||||||
|
POPOVER_ARROW_SVG_SIZE,
|
||||||
|
Popover2Arrow,
|
||||||
|
} from '@blueprintjs/popover2/lib/cjs/popover2Arrow';
|
||||||
|
import { useI18n } from '@shopify/react-i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
DismissButton,
|
||||||
|
FocusScope,
|
||||||
|
mergeProps,
|
||||||
|
useDialog,
|
||||||
|
useId,
|
||||||
|
useModal,
|
||||||
|
useOverlay,
|
||||||
|
} from 'react-aria';
|
||||||
|
import { usePopper } from 'react-popper';
|
||||||
|
import { I18nId } from './i18n';
|
||||||
|
|
||||||
|
type HelpDialogProps = {
|
||||||
|
/** The title of the dialog. */
|
||||||
|
title: string;
|
||||||
|
/** Controls the dialog open state. */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** The button that triggered the dialog to open. */
|
||||||
|
openButton: HTMLButtonElement | null;
|
||||||
|
/** Called when the dialog has been requested to close. */
|
||||||
|
onClose: () => void;
|
||||||
|
/** Called if/when CSS animation ends on the `.pb-help-dialog` element. */
|
||||||
|
onAnimationEnd?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** React component for showing help as a dialog. */
|
||||||
|
const HelpDialog: React.FunctionComponent<HelpDialogProps> = ({
|
||||||
|
title,
|
||||||
|
isOpen,
|
||||||
|
openButton,
|
||||||
|
onClose,
|
||||||
|
onAnimationEnd,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
// istanbul ignore next: babel-loader rewrites this line
|
||||||
|
const [i18n] = useI18n();
|
||||||
|
|
||||||
|
// this is the dialog element and the popper element
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
|
{ isOpen, onClose, isDismissable: true },
|
||||||
|
ref,
|
||||||
|
);
|
||||||
|
const descId = useId();
|
||||||
|
const { modalProps } = useModal();
|
||||||
|
const { dialogProps } = useDialog(
|
||||||
|
{ 'aria-label': title, 'aria-describedby': descId },
|
||||||
|
ref,
|
||||||
|
);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const [arrow, setArrow] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { styles, attributes, state } = usePopper(
|
||||||
|
openButton,
|
||||||
|
popperElement,
|
||||||
|
useMemo(
|
||||||
|
() => ({
|
||||||
|
placement: 'right',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'arrow',
|
||||||
|
options: {
|
||||||
|
element: arrow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, POPOVER_ARROW_SVG_SIZE / 2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[arrow],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// HACK: workaround https://github.com/palantir/blueprint/pull/5302
|
||||||
|
useEffect(() => {
|
||||||
|
arrow?.setAttribute('aria-hidden', 'true');
|
||||||
|
}, [arrow]);
|
||||||
|
|
||||||
|
// HACK: since there are not keyboard focusable elements for autoFocus
|
||||||
|
// we have to manually focus the only focusable element in the dialog,
|
||||||
|
// otherwise the FocusScope doesn't work.
|
||||||
|
|
||||||
|
const dismissId = useId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.getElementById(dismissId)?.focus();
|
||||||
|
}, [dismissId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(Classes.OVERLAY, { [Classes.OVERLAY_OPEN]: isOpen })}
|
||||||
|
>
|
||||||
|
<div className={Classes.OVERLAY_BACKDROP} {...underlayProps}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
Classes2.POPOVER2_TRANSITION_CONTAINER,
|
||||||
|
Classes.OVERLAY_CONTENT,
|
||||||
|
)}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
Classes2.POPOVER2,
|
||||||
|
`${Classes2.POPOVER2}-${isOpen ? 'open' : 'close'}`,
|
||||||
|
`${Classes2.POPOVER2_CONTENT_PLACEMENT}-right`,
|
||||||
|
Classes2.POPOVER2_CONTENT_SIZING,
|
||||||
|
)}
|
||||||
|
onAnimationEnd={onAnimationEnd}
|
||||||
|
>
|
||||||
|
<Popover2Arrow
|
||||||
|
arrowProps={{ ref: setArrow, style: styles.arrow }}
|
||||||
|
placement={state?.placement ?? 'auto'}
|
||||||
|
/>
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div
|
||||||
|
aria-modal
|
||||||
|
className="pb-help-dialog"
|
||||||
|
ref={ref}
|
||||||
|
{...mergeProps(overlayProps, dialogProps, modalProps)}
|
||||||
|
// dialog itself should not be focusable
|
||||||
|
tabIndex={undefined}
|
||||||
|
// clicking anywhere in the dialog closes it
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div id={descId} className={Classes2.POPOVER2_CONTENT}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<DismissButton
|
||||||
|
id={dismissId}
|
||||||
|
aria-label={i18n.translate(
|
||||||
|
I18nId.HelpDialogCloseButtonLabel,
|
||||||
|
)}
|
||||||
|
onDismiss={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HelpDialog;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This folder contains generic components.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||||
|
|
||||||
|
import { lookup } from '../../test';
|
||||||
|
import { I18nId } from './i18n';
|
||||||
|
import en from './translations/en.json';
|
||||||
|
|
||||||
|
describe('Ensure .json file has matches for I18nId', () => {
|
||||||
|
test.each(Object.values(I18nId))('%s', (id) => {
|
||||||
|
expect(lookup(en, id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2022 The Pybricks Authors
|
||||||
|
|
||||||
|
export enum I18nId {
|
||||||
|
HelpButtonLabel = 'helpButton.label',
|
||||||
|
HelpButtonDescription = 'helpButton.description',
|
||||||
|
HelpDialogTitle = 'helpDialog.title',
|
||||||
|
HelpDialogCloseButtonLabel = 'helpDialog.closeButton.label',
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"helpButton": {
|
||||||
|
"label": "Help",
|
||||||
|
"description": "Click to show help for the {helpForLabel} checkbox."
|
||||||
|
},
|
||||||
|
"helpDialog": {
|
||||||
|
"title": "Help",
|
||||||
|
"closeButton": {
|
||||||
|
"label": "Close"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,6 +112,11 @@ a.#{bp.$ns}-button {
|
|||||||
min-width: math.div(bp.$pt-grid-size, 2);
|
min-width: math.div(bp.$pt-grid-size, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{bp.$ns}-focus-disabled :focus.#{bp.$ns}-focus-style-manager-ignore {
|
||||||
|
outline: bp.$pt-outline-color auto 2px !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@
|
|||||||
import { FocusStyleManager } from '@blueprintjs/core';
|
import { FocusStyleManager } from '@blueprintjs/core';
|
||||||
import { I18nContext } from '@shopify/react-i18n';
|
import { I18nContext } from '@shopify/react-i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { OverlayProvider } from 'react-aria';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { applyMiddleware, createStore } from 'redux';
|
import { applyMiddleware, createStore } from 'redux';
|
||||||
@@ -53,7 +54,9 @@ ReactDOM.render(
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nContext.Provider value={i18nManager}>
|
<I18nContext.Provider value={i18nManager}>
|
||||||
<ViewHeightSensor />
|
<ViewHeightSensor />
|
||||||
<App />
|
<OverlayProvider>
|
||||||
|
<App />
|
||||||
|
</OverlayProvider>
|
||||||
</I18nContext.Provider>
|
</I18nContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|||||||
+34
-70
@@ -3,20 +3,17 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AnchorButton,
|
AnchorButton,
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ControlGroup,
|
ControlGroup,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
IRef,
|
|
||||||
Icon,
|
Icon,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
Switch,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { Classes as Classes2, Popover2 } from '@blueprintjs/popover2';
|
|
||||||
import { useI18n } from '@shopify/react-i18n';
|
import { useI18n } from '@shopify/react-i18n';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useTernaryDarkMode } from 'usehooks-ts';
|
import { useTernaryDarkMode } from 'usehooks-ts';
|
||||||
import AboutDialog from '../about/AboutDialog';
|
import AboutDialog from '../about/AboutDialog';
|
||||||
@@ -28,6 +25,8 @@ import {
|
|||||||
pybricksProjectsUrl,
|
pybricksProjectsUrl,
|
||||||
pybricksSupportUrl,
|
pybricksSupportUrl,
|
||||||
} from '../app/constants';
|
} from '../app/constants';
|
||||||
|
import { Button } from '../components/Button';
|
||||||
|
import HelpButton from '../components/HelpButton';
|
||||||
import { pseudolocalize } from '../i18n';
|
import { pseudolocalize } from '../i18n';
|
||||||
import { useSelector } from '../reducers';
|
import { useSelector } from '../reducers';
|
||||||
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
|
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
|
||||||
@@ -40,43 +39,6 @@ import {
|
|||||||
import { I18nId } from './i18n';
|
import { I18nId } from './i18n';
|
||||||
import './settings.scss';
|
import './settings.scss';
|
||||||
|
|
||||||
type HelpButtonProps = {
|
|
||||||
label: string;
|
|
||||||
content: string | JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const HelpButton: React.VoidFunctionComponent<HelpButtonProps> = ({
|
|
||||||
label,
|
|
||||||
content,
|
|
||||||
}) => {
|
|
||||||
const handleOpening = useCallback((node: HTMLElement) => {
|
|
||||||
// role must match aria-haspopup
|
|
||||||
node.setAttribute('role', 'dialog');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover2
|
|
||||||
onOpening={handleOpening}
|
|
||||||
placement="right"
|
|
||||||
shouldReturnFocusOnClose
|
|
||||||
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
|
|
||||||
content={content}
|
|
||||||
renderTarget={({ isOpen, ref, ...targetProps }) => (
|
|
||||||
<Button
|
|
||||||
aria-label={label}
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
minimal
|
|
||||||
icon="help"
|
|
||||||
elementRef={ref as IRef<HTMLButtonElement>}
|
|
||||||
{...targetProps}
|
|
||||||
// override targetProps
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Settings: React.VoidFunctionComponent = () => {
|
const Settings: React.VoidFunctionComponent = () => {
|
||||||
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
|
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
|
||||||
useSettingIsShowDocsEnabled();
|
useSettingIsShowDocsEnabled();
|
||||||
@@ -103,7 +65,7 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
const [i18n] = useI18n();
|
const [i18n] = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-settings" aria-label={i18n.translate(I18nId.Title)}>
|
<div className="pb-settings">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={i18n.translate(I18nId.AppearanceTitle)}
|
label={i18n.translate(I18nId.AppearanceTitle)}
|
||||||
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
|
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
|
||||||
@@ -122,10 +84,10 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<HelpButton
|
<HelpButton
|
||||||
label={i18n.translate(I18nId.AppearanceDocumentationHelpLabel)}
|
helpForLabel={i18n.translate(
|
||||||
content={i18n.translate(
|
I18nId.AppearanceDocumentationLabel,
|
||||||
I18nId.AppearanceDocumentationHelpContent,
|
|
||||||
)}
|
)}
|
||||||
|
content={i18n.translate(I18nId.AppearanceDocumentationHelp)}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
@@ -141,8 +103,8 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<HelpButton
|
<HelpButton
|
||||||
label={i18n.translate(I18nId.AppearanceDarkModeHelpLabel)}
|
helpForLabel={i18n.translate(I18nId.AppearanceDarkModeLabel)}
|
||||||
content={i18n.translate(I18nId.AppearanceDarkModeHelpContent)}
|
content={i18n.translate(I18nId.AppearanceDarkModeHelp)}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -158,11 +120,12 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<HelpButton
|
<HelpButton
|
||||||
label={i18n.translate(I18nId.FirmwareCurrentProgramHelpLabel)}
|
helpForLabel={i18n.translate(
|
||||||
content={i18n.translate(
|
I18nId.FirmwareCurrentProgramLabel,
|
||||||
I18nId.FirmwareCurrentProgramHelpContent,
|
|
||||||
{ appName },
|
|
||||||
)}
|
)}
|
||||||
|
content={i18n.translate(I18nId.FirmwareCurrentProgramHelp, {
|
||||||
|
appName,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
<Label htmlFor="hub-name-input">
|
<Label htmlFor="hub-name-input">
|
||||||
@@ -187,8 +150,8 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<HelpButton
|
<HelpButton
|
||||||
label={i18n.translate(I18nId.FirmwareHubNameHelpLabel)}
|
helpForLabel={i18n.translate(I18nId.FirmwareHubNameLabel)}
|
||||||
content={i18n.translate(I18nId.FirmwareHubNameHelpContent)}
|
content={i18n.translate(I18nId.FirmwareHubNameHelp)}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -233,36 +196,37 @@ const Settings: React.VoidFunctionComponent = () => {
|
|||||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||||
{hasUnresolvedInstallPrompt && (
|
{hasUnresolvedInstallPrompt && (
|
||||||
<Button
|
<Button
|
||||||
|
label={i18n.translate(I18nId.AppInstallLabel)}
|
||||||
icon="add"
|
icon="add"
|
||||||
onClick={() => dispatch(appShowInstallPrompt())}
|
onPress={() => dispatch(appShowInstallPrompt())}
|
||||||
loading={promptingInstall}
|
loading={promptingInstall}
|
||||||
>
|
/>
|
||||||
{i18n.translate(I18nId.AppInstallLabel)}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{isServiceWorkerRegistered && !updateAvailable && (
|
{(process.env.NODE_ENV === 'development' ||
|
||||||
|
(isServiceWorkerRegistered && !updateAvailable)) && (
|
||||||
<Button
|
<Button
|
||||||
|
label={i18n.translate(I18nId.AppCheckForUpdateLabel)}
|
||||||
icon="refresh"
|
icon="refresh"
|
||||||
onClick={() => dispatch(appCheckForUpdate())}
|
onPress={() => dispatch(appCheckForUpdate())}
|
||||||
loading={checkingForUpdate}
|
loading={checkingForUpdate}
|
||||||
>
|
/>
|
||||||
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{isServiceWorkerRegistered && updateAvailable && (
|
{(process.env.NODE_ENV === 'development' ||
|
||||||
<Button icon="refresh" onClick={() => dispatch(appReload())}>
|
(isServiceWorkerRegistered && updateAvailable)) && (
|
||||||
{i18n.translate(I18nId.AppRestartLabel)}
|
<Button
|
||||||
</Button>
|
label={i18n.translate(I18nId.AppRestartLabel)}
|
||||||
|
icon="refresh"
|
||||||
|
onPress={() => dispatch(appReload())}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
label={i18n.translate(I18nId.AppAboutLabel)}
|
||||||
icon="info-sign"
|
icon="info-sign"
|
||||||
onClick={() => {
|
onPress={() => {
|
||||||
setIsAboutDialogOpen(true);
|
setIsAboutDialogOpen(true);
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{i18n.translate(I18nId.AppAboutLabel)}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
|||||||
@@ -7,19 +7,15 @@ export enum I18nId {
|
|||||||
Title = 'title',
|
Title = 'title',
|
||||||
AppearanceTitle = 'appearance.title',
|
AppearanceTitle = 'appearance.title',
|
||||||
AppearanceDocumentationLabel = 'appearance.documentation.label',
|
AppearanceDocumentationLabel = 'appearance.documentation.label',
|
||||||
AppearanceDocumentationHelpLabel = 'appearance.documentation.help.label',
|
AppearanceDocumentationHelp = 'appearance.documentation.help',
|
||||||
AppearanceDocumentationHelpContent = 'appearance.documentation.help.content',
|
|
||||||
AppearanceDarkModeLabel = 'appearance.darkMode.label',
|
AppearanceDarkModeLabel = 'appearance.darkMode.label',
|
||||||
AppearanceDarkModeHelpLabel = 'appearance.darkMode.help.label',
|
AppearanceDarkModeHelp = 'appearance.darkMode.help',
|
||||||
AppearanceDarkModeHelpContent = 'appearance.darkMode.help.content',
|
|
||||||
AppearanceZoomHelp = 'appearance.zoom.help',
|
AppearanceZoomHelp = 'appearance.zoom.help',
|
||||||
FirmwareTitle = 'firmware.title',
|
FirmwareTitle = 'firmware.title',
|
||||||
FirmwareCurrentProgramLabel = 'firmware.flashCurrentProgram.label',
|
FirmwareCurrentProgramLabel = 'firmware.flashCurrentProgram.label',
|
||||||
FirmwareCurrentProgramHelpLabel = 'firmware.flashCurrentProgram.help.label',
|
FirmwareCurrentProgramHelp = 'firmware.flashCurrentProgram.help',
|
||||||
FirmwareCurrentProgramHelpContent = 'firmware.flashCurrentProgram.help.content',
|
|
||||||
FirmwareHubNameLabel = 'firmware.hubName.label',
|
FirmwareHubNameLabel = 'firmware.hubName.label',
|
||||||
FirmwareHubNameHelpLabel = 'firmware.hubName.help.label',
|
FirmwareHubNameHelp = 'firmware.hubName.help',
|
||||||
FirmwareHubNameHelpContent = 'firmware.hubName.help.content',
|
|
||||||
FirmwareHubNameError = 'firmware.hubName.error',
|
FirmwareHubNameError = 'firmware.hubName.error',
|
||||||
HelpTitle = 'help.title',
|
HelpTitle = 'help.title',
|
||||||
HelpProjectsLabel = 'help.projects.label',
|
HelpProjectsLabel = 'help.projects.label',
|
||||||
|
|||||||
@@ -4,17 +4,11 @@
|
|||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"label": "Documentation",
|
"label": "Documentation",
|
||||||
"help": {
|
"help": "Enable to show the documentation and disable to hide the documentation."
|
||||||
"label": "Show help for documentation toggle.",
|
|
||||||
"content": "Enable to show the documentation and disable to hide the documentation."
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
"label": "Dark mode",
|
"label": "Dark mode",
|
||||||
"help": {
|
"help": "Enable to set theme to dark mode and disable to set theme to light mode."
|
||||||
"label": "Show help for dark mode toggle.",
|
|
||||||
"content": "Enable to set theme to dark mode and disable to set theme to light mode."
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"zoom": {
|
"zoom": {
|
||||||
"help": "Use {in} and {out} to zoom."
|
"help": "Use {in} and {out} to zoom."
|
||||||
@@ -24,17 +18,11 @@
|
|||||||
"title": "Firmware",
|
"title": "Firmware",
|
||||||
"flashCurrentProgram": {
|
"flashCurrentProgram": {
|
||||||
"label": "Include current program",
|
"label": "Include current program",
|
||||||
"help": {
|
"help": "Enable to include your program when flashing the firmware or disable to use the default program. Flashing your program along with the firmware will allow you to run your program without being connected to {appName}"
|
||||||
"label": "Show help for include current program toggle.",
|
|
||||||
"content": "Enable to include your program when flashing the firmware or disable to use the default program. Flashing your program along with the firmware will allow you to run your program without being connected to {appName}"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"hubName": {
|
"hubName": {
|
||||||
"label": "Hub name",
|
"label": "Hub name",
|
||||||
"help": {
|
"help": "Enter a name here to customize the hub name when flashing the firmware. This name will be used in the Bluetooth advertising data and can be used to identify the hub when connecting.",
|
||||||
"label": "Show help on hub name input",
|
|
||||||
"content": "Enter a name here to customize the hub name when flashing the firmware. This name will be used in the Bluetooth advertising data and can be used to identify the hub when connecting."
|
|
||||||
},
|
|
||||||
"error": "The name is too long."
|
"error": "The name is too long."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user