diff --git a/src/select/SelectDropdown.ts b/src/select/SelectDropdown.ts index 7daeeb6d1..58db6862a 100644 --- a/src/select/SelectDropdown.ts +++ b/src/select/SelectDropdown.ts @@ -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({ 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(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; @@ -78,7 +63,7 @@ const useSelectDropdown = createHook({ setCurrentId(items[0].id); move(items[0].id); } - }, [isDropdownOpen]); + }, [visible]); React.useEffect( debounce(() => { @@ -104,29 +89,17 @@ const useSelectDropdown = createHook({ [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, }; diff --git a/src/select/SelectMenu.ts b/src/select/SelectMenu.ts index 7532f94eb..a18128aa2 100644 --- a/src/select/SelectMenu.ts +++ b/src/select/SelectMenu.ts @@ -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; }; @@ -22,14 +18,7 @@ const useSelectMenu = createHook({ keys: SELECT_KEYS, useProps( - { - setTypehead, - isDropdownOpen, - selected, - onChange, - openDropdown, - closeDropdown, - }, + { setTypehead, selected, onChange, hide, show, visible }, { ...htmlProps }, ) { const keyClear = React.useRef(null); @@ -42,19 +31,11 @@ const useSelectMenu = createHook({ const onKeyDown = React.useMemo(() => { return createOnKeyDown({ stopPropagation: true, - keyMap: () => { - return { - Escape: () => { - closeDropdown(); - }, - ArrowUp: () => { - openDropdown(); - }, - ArrowDown: () => { - openDropdown(); - }, - }; - }, + keyMap: () => ({ + Escape: hide, + ArrowUp: show, + ArrowDown: show, + }), }); }, []); @@ -87,7 +68,7 @@ const useSelectMenu = createHook({ return { role: "button", - "aria-expanded": isDropdownOpen, + "aria-expanded": visible, "aria-haspopup": "listbox", onKeyDown, onKeyPress: handleOnKeyPress, diff --git a/src/select/SelectTrigger.ts b/src/select/SelectTrigger.ts index 8db0fd764..b03de65e8 100644 --- a/src/select/SelectTrigger.ts +++ b/src/select/SelectTrigger.ts @@ -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({ name: "selectTrigger", + compose: useDialogDisclosure, keys: SELECT_KEYS, - useProps({ toggleDropdown, isDropdownOpen }, { ref: htmlRef, ...htmlProps }) { - const ref = React.useRef(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, }; }, diff --git a/src/select/__keys.ts b/src/select/__keys.ts index 3123e9db8..80d227f25 100644 --- a/src/select/__keys.ts +++ b/src/select/__keys.ts @@ -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", @@ -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; diff --git a/src/select/stories/Select.stories.tsx b/src/select/stories/Select.stories.tsx index cc2e81588..c36dce9d7 100644 --- a/src/select/stories/Select.stories.tsx +++ b/src/select/stories/Select.stories.tsx @@ -32,7 +32,7 @@ const Select: React.FC<{ state: any }> = ({ state }) => { - + {countries.map(item => { return ( @@ -62,3 +62,13 @@ export const DefaultSelected: React.FC = () => { return + + ); +}; diff --git a/src/select/useSelectState.ts b/src/select/useSelectState.ts index 6628fdf15..14d04381b 100644 --- a/src/select/useSelectState.ts +++ b/src/select/useSelectState.ts @@ -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; }; @@ -21,16 +21,14 @@ export interface ISelectInitialState { export type SelectActions = CompositeActions & { setTypehead: React.Dispatch>; - setDropdown: React.Dispatch>; - openDropdown: React.DispatchWithoutAction; - closeDropdown: React.DispatchWithoutAction; - toggleDropdown: React.DispatchWithoutAction; removeSelected: (value: string) => void; setSelected: (value: string, shouldRemainOpen?: boolean) => void; _setSelected: React.Dispatch>; }; -export type SelectStateReturn = SelectState & SelectActions; +export type SelectStateReturn = SelectState & + SelectActions & + PopoverStateReturn; export const useSelectState = ({ selected, @@ -38,23 +36,11 @@ export const useSelectState = ({ loop = true, }: ISelectInitialState): SelectStateReturn => { const composite = useCompositeState({ loop }); + const popover = usePopoverState({ placement: "bottom-start" }); const [typehead, setTypehead] = React.useState(""); - const [isDropdownOpen, setDropdown] = React.useState(false); const [isPlaceholder, setIsPlaceholder] = React.useState(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([]); const removeSelected = (value: string) => { @@ -71,7 +57,7 @@ export const useSelectState = ({ setTypehead(""); } else { _setSelected([value]); - !shouldRemainOpen && setDropdown(false); + !shouldRemainOpen && popover.hide(); setTypehead(""); } }, @@ -90,14 +76,10 @@ export const useSelectState = ({ return { ...composite, + ...popover, allowMultiselect, typehead, setTypehead, - setDropdown, - openDropdown, - closeDropdown, - isDropdownOpen, - toggleDropdown, removeSelected, selected: _selected, setSelected,