Skip to content

Commit

Permalink
Merge pull request #7 from timelessco/feat/select-popper
Browse files Browse the repository at this point in the history
feat(select): select popover
  • Loading branch information
anuraghazra authored Aug 24, 2020
2 parents af5e9c8 + d59b33c commit fd1817b
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 121 deletions.
75 changes: 24 additions & 51 deletions src/select/SelectDropdown.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,51 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React from "react";
import { SELECT_KEYS } from "./__keys";
import { useDialog } from "reakit";
import debounce from "lodash.debounce";
import { useForkRef } from "reakit-utils";
import { BoxHTMLProps } from "reakit/ts/Box/Box";
import { useForkRef, contains } from "reakit-utils";
import { SelectStateReturn } from "./useSelectState";
import { SELECT_KEYS, POPOVER_KEYS } from "./__keys";
import { createComponent, createHook } from "reakit-system";
import { useComposite, CompositeProps } from "reakit/Composite";

export type SelectDropdownOptions = CompositeProps &
Pick<
SelectStateReturn,
"isDropdownOpen" | "selected" | "setSelected" | "typehead" | "closeDropdown"
| "selected"
| "setSelected"
| "typehead"
| "visible"
| "unstable_popoverStyles"
| "unstable_popoverRef"
> & {
maxHeight?: string | number;
modal?: boolean;
};

