UNPKG

@hitachivantara/uikit-react-core

Version:

Core React components for the NEXT Design System.

289 lines (288 loc) 10.1 kB
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 };