Improved accessibility for activity toggle. (#4928)

* Fix issue with activity toggle accessibility

* add unit tests

* update styled components to the object format

Co-authored-by: Mark Kim <mark.kim@konghq.com>
Co-authored-by: gatzjames <jamesgatzos@gmail.com>
This commit is contained in:
Pavlos Koutoglou 2022-07-13 16:05:23 +03:00 committed by GitHub
parent 57b5493e9e
commit 0ab0d72462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 213 additions and 60 deletions

View File

@ -1,58 +0,0 @@
import { MultiSwitch } from 'insomnia-components';
import React, { FunctionComponent, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalActivity } from '../../common/constants';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../common/constants';
import { isDesign } from '../../models/workspace';
import { selectActiveActivity, selectActiveWorkspace } from '../redux/selectors';
import { HandleActivityChange } from './wrapper';
interface Props {
handleActivityChange: HandleActivityChange;
}
export const ActivityToggle: FunctionComponent<Props> = ({ handleActivityChange }) => {
const choices = [
{
label: 'Design',
value: ACTIVITY_SPEC,
},
{
label: 'Debug',
value: ACTIVITY_DEBUG,
},
{
label: 'Test',
value: ACTIVITY_UNIT_TEST,
},
];
const activeActivity = useSelector(selectActiveActivity);
const activeWorkspace = useSelector(selectActiveWorkspace);
const onChange = useCallback((nextActivity: string) => {
handleActivityChange({
workspaceId: activeWorkspace?._id,
// TODO: unsound cast
nextActivity: nextActivity as GlobalActivity,
});
}, [handleActivityChange, activeWorkspace]);
if (!activeActivity) {
return null;
}
if (!activeWorkspace || !isDesign(activeWorkspace)) {
return null;
}
return (
<MultiSwitch
name="activity-toggle"
onChange={onChange}
choices={choices}
selectedValue={activeActivity}
/>
);
};

View File

@ -0,0 +1,100 @@
import { describe } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ACTIVITY_DEBUG, GlobalActivity } from '../../../common/constants';
import { Workspace } from '../../../models/workspace';
import { ActivityToggle } from './activity-toggle';
const mockWorkspace: Workspace = {
_id: 'wrk_fddff92a88ce4cd2b05bf00506384321',
type: 'Workspace',
parentId: 'proj_default-project',
modified: 1655301684731,
created: 1655301684731,
name: 'testing',
description: '',
scope: 'design',
isPrivate: false,
};
describe('<ActivityToggle />', () => {
test('renders without exploding', async () => {
render(
<MemoryRouter>
<ActivityToggle
activity={ACTIVITY_DEBUG}
workspace={mockWorkspace}
handleActivityChange={async () => {}}
/>
</MemoryRouter>
);
const debug = screen.getByText('Debug');
expect(debug).toBeDefined();
expect(debug).toHaveClass('active');
expect(screen.getByText('Test')).toBeDefined();
expect(screen.getByText('Design')).toBeDefined();
});
test('toggles to a different activity', async () => {
let activity = ACTIVITY_DEBUG;
const user = userEvent.setup();
const handleActivityChange = async ({ nextActivity }: { nextActivity: GlobalActivity }) => {
activity = nextActivity;
};
const { rerender } = render(
<MemoryRouter>
<ActivityToggle
activity={activity}
workspace={mockWorkspace}
handleActivityChange={handleActivityChange}
/>
</MemoryRouter>
);
const debug = screen.getByText('Debug');
expect(debug).toBeDefined();
expect(debug).toHaveClass('active');
const test = screen.getByText('Test');
expect(test).toBeDefined();
const design = screen.getByText('Design');
expect(design).toBeDefined();
await user.click(test);
rerender(
<MemoryRouter>
<ActivityToggle
activity={activity}
workspace={mockWorkspace}
handleActivityChange={handleActivityChange}
/>
</MemoryRouter>
);
expect(debug).not.toHaveClass('active');
expect(test).toHaveClass('active');
await user.click(design);
rerender(
<MemoryRouter>
<ActivityToggle
activity={activity}
workspace={mockWorkspace}
handleActivityChange={handleActivityChange}
/>
</MemoryRouter>
);
expect(test).not.toHaveClass('active');
expect(debug).not.toHaveClass('active');
expect(design).toHaveClass('active');
});
});

