UNPKG

@clayui/drop-down

Version:
340 lines (339 loc) 9.27 kB
import { ClayCheckbox, ClayRadio } from "@clayui/form"; import Icon from "@clayui/icon"; import { FOCUSABLE_ELEMENTS, Keys, MouseSafeArea, throttle, useNavigation } from "@clayui/shared"; import classNames from "classnames"; import React, { useCallback, useContext, useRef, useState } from "react"; import warning from "warning"; import Divider from "./Divider"; import DropDown from "./DropDown"; import { DropDownContext } from "./DropDownContext"; import { FocusMenu } from "./FocusMenu"; import DropDownGroup from "./Group"; function findNested(items, key) { return items.some((item) => { if (item[key]) { return true; } if (item.items && item["type"] !== "contextual") { return findNested(item.items, key); } return false; }); } function Checkbox({ checked = false, onChange = () => { }, title, ...otherProps }) { const [value, setValue] = useState(checked); const { tabFocus } = useContext(DropDownContext); return /* @__PURE__ */ React.createElement(DropDown.Section, { role: "none" }, /* @__PURE__ */ React.createElement( ClayCheckbox, { ...otherProps, checked: value, label: title, onChange: () => { setValue((val) => !val); onChange(!value); }, tabIndex: !tabFocus ? -1 : void 0 } )); } const ClayDropDownContext = React.createContext({ close: () => { } }); function Item({ child, label, onClick, spritemap, title, ...props }) { const { back, close, messages, onForward } = useContext(ClayDropDownContext); title = label || title; return /* @__PURE__ */ React.createElement( DropDown.Item, { ...props, onClick: (event) => { if (onForward && child && title) { event.preventDefault(); onForward(title, child); return; } if (onClick) { onClick(event); } close(); }, onKeyDown: (event) => { if (back && event.key === Keys.Left) { back(); } }, spritemap }, title, child && /* @__PURE__ */ React.createElement( "span", { "aria-label": `${messages["goTo"]} ${title}`, className: "dropdown-item-indicator-end", title: `${messages["goTo"]} ${title}` }, /* @__PURE__ */ React.createElement(Icon, { spritemap, symbol: "angle-right" }) ) ); } function Group({ items, label, spritemap, title }) { title = label || title; warning( typeof items !== "undefined", `ClayDropDownWithItems -> The '${title}' group contains no items to render.` ); if (typeof items === "undefined") { return null; } return /* @__PURE__ */ React.createElement(DropDownGroup, { header: title }, items && /* @__PURE__ */ React.createElement(Items, { items, spritemap })); } const BOTTOM_OFFSET = [0, 1]; const LEFT_OFFSET = [-1, 6]; const RIGHT_OFFSET = [1, -6]; const TOP_OFFSET = [0, -1]; const OFFSET_MAP = { bctc: TOP_OFFSET, blbr: RIGHT_OFFSET, bltl: TOP_OFFSET, brbl: LEFT_OFFSET, brtr: TOP_OFFSET, clcr: RIGHT_OFFSET, crcl: LEFT_OFFSET, tcbc: BOTTOM_OFFSET, tlbl: BOTTOM_OFFSET, tltr: RIGHT_OFFSET, trbr: BOTTOM_OFFSET, trtl: LEFT_OFFSET }; function offsetFn(points) { return OFFSET_MAP[points.join("")]; } function Contextual({ items, label, spritemap, title, ...otherProps }) { const [visible, setVisible] = useState(false); const { close } = useContext(ClayDropDownContext); const triggerElementRef = useRef(null); const menuElementRef = useRef(null); const timeoutHandleRef = useRef(null); const keyboardRef = useRef(false); const hasRightSymbols = React.useMemo( () => items && findNested(items, "symbolRight"), [items] ); const hasLeftSymbols = React.useMemo( () => items && findNested(items, "symbolLeft"), [items] ); const { navigationProps } = useNavigation({ activation: "manual", containerRef: menuElementRef, loop: true, orientation: "vertical", typeahead: true, visible }); const setThrottleVisible = useCallback( // eslint-disable-next-line react-compiler/react-compiler throttle((value) => setVisible(value), 100), [] ); title = label || title; return /* @__PURE__ */ React.createElement( DropDown.Item, { ...otherProps, "aria-expanded": visible, "aria-haspopup": Boolean(items), className: classNames({ active: visible }), onClick: (event) => { keyboardRef.current = false; if (event.currentTarget === event.target) { setVisible(true); clearTimeout(timeoutHandleRef.current); timeoutHandleRef.current = null; } }, onKeyDown: (event) => { switch (event.key) { case Keys.Enter: case Keys.Right: setVisible(true); keyboardRef.current = true; break; default: break; } }, onMouseEnter: () => { if (!visible) { keyboardRef.current = false; timeoutHandleRef.current = setTimeout( () => setThrottleVisible(true), 400 ); } }, onMouseLeave: () => { keyboardRef.current = false; setThrottleVisible(false); clearTimeout(timeoutHandleRef.current); timeoutHandleRef.current = null; }, ref: triggerElementRef, spritemap, symbolRight: "angle-right" }, title, items && /* @__PURE__ */ React.createElement( DropDown.Menu, { active: visible, alignElementRef: triggerElementRef, alignmentPosition: 8, hasLeftSymbols, hasRightSymbols, offsetFn, onActiveChange: setVisible, onKeyDown: navigationProps.onKeyDown, ref: menuElementRef }, visible && /* @__PURE__ */ React.createElement(MouseSafeArea, { parentRef: menuElementRef }), /* @__PURE__ */ React.createElement( ClayDropDownContext.Provider, { value: { back: () => { (triggerElementRef.current?.children[0]).focus(); setVisible(false); }, close: () => { setVisible(false); close(); } } }, /* @__PURE__ */ React.createElement( FocusMenu, { condition: visible, onRender: () => { if (menuElementRef.current && keyboardRef.current) { const first = menuElementRef.current.querySelector( FOCUSABLE_ELEMENTS.join(",") ); if (first) { first.focus(); } } } }, /* @__PURE__ */ React.createElement( DropDownItems, { items, spritemap } ) ) ) ) ); } const RadioGroupContext = React.createContext({}); function Radio({ title, value = "", ...otherProps }) { const { checked, name, onChange } = useContext(RadioGroupContext); const { tabFocus } = useContext(DropDownContext); return /* @__PURE__ */ React.createElement(DropDown.Section, { role: "none" }, /* @__PURE__ */ React.createElement( ClayRadio, { ...otherProps, checked: checked === value, inline: true, label: title, name, onChange: () => onChange(value), tabIndex: !tabFocus ? -1 : void 0, value } )); } function RadioGroup({ items, label, name, onChange = () => { }, spritemap, title, value: defaultValue = "" }) { const [value, setValue] = useState(defaultValue); const params = { checked: value, name, onChange: (value2) => { onChange(value2); setValue(value2); } }; title = label || title; warning( items && !items.filter((item) => item.type !== "radio").length, "ClayDropDownWithItems -> Items of type `radiogroup` should be used `radio` if you need to use others, it is recommended to use type `group`." ); return /* @__PURE__ */ React.createElement(DropDownGroup, { header: title, role: "radiogroup" }, items && /* @__PURE__ */ React.createElement(RadioGroupContext.Provider, { value: params }, /* @__PURE__ */ React.createElement(Items, { items, spritemap }))); } function DividerWithItem() { return /* @__PURE__ */ React.createElement(Divider, null); } const TYPE_MAP = { checkbox: Checkbox, contextual: Contextual, divider: DividerWithItem, group: Group, item: Item, radio: Radio, radiogroup: RadioGroup }; function DropDownItems({ items, role, spritemap, ...otherProps }) { return /* @__PURE__ */ React.createElement(DropDown.ItemList, { "aria-label": otherProps["aria-label"], role }, /* @__PURE__ */ React.createElement(Items, { items, spritemap })); } function Items({ items, spritemap }) { return /* @__PURE__ */ React.createElement(React.Fragment, null, items.map(({ type, ...item }, key) => { const Item2 = TYPE_MAP[type || "item"]; return /* @__PURE__ */ React.createElement(Item2, { ...item, key, spritemap }); })); } export { ClayDropDownContext, DropDownItems, findNested };