import type { JSX, ValidComponent } from "@solidjs/web";
/**
* `
` — an activatable menu option. Solid 2 port of Base UI
* React's `MenuItem` / `useMenuItem`.
*
* Registers with the enclosing `` (mounted by the
* positioner) so `useListNavigation` + `useTypeahead` can target it.
* `role="menuitem"` by default. Closes the menu on click (unless
* `closeOnClick={false}`).
*/
import { dynamic } from "@solidjs/web";
import { createMemo, omit } from "solid-js";
import { useBaseUiId } from "../../internals/base-ui-id";
import {
createChangeEventDetails,
REASONS,
} from "../../internals/change-event-details";
import { composeRefs, userRef } from "../../internals/compose-refs";
import { useCompositeListItem } from "../../internals/composite/composite-list";
import { useFloatingParentNodeId } from "../../internals/floating-ui/components/floating-tree";
import { mergeRootProps } from "../../internals/merge-root-props";
import { resolveClassStyle } from "../../internals/resolve-class-style";
import type {
BaseUIComponentProps,
NonNativeButtonProps,
} from "../../internals/types";
import { useButton } from "../../internals/use-button/use-button";
import { useMenuRootContext } from "../root/menu-root-context";
import { createItemHoverEmitter } from "../utils/item-hover";
export interface MenuItemState {
/** Whether the item should ignore user interaction. */
disabled: boolean;
/** Whether the item is currently highlighted (focus-visible). */
highlighted: boolean;
}
export interface MenuItemProps
extends NonNativeButtonProps,
Omit, "id"> {
/**
* Tag/component to render. Defaults to `"div"`. Replaces the
* pre-spike `render` prop (axis G).
*/
as?: ValidComponent;
id?: string | undefined;
/** Text label used for typeahead matching. */
label?: string | undefined;
/** Whether the item should ignore user interaction. @default false */
disabled?: boolean | undefined;
/** Whether to close the menu on click. @default true */
closeOnClick?: boolean | undefined;
}
export function MenuItem(props: MenuItemProps): JSX.Element {
const root = useMenuRootContext();
const emitItemHover = createItemHoverEmitter(root, useFloatingParentNodeId());
const fallbackId = useBaseUiId();
const id = (): string | undefined =>
typeof props.id === "string" ? props.id : fallbackId;
const disabled = (): boolean => Boolean(props.disabled);
const closeOnClick = (): boolean => props.closeOnClick ?? true;
const nativeButton = (): boolean => props.nativeButton ?? false;
const listItem = useCompositeListItem({ label: props.label });
const highlighted = createMemo(
() => root.activeIndex() === listItem.index(),
);
const { props: buttonProps, ref: buttonRef } = useButton({
disabled,
focusableWhenDisabled: () => true,
native: nativeButton,
composite: () => true,
});
const state = createMemo(() => ({
disabled: disabled(),
highlighted: highlighted(),
}));
const internal = {
get id() {
return id();
},
role: "menuitem",
get tabindex() {
return highlighted() ? 0 : -1;
},
onKeyDown(event: KeyboardEvent) {
if (event.key === " " && root.typingRef.current) {
event.preventDefault();
}
},
onMouseMove: emitItemHover,
onClick(event: MouseEvent) {
if (disabled()) return;
if (closeOnClick()) {
root.setOpen(false, createChangeEventDetails(REASONS.itemPress, event));
}
},
get "data-highlighted"() {
return state().highlighted ? "" : undefined;
},
get "data-disabled"() {
return state().disabled ? "" : undefined;
},
};
const userClassStyle = resolveClassStyle(props, () => state());
const userProps = omit(
props,
"as",
"class",
"style",
"attrs",
"id",
"label",
"disabled",
"closeOnClick",
"nativeButton",
);
let merged: Record = mergeRootProps(
root.interactions.getItemProps(),
buttonProps,
);
merged = mergeRootProps(merged, internal);
merged = mergeRootProps(merged, userClassStyle);
merged = mergeRootProps(merged, userProps);
const Tag = dynamic(() => props.as ?? "div");
return (
);
}