Skip to content

Commit a90137e

Browse files
yeion7SaraVieira
authored andcommitted
Notifications, showing, short selector a11y (codesandbox#2652)
* use reakit overlay * drop button styles * update document usage * use menu and migrate to ts * add focus menuitems * fix list elements * fix typos * new sandbox haspopup * add disclosure type * fix typecheck
1 parent 7c1e2c9 commit a90137e

File tree

12 files changed

+305
-254
lines changed

12 files changed

+305
-254
lines changed

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@
193193
"react-tagsinput": "^3.19.0",
194194
"react-use": "^9.7.2",
195195
"react-virtualized": "^9.19.1",
196-
"reakit": "^1.0.0-beta.4",
196+
"reakit": "^1.0.0-beta.8",
197197
"rebound": "^0.1.0",
198198
"resize-observer-polyfill": "^1.5.1",
199199
"sha1": "^1.1.1",
Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React from 'react';
22
import { useTransition, animated, config } from 'react-spring';
33
import track from '@codesandbox/common/lib/utils/analytics';
4+
import {
5+
usePopoverState,
6+
Popover,
7+
PopoverDisclosure,
8+
PopoverDisclosureHTMLProps,
9+
} from 'reakit/Popover';
410
import { Container } from './elements';
511

612
interface IOverlayProps {
713
event: string;
814
isOpen?: boolean;
915
onOpen?: () => void;
1016
onClose?: () => void;
11-
children: (handleOpen: () => void) => React.ReactNode;
17+
children: (props: PopoverDisclosureHTMLProps) => React.ReactNode;
1218
content: React.ComponentType;
1319
noHeightAnimation?: boolean;
1420
}
@@ -22,47 +28,27 @@ export const Overlay: React.FC<IOverlayProps> = ({
2228
content: Content,
2329
noHeightAnimation = true,
2430
}) => {
25-
const [open, setOpen] = useState(isOpen === undefined ? false : isOpen);
2631
const isControlled = isOpen !== undefined;
27-
const openState = isControlled ? isOpen : open;
28-
29-
useEffect(() => {
30-
const handleClick = (e: MouseEvent) => {
31-
if (!e.defaultPrevented && openState) {
32-
if (event) {
33-
track(`Closed ${event}`);
34-
}
35-
if (isControlled) {
36-
if (onClose) {
37-
onClose();
38-
}
39-
} else {
40-
setOpen(false);
41-
}
42-
}
43-
};
44-
45-
document.addEventListener('mousedown', handleClick);
46-
47-
return () => {
48-
document.removeEventListener('mousedown', handleClick);
49-
};
50-
}, [isOpen, onClose, event, openState, isControlled]);
32+
const popover = usePopoverState({
33+
visible: isControlled ? isOpen : undefined,
34+
placement: 'bottom-end',
35+
});
5136

52-
const handleOpen = () => {
53-
if (event) {
37+
React.useEffect(() => {
38+
if (popover.visible) {
5439
track(`Opened ${event}`);
55-
}
56-
if (isControlled) {
57-
if (onOpen) {
40+
if (isControlled) {
5841
onOpen();
5942
}
6043
} else {
61-
setOpen(true);
44+
track(`Closed ${event}`);
45+
if (isControlled) {
46+
onClose();
47+
}
6248
}
63-
};
49+
}, [event, isControlled, onClose, onOpen, popover.visible]);
6450

65-
const transitions = useTransition(openState, null, {
51+
const transitions = useTransition(popover.visible, null, {
6652
config: config.default,
6753
from: {
6854
...(noHeightAnimation ? {} : { height: 0 }),
@@ -79,19 +65,23 @@ export const Overlay: React.FC<IOverlayProps> = ({
7965
});
8066

8167
return (
82-
<Container onMouseDown={e => e.preventDefault()}>
83-
{children(handleOpen)}
84-
{transitions.map(({ item, props }, i) =>
85-
item ? (
86-
// eslint-disable-next-line
87-
<animated.div key={i} style={props}>
88-
<Content />
89-
</animated.div>
90-
) : (
91-
// eslint-disable-next-line
92-
<animated.span key={i} style={props} />
93-
)
94-
)}
68+
<Container>
69+
<PopoverDisclosure {...popover}>
70+
{props => children(props)}
71+
</PopoverDisclosure>
72+
<Popover unstable_portal {...popover} aria-label={event}>
73+
{transitions.map(({ item, props }, i) =>
74+
item ? (
75+
// eslint-disable-next-line
76+
<animated.div key={i} style={props}>
77+
<Content />
78+
</animated.div>
79+
) : (
80+
// eslint-disable-next-line
81+
<animated.span key={i} style={props} />
82+
)
83+
)}
84+
</Popover>
9585
</Container>
9686
);
9787
};

packages/app/src/app/pages/Dashboard/Content/Sandboxes/Filters/FilterOptions/Option.tsx

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,34 @@ interface Props {
1111
toggleTemplate: (name: string, selected: boolean) => void;
1212
}
1313

14-
export const Option = ({
15-
color,
16-
id,
17-
style,
18-
niceName,
19-
selected,
20-
toggleTemplate,
21-
}: Props) => {
22-
const checkBoxName = `${id}-checkbox`;
23-
return (
24-
<Container
25-
selected={selected}
26-
onClick={e => {
27-
e.preventDefault();
28-
toggleTemplate(id, !selected);
29-
}}
30-
onMouseDown={e => {
31-
e.preventDefault();
32-
}}
33-
style={style}
34-
>
35-
<label htmlFor={checkBoxName} style={{ display: 'none' }}>
36-
{checkBoxName}
37-
</label>
38-
<CheckBox id={checkBoxName} color={color} selected={selected} />
39-
<OptionName style={{ fontWeight: 500 }}>{niceName}</OptionName>
40-
</Container>
41-
);
42-
};
14+
export const Option = React.forwardRef<HTMLDivElement, Props>(
15+
({ color, id, style, niceName, selected, toggleTemplate, ...props }, ref) => {
16+
const checkBoxName = `${id}-checkbox`;
17+
return (
18+
<Container
19+
as="li"
20+
{...props}
21+
ref={ref}
22+
selected={selected}
23+
onClick={e => {
24+
e.preventDefault();
25+
toggleTemplate(id, !selected);
26+
}}
27+
style={style}
28+
aria-label={`${checkBoxName} ${selected ? 'selected' : ''}`}
29+
>
30+
<CheckBox
31+
tabIndex={0}
32+
aria-checked={selected}
33+
role="checkbox"
34+
id={checkBoxName}
35+
color={color}
36+
selected={selected}
37+
/>
38+
<OptionName htmlFor={checkBoxName} style={{ fontWeight: 500 }}>
39+
{niceName}
40+
</OptionName>
41+
</Container>
42+
);
43+
}
44+
);

packages/app/src/app/pages/Dashboard/Content/Sandboxes/Filters/FilterOptions/elements.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export const Container = styled.div<{ hideFilters: boolean }>`
2020
export const TemplatesName = styled.span`
2121
transition: 0.3s ease color;
2222
color: rgba(255, 255, 255, 0.8);
23-
23+
appearance: none !important;
24+
background: none;
25+
border: none;
2426
cursor: pointer;
2527
2628
&:hover {
@@ -46,12 +48,13 @@ export const OverlayContainer = styled.div`
4648
background-color: ${props => props.theme.background};
4749
`;
4850

49-
export const OptionName = styled.span`
51+
export const OptionName = styled.label`
5052
font-weight: 600;
5153
cursor: pointer;
5254
`;
5355

5456
export const Option = styled.div<{ selected: boolean }>`
57+
list-style: none;
5558
transition: 0.3s ease color;
5659
cursor: pointer;
5760
color: ${props =>

packages/app/src/app/pages/Dashboard/Content/Sandboxes/Filters/FilterOptions/index.tsx

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { orderBy } from 'lodash-es';
33
import { useOvermind } from 'app/overmind';
4-
import { Overlay as OverlayComponent } from 'app/components/Overlay';
4+
import { useMenuState, Menu, MenuItem, MenuDisclosure } from 'reakit/Menu';
55
import { Container, TemplatesName, OverlayContainer } from './elements';
66
import { Option } from './Option';
77
import { ITemplate } from '../../types';
@@ -15,6 +15,10 @@ const FilterOptionsComponent: React.FC<IFilterOptionsProps> = ({
1515
possibleTemplates,
1616
hideFilters,
1717
}: IFilterOptionsProps) => {
18+
const menu = useMenuState({
19+
placement: 'bottom-end',
20+
});
21+
1822
const {
1923
state: {
2024
dashboard: { isTemplateSelected, filters },
@@ -40,49 +44,8 @@ const FilterOptionsComponent: React.FC<IFilterOptionsProps> = ({
4044

4145
const allSelected = possibleTemplates.every(t => isTemplateSelected(t.id));
4246

43-
const Overlay = () => (
44-
<OverlayContainer>
45-
{possibleTemplates.length > 0 ? (
46-
<>
47-
{orderBy(possibleTemplates, 'niceName').map(template => {
48-
const selected = isTemplateSelected(template.id);
49-
50-
return (
51-
<Option
52-
toggleTemplate={toggleTemplate}
53-
selected={selected}
54-
key={template.name}
55-
color={template.color}
56-
id={template.id}
57-
niceName={template.niceName || template.name}
58-
/>
59-
);
60-
})}
61-
62-
<Option
63-
toggleTemplate={() => {
64-
if (!allSelected) {
65-
blacklistedTemplatesCleared();
66-
} else {
67-
blacklistedTemplatesChanged({
68-
templates: possibleTemplates.map(t => t.id) || [],
69-
});
70-
}
71-
}}
72-
selected={allSelected}
73-
color="#374140"
74-
id="all"
75-
style={{ marginTop: '1rem' }}
76-
niceName="Select All"
77-
/>
78-
</>
79-
) : (
80-
'No environments found'
81-
)}
82-
</OverlayContainer>
83-
);
84-
8547
const { blacklistedTemplates } = filters;
48+
8649
const templateCount = possibleTemplates.length - blacklistedTemplates.length;
8750
const templateMessage =
8851
templateCount === possibleTemplates.length && templateCount > 0
@@ -92,14 +55,68 @@ const FilterOptionsComponent: React.FC<IFilterOptionsProps> = ({
9255
}`;
9356

9457
return (
95-
<OverlayComponent event="Dashboard - Order By" content={Overlay}>
96-
{open => (
97-
<Container hideFilters={hideFilters}>
98-
Showing{' '}
99-
<TemplatesName onClick={open}>{templateMessage}</TemplatesName>
100-
</Container>
101-
)}
102-
</OverlayComponent>
58+
<>
59+
<MenuDisclosure {...menu}>
60+
{disclosureProps => (
61+
<Container hideFilters={hideFilters}>
62+
<span aria-hidden>Showing </span>
63+
<TemplatesName
64+
{...disclosureProps}
65+
aria-label={`select showing sandboxes, current ${templateMessage}`}
66+
>
67+
{templateMessage}
68+
</TemplatesName>
69+
</Container>
70+
)}
71+
</MenuDisclosure>
72+
<Menu unstable_portal {...menu} aria-label="Dashboard - Order By">
73+
<OverlayContainer as="ul">
74+
{possibleTemplates.length > 0 ? (
75+
<>
76+
{orderBy(possibleTemplates, 'niceName').map(template => {
77+
const selected = isTemplateSelected(template.id);
78+
79+
return (
80+
<MenuItem
81+
as={Option}
82+
{...menu}
83+
toggleTemplate={toggleTemplate}
84+
selected={selected}
85+
key={template.name}
86+
color={template.color}
87+
id={template.id}
88+
niceName={template.niceName || template.name}
89+
/>
90+
);
91+
})}
92+
93+
<MenuItem
94+
as={Option}
95+
{...menu}
96+
toggleTemplate={() => {
97+
if (!allSelected) {
98+
blacklistedTemplatesCleared();
99+
} else {
100+
blacklistedTemplatesChanged({
101+
templates: possibleTemplates.map(t => t.id) || [],
102+
});
103+
}
104+
}}
105+
selected={allSelected}
106+
color="#374140"
107+
id="all"
108+
style={{ marginTop: '1rem' }}
109+
niceName="Select All"
110+
/>
111+
</>
112+
) : (
113+
<MenuItem {...menu} disabled>
114+
No environments found
115+
</MenuItem>
116+
)}
117+
</OverlayContainer>
118+
</Menu>
119+
</>
103120
);
104121
};
105122

packages/app/src/app/pages/Dashboard/Content/Sandboxes/Filters/SortOptions/Option.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)