@hitachivantara/uikit-react-core
Version:
Core React components for the NEXT Design System.
289 lines (288 loc) • 10.1 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { forwardRef, useState, useCallback, useMemo, isValidElement, cloneElement, Fragment } from "react";
import { usePopper } from "react-popper";
import { useForkRef } from "@mui/material/utils";
import { detectOverflow } from "@popperjs/core";
import { useDefaultProps } from "@hitachivantara/uikit-react-utils";
import { useControlled } from "../hooks/useControlled.js";
import { useUniqueId } from "../hooks/useUniqueId.js";
import { HvIcon } from "../icons.js";
import { getFirstAndLastFocus } from "../utils/focusableElementFinder.js";
import { isKey, isOneOfKeys } from "../utils/keyboardUtils.js";
import { setId } from "../utils/setId.js";
import { useClasses } from "./BaseDropdown.styles.js";
import { staticClasses } from "./BaseDropdown.styles.js";
import { BaseDropdownPanel } from "./BaseDropdownPanel.js";
import { BaseDropdownContext, useBaseDropdownContext } from "./context.js";
import { HvTypography } from "../Typography/Typography.js";
const BaseDropdown = forwardRef(function BaseDropdown2(props, ref) {
const {
id: idProp,
className,
classes: classesProp,
children,
role,
placeholder,
component,
headerComponent: HeaderComponentProp,
adornment,
expanded,
dropdownHeaderProps,
defaultExpanded,
disabled,
readOnly,
required,
disablePortal,
"aria-expanded": ariaExpandedProp,
"aria-label": ariaLabelProp,
"aria-labelledby": ariaLabelledByProp,
dropdownHeaderRef: dropdownHeaderRefProp,
onToggle,
onClickOutside,
...others
} = props;
const { classes, cx } = useClasses(classesProp);
const {
popperPlacement,
popperElement,
referenceElement,
setReferenceElement
} = useBaseDropdownContext();
const [isOpen, setIsOpen] = useControlled(expanded, Boolean(defaultExpanded));
const headerRef = useForkRef(
setReferenceElement,
dropdownHeaderRefProp,
dropdownHeaderProps?.ref
);
const customHeaderRef = useForkRef(ref, headerRef);
const ariaRole = role || (component == null ? "combobox" : void 0);
const ariaExpanded = ariaExpandedProp ?? (ariaRole ? !!isOpen : void 0);
const id = useUniqueId(idProp);
const containerId = setId(id, "children-container");
const headerControlArias = {
"aria-required": required ?? void 0,
"aria-readonly": readOnly ?? void 0,
"aria-disabled": disabled ?? void 0,
"aria-expanded": ariaExpanded,
"aria-owns": isOpen ? containerId : void 0,
"aria-controls": isOpen ? containerId : void 0
};
const headerAriaLabels = {
"aria-label": ariaLabelProp,
"aria-labelledby": ariaLabelledByProp
};
const handleToggle = useCallback(
(event) => {
if (event && !isKey(event, "Tab")) {
event.preventDefault();
}
const notControlKey = !!event?.code && !isOneOfKeys(event, ["Tab", "Enter", "Esc", "ArrowDown", "Space"]);
const ignoredCombinations = isKey(event, "Esc") && !isOpen || isKey(event, "ArrowDown") && isOpen || isKey(event, "Tab") && !isOpen;
if (disabled || notControlKey || ignoredCombinations) return;
const newOpen = !isOpen;
setIsOpen(() => {
if (!newOpen) {
referenceElement?.focus({ preventScroll: true });
}
return newOpen;
});
onToggle?.(event, newOpen);
},
[isOpen, disabled, setIsOpen, onToggle, referenceElement]
);
const defaultHeaderElement = /* @__PURE__ */ jsxs(
"div",
{
id: setId(id, "header"),
"data-popper-placement": popperPlacement,
className: cx(classes.header, {
[classes.headerOpen]: isOpen,
[classes.headerOpenUp]: isOpen && popperPlacement?.includes("top"),
[classes.headerOpenDown]: isOpen && popperPlacement?.includes("bottom"),
[classes.headerReadOnly]: readOnly,
[classes.headerDisabled]: disabled
}),
role: ariaRole === "combobox" ? "textbox" : void 0,
...headerAriaLabels,
style: disabled || readOnly ? { pointerEvents: "none" } : void 0,
tabIndex: disabled ? -1 : 0,
ref: headerRef,
...dropdownHeaderProps,
children: [
/* @__PURE__ */ jsx(
"div",
{
className: cx(classes.selection, {
[classes.selectionDisabled]: disabled
}),
children: placeholder && typeof placeholder === "string" ? /* @__PURE__ */ jsx(HvTypography, { noWrap: true, className: classes.placeholder, children: placeholder }) : placeholder
}
),
/* @__PURE__ */ jsx("div", { className: classes.arrowContainer, children: adornment || /* @__PURE__ */ jsx(
HvIcon,
{
name: "CaretDown",
size: "xs",
rotate: isOpen,
className: classes.arrow
}
) })
]
}
);
const headerElement = component && isValidElement(component) ? cloneElement(component, {
ref: headerRef,
...headerControlArias
}) : defaultHeaderElement;
const handleContainerKeyDown = (event) => {
if (isKey(event, "Esc")) {
handleToggle(event);
}
if (isKey(event, "Tab") && !event.shiftKey) {
const focusList = getFirstAndLastFocus(popperElement);
if (document.activeElement === focusList?.last) {
event.preventDefault();
focusList?.first?.focus();
}
}
};
const handleOutside = (event) => {
const isButtonClick = referenceElement?.contains(event.target);
if (!isButtonClick) {
onClickOutside?.(event);
setIsOpen(false);
onToggle?.(event, false);
}
};
const hasCustomHeader = !!HeaderComponentProp;
const HeaderComponent = HeaderComponentProp || "div";
const RootComponent = HeaderComponentProp ? Fragment : "div";
return /* @__PURE__ */ jsxs(RootComponent, { ...!hasCustomHeader && { className: classes.root }, children: [
/* @__PURE__ */ jsx(
HeaderComponent,
{
ref: hasCustomHeader ? customHeaderRef : ref,
id,
disabled: hasCustomHeader && disabled,
className: cx(className, {
[classes.anchor]: !hasCustomHeader,
[classes.rootDisabled]: disabled
}),
...!readOnly && {
onKeyDown: handleToggle,
onClick: handleToggle
},
...(ariaRole || hasCustomHeader) && {
role: hasCustomHeader ? void 0 : ariaRole,
...headerAriaLabels,
...headerControlArias
},
tabIndex: hasCustomHeader ? void 0 : -1,
...others,
children: headerElement
}
),
isOpen && /* @__PURE__ */ jsx(
BaseDropdownPanel,
{
classes,
containerId,
onClickAway: handleOutside,
disablePortal,
onContainerKeyDown: handleContainerKeyDown,
children
}
)
] });
});
const HvBaseDropdown = forwardRef(
function HvBaseDropdown2(props, ref) {
const {
popperProps = {},
variableWidth,
placement: placementProp = "right",
onContainerCreation,
...others
} = useDefaultProps("HvBaseDropdown", props);
const placement = `bottom-${placementProp === "right" ? "start" : "end"}`;
const { modifiers: popperPropsModifiers, ...otherPopperProps } = popperProps;
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(
null
);
const onFirstUpdate = useCallback(() => {
onContainerCreation?.(popperElement);
}, [onContainerCreation, popperElement]);
const modifiers = useMemo(
() => [
{
name: "variableWidth",
enabled: !variableWidth,
phase: "beforeWrite",
requires: ["computeStyles"],
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
effect: ({ state }) => {
state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`;
}
},
{
name: "maxSize",
enabled: true,
phase: "main",
requiresIfExists: ["offset", "preventOverflow", "flip"],
fn: ({ state, name, options }) => {
const overflow = detectOverflow(state, options);
const x = state.modifiersData.preventOverflow?.x || 0;
const y = state.modifiersData.preventOverflow?.y || 0;
const popperWidth = state.rects.popper.width;
const popperHeight = state.rects.popper.height;
const basePlacement = state.placement.split("-")[0];
const widthProp = basePlacement === "left" ? "left" : "right";
const heightProp = basePlacement === "top" ? "top" : "bottom";
state.modifiersData[name] = {
width: popperWidth - overflow[widthProp] - x,
height: popperHeight - overflow[heightProp] - y
};
}
},
{
name: "applyMaxSize",
enabled: true,
phase: "beforeWrite",
requires: ["maxSize"],
fn: ({ state }) => {
const { width, height } = state.modifiersData.maxSize;
state.styles.popper.maxWidth = `${width}px`;
state.styles.popper.maxHeight = `${height}px`;
}
},
...popperPropsModifiers || []
],
[popperPropsModifiers, variableWidth]
);
const popper = usePopper(referenceElement, popperElement, {
placement,
modifiers,
onFirstUpdate,
...otherPopperProps
});
const value = useMemo(
() => ({
popperPlacement: popper?.attributes.popper?.["data-popper-placement"] ?? "bottom",
popper,
popperElement,
setPopperElement,
referenceElement,
setReferenceElement
}),
[popper, popperElement, referenceElement]
);
return /* @__PURE__ */ jsx(BaseDropdownContext.Provider, { value, children: /* @__PURE__ */ jsx(BaseDropdown, { ref, ...others }) });
}
);
export {
HvBaseDropdown,
staticClasses as baseDropdownClasses
};