import type { JSX, ValidComponent } from "@solidjs/web";
/**
* `
` — a toggleable menu item. Mirrors React.
*/
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 { createControlledState } from "../../internals/create-controlled-state";
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 type { MenuRootChangeEventDetails } from "../root/menu-root-context";
import { useMenuRootContext } from "../root/menu-root-context";
import { createItemHoverEmitter } from "../utils/item-hover";
import {
MenuCheckboxItemContext,
type MenuCheckboxItemContextValue,
} from "./menu-checkbox-item-context";
export type MenuCheckboxItemChangeEventDetails = MenuRootChangeEventDetails;
export interface MenuCheckboxItemState {
disabled: boolean;
highlighted: boolean;
checked: boolean;
}
export interface MenuCheckboxItemProps
extends NonNativeButtonProps,
Omit, "id"> {
/**
* Tag/component to render. Defaults to `"div"`. Replaces the
* pre-spike `render` prop (axis G).
*/
as?: ValidComponent;
id?: string | undefined;
/**
* Controlled checked state. When provided (not `undefined`), the
* consumer owns the state — pair with {@link onCheckedChange}. When
* omitted, the item manages its own checked state (seeded from
* {@link defaultChecked}).
*/
checked?: boolean | undefined;
/**
* Initial checked state when uncontrolled. Read once at mount.
* @default false
*/
defaultChecked?: boolean | undefined;
/**
* Fires when the item is ticked or unticked. Calling
* `details.cancel()` keeps the item's own (uncontrolled) state
* unchanged, mirroring React Base UI.
*/
onCheckedChange?:
| ((checked: boolean, details: MenuCheckboxItemChangeEventDetails) => void)
| undefined;
/** @default false */
disabled?: boolean | undefined;
label?: string | undefined;
/** @default false */
closeOnClick?: boolean | undefined;
}
export function MenuCheckboxItem(props: MenuCheckboxItemProps): 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 ?? false;
const nativeButton = (): boolean => props.nativeButton ?? false;
const listItem = useCompositeListItem({ label: props.label });
const highlighted = createMemo(
() => root.activeIndex() === listItem.index(),
);
// Controlled / uncontrolled checked state (Base UI `checked` /
// `defaultChecked` contract): the `checked` prop wins while defined;
// otherwise an internal signal (seeded once from `defaultChecked`)
// backs the state and `setChecked` writes it.
const [checked, setUncontrolledChecked] = createControlledState({
controlled: () =>
props.checked === undefined ? undefined : Boolean(props.checked),
default: () => props.defaultChecked,
fallback: false,
name: "",
state: "checked",
});
// Base UI change flow: fire `onCheckedChange`, honor `details.cancel()`,
// then write the uncontrolled state (inert while controlled).
const setChecked = (
next: boolean,
details: MenuCheckboxItemChangeEventDetails,
): void => {
props.onCheckedChange?.(next, details);
if (details.isCanceled) return;
setUncontrolledChecked(next);
};
const { props: buttonProps, ref: buttonRef } = useButton({
disabled,
focusableWhenDisabled: () => true,
native: nativeButton,
composite: () => true,
});
const state = createMemo(() => ({
disabled: disabled(),
highlighted: highlighted(),
checked: checked(),
}));
const contextValue: MenuCheckboxItemContextValue = {
checked,
disabled,
highlighted,
};
const internal = {
get id() {
return id();
},
role: "menuitemcheckbox",
get "aria-checked"() {
return checked() ? "true" : "false";
},
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;
const next = !checked();
setChecked(
next,
createChangeEventDetails(
REASONS.itemPress,
event,
) as MenuCheckboxItemChangeEventDetails,
);
if (closeOnClick()) {
root.setOpen(false, createChangeEventDetails(REASONS.itemPress, event));
}
},
get "data-highlighted"() {
return state().highlighted ? "" : undefined;
},
get "data-disabled"() {
return state().disabled ? "" : undefined;
},
get "data-checked"() {
return state().checked ? "" : undefined;
},
get "data-unchecked"() {
return state().checked ? undefined : "";
},
};
const userClassStyle = resolveClassStyle(props, () => state());
const userProps = omit(
props,
"as",
"class",
"style",
"attrs",
"children",
"id",
"checked",
"defaultChecked",
"onCheckedChange",
"disabled",
"label",
"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 (
{props.children}
);
}