mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
Add Activities
This adds a new Activities tab list and view similar to VS code. The Explorer and Settings are migrated to this view.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { useIsFirstRender } from 'usehooks-ts';
|
||||
import { testRender } from '../../test';
|
||||
import { Activity, useActivities } from './Activities';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
jest.resetAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
type TestActivityProps = {
|
||||
expectedActivity: Activity;
|
||||
};
|
||||
|
||||
const TestActivity: React.VoidFunctionComponent<TestActivityProps> = ({
|
||||
expectedActivity,
|
||||
}) => {
|
||||
const [selectedActivity, activitiesComponent] = useActivities();
|
||||
|
||||
if (useIsFirstRender()) {
|
||||
expect(selectedActivity).toBe(expectedActivity);
|
||||
}
|
||||
|
||||
return activitiesComponent;
|
||||
};
|
||||
|
||||
describe('Activities', () => {
|
||||
it('should select explorer by default', () => {
|
||||
const [activities] = testRender(
|
||||
<TestActivity expectedActivity={Activity.Explorer} />,
|
||||
);
|
||||
|
||||
const tab = activities.getByRole('tab', { name: 'File Explorer' });
|
||||
|
||||
expect(tab).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('should use localStorage for default value', () => {
|
||||
localStorage.setItem(
|
||||
'activities.selectedActivity',
|
||||
JSON.stringify(Activity.Settings),
|
||||
);
|
||||
|
||||
const [activities] = testRender(
|
||||
<TestActivity expectedActivity={Activity.Settings} />,
|
||||
);
|
||||
|
||||
const tab = activities.getByRole('tab', { name: 'Settings & Help' });
|
||||
|
||||
expect(tab).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('should select none when clicking already selected tab', () => {
|
||||
const [activities] = testRender(
|
||||
<TestActivity expectedActivity={Activity.Explorer} />,
|
||||
);
|
||||
|
||||
const explorerTab = activities.getByRole('tab', { name: 'File Explorer' });
|
||||
|
||||
for (const tab of activities.getAllByRole('tab')) {
|
||||
expect(tab).toHaveAttribute(
|
||||
'aria-selected',
|
||||
tab === explorerTab ? 'true' : 'false',
|
||||
);
|
||||
}
|
||||
|
||||
userEvent.click(explorerTab);
|
||||
|
||||
for (const tab of activities.getAllByRole('tab')) {
|
||||
expect(tab).toHaveAttribute('aria-selected', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
it('should select new tab when clicking not already selected tab', () => {
|
||||
const [activities] = testRender(
|
||||
<TestActivity expectedActivity={Activity.Explorer} />,
|
||||
);
|
||||
|
||||
const explorerTab = activities.getByRole('tab', { name: 'File Explorer' });
|
||||
const settingsTab = activities.getByRole('tab', { name: 'Settings & Help' });
|
||||
|
||||
for (const tab of activities.getAllByRole('tab')) {
|
||||
expect(tab).toHaveAttribute(
|
||||
'aria-selected',
|
||||
tab === explorerTab ? 'true' : 'false',
|
||||
);
|
||||
}
|
||||
|
||||
userEvent.click(settingsTab);
|
||||
|
||||
for (const tab of activities.getAllByRole('tab')) {
|
||||
expect(tab).toHaveAttribute(
|
||||
'aria-selected',
|
||||
tab === settingsTab ? 'true' : 'false',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Classes, Icon, IconName } from '@blueprintjs/core';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocalStorage } from 'usehooks-ts';
|
||||
import { I18nId } from './i18n';
|
||||
|
||||
/** Indicates the selected activity. */
|
||||
export enum Activity {
|
||||
/** No activity is selected. */
|
||||
None = 'activity.none',
|
||||
/** The explorer activity is selected. */
|
||||
Explorer = 'activity.explorer',
|
||||
/** The settings activity is selected. */
|
||||
Settings = 'activity.settings',
|
||||
}
|
||||
|
||||
type ActivityTabProps = {
|
||||
/** The label for the tab. */
|
||||
label: string;
|
||||
/** The icon for the tab. */
|
||||
icon: IconName;
|
||||
/** Controls the selected state of the tab. */
|
||||
selected: boolean;
|
||||
/** Callback called when the tab is clicked. */
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* React component for tabs in {@link Activities}.
|
||||
*/
|
||||
const ActivityTab: React.VoidFunctionComponent<ActivityTabProps> = ({
|
||||
label,
|
||||
icon,
|
||||
selected,
|
||||
onClick,
|
||||
}) => {
|
||||
// not using Button component so we can set role to "tab"
|
||||
return (
|
||||
<div
|
||||
role="tab"
|
||||
title={label}
|
||||
aria-selected={selected}
|
||||
tabIndex={0}
|
||||
className={[
|
||||
'pb-activity-tablist-tab',
|
||||
Classes.BUTTON,
|
||||
Classes.MINIMAL,
|
||||
selected ? Classes.INTENT_PRIMARY : undefined,
|
||||
]
|
||||
.filter((c) => c)
|
||||
.join(' ')}
|
||||
{...{ onClick }}
|
||||
>
|
||||
<Icon size={35} {...{ icon }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ActivitiesProps = {
|
||||
/** The currently selected activity. */
|
||||
selectedActivity: Activity;
|
||||
/** Callback called when a tab is clicked. */
|
||||
onAction: (activity: Activity) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* React component that acts as a tab control to select activities.
|
||||
*/
|
||||
const Activities: React.VoidFunctionComponent<ActivitiesProps> = ({
|
||||
selectedActivity,
|
||||
onAction,
|
||||
}) => {
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={i18n.translate(I18nId.Title)}
|
||||
role="tablist"
|
||||
className="pb-activity-tablist"
|
||||
>
|
||||
<ActivityTab
|
||||
label={i18n.translate(I18nId.Explorer)}
|
||||
selected={selectedActivity === Activity.Explorer}
|
||||
icon="document"
|
||||
onClick={() => onAction(Activity.Explorer)}
|
||||
/>
|
||||
<ActivityTab
|
||||
label={i18n.translate(I18nId.Settings)}
|
||||
selected={selectedActivity === Activity.Settings}
|
||||
icon="cog"
|
||||
onClick={() => onAction(Activity.Settings)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook to get selected state and component.
|
||||
* @returns The current selected activity (state) and the activity component.
|
||||
*/
|
||||
export function useActivities(): [
|
||||
selectedActivity: Activity,
|
||||
activitiesComponent: React.ReactElement,
|
||||
] {
|
||||
const [selectedActivity, setSelectedActivity] = useLocalStorage(
|
||||
'activities.selectedActivity',
|
||||
Activity.Explorer,
|
||||
);
|
||||
|
||||
const handleAction = useCallback(
|
||||
(newActivity: Activity) => {
|
||||
// if activity is already selected, select none
|
||||
if (selectedActivity === newActivity) {
|
||||
setSelectedActivity(Activity.None);
|
||||
} else {
|
||||
// otherwise select the new activity
|
||||
setSelectedActivity(newActivity);
|
||||
}
|
||||
},
|
||||
[selectedActivity, setSelectedActivity],
|
||||
);
|
||||
|
||||
const activitiesComponent = useMemo(
|
||||
() => (
|
||||
<Activities
|
||||
selectedActivity={selectedActivity}
|
||||
onAction={(a) => handleAction(a)}
|
||||
/>
|
||||
),
|
||||
[Activities, selectedActivity, handleAction],
|
||||
);
|
||||
|
||||
return [selectedActivity, activitiesComponent];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../../../test';
|
||||
import { lookup } from '../../test';
|
||||
import { I18nId } from './i18n';
|
||||
import en from './translations/en.json';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
//
|
||||
// Explorer translation keys.
|
||||
|
||||
export enum I18nId {
|
||||
Title = 'title',
|
||||
Explorer = 'explorer',
|
||||
Settings = 'settings',
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Activities",
|
||||
"explorer": "File Explorer",
|
||||
"settings": "Settings & Help"
|
||||
}
|
||||
+17
-8
@@ -5,8 +5,10 @@ import { Classes } from '@blueprintjs/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import SplitterLayout from 'react-splitter-layout';
|
||||
import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
|
||||
import { Activity, useActivities } from '../activities/Activities';
|
||||
import Editor from '../editor/Editor';
|
||||
import Explorer from '../explorer/Explorer';
|
||||
import Settings from '../settings/Settings';
|
||||
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
|
||||
import StatusBar from '../status-bar/StatusBar';
|
||||
import Terminal from '../terminal/Terminal';
|
||||
@@ -155,16 +157,20 @@ const App: React.VFC = () => {
|
||||
return () => removeEventListener('keydown', listener);
|
||||
}, []);
|
||||
|
||||
const [selectedActivity, activitiesComponent] = useActivities();
|
||||
|
||||
return (
|
||||
<div className="pb-app h-100 w-100 p-absolute">
|
||||
<Toolbar />
|
||||
<div
|
||||
className="pb-app-body"
|
||||
style={{ display: 'grid', gridTemplateColumns: '250px auto' }}
|
||||
>
|
||||
<Explorer />
|
||||
<div className="pb-app-body">
|
||||
{activitiesComponent}
|
||||
{selectedActivity !== Activity.None && (
|
||||
<div className="pb-app-activity-view">
|
||||
{selectedActivity === Activity.Explorer && <Explorer />}
|
||||
{selectedActivity === Activity.Settings && <Settings />}
|
||||
</div>
|
||||
)}
|
||||
{/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="pb-app-main" style={{ position: 'relative' }}>
|
||||
<SplitterLayout
|
||||
customClassName={
|
||||
isSettingShowDocsEnabled ? 'pb-show-docs' : 'pb-hide-docs'
|
||||
@@ -181,7 +187,10 @@ const App: React.VFC = () => {
|
||||
secondaryInitialSize={terminalSplit}
|
||||
onSecondaryPaneSizeChange={setTerminalSplit}
|
||||
>
|
||||
<Editor />
|
||||
<div className="pb-app-editor">
|
||||
<Toolbar />
|
||||
<Editor />
|
||||
</div>
|
||||
<div className="pb-app-terminal-padding h-100">
|
||||
<Terminal />
|
||||
</div>
|
||||
|
||||
+54
-3
@@ -16,9 +16,60 @@
|
||||
|
||||
.pb-app-body {
|
||||
// height makes everything fit without scrolling
|
||||
height: calc(
|
||||
var(--pb-vh, 100vh) - #{pb.$toolbar-height} - #{pb.$status-bar-height}
|
||||
) !important;
|
||||
height: calc(var(--pb-vh, 100vh) - #{pb.$status-bar-height}) !important;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
$switcher-width: 50px;
|
||||
|
||||
.pb-activity-tablist {
|
||||
width: $switcher-width;
|
||||
$padding: bp.$pt-grid-size * 0.5;
|
||||
padding: $padding;
|
||||
gap: $padding;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
@include pb.background-contrast(6%);
|
||||
}
|
||||
|
||||
.pb-app-activity-view {
|
||||
width: 250px;
|
||||
overflow: hidden;
|
||||
padding: bp.$pt-grid-size;
|
||||
|
||||
// on small screens, make the activity view an overlay instead of inline
|
||||
@media screen and (max-width: pb.$narrow-screen-limit) {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: $switcher-width;
|
||||
height: 100%;
|
||||
z-index: bp.$pt-z-index-overlay;
|
||||
background-color: bp.$pt-app-background-color;
|
||||
|
||||
.#{bp.$ns}-dark & {
|
||||
background-color: bp.$pt-dark-app-background-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pb-app-main {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
@include pb.background-contrast(4%);
|
||||
}
|
||||
}
|
||||
|
||||
.pb-app-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& .pb-editor {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// make layout splitter match app color scheme
|
||||
|
||||
@@ -410,7 +410,7 @@ const Editor: React.VFC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-100" onContextMenu={preventBrowserNativeContextMenu}>
|
||||
<div className="pb-editor" onContextMenu={preventBrowserNativeContextMenu}>
|
||||
<EditorTabs onChange={() => editor?.focus()} i18n={i18n} />
|
||||
<ResizeSensor2 onResize={() => editor?.layout()}>
|
||||
<ContextMenu2
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
|
||||
@use '@blueprintjs/core/lib/scss/variables' as bp;
|
||||
|
||||
.pb-explorer-file-tree {
|
||||
// to allow for focus outline
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
// reveal file action buttons on hover
|
||||
.#{bp.$ns}-tree-node-content:not(:hover) .pb-explorer-file-action-button-group {
|
||||
display: none;
|
||||
|
||||
+7
-3
@@ -51,15 +51,19 @@ body {
|
||||
user-select: none;
|
||||
|
||||
&.#{bp.$ns}-intent-primary {
|
||||
background-color: pb.$pybricks-blue;
|
||||
background: pb.$pybricks-blue;
|
||||
|
||||
&.#{bp.$ns}-minimal {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: color.adjust(pb.$pybricks-blue, $lightness: -5%);
|
||||
background: color.adjust(pb.$pybricks-blue, $lightness: -5%);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.#{bp.$ns}-disabled {
|
||||
background-color: rgba(pb.$pybricks-blue, 0.5);
|
||||
background: rgba(pb.$pybricks-blue, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cleanup, getByLabelText, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../test';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import Settings from './Settings';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -14,9 +14,7 @@ afterEach(() => {
|
||||
|
||||
describe('showDocs setting switch', () => {
|
||||
it('should toggle setting', async () => {
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
const showDocs = settings.getByLabelText('Documentation');
|
||||
expect(showDocs).toBeChecked();
|
||||
@@ -28,9 +26,7 @@ describe('showDocs setting switch', () => {
|
||||
|
||||
describe('darkMode setting switch', () => {
|
||||
it('should toggle setting', async () => {
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
const darkMode = settings.getByLabelText('Dark mode');
|
||||
expect(darkMode).not.toBeChecked();
|
||||
@@ -45,11 +41,10 @@ describe('darkMode setting switch', () => {
|
||||
expect(localStorage.getItem('usehooks-ts-ternary-dark-mode')).toBe('"light"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flashCurrentProgram setting switch', () => {
|
||||
it('should toggle the setting', () => {
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
expect(localStorage.getItem('setting.flashCurrentProgram')).toBe(null);
|
||||
|
||||
@@ -66,9 +61,7 @@ describe('hubName setting', () => {
|
||||
// old settings did not use json format, so lack quotes
|
||||
localStorage.setItem('setting.hubName', 'old name');
|
||||
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
const textBox = settings.getByLabelText('Hub name');
|
||||
|
||||
@@ -76,9 +69,7 @@ describe('hubName setting', () => {
|
||||
});
|
||||
|
||||
it('should update the setting', () => {
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
expect(localStorage.getItem('setting.hubName')).toBe(null);
|
||||
|
||||
@@ -91,9 +82,7 @@ describe('hubName setting', () => {
|
||||
|
||||
describe('about dialog', () => {
|
||||
it('should open the dialog when the button is clicked', async () => {
|
||||
const [settings] = testRender(
|
||||
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
|
||||
);
|
||||
const [settings] = testRender(<Settings />);
|
||||
|
||||
const appName = process.env.REACT_APP_NAME;
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import {
|
||||
AnchorButton,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ControlGroup,
|
||||
FormGroup,
|
||||
Icon,
|
||||
InputGroup,
|
||||
Intent,
|
||||
Label,
|
||||
Switch,
|
||||
} from '@blueprintjs/core';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTernaryDarkMode } from 'usehooks-ts';
|
||||
import AboutDialog from '../about/AboutDialog';
|
||||
import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions';
|
||||
import {
|
||||
pybricksBugReportsUrl,
|
||||
pybricksGitterUrl,
|
||||
pybricksProjectsUrl,
|
||||
pybricksSupportUrl,
|
||||
tooltipDelay,
|
||||
} from '../app/constants';
|
||||
import { pseudolocalize } from '../i18n';
|
||||
import { useSelector } from '../reducers';
|
||||
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import {
|
||||
useSettingFlashCurrentProgram,
|
||||
useSettingHubName,
|
||||
useSettingIsShowDocsEnabled,
|
||||
} from './hooks';
|
||||
import { I18nId } from './i18n';
|
||||
import './settings.scss';
|
||||
|
||||
const Settings: React.VoidFunctionComponent = () => {
|
||||
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
|
||||
useSettingIsShowDocsEnabled();
|
||||
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
|
||||
const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode();
|
||||
|
||||
const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] =
|
||||
useSettingFlashCurrentProgram();
|
||||
const isServiceWorkerRegistered = useSelector(
|
||||
(s) => s.app.isServiceWorkerRegistered,
|
||||
);
|
||||
const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate);
|
||||
const updateAvailable = useSelector((s) => s.app.updateAvailable);
|
||||
const hasUnresolvedInstallPrompt = useSelector(
|
||||
(s) => s.app.hasUnresolvedInstallPrompt,
|
||||
);
|
||||
const promptingInstall = useSelector((s) => s.app.promptingInstall);
|
||||
const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse);
|
||||
const { hubName, isHubNameValid, setHubName } = useSettingHubName();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
|
||||
return (
|
||||
<div aria-label={i18n.translate(I18nId.Title)}>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.AppearanceTitle)}
|
||||
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
|
||||
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
|
||||
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
|
||||
})}
|
||||
>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.AppearanceDocumentationTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(I18nId.AppearanceDocumentationLabel)}
|
||||
checked={isSettingShowDocsEnabled}
|
||||
onChange={(e) =>
|
||||
setIsSettingShowDocsEnabled(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.AppearanceDarkModeTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
|
||||
checked={isDarkMode}
|
||||
onChange={(e) =>
|
||||
setTernaryDarkMode(
|
||||
(e.target as HTMLInputElement).checked
|
||||
? 'dark'
|
||||
: 'light',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.FirmwareCurrentProgramTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(I18nId.FirmwareCurrentProgramLabel)}
|
||||
checked={isFlashCurrentProgramEnabled}
|
||||
onChange={(e) =>
|
||||
setIsFlashCurrentProgramEnabled(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
<ControlGroup vertical={true}>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.FirmwareHubNameTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
openOnTargetFocus={false}
|
||||
>
|
||||
<Label htmlFor="hub-name-input">
|
||||
{i18n.translate(I18nId.FirmwareHubNameLabel)}
|
||||
</Label>
|
||||
</Tooltip2>
|
||||
<InputGroup
|
||||
id="hub-name-input"
|
||||
value={hubName}
|
||||
onChange={(e) => setHubName(e.currentTarget.value)}
|
||||
onMouseOver={(e) => e.preventDefault()}
|
||||
className="pb-hub-name-input"
|
||||
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
|
||||
placeholder="Pybricks Hub"
|
||||
rightElement={
|
||||
isHubNameValid ? undefined : (
|
||||
<Tooltip2
|
||||
content={i18n.translate(
|
||||
I18nId.FirmwareHubNameErrorTooltip,
|
||||
)}
|
||||
rootBoundary="document"
|
||||
placement="bottom"
|
||||
targetTagName="div"
|
||||
>
|
||||
<Icon
|
||||
icon="error"
|
||||
intent={Intent.DANGER}
|
||||
itemType="div"
|
||||
/>
|
||||
</Tooltip2>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
|
||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||
<AnchorButton
|
||||
icon="lightbulb"
|
||||
href={pybricksProjectsUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpProjectsLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton icon="help" href={pybricksSupportUrl} target="blank_">
|
||||
{i18n.translate(I18nId.HelpSupportLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton icon="chat" href={pybricksGitterUrl} target="blank_">
|
||||
{i18n.translate(I18nId.HelpChatLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="virus"
|
||||
href={pybricksBugReportsUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpBugsLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AboutDialog
|
||||
isOpen={isAboutDialogOpen}
|
||||
onClose={() => setIsAboutDialogOpen(false)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.AppTitle)}
|
||||
helperText={
|
||||
readyForOfflineUse && i18n.translate(I18nId.AppOfflineUseHelp)
|
||||
}
|
||||
>
|
||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||
{hasUnresolvedInstallPrompt && (
|
||||
<Button
|
||||
icon="add"
|
||||
onClick={() => dispatch(appShowInstallPrompt())}
|
||||
loading={promptingInstall}
|
||||
>
|
||||
{i18n.translate(I18nId.AppInstallLabel)}
|
||||
</Button>
|
||||
)}
|
||||
{isServiceWorkerRegistered && !updateAvailable && (
|
||||
<Button
|
||||
icon="refresh"
|
||||
onClick={() => dispatch(appCheckForUpdate())}
|
||||
loading={checkingForUpdate}
|
||||
>
|
||||
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
|
||||
</Button>
|
||||
)}
|
||||
{isServiceWorkerRegistered && updateAvailable && (
|
||||
<Button icon="refresh" onClick={() => dispatch(appReload())}>
|
||||
{i18n.translate(I18nId.AppRestartLabel)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon="info-sign"
|
||||
onClick={() => {
|
||||
setIsAboutDialogOpen(true);
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
{i18n.translate(I18nId.AppAboutLabel)}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<FormGroup label="Developer">
|
||||
<Switch
|
||||
checked={i18n.pseudolocalize !== false}
|
||||
onChange={() => pseudolocalize(!i18n.pseudolocalize)}
|
||||
label="Pseudolocalize"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,317 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import {
|
||||
AnchorButton,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Classes,
|
||||
ControlGroup,
|
||||
Drawer,
|
||||
DrawerSize,
|
||||
FormGroup,
|
||||
Icon,
|
||||
InputGroup,
|
||||
Intent,
|
||||
Label,
|
||||
Switch,
|
||||
} from '@blueprintjs/core';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTernaryDarkMode } from 'usehooks-ts';
|
||||
import AboutDialog from '../about/AboutDialog';
|
||||
import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions';
|
||||
import {
|
||||
pybricksBugReportsUrl,
|
||||
pybricksGitterUrl,
|
||||
pybricksProjectsUrl,
|
||||
pybricksSupportUrl,
|
||||
tooltipDelay,
|
||||
} from '../app/constants';
|
||||
import { pseudolocalize } from '../i18n';
|
||||
import { useSelector } from '../reducers';
|
||||
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import {
|
||||
useSettingFlashCurrentProgram,
|
||||
useSettingHubName,
|
||||
useSettingIsShowDocsEnabled,
|
||||
} from './hooks';
|
||||
import { I18nId } from './i18n';
|
||||
import './settings.scss';
|
||||
|
||||
type SettingsProps = {
|
||||
isOpen: boolean;
|
||||
onClose(): void;
|
||||
};
|
||||
|
||||
const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
|
||||
useSettingIsShowDocsEnabled();
|
||||
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
|
||||
const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode();
|
||||
|
||||
const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] =
|
||||
useSettingFlashCurrentProgram();
|
||||
const isServiceWorkerRegistered = useSelector(
|
||||
(s) => s.app.isServiceWorkerRegistered,
|
||||
);
|
||||
const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate);
|
||||
const updateAvailable = useSelector((s) => s.app.updateAvailable);
|
||||
const hasUnresolvedInstallPrompt = useSelector(
|
||||
(s) => s.app.hasUnresolvedInstallPrompt,
|
||||
);
|
||||
const promptingInstall = useSelector((s) => s.app.promptingInstall);
|
||||
const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse);
|
||||
const { hubName, isHubNameValid, setHubName } = useSettingHubName();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
|
||||
// HACK: set additional attributes that are not supported via Drawer props
|
||||
const handleDrawerOpening = useCallback<(node: HTMLElement) => void>((n) => {
|
||||
n.setAttribute('role', 'dialog');
|
||||
n.setAttribute('aria-modal', 'true');
|
||||
n.setAttribute('aria-labelledby', 'settings-drawer-dialog-title');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
icon="cog"
|
||||
size={DrawerSize.SMALL}
|
||||
title={
|
||||
<span id="settings-drawer-dialog-title">
|
||||
{i18n.translate(I18nId.Title)}
|
||||
</span>
|
||||
}
|
||||
onOpening={handleDrawerOpening}
|
||||
onClose={onClose}
|
||||
// work around https://github.com/palantir/blueprint/issues/5169
|
||||
shouldReturnFocusOnClose={false}
|
||||
>
|
||||
<div className={Classes.DRAWER_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.AppearanceTitle)}
|
||||
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
|
||||
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
|
||||
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
|
||||
})}
|
||||
>
|
||||
<Tooltip2
|
||||
content={i18n.translate(
|
||||
I18nId.AppearanceDocumentationTooltip,
|
||||
)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
I18nId.AppearanceDocumentationLabel,
|
||||
)}
|
||||
checked={isSettingShowDocsEnabled}
|
||||
onChange={(e) =>
|
||||
setIsSettingShowDocsEnabled(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.AppearanceDarkModeTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
|
||||
checked={isDarkMode}
|
||||
onChange={(e) =>
|
||||
setTernaryDarkMode(
|
||||
(e.target as HTMLInputElement).checked
|
||||
? 'dark'
|
||||
: 'light',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
|
||||
<Tooltip2
|
||||
content={i18n.translate(
|
||||
I18nId.FirmwareCurrentProgramTooltip,
|
||||
)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
I18nId.FirmwareCurrentProgramLabel,
|
||||
)}
|
||||
checked={isFlashCurrentProgramEnabled}
|
||||
onChange={(e) =>
|
||||
setIsFlashCurrentProgramEnabled(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip2>
|
||||
<ControlGroup>
|
||||
<Tooltip2
|
||||
content={i18n.translate(I18nId.FirmwareHubNameTooltip)}
|
||||
rootBoundary="document"
|
||||
placement="left"
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
openOnTargetFocus={false}
|
||||
>
|
||||
<Label
|
||||
className={Classes.INLINE}
|
||||
htmlFor="hub-name-input"
|
||||
>
|
||||
{i18n.translate(I18nId.FirmwareHubNameLabel)}
|
||||
</Label>
|
||||
</Tooltip2>
|
||||
<InputGroup
|
||||
id="hub-name-input"
|
||||
value={hubName}
|
||||
onChange={(e) => setHubName(e.currentTarget.value)}
|
||||
onMouseOver={(e) => e.preventDefault()}
|
||||
className="pb-hub-name-input"
|
||||
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
|
||||
placeholder="Pybricks Hub"
|
||||
rightElement={
|
||||
isHubNameValid ? undefined : (
|
||||
<Tooltip2
|
||||
content={i18n.translate(
|
||||
I18nId.FirmwareHubNameErrorTooltip,
|
||||
)}
|
||||
rootBoundary="document"
|
||||
placement="bottom"
|
||||
targetTagName="div"
|
||||
>
|
||||
<Icon
|
||||
icon="error"
|
||||
intent={Intent.DANGER}
|
||||
itemType="div"
|
||||
/>
|
||||
</Tooltip2>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
|
||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||
<AnchorButton
|
||||
icon="lightbulb"
|
||||
href={pybricksProjectsUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpProjectsLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="help"
|
||||
href={pybricksSupportUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpSupportLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="chat"
|
||||
href={pybricksGitterUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpChatLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="virus"
|
||||
href={pybricksBugReportsUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(I18nId.HelpBugsLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AboutDialog
|
||||
isOpen={isAboutDialogOpen}
|
||||
onClose={() => setIsAboutDialogOpen(false)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.AppTitle)}
|
||||
helperText={
|
||||
readyForOfflineUse &&
|
||||
i18n.translate(I18nId.AppOfflineUseHelp)
|
||||
}
|
||||
>
|
||||
<ButtonGroup minimal={true} vertical={true} alignText="left">
|
||||
{hasUnresolvedInstallPrompt && (
|
||||
<Button
|
||||
icon="add"
|
||||
onClick={() => dispatch(appShowInstallPrompt())}
|
||||
loading={promptingInstall}
|
||||
>
|
||||
{i18n.translate(I18nId.AppInstallLabel)}
|
||||
</Button>
|
||||
)}
|
||||
{isServiceWorkerRegistered && !updateAvailable && (
|
||||
<Button
|
||||
icon="refresh"
|
||||
onClick={() => dispatch(appCheckForUpdate())}
|
||||
loading={checkingForUpdate}
|
||||
>
|
||||
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
|
||||
</Button>
|
||||
)}
|
||||
{isServiceWorkerRegistered && updateAvailable && (
|
||||
<Button
|
||||
icon="refresh"
|
||||
onClick={() => dispatch(appReload())}
|
||||
>
|
||||
{i18n.translate(I18nId.AppRestartLabel)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon="info-sign"
|
||||
onClick={() => {
|
||||
setIsAboutDialogOpen(true);
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
{i18n.translate(I18nId.AppAboutLabel)}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<FormGroup label="Developer">
|
||||
<Switch
|
||||
checked={i18n.pseudolocalize !== false}
|
||||
onChange={() => pseudolocalize(!i18n.pseudolocalize)}
|
||||
label="Pseudolocalize"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsDrawer;
|
||||
@@ -1,33 +1,48 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { getByLabelText, waitFor } from '@testing-library/dom';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../test';
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
describe('settings button', () => {
|
||||
it('should open settings drawer', async () => {
|
||||
describe('toolbar', () => {
|
||||
it('should have bluetooth button', () => {
|
||||
const [toolbar] = testRender(<Toolbar />);
|
||||
|
||||
const settingButton = toolbar.getByLabelText('Settings');
|
||||
const runButton = toolbar.getByRole('button', { name: 'Bluetooth' });
|
||||
|
||||
expect(
|
||||
toolbar.queryByRole('dialog', {
|
||||
name: 'Settings & Help',
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(runButton).toBeDefined();
|
||||
});
|
||||
|
||||
settingButton.click();
|
||||
it('should have flash button', () => {
|
||||
const [toolbar] = testRender(<Toolbar />);
|
||||
|
||||
const settingsDrawer = toolbar.getByRole('dialog', {
|
||||
name: 'Settings & Help',
|
||||
});
|
||||
const runButton = toolbar.getByRole('button', { name: 'Flash' });
|
||||
|
||||
expect(settingsDrawer).toBeVisible();
|
||||
expect(runButton).toBeDefined();
|
||||
});
|
||||
|
||||
getByLabelText(settingsDrawer, 'Close').click();
|
||||
it('should have run button', () => {
|
||||
const [toolbar] = testRender(<Toolbar />);
|
||||
|
||||
await waitFor(() => expect(settingsDrawer).not.toBeVisible());
|
||||
const runButton = toolbar.getByRole('button', { name: 'Run' });
|
||||
|
||||
expect(runButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have stop button', () => {
|
||||
const [toolbar] = testRender(<Toolbar />);
|
||||
|
||||
const runButton = toolbar.getByRole('button', { name: 'Stop' });
|
||||
|
||||
expect(runButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have repl button', () => {
|
||||
const [toolbar] = testRender(<Toolbar />);
|
||||
|
||||
const runButton = toolbar.getByRole('button', { name: 'REPL' });
|
||||
|
||||
expect(runButton).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-12
@@ -2,21 +2,17 @@
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import { ButtonGroup } from '@blueprintjs/core';
|
||||
import React, { useState } from 'react';
|
||||
import SettingsDrawer from '../settings/SettingsDrawer';
|
||||
import React from 'react';
|
||||
import { preventBrowserNativeContextMenu } from '../utils/react';
|
||||
import BluetoothButton from './buttons/bluetooth/BluetoothButton';
|
||||
import FlashButton from './buttons/flash/FlashButton';
|
||||
import ReplButton from './buttons/repl/ReplButton';
|
||||
import RunButton from './buttons/run/RunButton';
|
||||
import SettingsButton from './buttons/settings/SettingsButton';
|
||||
import StopButton from './buttons/stop/StopButton';
|
||||
|
||||
import './toolbar.scss';
|
||||
|
||||
const Toolbar: React.VFC = (_props) => {
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="toolbar"
|
||||
@@ -32,13 +28,6 @@ const Toolbar: React.VFC = (_props) => {
|
||||
<StopButton />
|
||||
<ReplButton />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="pb-toolbar-group pb-align-right">
|
||||
<SettingsButton onAction={() => setIsSettingsDrawerOpen(true)} />
|
||||
<SettingsDrawer
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={() => setIsSettingsDrawerOpen(false)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../../../test';
|
||||
import SettingsButton from './SettingsButton';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should invoke callback when clicked', () => {
|
||||
const handleAction = jest.fn();
|
||||
|
||||
const [button] = testRender(<SettingsButton onAction={handleAction} />);
|
||||
|
||||
button.getByRole('button', { name: 'Settings' }).click();
|
||||
|
||||
expect(handleAction).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import ActionButton, { ActionButtonProps } from '../../ActionButton';
|
||||
import { I18nId } from './i18n';
|
||||
import icon from './icon.svg';
|
||||
|
||||
type SettingsButtonProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
|
||||
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
|
||||
onAction,
|
||||
}) => {
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
label={i18n.translate(I18nId.Label)}
|
||||
tooltip={i18n.translate(I18nId.Tooltip)}
|
||||
icon={icon}
|
||||
onAction={onAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsButton;
|
||||
@@ -1,7 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
export enum I18nId {
|
||||
Label = 'label',
|
||||
Tooltip = 'tooltip',
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="50" height="50" enable-background="new 0 0 20 20" version="1.1" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g id="cog_2_" transform="matrix(2.25,0,0,2.25,2.5,2.5)" fill="#fff"><g fill="#fff"><path d="m19 8h-2.31c-0.14-0.46-0.33-0.89-0.56-1.3l1.7-1.7c0.39-0.39 0.39-1.02 0-1.41l-1.41-1.41c-0.39-0.39-1.02-0.39-1.41 0l-1.7 1.7c-0.41-0.22-0.84-0.41-1.3-0.55v-2.33c0-0.55-0.45-1-1-1h-2.01c-0.55 0-1 0.45-1 1v2.33c-0.48 0.14-0.94 0.34-1.37 0.58l-1.63-1.63c-0.37-0.37-0.98-0.37-1.36 0l-1.36 1.36c-0.37 0.38-0.37 0.99 0 1.36l1.62 1.62c-0.24 0.44-0.44 0.89-0.59 1.38h-2.31c-0.55 0-1 0.45-1 1v2c0 0.55 0.45 1 1 1h2.31c0.14 0.46 0.33 0.89 0.56 1.3l-1.7 1.7c-0.39 0.39-0.39 1.02 0 1.41l1.41 1.41c0.39 0.39 1.02 0.39 1.41 0l1.7-1.7c0.41 0.22 0.84 0.41 1.3 0.55v2.33c0 0.55 0.45 1 1 1h2c0.55 0 1-0.45 1-1v-2.33c0.48-0.14 0.94-0.35 1.37-0.59l1.64 1.64c0.37 0.37 0.98 0.37 1.36 0l1.36-1.36c0.37-0.37 0.37-0.98 0-1.36l-1.62-1.62c0.24-0.43 0.45-0.89 0.6-1.38h2.3c0.55 0 1-0.45 1-1v-2c0-0.55-0.45-1-1-1zm-9 6c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" clip-rule="evenodd" fill="#fff" fill-rule="evenodd"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"label": "Settings",
|
||||
"tooltip": "Open settings and help"
|
||||
}
|
||||
@@ -7,11 +7,16 @@
|
||||
@use '../variables' as pb;
|
||||
|
||||
.pb-toolbar {
|
||||
height: pb.$toolbar-height;
|
||||
padding: 0 bp.$pt-grid-size * 1.5;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: bp.$pt-grid-size bp.$pt-grid-size * 1.5;
|
||||
z-index: bp.$pt-z-index-content;
|
||||
|
||||
// make small buttons more square and slightly smaller (some of this is
|
||||
// done in react)
|
||||
@media screen and (max-width: pb.$narrow-screen-limit) {
|
||||
.#{bp.$ns}-button {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pb-toolbar * {
|
||||
@@ -20,7 +25,6 @@
|
||||
|
||||
.pb-toolbar-group {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&.pb-align-left {
|
||||
float: left;
|
||||
@@ -32,18 +36,3 @@
|
||||
margin-left: bp.$pt-grid-size * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// make small buttons more square and slightly smaller
|
||||
@media screen and (max-width: 700px) {
|
||||
.pb-toolbar .#{bp.$ns}-button {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the point where the buttons start wrapping to 2 rows so we need to
|
||||
// adjust the height so that they stay in the toolbar
|
||||
@media screen and (max-width: 350px) {
|
||||
.pb-toolbar-group {
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-1
@@ -1,7 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
$toolbar-height: 72px;
|
||||
@use 'sass:color';
|
||||
@use '@blueprintjs/core/lib/scss/variables' as bp;
|
||||
|
||||
$status-bar-height: 30px;
|
||||
|
||||
// point at which we change the UI for narrow screens
|
||||
$narrow-screen-limit: 700px;
|
||||
|
||||
// Official Pybricks branding color.
|
||||
$pybricks-blue: #0088ce;
|
||||
|
||||
/**
|
||||
* Adjusts the background contrast by $background-adjust (usually a percent).
|
||||
*
|
||||
* The light theme will be made darker by that amount and the dark theme will
|
||||
* be made lighter.
|
||||
*/
|
||||
@mixin background-contrast($background-adjust) {
|
||||
background-color: color.adjust(
|
||||
bp.$pt-app-background-color,
|
||||
$lightness: -$background-adjust
|
||||
);
|
||||
|
||||
.#{bp.$ns}-dark & {
|
||||
background-color: color.adjust(
|
||||
bp.$pt-dark-app-background-color,
|
||||
$lightness: $background-adjust
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user