Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 10 additions & 49 deletions packages/react/src/checkbox/indicator/CheckboxIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
'use client';
import * as React from 'react';
import { useCheckboxRootContext } from '../root/CheckboxRootContext';
import { useRenderElement } from '../../internals/useRenderElement';
import { getCheckboxStateAttributesMapping } from '../utils/getCheckboxStateAttributesMapping';
import type { CheckboxRootState } from '../root/CheckboxRoot';
import type { BaseUIComponentProps } from '../../internals/types';
import { useOpenChangeComplete } from '../../internals/useOpenChangeComplete';
import { useTransitionStatus } from '../../internals/useTransitionStatus';
import type { TransitionStatus } from '../../internals/useTransitionStatus';
import type { StateAttributesMapping } from '../../internals/getStateAttributesProps';
import { transitionStatusMapping } from '../../internals/stateAttributesMapping';
import { useSelectionIndicator } from '../../utils/ItemIndicator';

/**
* Indicates whether the checkbox is ticked.
Expand All @@ -21,54 +18,18 @@ export const CheckboxIndicator = React.forwardRef(function CheckboxIndicator(
componentProps: CheckboxIndicator.Props,
forwardedRef: React.ForwardedRef<HTMLSpanElement>,
) {
const { render, className, style, keepMounted = false, ...elementProps } = componentProps;

const rootState = useCheckboxRootContext();

const rendered = rootState.checked || rootState.indeterminate;

const { mounted, transitionStatus, setMounted } = useTransitionStatus(rendered);

const indicatorRef = React.useRef<HTMLSpanElement | null>(null);

const state: CheckboxIndicatorState = {
...rootState,
transitionStatus,
};

useOpenChangeComplete({
batch: true,
enabled: !rendered,
open: rendered,
ref: indicatorRef,
onComplete() {
if (!rendered) {
setMounted(false);
}
return useSelectionIndicator(
componentProps,
forwardedRef,
rootState.checked || rootState.indeterminate,
rootState,
{
...getCheckboxStateAttributesMapping(rootState),
...transitionStatusMapping,
},
});

const baseStateAttributesMapping = getCheckboxStateAttributesMapping(rootState);

const stateAttributesMapping: StateAttributesMapping<CheckboxIndicatorState> = {
...baseStateAttributesMapping,
...transitionStatusMapping,
};

const shouldRender = keepMounted || mounted;

const element = useRenderElement('span', componentProps, {
ref: [forwardedRef, indicatorRef],
state,
stateAttributesMapping,
props: elementProps,
});

if (!shouldRender) {
return null;
}

return element;
);
});

export interface CheckboxIndicatorState extends CheckboxRootState {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { EMPTY_OBJECT } from '@base-ui/utils/empty';
import type { StateAttributesMapping } from '../../internals/getStateAttributesProps';
import type { CheckboxRootState } from '../root/CheckboxRoot';
import { fieldValidityMapping } from '../../internals/field-constants/constants';
import * as CheckboxRootDataAttributes from '../root/CheckboxRootDataAttributes';
import { mapCheckedStateAttribute } from '../../internals/stateAttributesMapping';

export function getCheckboxStateAttributesMapping(
state: CheckboxRootState,
): StateAttributesMapping<CheckboxRootState> {
return {
checked(value): Record<string, string> {
if (state.indeterminate) {
// `data-indeterminate` is already handled by the `indeterminate` prop.
return {};
}

if (value) {
return { [CheckboxRootDataAttributes.checked]: '' };
}

return { [CheckboxRootDataAttributes.unchecked]: '' };
},
// `data-indeterminate` is already handled by the `indeterminate` prop.
checked: (value) => (state.indeterminate ? EMPTY_OBJECT : mapCheckedStateAttribute(value)),
...fieldValidityMapping,
};
}
6 changes: 6 additions & 0 deletions packages/react/src/internals/stateAttributesMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export { TransitionStatusDataAttributes };

const STARTING_HOOK = { [TransitionStatusDataAttributes.startingStyle]: '' };
const ENDING_HOOK = { [TransitionStatusDataAttributes.endingStyle]: '' };
const CHECKED_HOOK = { 'data-checked': '' };
const UNCHECKED_HOOK = { 'data-unchecked': '' };

export function mapCheckedStateAttribute(value: boolean): Record<string, string> {
return value ? CHECKED_HOOK : UNCHECKED_HOOK;
}

export const transitionStatusMapping = {
transitionStatus(value): Record<string, string> | null {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use client';
import * as React from 'react';
import { useMenuCheckboxItemContext } from '../checkbox-item/MenuCheckboxItemContext';
import { useRenderElement } from '../../internals/useRenderElement';
import type { BaseUIComponentProps } from '../../internals/types';
import { itemMapping } from '../utils/stateAttributesMapping';
import type { TransitionStatus } from '../../internals/useTransitionStatus';
import { useTransitionStatus } from '../../internals/useTransitionStatus';
import { useOpenChangeComplete } from '../../internals/useOpenChangeComplete';
import { ARIA_HIDDEN_PROPS, useSelectionIndicator } from '../../utils/ItemIndicator';

/**
* Indicates whether the checkbox item is ticked.
Expand All @@ -18,45 +16,16 @@ export const MenuCheckboxItemIndicator = React.forwardRef(function MenuCheckboxI
componentProps: MenuCheckboxItemIndicator.Props,
forwardedRef: React.ForwardedRef<HTMLSpanElement>,
) {
const { render, className, style, keepMounted = false, ...elementProps } = componentProps;

const item = useMenuCheckboxItemContext();

const indicatorRef = React.useRef<HTMLSpanElement | null>(null);

const { transitionStatus, mounted, setMounted } = useTransitionStatus(item.checked);

useOpenChangeComplete({
batch: true,
enabled: !item.checked,
open: item.checked,
ref: indicatorRef,
onComplete() {
if (!item.checked) {
setMounted(false);
}
},
});

const state: MenuCheckboxItemIndicatorState = {
checked: item.checked,
disabled: item.disabled,
highlighted: item.highlighted,
transitionStatus,
};

const element = useRenderElement('span', componentProps, {
state,
ref: [forwardedRef, indicatorRef],
stateAttributesMapping: itemMapping,
props: {
'aria-hidden': true,
...elementProps,
},
enabled: keepMounted || mounted,
});

return element;
return useSelectionIndicator(
componentProps,
forwardedRef,
item.checked,
item,
itemMapping,
ARIA_HIDDEN_PROPS,
);
});

export interface MenuCheckboxItemIndicatorProps extends BaseUIComponentProps<
Expand Down
91 changes: 10 additions & 81 deletions packages/react/src/menu/checkbox-item/MenuCheckboxItem.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,25 @@
'use client';
import * as React from 'react';
import { useControlled } from '@base-ui/utils/useControlled';
import { NOOP } from '@base-ui/utils/empty';
import { useMenuFilterItem } from '../filter-root/MenuFilterContext';
import { MenuCheckboxItemContext } from './MenuCheckboxItemContext';
import { REGULAR_ITEM, useMenuItem } from '../item/useMenuItem';
import { useCompositeListItem } from '../../internals/composite/list/useCompositeListItem';
import { useMenuRootContext } from '../root/MenuRootContext';
import { useRenderElement } from '../../internals/useRenderElement';
import { useBaseUiId } from '../../internals/useBaseUiId';
import { useSelectableMenuItem } from '../item/useMenuItem';
import type { BaseUIComponentProps, NonNativeButtonProps } from '../../internals/types';
import { itemMapping } from '../utils/stateAttributesMapping';
import { createChangeEventDetails } from '../../internals/createBaseUIEventDetails';
import { REASONS } from '../../internals/reasons';
import type { MenuRoot } from '../root/MenuRoot';

const MenuCheckboxItemPlain = React.forwardRef(function MenuCheckboxItemPlain(
componentProps: MenuCheckboxItem.Props,
forwardedRef: React.ForwardedRef<HTMLElement>,
) {
const {
render,
className,
id: idProp,
label,
nativeButton = false,
disabled: disabledProp = false,
closeOnClick = false,
checked = false,
onCheckedChange,
style,
...elementProps
} = componentProps;

const { store } = useMenuRootContext();

const listItem = useCompositeListItem({ guess: true, label });
const id = useBaseUiId(idProp);

const rootDisabled = store.useState('disabled');
const highlighted = store.useState('isActive', listItem.index);
const nodeId = store.useState('floatingNodeId');
const itemProps = store.useState('itemProps');

const disabled = disabledProp || rootDisabled;

const { getItemProps, itemRef } = useMenuItem({
closeOnClick,
disabled,
highlighted,
id,
store,
nativeButton,
nodeId,
itemMetadata: REGULAR_ITEM,
});

const state: MenuCheckboxItemState = React.useMemo(
() => ({
disabled,
highlighted,
checked,
}),
[disabled, highlighted, checked],
);

function handleClick(event: React.MouseEvent) {
const details = createChangeEventDetails(REASONS.itemPress, event.nativeEvent, undefined, {
preventUnmountOnClose: NOOP,
});

onCheckedChange?.(!checked, details);
}

const element = useRenderElement('div', componentProps, {
state,
stateAttributesMapping: itemMapping,
props: [
itemProps,
{
role: 'menuitemcheckbox',
'aria-checked': checked,
onClick: handleClick,
},
elementProps,
getItemProps,
],
ref: [itemRef, forwardedRef, listItem.ref],
});

return (
<MenuCheckboxItemContext.Provider value={state}>{element}</MenuCheckboxItemContext.Provider>
const { checked = false, onCheckedChange } = componentProps;

return useSelectableMenuItem(
componentProps,
forwardedRef,
checked,
'menuitemcheckbox',
MenuCheckboxItemContext,
(details) => onCheckedChange?.(!checked, details),
);
});

Expand Down
31 changes: 4 additions & 27 deletions packages/react/src/menu/item/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
'use client';
import * as React from 'react';
import { useMenuFilterItem } from '../filter-root/MenuFilterContext';
import { REGULAR_ITEM, useMenuItem } from './useMenuItem';
import { useMenuRootContext } from '../root/MenuRootContext';
import { useRegularMenuItem } from './useMenuItem';
import { useRenderElement } from '../../internals/useRenderElement';
import { useBaseUiId } from '../../internals/useBaseUiId';
import type { BaseUIComponentProps, NonNativeButtonProps } from '../../internals/types';
import { useCompositeListItem } from '../../internals/composite/list/useCompositeListItem';

const MenuItemPlain = React.forwardRef(function MenuItemPlain(
componentProps: MenuItem.Props,
Expand All @@ -24,28 +21,8 @@ const MenuItemPlain = React.forwardRef(function MenuItemPlain(
...elementProps
} = componentProps;

const { store } = useMenuRootContext();

const listItem = useCompositeListItem({ guess: true, label });
const id = useBaseUiId(idProp);

const rootDisabled = store.useState('disabled');
const highlighted = store.useState('isActive', listItem.index);
const nodeId = store.useState('floatingNodeId');
const itemProps = store.useState('itemProps');

const disabled = disabledProp || rootDisabled;

const { getItemProps, itemRef } = useMenuItem({
closeOnClick,
disabled,
highlighted,
id,
store,
nativeButton,
nodeId,
itemMetadata: REGULAR_ITEM,
});
const { disabled, highlighted, itemProps, getItemProps, itemRef, listItemRef } =
useRegularMenuItem(label, idProp, disabledProp, closeOnClick, nativeButton);

const state: MenuItemState = {
disabled,
Expand All @@ -55,7 +32,7 @@ const MenuItemPlain = React.forwardRef(function MenuItemPlain(
return useRenderElement('div', componentProps, {
state,
props: [itemProps, elementProps, getItemProps],
ref: [itemRef, forwardedRef, listItem.ref],
ref: [itemRef, forwardedRef, listItemRef],
});
});

Expand Down
Loading
Loading