const useSelectDropdown = createHook<SelectDropdownOptions, BoxHTMLProps>({
name: "selectDropdown",
compose: useComposite,
keys: ["maxHeight", ...SELECT_KEYS],
compose: [useComposite, useDialog],
keys: ["maxHeight", ...SELECT_KEYS, ...POPOVER_KEYS],

useOptions({ modal = false, ...options }) {
return { modal, ...options };
},
useProps(
{
maxHeight = 500,
move,
isDropdownOpen,
items,
selected,
setSelected,
setCurrentId,
typehead,
closeDropdown,
visible,
unstable_popoverStyles,
unstable_popoverRef,
},
{ ref: htmlRef, ...htmlProps },
{ ref: htmlRef, style: htmlStyle, ...htmlProps },
) {
const ref = React.useRef<HTMLElement>(null);
const [position, setPosition] = React.useState("top");

const calculateDropdownPosition = React.useCallback(() => {
if (ref.current) {
const bounds = ref.current.getBoundingClientRect();
if (bounds.y < 0) {
setPosition("top");
}

if (bounds.bottom - 50 > window.innerHeight) {
setPosition("bottom");
}
}
}, []);

React.useLayoutEffect(() => {
calculateDropdownPosition();
}, [calculateDropdownPosition, isDropdownOpen]);

React.useLayoutEffect(() => {
window.addEventListener("scroll", calculateDropdownPosition);
return () =>
window.removeEventListener("scroll", calculateDropdownPosition);
}, [calculateDropdownPosition]);

React.useEffect(() => {
if (!items[0]) return;

Expand All @@ -78,7 +63,7 @@ const useSelectDropdown = createHook<SelectDropdownOptions, BoxHTMLProps>({
setCurrentId(items[0].id);
move(items[0].id);
}
}, [isDropdownOpen]);
}, [visible]);

React.useEffect(
debounce(() => {
Expand All @@ -104,29 +89,17 @@ const useSelectDropdown = createHook<SelectDropdownOptions, BoxHTMLProps>({
[typehead],
);

const clickOutside = React.useCallback(e => {
e.stopPropagation();
if (contains(e.target, ref.current as Element)) {
closeDropdown();
}
}, []);

React.useEffect(() => {
window.addEventListener("click", clickOutside);
return () => window.removeEventListener("click", clickOutside);
}, [clickOutside]);

return {
tabIndex: -1,
ref: useForkRef(ref, htmlRef),
role: "listbox",
"aria-orientation": "vertical",
"aria-hidden": !isDropdownOpen,
"aria-hidden": !visible,
ref: useForkRef(unstable_popoverRef, htmlRef),
style: {
maxHeight: maxHeight,
overflowY: "scroll",
display: isDropdownOpen ? "block" : "none",
[position]: "100%",
...unstable_popoverStyles,
...htmlStyle,
},
...htmlProps,
};
Expand Down
35 changes: 8 additions & 27 deletions src/select/SelectMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import { SELECT_KEYS } from "./__keys";

export type SelectMenuOptions = Pick<
SelectStateReturn,
| "setTypehead"
| "isDropdownOpen"
| "selected"
| "openDropdown"
| "closeDropdown"
"setTypehead" | "selected" | "hide" | "show" | "visible"
> & {
onChange?: (value: any) => void;
};
Expand All @@ -22,14 +18,7 @@ const useSelectMenu = createHook<SelectMenuOptions, BoxHTMLProps>({
keys: SELECT_KEYS,

useProps(
{
setTypehead,
isDropdownOpen,
selected,
onChange,
openDropdown,
closeDropdown,
},
{ setTypehead, selected, onChange, hide, show, visible },
{ ...htmlProps },
) {
const keyClear = React.useRef<any>(null);
Expand All @@ -42,19 +31,11 @@ const useSelectMenu = createHook<SelectMenuOptions, BoxHTMLProps>({
const onKeyDown = React.useMemo(() => {
return createOnKeyDown({
stopPropagation: true,
keyMap: () => {
return {
Escape: () => {
closeDropdown();
},
ArrowUp: () => {
openDropdown();
},
ArrowDown: () => {
openDropdown();
},
};
},
keyMap: () => ({
Escape: hide,
ArrowUp: show,
ArrowDown: show,
}),
});
}, []);

Expand Down Expand Up @@ -87,7 +68,7 @@ const useSelectMenu = createHook<SelectMenuOptions, BoxHTMLProps>({

return {
role: "button",
"aria-expanded": isDropdownOpen,
"aria-expanded": visible,
"aria-haspopup": "listbox",
onKeyDown,
onKeyPress: handleOnKeyPress,
Expand Down
24 changes: 7 additions & 17 deletions src/select/SelectTrigger.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
import React from "react";
import { enterOrSpace } from "./common";
import { useForkRef } from "reakit-utils";
import { BoxHTMLProps } from "reakit/ts/Box/Box";
import { SelectStateReturn } from "./useSelectState";
import { createHook, createComponent } from "reakit-system";
import { SELECT_KEYS } from "./__keys";
import { SELECT_KEYS, POPOVER_DISCLOSURE_KEYS } from "./__keys";
import { useDialogDisclosure } from "reakit";

export type SelectTriggerOptions = Pick<
SelectStateReturn,
"toggleDropdown" | "isDropdownOpen"
"toggle" | "visible" | "unstable_referenceRef"
>;

const useSelectTrigger = createHook<SelectTriggerOptions, BoxHTMLProps>({
name: "selectTrigger",
compose: useDialogDisclosure,
keys: SELECT_KEYS,

useProps({ toggleDropdown, isDropdownOpen }, { ref: htmlRef, ...htmlProps }) {
const ref = React.useRef<HTMLElement>(null);

React.useEffect(() => {
if (isDropdownOpen === false && ref.current) {
ref.current.focus();
}
}, [isDropdownOpen]);

useProps({ unstable_referenceRef, visible }, { ref: htmlRef, ...htmlProps }) {
return {
ref: useForkRef(ref, htmlRef),
ref: useForkRef(unstable_referenceRef, htmlRef),
role: "button",
"aria-haspopup": "listbox",
"aria-expanded": isDropdownOpen,
"aria-expanded": visible,
tabIndex: 0,
onKeyDown: e => enterOrSpace(e, toggleDropdown),
onClick: toggleDropdown,
...htmlProps,
};
},
Expand Down
31 changes: 31 additions & 0 deletions src/select/__keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
const POPOVER_STATE_KEYS = [
"baseId",
"unstable_idCountRef",
"visible",
"animated",
"animating",
"setBaseId",
"show",
"hide",
"toggle",
"setVisible",
"setAnimated",
"stopAnimation",
"modal",
"unstable_disclosureRef",
"setModal",
"unstable_referenceRef",
"unstable_popoverRef",
"unstable_arrowRef",
"unstable_popoverStyles",
"unstable_arrowStyles",
"unstable_originalPlacement",
"unstable_update",
"placement",
"place",
] as const;

export const SELECT_KEYS = [
"baseId",
"unstable_idCountRef",
Expand Down Expand Up @@ -52,4 +79,8 @@ export const SELECT_KEYS = [
"setSelected",
"_setSelected",
"isPlaceholder",
...POPOVER_STATE_KEYS,
] as const;

export const POPOVER_KEYS = POPOVER_STATE_KEYS;
export const POPOVER_DISCLOSURE_KEYS = POPOVER_KEYS;
12 changes: 11 additions & 1 deletion src/select/stories/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const Select: React.FC<{ state: any }> = ({ state }) => {
</b>
</SelectTrigger>

<SelectDropdown maxHeight={200} {...state}>
<SelectDropdown style={{ width: "auto" }} maxHeight={200} {...state}>
{countries.map(item => {
return (
<SelectItem {...state} key={item.name} value={item.name}>
Expand Down Expand Up @@ -62,3 +62,13 @@ export const DefaultSelected: React.FC = () => {

return <Select state={state} />;
};

export const Scrolling: React.FC = () => {
const state = useSelectState({ selected: "india" });

return (
<div style={{ margin: "800px 0" }}>
<Select state={state} />
</div>
);
};
32 changes: 7 additions & 25 deletions src/select/useSelectState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
CompositeActions,
CompositeState,
} from "reakit/Composite";
import { usePopoverState, PopoverStateReturn } from "reakit";

export type SelectState = CompositeState & {
allowMultiselect?: boolean;
selected: string[];
typehead: string;
isDropdownOpen: boolean;
isPlaceholder: boolean;
};

Expand All @@ -21,40 +21,26 @@ export interface ISelectInitialState {

export type SelectActions = CompositeActions & {
setTypehead: React.Dispatch<React.SetStateAction<string>>;
setDropdown: React.Dispatch<React.SetStateAction<boolean>>;
openDropdown: React.DispatchWithoutAction;
closeDropdown: React.DispatchWithoutAction;
toggleDropdown: React.DispatchWithoutAction;
removeSelected: (value: string) => void;
setSelected: (value: string, shouldRemainOpen?: boolean) => void;
_setSelected: React.Dispatch<React.SetStateAction<string[]>>;
};

export type SelectStateReturn = SelectState & SelectActions;
export type SelectStateReturn = SelectState &
SelectActions &
PopoverStateReturn;

export const useSelectState = ({
selected,
allowMultiselect,
loop = true,
}: ISelectInitialState): SelectStateReturn => {
const composite = useCompositeState({ loop });
const popover = usePopoverState({ placement: "bottom-start" });

const [typehead, setTypehead] = React.useState<string>("");
const [isDropdownOpen, setDropdown] = React.useState<boolean>(false);
const [isPlaceholder, setIsPlaceholder] = React.useState<boolean>(false);

const toggleDropdown = React.useCallback(() => {
setDropdown(prev => !prev);
}, []);

const openDropdown = React.useCallback(() => {
setDropdown(true);
}, []);

const closeDropdown = React.useCallback(() => {
setDropdown(false);
}, []);

const [_selected, _setSelected] = React.useState<string[]>([]);

const removeSelected = (value: string) => {
Expand All @@ -71,7 +57,7 @@ export const useSelectState = ({
setTypehead("");
} else {
_setSelected([value]);
!shouldRemainOpen && setDropdown(false);
!shouldRemainOpen && popover.hide();
setTypehead("");
}
},
Expand All @@ -90,14 +76,10 @@ export const useSelectState = ({

return {
...composite,
...popover,
allowMultiselect,
typehead,
setTypehead,
setDropdown,
openDropdown,
closeDropdown,
isDropdownOpen,
toggleDropdown,
removeSelected,
selected: _selected,
setSelected,
Expand Down

0 comments on commit fd1817b

Please sign in to comment.