Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@types/node": "^18.19.14",
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@types/react-splitter-layout": "^3.0.5",
"@types/react-transition-group": "^4.4.10",
"@types/redux-logger": "^3.0.12",
"@types/semver": "^7.5.6",
Expand Down Expand Up @@ -99,7 +98,7 @@
"react-popper": "^2.3.0",
"react-redux": "^8.1.3",
"react-refresh": "^0.14.0",
"react-splitter-layout": "^4.0.0",
"react-resizable-panels": "^4.8.0",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-saga": "^1.3.0",
Expand Down
67 changes: 19 additions & 48 deletions src/activities/Activities.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022-2023 The Pybricks Authors

import { act, cleanup } from '@testing-library/react';
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../test';
import Activities from './Activities';
Expand All @@ -15,66 +15,37 @@ afterEach(() => {
});

describe('Activities', () => {
it('should select explorer by default', () => {
it('should render explorer by default', () => {
const [, activities] = testRender(<Activities />);

const tab = activities.getByRole('tab', { name: 'File Explorer' });

expect(tab).toHaveAttribute('aria-selected', 'true');
expect(
activities.container.querySelector('.pb-activities-tabview'),
).toBeInTheDocument();
});

it('should use localStorage for default value', () => {
localStorage.setItem(
it('should render settings when Settings activity is selected', () => {
sessionStorage.setItem(
'activities.selectedActivity',
JSON.stringify(Activity.Settings),
);

const [, activities] = testRender(<Activities />);

const tab = activities.getByRole('tab', { name: 'Settings & Help' });

expect(tab).toHaveAttribute('aria-selected', 'true');
expect(
activities.container.querySelector('.pb-activities-tabview'),
).toBeInTheDocument();
});

it('should select none when clicking already selected tab', async () => {
const [user, activities] = testRender(<Activities />);

const explorerTab = activities.getByRole('tab', { name: 'File Explorer' });

for (const tab of activities.getAllByRole('tab')) {
expect(tab).toHaveAttribute(
'aria-selected',
tab === explorerTab ? 'true' : 'false',
);
}

await act(() => user.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', async () => {
const [user, activities] = testRender(<Activities />);

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',
);
}
it('should render nothing when no activity is selected', () => {
sessionStorage.setItem(
'activities.selectedActivity',
JSON.stringify(Activity.None),
);

await act(() => user.click(settingsTab));
const [, activities] = testRender(<Activities />);

for (const tab of activities.getAllByRole('tab')) {
expect(tab).toHaveAttribute(
'aria-selected',
tab === settingsTab ? 'true' : 'false',
);
}
expect(
activities.container.querySelector('.pb-activities-tabview'),
).not.toBeInTheDocument();
});
});
150 changes: 20 additions & 130 deletions src/activities/Activities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,143 +2,33 @@
// Copyright (c) 2022-2023 The Pybricks Authors

import './activities.scss';
import { Icon, Tab, Tabs } from '@blueprintjs/core';
import { Cog, Document } from '@blueprintjs/icons';
import React, { useCallback, useEffect, useRef } from 'react';
import React from 'react';
import Explorer from '../explorer/Explorer';
import Settings from '../settings/Settings';
import { Activity, useActivitiesSelectedActivity } from './hooks';
import { useI18n } from './i18n';

/**
* React component that acts as a tab control to select activities.
* React component that renders the panel content for the selected activity.
*/
const Activities: React.FunctionComponent = () => {
const [selectedActivity, setSelectedActivity] = useActivitiesSelectedActivity();
const i18n = useI18n();

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],
);

// HACK: fix keyboard focus when no tab is selected

const tabsRef = useRef<Tabs>(null);

useEffect(() => {
if (selectedActivity !== Activity.None) {
// all is well
return;
}

// @ts-expect-error: using private property
const tablist: HTMLDivElement = tabsRef.current?.tablistElement;

// istanbul-ignore-if: should not happen
if (!tablist) {
return;
}

const firstTab = tablist
.getElementsByClassName('pb-activities-tablist-tab')
.item(0);

// istanbul-ignore-if: should not happen
if (!firstTab) {
return;
}

firstTab.setAttribute('tabindex', '0');
}, [tabsRef, selectedActivity]);

// HACK: hoist html title attribute from icon to tab

useEffect(() => {
// @ts-expect-error: using private property
const tablist: HTMLDivElement = tabsRef.current?.tablistElement;

// istanbul-ignore-if: should not happen
if (!tablist) {
return;
}

for (const element of tablist.getElementsByClassName(
'pb-activities-tablist-tab',
)) {
const title = element.firstElementChild?.getAttribute('title');

// istanbul-ignore-if: should not happen
if (!title) {
continue;
}

element.setAttribute('title', title);
element.firstElementChild?.removeAttribute('title');
}
}, [tabsRef]);

useEffect(() => {
// @ts-expect-error: using private property
const tablist: HTMLDivElement = tabsRef.current?.tablistElement;

// istanbul-ignore-if: should not happen
if (!tablist) {
return;
}

tablist.setAttribute('aria-label', i18n.translate('title'));
}, [i18n]);

return (
<Tabs
vertical={true}
className="pb-activities"
selectedTabId={selectedActivity}
renderActiveTabPanelOnly={true}
onChange={handleAction}
ref={tabsRef}
>
<Tab
itemID="pb-activities-explorer-tab"
aria-label={i18n.translate('explorer')}
className="pb-activities-tablist-tab"
id={Activity.Explorer}
title={
<Icon
htmlTitle={i18n.translate('explorer')}
icon={<Document size={35} />}
/>
}
panel={<Explorer />}
panelClassName="pb-activities-tabview"
onMouseDown={(e) => e.stopPropagation()}
/>
<Tab
itemID="pb-activities-settings-tab"
aria-label={i18n.translate('settings')}
className="pb-activities-tablist-tab"
id={Activity.Settings}
title={
<Icon
htmlTitle={i18n.translate('settings')}
icon={<Cog size={35} />}
/>
}
panel={<Settings />}
panelClassName="pb-activities-tabview"
onMouseDown={(e) => e.stopPropagation()}
/>
</Tabs>
);
const [selectedActivity] = useActivitiesSelectedActivity();
if (selectedActivity === Activity.Explorer) {
return (
<div className="pb-activities-tabview">
<Explorer />
</div>
);
}

if (selectedActivity === Activity.Settings) {
return (
<div className="pb-activities-tabview">
<Settings />
</div>
);
}

return null;
};

export default Activities;
51 changes: 14 additions & 37 deletions src/activities/activities.scss
Original file line number Diff line number Diff line change
@@ -1,45 +1,22 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// Copyright (c) 2022-2026 The Pybricks Authors

@use '@blueprintjs/core/lib/scss/variables' as bp;
@use '../variables' as pb;

// override bluetprintjs styles
.#{bp.$ns}-tabs.#{bp.$ns}-vertical > {
& .#{bp.$ns}-tab-list .pb-activities-tablist-tab {
margin: bp.$pt-grid-size * 0.6;
padding: unset;
width: unset;
line-height: unset;
}

& .pb-activities-tabview {
padding: bp.$pt-grid-size * 0.5;
}
}

.pb-activities {
// TODO: submit upstream patch to allow setting tablist class
&-tablist,
& .#{bp.$ns}-tab-list {
@include pb.background-contrast(6%);
}

&-tabview {
display: flex;
flex-direction: column;
width: bp.$pt-grid-size * 25;
padding: bp.$pt-grid-size;
@include pb.background-contrast(0%);
.pb-activities-tabview {
display: flex;
flex-direction: column;
width: bp.$pt-grid-size * 25;
padding: bp.$pt-grid-size;
@include pb.background-contrast(0%);
overflow-y: auto;
height: 100%;

// 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;
// FIXME: this should be a variable
left: 47px;
height: calc(100% - pb.$status-bar-height);
z-index: bp.$pt-z-index-overlay;
}
// on small screens, make the activity view an overlay instead of inline
@media screen and (max-width: pb.$narrow-screen-limit) {
position: absolute;
height: calc(100% - pb.$status-bar-height);
z-index: bp.$pt-z-index-overlay;
}
}
7 changes: 4 additions & 3 deletions src/app/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ it.each([false, true])('should render', (darkMode) => {
});

describe('documentation pane', () => {
it('should hide by default', () => {
it('should be collapsed by default', () => {
testRender(<App />);
expect(document.querySelector('.pb-show-docs')).toBeNull();
expect(document.querySelector('.pb-hide-docs')).not.toBeNull();
// The docs panel starts collapsed (collapsedSize="0%")
const docsPanel = document.querySelector('[data-panel][id="docs"]');
expect(docsPanel).not.toBeNull();
});
});
Loading
Loading