View File

@ -0,0 +1,105 @@
import React, { FunctionComponent, useCallback } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import type { GlobalActivity } from '../../../common/constants';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../../common/constants';
import { isDesign, Workspace } from '../../../models/workspace';
import { HandleActivityChange } from '../wrapper';
const StyledNav = styled.nav({
display: 'flex',
justifyContent: 'space-between',
alignContent: 'space-evenly',
fontWeight: '500',
color: 'var(--color-font)',
background: 'var(--hl-xs)',
border: '0',
borderRadius: '100px',
padding: 'var(--padding-xxs)',
transform: 'scale(0.9)',
transformOrigin: 'center',
'& > * :not(:last-child)': {
marginRight: 'var(--padding-xs)',
},
});
const StyledLink = styled(Link)({
minWidth: '4rem',
margin: '0 auto',
textTransform: 'uppercase',
textAlign: 'center',
fontSize: 'var(--font-size-xs)',
padding: 'var(--padding-xs) var(--padding-xxs)',
borderRadius: 'var(--line-height-sm)',
color: 'var(--hl)!important',
background: 'transparent',
'&.active': {
color: 'var(--color-font)!important',
background: 'var(--color-bg)',
},
'&:hover,&:active': {
textDecoration: 'none',
},
});
interface Props {
activity: GlobalActivity;
workspace: Workspace;
handleActivityChange: HandleActivityChange;
}
export const ActivityToggle: FunctionComponent<Props> = ({
activity,
workspace,
handleActivityChange,
}) => {
const onChange = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, nextActivity: GlobalActivity) => {
// Prevent the default behavior in order to avoid extra re-render.
e.preventDefault();
handleActivityChange({
workspaceId: workspace?._id,
nextActivity,
});
}, [handleActivityChange, workspace]);
if (!activity) {
return null;
}
if (!workspace || !isDesign(workspace)) {
return null;
}
return (
<StyledNav>
<StyledLink
to={ACTIVITY_SPEC}
className={activity === ACTIVITY_SPEC ? 'active' : undefined }
onClick={e => {
onChange(e, ACTIVITY_SPEC);
}}
>
Design
</StyledLink>
<StyledLink
to={ACTIVITY_DEBUG}
className={activity === ACTIVITY_DEBUG ? 'active' : undefined }
onClick={e => {
onChange(e, ACTIVITY_DEBUG);
}}
>
Debug
</StyledLink>
<StyledLink
to={ACTIVITY_UNIT_TEST}
className={activity === ACTIVITY_UNIT_TEST ? 'active' : undefined }
onClick={e => {
onChange(e, ACTIVITY_UNIT_TEST);
}}
>
Test
</StyledLink>
</StyledNav>
);
};

View File

@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { ACTIVITY_HOME } from '../../common/constants';
import { selectActiveActivity, selectActiveApiSpec, selectActiveProjectName, selectActiveWorkspace } from '../redux/selectors';
import { ActivityToggle } from './activity-toggle';
import { ActivityToggle } from './activity-toggle/activity-toggle';
import { AppHeader } from './app-header';
import { WorkspaceDropdown } from './dropdowns/workspace-dropdown';
import { HandleActivityChange } from './wrapper';
@ -38,7 +38,13 @@ export const WorkspacePageHeader: FunctionComponent<Props> = ({
return (
<AppHeader
breadcrumbProps={{ crumbs }}
gridCenter={<ActivityToggle handleActivityChange={handleActivityChange} />}
gridCenter={
<ActivityToggle
workspace={activeWorkspace}
activity={activity}
handleActivityChange={handleActivityChange}
/>
}
gridRight={gridRight}
/>
);