@stratakit/structures
Version:
Medium-sized component structures for the Strata design system
397 lines (396 loc) • 13.8 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import * as React from "react";
import { CompositeItem } from "@ariakit/react/composite";
import { PopoverProvider } from "@ariakit/react/popover";
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) return;
event.stopPropagation();
onSelectedChange?.(!selected);
};
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]
)
}
) });
})
);
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
}
) }) }) }) }) })
}
)
}
) }) }) });
}
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}tree-item-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}-tree-item", props.className),
style,
ref: forwardedRef
}
),
children: props.children
}
);
})
);
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(([entry]) => {
if (!entry.isIntersecting) return;
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-kiwi-expanded": expanded,
"data-kiwi-selected": selected,
"data-kiwi-error": error ? true : void 0,
className: "\u{1F95D}-tree-item-node",
role: void 0,
ref,
children: [
/* @__PURE__ */ jsx(TreeItemDecorations, { onExpanderClick }),
/* @__PURE__ */ jsx(TreeItemContent, {}),
/* @__PURE__ */ jsx(TreeItemDescription, {}),
renderActions && /* @__PURE__ */ jsx(TreeItemActions, {})
]
}
);
});
const TreeItemDecorations = React.memo((props) => {
return /* @__PURE__ */ jsxs(ListItem.Decoration, { children: [
/* @__PURE__ */ jsx(TreeItemExpander, { onClick: props.onExpanderClick }),
/* @__PURE__ */ jsx(TreeItemDecoration, {})
] });
});
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}-tree-item-decoration",
id: decorationId,
render: React.isValidElement(icon) ? icon : typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { href: icon }) : void 0,
children: !icon ? decorations : null
}
) : null;
}
const TreeItemContent = React.memo(() => {
const labelId = React.useContext(TreeItemLabelIdContext);
const label = React.useContext(TreeItemLabelContext);
return /* @__PURE__ */ jsx(ListItem.Content, { id: labelId, className: "\u{1F95D}-tree-item-content", children: label });
});
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}-tree-item-description", children: description }) : void 0;
});
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}-tree-item-actions-container", props.className),
ref: forwardedRef,
render: /* @__PURE__ */ jsx(Toolbar, { focusLoop: false }),
children: [
/* @__PURE__ */ jsx(TreeItemInlineActionsRenderer, {}),
/* @__PURE__ */ jsx(TreeItemActionMenu, {})
]
}
);
})
);
function TreeItemInlineActionsRenderer() {
const actions = React.useContext(TreeItemInlineActionsContext) ?? [];
const actionsToDisplay = actions.slice(0, 2);
return actionsToDisplay;
}
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__ */ jsx(PopoverProvider, { placement: "right-start", children: /* @__PURE__ */ jsxs(DropdownMenu.Root, { open, setOpen, 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, {})
] }) });
})
);
function TreeItemMenuActionsContent() {
return /* @__PURE__ */ jsx(TreeItemActionsMenuContext.Provider, { value: true, children: /* @__PURE__ */ jsx(DropdownMenu.Content, { children: /* @__PURE__ */ jsx(TreeItemActionsRenderer, {}) }) });
}
function TreeItemActionsRenderer() {
const actions = React.useContext(TreeItemActionsContext) ?? [];
return actions;
}
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 });
})
);
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;
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}-tree-item-action", props.className),
"data-kiwi-visible": visible,
ref: forwardedRef
}
);
})
);
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}-tree-item-expander", props.className),
variant: "ghost",
ref: forwardedRef,
children: /* @__PURE__ */ jsx(ChevronDown, {})
}
) });
}
);
export {
TreeItemAction as Action,
TreeItem as Root
};