fix: Keyboard accessible context menus (#1768)

- Makes menus fully accessible and keyboard driven
- Currently adds 2.8% to initial bundle size due to the inclusion of Reakit and its dependency, popperjs.
- Converts all menus to functional components
- Remove old custom menu system
- Various layout and flow improvements around the menus

closes #1766
This commit is contained in:
Tom Moor
2021-01-13 22:00:25 -08:00
committed by GitHub
parent 47369dd968
commit e8b7782f5e
54 changed files with 1788 additions and 1881 deletions

View File

@@ -1,45 +1,61 @@
// @flow
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import { MenuItem } from "reakit/Menu";
import styled from "styled-components";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
type Props = {
type Props = {|
label: string,
note?: string,
onSelect: () => void,
active: boolean,
};
|};
const FilterOption = ({ label, note, onSelect, active }: Props) => {
const FilterOption = ({ label, note, onSelect, active, ...rest }: Props) => {
return (
<ListItem active={active}>
<Anchor onClick={active ? undefined : onSelect}>
<Flex align="center" justify="space-between">
<span>
{label}
{note && <HelpText small>{note}</HelpText>}
</span>
{active && <Checkmark />}
</Flex>
</Anchor>
</ListItem>
<MenuItem onClick={active ? undefined : onSelect} {...rest}>
{(props) => (
<ListItem>
<Button active={active} {...props}>
<Flex align="center" justify="space-between">
<span>
{label}
{note && <Description small>{note}</Description>}
</span>
{active && <Checkmark />}
</Flex>
</Button>
</ListItem>
)}
</MenuItem>
);
};
const Description = styled(HelpText)`
margin-bottom: 0;
`;
const Checkmark = styled(CheckmarkIcon)`
flex-shrink: 0;
padding-left: 4px;
fill: ${(props) => props.theme.text};
`;
const Anchor = styled("a")`
const Button = styled.button`
display: flex;
flex-direction: column;
font-size: 15px;
padding: 4px 8px;
margin: 0;
border: 0;
background: none;
color: ${(props) => props.theme.text};
text-align: left;
font-weight: ${(props) => (props.active ? "600" : "normal")};
justify-content: center;
width: 100%;
min-height: 32px;
${HelpText} {
@@ -54,7 +70,7 @@ const Anchor = styled("a")`
const ListItem = styled("li")`
list-style: none;
font-weight: ${(props) => (props.active ? "600" : "normal")};
max-width: 250px;
`;
export default FilterOption;

View File

@@ -1,63 +1,74 @@
// @flow
import { find } from "lodash";
import * as React from "react";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import Button, { Inner } from "components/Button";
import { DropdownMenu } from "components/DropdownMenu";
import ContextMenu from "components/ContextMenu";
import FilterOption from "./FilterOption";
type Props = {
options: {
key: string,
label: string,
note?: string,
}[],
type TFilterOption = {|
key: string,
label: string,
note?: string,
|};
type Props = {|
options: TFilterOption[],
activeKey: ?string,
defaultLabel?: string,
selectedPrefix?: string,
className?: string,
onSelect: (key: ?string) => void,
};
|};
const FilterOptions = ({
options,
activeKey = "",
defaultLabel,
selectedPrefix = "",
className,
onSelect,
}: Props) => {
const menu = useMenuState();
const selected = find(options, { key: activeKey }) || options[0];
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
return (
<DropdownButton label={activeKey ? selectedLabel : defaultLabel}>
{({ closeMenu }) => (
<SearchFilter>
<MenuButton {...menu}>
{(props) => (
<StyledButton
{...props}
className={className}
neutral
disclosure
small
>
{activeKey ? selectedLabel : defaultLabel}
</StyledButton>
)}
</MenuButton>
<ContextMenu aria-label={defaultLabel} {...menu}>
<List>
{options.map((option) => (
<FilterOption
key={option.key}
onSelect={() => {
onSelect(option.key);
closeMenu();
menu.hide();
}}
active={option.key === activeKey}
{...option}
{...menu}
/>
))}
</List>
)}
</DropdownButton>
</ContextMenu>
</SearchFilter>
);
};
const Content = styled("div")`
padding: 0 8px;
width: 250px;
p {
margin-bottom: 0;
}
`;
const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
@@ -73,32 +84,14 @@ const StyledButton = styled(Button)`
}
`;
const SearchFilter = (props) => {
return (
<DropdownMenu
className={props.className}
label={
<StyledButton neutral disclosure small>
{props.label}
</StyledButton>
}
position="right"
>
{({ closePortal }) => (
<Content>{props.children({ closeMenu: closePortal })}</Content>
)}
</DropdownMenu>
);
};
const DropdownButton = styled(SearchFilter)`
const SearchFilter = styled.div`
margin-right: 8px;
`;
const List = styled("ol")`
list-style: none;
margin: 0;
padding: 0;
padding: 0 8px;
`;
export default FilterOptions;