@clayui/drop-down
Version:
ClayDropDown component
340 lines (339 loc) • 9.27 kB
JavaScript
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
};