fix: Implement custom Select Input (#2571)

This commit is contained in:
Saumya Pandey
2021-10-07 10:18:43 +05:30
committed by GitHub
parent 99381d10ff
commit 40e09dd829
16 changed files with 636 additions and 264 deletions

View File

@ -1,80 +1,91 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import {
Select,
SelectOption,
useSelectState,
useSelectPopover,
SelectPopover,
} from "@renderlesskit/react";
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { Outline, LabelText } from "./Input";
const Select = styled.select`
border: 0;
flex: 1;
padding: 4px 0;
margin: 0 12px;
outline: none;
background: none;
color: ${(props) => props.theme.text};
height: 30px;
font-size: 14px;
option {
background: ${(props) => props.theme.buttonNeutralBackground};
}
&:disabled,
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
${breakpoint("mobile", "tablet")`
font-size: 16px;
`};
`;
const Wrapper = styled.label`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
import styled, { css } from "styled-components";
import Button, { Inner } from "components/Button";
import { Position, Background, Backdrop } from "./ContextMenu";
import { MenuAnchorCSS } from "./ContextMenu/MenuItem";
import { LabelText } from "./Input";
import useMenuHeight from "hooks/useMenuHeight";
export type Option = { label: string, value: string };
export type Props = {
value?: string,
label?: string,
nude?: boolean,
ariaLabel: string,
short?: boolean,
className?: string,
labelHidden?: boolean,
options: Option[],
onBlur?: () => void,
onFocus?: () => void,
onChange: (string) => Promise<void> | void,
};
@observer
class InputSelect extends React.Component<Props> {
@observable focused: boolean = false;
const getOptionFromValue = (options: Option[], value) => {
return options.find((option) => option.value === value) || {};
};
handleBlur = () => {
this.focused = false;
};
const InputSelect = (props: Props) => {
const {
value,
label,
className,
labelHidden,
options,
short,
ariaLabel,
onChange,
nude,
} = props;
handleFocus = () => {
this.focused = true;
};
const select = useSelectState({
gutter: 0,
modal: true,
selectedValue: value,
});
render() {
const {
label,
className,
labelHidden,
options,
short,
...rest
} = this.props;
const popOver = useSelectPopover({
...select,
hideOnClickOutside: true,
preventBodyScroll: true,
});
const wrappedLabel = <LabelText>{label}</LabelText>;
const previousValue = React.useRef(value);
const buttonRef = React.useRef();
const minWidth = buttonRef.current?.offsetWidth || 0;
return (
const maxHeight = useMenuHeight(
select.visible,
select.unstable_disclosureRef
);
React.useEffect(() => {
if (previousValue.current === select.selectedValue) return;
previousValue.current = select.selectedValue;
async function load() {
await onChange(select.selectedValue);
}
load();
}, [onChange, select.selectedValue]);
const wrappedLabel = <LabelText>{label}</LabelText>;
const selectedValueIndex = options.findIndex(
(option) => option.value === select.selectedValue
);
return (
<>
<Wrapper short={short}>
{label &&
(labelHidden ? (
@ -82,18 +93,126 @@ class InputSelect extends React.Component<Props> {
) : (
wrappedLabel
))}
<Outline focused={this.focused} className={className}>
<Select onBlur={this.handleBlur} onFocus={this.handleFocus} {...rest}>
{options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
))}
</Select>
</Outline>
<Select {...select} aria-label={ariaLabel} ref={buttonRef}>
{(props) => (
<StyledButton
neutral
disclosure
className={className}
nude={nude}
{...props}
>
{getOptionFromValue(options, select.selectedValue).label ||
`Select a ${ariaLabel}`}
</StyledButton>
)}
</Select>
<SelectPopover {...select} {...popOver}>
{(props) => {
const topAnchor = props.style.top === "0";
const rightAnchor = props.placement === "bottom-end";
// offset top of select to place selected item under the cursor
if (selectedValueIndex !== -1) {
props.style.top = `-${(selectedValueIndex + 1) * 32}px`;
}
return (
<Positioner {...props}>
<Background
dir="auto"
topAnchor={topAnchor}
rightAnchor={rightAnchor}
style={
maxHeight && topAnchor
? { maxHeight, minWidth }
: { minWidth }
}
>
{select.visible || select.animating
? options.map((option) => (
<StyledSelectOption
{...select}
value={option.value}
key={option.value}
>
{select.selectedValue !== undefined && (
<>
{select.selectedValue === option.value ? (
<CheckmarkIcon color="currentColor" />
) : (
<Spacer />
)}
&nbsp;
</>
)}
{option.label}
</StyledSelectOption>
))
: null}
</Background>
</Positioner>
);
}}
</SelectPopover>
</Wrapper>
);
{(select.visible || select.animating) && <Backdrop />}
</>
);
};
const Spacer = styled.svg`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
const StyledButton = styled(Button)`
font-weight: normal;
text-transform: none;
margin-bottom: 16px;
display: block;
width: 100%;
${(props) =>
props.nude &&
css`
border-color: transparent;
box-shadow: none;
`}
${Inner} {
line-height: 28px;
padding-left: 16px;
padding-right: 8px;
justify-content: space-between;
}
}
`;
export const StyledSelectOption = styled(SelectOption)`
${MenuAnchorCSS}
`;
const Wrapper = styled.label`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
const Positioner = styled(Position)`
&.focus-visible {
${StyledSelectOption} {
&[aria-selected="true"] {
color: ${(props) => props.theme.white};
background: ${(props) => props.theme.primary};
box-shadow: none;
cursor: pointer;
svg {
fill: ${(props) => props.theme.white};
}
}
}
}
`;
export default InputSelect;