UNPKG

@stratakit/structures

Version:

Medium-sized component structures for the Strata design system

433 lines (432 loc) 15.1 kB
import { jsx, jsxs } from "react/jsx-runtime"; import * as React from "react"; import { CompositeItem } from "@ariakit/react/composite"; import { Role } from "@ariakit/react/role"; import { Toolbar, ToolbarItem } from "@ariakit/react/toolbar"; import { IconButton } from "@stratakit/bricks"; import { GhostAligner, IconButtonPresentation } from "@stratakit/bricks/secret-internals"; import { Icon } from "@stratakit/foundations"; import { forwardRef, useEventHandlers } from "@stratakit/foundations/secret-internals"; import cx from "classnames"; import { ChevronDown, MoreHorizontal, StatusIcon } from "./~utils.icons.js"; import * as ListItem from "./~utils.ListItem.js"; import * as DropdownMenu from "./DropdownMenu.js"; const TreeItemErrorContext = React.createContext(void 0); const TreeItemInlineActionsContext = React.createContext(void 0); const TreeItemActionsContext = React.createContext(void 0); const TreeItemDecorationsContext = React.createContext(void 0); const TreeItemIconContext = React.createContext(void 0); const TreeItemDecorationIdContext = React.createContext( void 0 ); const TreeItemLabelContext = React.createContext(void 0); const TreeItemLabelIdContext = React.createContext( void 0 ); const TreeItemDescriptionContext = React.createContext(void 0); const TreeItemDescriptionIdContext = React.createContext( void 0 ); const TreeItem = React.memo( forwardRef((props, forwardedRef) => { const { selected, onSelectedChange, expanded, onExpandedChange, icon: _icon, unstable_decorations: _unstable_decorations, label: _label, description: _description, inlineActions: _inlineActions, actions: _actions, error: _error, onClick: onClickProp, onKeyDown: onKeyDownProp, ...rest } = props; const onExpanderClick = useEventHandlers(() => { if (expanded === void 0) return; onExpandedChange?.(!expanded); }); const handleClick = (event) => { if (selected !== void 0) { event.stopPropagation(); onSelectedChange?.(!selected); return; } if (expanded === void 0) return; event.stopPropagation(); onExpandedChange?.(!expanded); }; const handleKeyDown = (event) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { return; } if (expanded === void 0) return; if (event.key === "ArrowRight" || event.key === "ArrowLeft") { event.preventDefault(); onExpandedChange?.(event.key === "ArrowRight"); } }; return /* @__PURE__ */ jsx(TreeItemRootProvider, { ...props, children: /* @__PURE__ */ jsx( TreeItemRoot, { ...rest, expanded, selected, onClick: useEventHandlers(onClickProp, handleClick), onKeyDown: useEventHandlers(onKeyDownProp, handleKeyDown), ref: forwardedRef, children: React.useMemo( () => /* @__PURE__ */ jsx( TreeItemNode, { onExpanderClick, expanded, selected } ), [onExpanderClick, expanded, selected] ) } ) }); }) ); DEV: TreeItem.displayName = "Tree.Item"; function TreeItemRootProvider(props) { const { inlineActions, actions, label, description, icon: iconProp, unstable_decorations: decorations, error } = props; const labelId = React.useId(); const descriptionId = React.useId(); const decorationId = React.useId(); const icon = error ? /* @__PURE__ */ jsx(StatusIcon, { tone: "attention" }) : iconProp; const hasDecoration = icon || decorations; return /* @__PURE__ */ jsx(TreeItemErrorContext.Provider, { value: error, children: /* @__PURE__ */ jsx(TreeItemInlineActionsContext.Provider, { value: inlineActions, children: /* @__PURE__ */ jsx(TreeItemActionsContext.Provider, { value: actions, children: /* @__PURE__ */ jsx( TreeItemDisplayActionsMenuContext.Provider, { value: actions ? actions.length > 0 : false, children: /* @__PURE__ */ jsx( TreeItemDecorationIdContext.Provider, { value: hasDecoration ? decorationId : void 0, children: /* @__PURE__ */ jsx(TreeItemDecorationsContext.Provider, { value: decorations, children: /* @__PURE__ */ jsx(TreeItemIconContext.Provider, { value: icon, children: /* @__PURE__ */ jsx(TreeItemLabelIdContext.Provider, { value: labelId, children: /* @__PURE__ */ jsx(TreeItemLabelContext.Provider, { value: label, children: /* @__PURE__ */ jsx(TreeItemDescriptionContext.Provider, { value: description, children: /* @__PURE__ */ jsx( TreeItemDescriptionIdContext.Provider, { value: description ? descriptionId : void 0, children: props.children } ) }) }) }) }) }) } ) } ) }) }) }); } DEV: TreeItemRootProvider.displayName = "TreeItemRootProvider"; const TreeItemRoot = React.memo( forwardRef((props, forwardedRef) => { const { style: styleProp, "aria-level": level, selected, expanded, ...rest } = props; const labelId = React.useContext(TreeItemLabelIdContext); const decorationId = React.useContext(TreeItemDecorationIdContext); const descriptionId = React.useContext(TreeItemDescriptionIdContext); const error = React.useContext(TreeItemErrorContext); const errorId = typeof error === "string" ? error : void 0; const describedBy = React.useMemo(() => { const ids = []; if (descriptionId) ids.push(descriptionId); if (decorationId) ids.push(decorationId); if (errorId) ids.push(errorId); return ids.length > 0 ? ids.join(" ") : void 0; }, [decorationId, descriptionId, errorId]); const style = React.useMemo( () => ({ ...styleProp, "--\u{1F95D}TreeItem-level": level }), [styleProp, level] ); return /* @__PURE__ */ jsx( CompositeItem, { render: /* @__PURE__ */ jsx( Role, { ...rest, role: "treeitem", "aria-expanded": expanded, "aria-selected": selected, "aria-labelledby": labelId, "aria-describedby": describedBy, "aria-level": level, className: cx("\u{1F95D}TreeItem", props.className), style, ref: forwardedRef } ), children: props.children } ); }) ); DEV: TreeItemRoot.displayName = "TreeItemRoot"; function useRenderActions() { const ref = React.useRef(null); const [renderActions, setRenderActions] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el || renderActions) return; const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue; React.startTransition(() => { setRenderActions(true); }); } }); observer.observe(el); return () => { observer.disconnect(); }; }, [renderActions]); return [ref, renderActions]; } const TreeItemNode = React.memo((props) => { const { expanded, selected, onExpanderClick } = props; const error = React.useContext(TreeItemErrorContext); const [ref, renderActions] = useRenderActions(); return /* @__PURE__ */ jsxs( ListItem.Root, { "data-_sk-expanded": expanded, "data-_sk-selected": selected, "data-_sk-error": error ? true : void 0, className: "\u{1F95D}TreeItemNode", role: void 0, ref, children: [ /* @__PURE__ */ jsx(TreeItemDecorations, { onExpanderClick }), /* @__PURE__ */ jsx(TreeItemContent, {}), /* @__PURE__ */ jsx(TreeItemDescription, {}), renderActions && /* @__PURE__ */ jsx(TreeItemActions, {}) ] } ); }); DEV: TreeItemNode.displayName = "TreeItemNode"; const TreeItemDecorations = React.memo((props) => { return /* @__PURE__ */ jsxs(ListItem.Decoration, { children: [ /* @__PURE__ */ jsx(TreeItemExpander, { onClick: props.onExpanderClick }), /* @__PURE__ */ jsx(TreeItemDecoration, {}) ] }); }); DEV: TreeItemDecorations.displayName = "TreeItemDecorations"; function TreeItemDecoration() { const decorationId = React.useContext(TreeItemDecorationIdContext); const decorations = React.useContext(TreeItemDecorationsContext); const icon = React.useContext(TreeItemIconContext); return icon || decorations ? /* @__PURE__ */ jsx( Role, { className: "\u{1F95D}TreeItemDecoration", id: decorationId, render: React.isValidElement(icon) ? icon : typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { href: icon }) : void 0, children: !icon ? decorations : null } ) : null; } DEV: TreeItemDecoration.displayName = "TreeItemDecoration"; const TreeItemContent = React.memo(() => { const labelId = React.useContext(TreeItemLabelIdContext); const label = React.useContext(TreeItemLabelContext); return /* @__PURE__ */ jsx(ListItem.Content, { id: labelId, className: "\u{1F95D}TreeItemContent", children: label }); }); DEV: TreeItemContent.displayName = "TreeItemContent"; const TreeItemDescription = React.memo(() => { const description = React.useContext(TreeItemDescriptionContext); const descriptionId = React.useContext(TreeItemDescriptionIdContext); return description ? /* @__PURE__ */ jsx(ListItem.Content, { id: descriptionId, className: "\u{1F95D}TreeItemDescription", children: description }) : void 0; }); DEV: TreeItemDescription.displayName = "TreeItemDescription"; const TreeItemActions = React.memo( forwardRef((props, forwardedRef) => { return /* @__PURE__ */ jsxs( ListItem.Decoration, { ...props, onClick: useEventHandlers(props.onClick, (e) => e.stopPropagation()), onKeyDown: useEventHandlers( props.onKeyDown, (e) => e.stopPropagation() ), className: cx("\u{1F95D}TreeItemActionsContainer", props.className), ref: forwardedRef, render: /* @__PURE__ */ jsx(Toolbar, { focusLoop: false }), children: [ /* @__PURE__ */ jsx(TreeItemInlineActionsRenderer, {}), /* @__PURE__ */ jsx(TreeItemActionMenu, {}) ] } ); }) ); DEV: TreeItemActions.displayName = "TreeItemActions"; function TreeItemInlineActionsRenderer() { const actions = React.useContext(TreeItemInlineActionsContext) ?? []; const actionsToDisplay = actions.slice(0, 2); return actionsToDisplay; } DEV: TreeItemInlineActionsRenderer.displayName = "TreeItemInlineActionsRenderer"; const arrowKeys = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"]; const TreeItemActionsMenuContext = React.createContext(false); const TreeItemDisplayActionsMenuContext = React.createContext(false); const TreeItemActionMenu = React.memo( forwardRef((props, forwardedRef) => { const [open, _setOpen] = React.useState(false); const isArrowKeyPressed = React.useRef(false); const displayMenu = React.useContext(TreeItemDisplayActionsMenuContext); const setOpen = React.useCallback((value) => { if (value && !isArrowKeyPressed.current) { _setOpen(true); } else { _setOpen(false); } }, []); if (!displayMenu) return null; return /* @__PURE__ */ jsxs( DropdownMenu.Provider, { open, setOpen, placement: "right-start", children: [ /* @__PURE__ */ jsx( DropdownMenu.Button, { ...props, onKeyDown: (e) => { if (arrowKeys.includes(e.key)) { isArrowKeyPressed.current = true; } queueMicrotask(() => { isArrowKeyPressed.current = false; }); }, render: /* @__PURE__ */ jsx(TreeItemInlineAction, { label: "More", icon: /* @__PURE__ */ jsx(MoreHorizontal, {}) }), ref: forwardedRef } ), /* @__PURE__ */ jsx(TreeItemMenuActionsContent, {}) ] } ); }) ); DEV: TreeItemActionMenu.displayName = "TreeItemActionMenu"; function TreeItemMenuActionsContent() { return /* @__PURE__ */ jsx(TreeItemActionsMenuContext.Provider, { value: true, children: /* @__PURE__ */ jsx(DropdownMenu.Content, { children: /* @__PURE__ */ jsx(TreeItemActionsRenderer, {}) }) }); } DEV: TreeItemMenuActionsContent.displayName = "TreeItemMenuActionsContent"; function TreeItemActionsRenderer() { const actions = React.useContext(TreeItemActionsContext) ?? []; return actions; } DEV: TreeItemActionsRenderer.displayName = "TreeItemActionsRenderer"; const TreeItemAction = React.memo( forwardRef((props, forwardedRef) => { const { label, icon, dot, ...rest } = props; const actionsMenuContext = React.useContext(TreeItemActionsMenuContext); if (actionsMenuContext) { return /* @__PURE__ */ jsx( DropdownMenu.Item, { ...rest, label, icon, unstable_dot: dot, ref: forwardedRef } ); } return /* @__PURE__ */ jsx(TreeItemInlineAction, { ...props, ref: forwardedRef }); }) ); DEV: TreeItemAction.displayName = "Tree.ItemAction"; const TreeItemInlineAction = React.memo( forwardRef((props, forwardedRef) => { const error = React.useContext(TreeItemErrorContext); const generatedId = React.useId(); const { id = generatedId, visible = error ? true : void 0, // visible by default during error state label, icon, dot, ...rest } = props; DEV: { if (!icon) throw new Error( "`icon` prop is required when the action is displayed as a button" ); } return /* @__PURE__ */ jsx( IconButton, { id, label, icon, inert: visible === false ? "true" : void 0, ...rest, render: /* @__PURE__ */ jsx(ToolbarItem, { render: props.render }), dot, variant: "ghost", className: cx("\u{1F95D}TreeItemAction", props.className), "data-_sk-visible": visible, ref: forwardedRef } ); }) ); DEV: TreeItemInlineAction.displayName = "TreeItemInlineAction"; const TreeItemExpander = forwardRef( (props, forwardedRef) => { const descriptionId = React.useContext(TreeItemDescriptionIdContext); return /* @__PURE__ */ jsx(GhostAligner, { align: descriptionId ? "block" : void 0, children: /* @__PURE__ */ jsx( IconButtonPresentation, { "aria-hidden": "true", ...props, onClick: useEventHandlers(props.onClick, (e) => e.stopPropagation()), className: cx("\u{1F95D}TreeItemExpander", props.className), variant: "ghost", ref: forwardedRef, children: /* @__PURE__ */ jsx(ChevronDown, {}) } ) }); } ); DEV: TreeItemExpander.displayName = "TreeItemExpander"; export { TreeItemAction as Action, TreeItem as Root };