diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-arrow.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-arrow.svelte index 0cb3af2ed..1106a9be5 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-arrow.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-arrow.svelte @@ -1,8 +1,14 @@ - + diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte index 28d87923f..c15c4799b 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte @@ -34,8 +34,8 @@ onSelect: box.with(() => handleSelect), }); - function handleSelect() { - onSelect(); + function handleSelect(e: Event) { + onSelect(e); state.toggleChecked(); } diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-group.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-group.svelte index 23ef51306..e151b7741 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-group.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-group.svelte @@ -1,22 +1,18 @@ {#if asChild} - + {@render child?.({ props: mergedProps })} {:else} -
- +
+ {@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-label.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-label.svelte index 885a76a68..bcde1955b 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-label.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-label.svelte @@ -1,24 +1,18 @@ {#if asChild} - + {@render child?.({ props: mergedProps })} {:else} -
- +
+ {@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte index 15868464f..dba2985a0 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-group.svelte @@ -1,41 +1,39 @@ {#if asChild} - + {@render child?.({ props: mergedProps })} {:else} -
- +
+ {@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-item.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-item.svelte index ac0bf4e6e..6429fe8ac 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-radio-item.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-radio-item.svelte @@ -1,44 +1,43 @@ {#if asChild} - + {@render child?.({ props: mergedProps })} {:else} -
- +
+ {@render children?.({ checked: state.isChecked })}
{/if} diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-separator.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-separator.svelte index 35d08df8e..4ba3bce3d 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-separator.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-separator.svelte @@ -1,12 +1,13 @@ {#if asChild} diff --git a/packages/bits-ui/src/lib/bits/menu/index.ts b/packages/bits-ui/src/lib/bits/menu/index.ts index 379c56d05..7d9c76599 100644 --- a/packages/bits-ui/src/lib/bits/menu/index.ts +++ b/packages/bits-ui/src/lib/bits/menu/index.ts @@ -24,5 +24,9 @@ export type { MenuSubContentProps as SubContentProps, MenuSeparatorProps as SeparatorProps, MenuArrowProps as ArrowProps, - MenucheckboxItemProps as CheckboxItemProps, + MenuCheckboxItemProps as CheckboxItemProps, + MenuLabelProps as LabelProps, + MenuGroupProps as GroupProps, + MenuRadioGroupProps as RadioGroupProps, + MenuRadioItemProps as RadioItemProps, } from "./types.js"; diff --git a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts index 716014212..f02f931e3 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -42,12 +42,14 @@ import { afterTick } from "$lib/internal/afterTick.js"; const TRIGGER_ATTR = "data-menu-trigger"; const CONTENT_ATTR = "data-menu-content"; const ITEM_ATTR = "data-menu-item"; +const SEPARATOR_ATTR = "data-menu-separator"; const SUB_TRIGGER_ATTR = "data-menu-subtrigger"; const CHECKBOX_ITEM_ATTR = "data-menu-checkbox-item"; const GROUP_ATTR = "data-menu-group"; const LABEL_ATTR = "data-menu-label"; const RADIO_GROUP_ATTR = "data-menu-radio-group"; const RADIO_ITEM_ATTR = "data-menu-radio-item"; +const ARROW_ATTR = "data-menu-arrow"; const [setMenuRootContext] = createContext("Menu.Root"); @@ -667,22 +669,30 @@ class MenuCheckboxItemState { } class MenuGroupState { - props = $derived.by( - () => - ({ - role: "group", - [GROUP_ATTR]: "", - }) as const - ); + props = { + role: "group", + [GROUP_ATTR]: "", + } as const; } class MenuLabelState { - props = $derived.by( - () => - ({ - [LABEL_ATTR]: "", - }) as const - ); + props = { + [LABEL_ATTR]: "", + } as const; +} + +class MenuSeparatorState { + props = { + [SEPARATOR_ATTR]: "", + role: "separator", + "aria-orientation": "horizontal", + } as const; +} + +class MenuArrowState { + props = { + [ARROW_ATTR]: "", + } as const; } type MenuRadioGroupStateProps = WritableBoxedValues<{ @@ -726,7 +736,7 @@ class MenuRadioItemState { #item: MenuItemState; #value: MenuRadioItemStateProps["value"]; #group: MenuRadioGroupState; - #isChecked = $derived.by(() => this.#group.value.value === this.#value.value); + isChecked = $derived.by(() => this.#group.value.value === this.#value.value); constructor(props: MenuRadioItemStateProps, item: MenuItemState, group: MenuRadioGroupState) { this.#item = item; @@ -744,8 +754,8 @@ class MenuRadioItemState { [RADIO_ITEM_ATTR]: "", ...this.#item.props, role: "menuitemradio", - "aria-checked": getAriaChecked(this.#isChecked), - "data-state": getCheckedState(this.#isChecked), + "aria-checked": getAriaChecked(this.isChecked), + "data-state": getCheckedState(this.isChecked), }) as const ); } @@ -869,3 +879,11 @@ export function useMenuGroup() { export function useMenuLabel() { return new MenuLabelState(); } + +export function useMenuSeparator() { + return new MenuSeparatorState(); +} + +export function useMenuArrow() { + return new MenuArrowState(); +} diff --git a/packages/bits-ui/src/lib/bits/menu/types.ts b/packages/bits-ui/src/lib/bits/menu/types.ts index 32c25f4fa..4e9253633 100644 --- a/packages/bits-ui/src/lib/bits/menu/types.ts +++ b/packages/bits-ui/src/lib/bits/menu/types.ts @@ -55,21 +55,32 @@ export type MenuItemPropsWithoutHTML = WithAsChild<{ /** * A callback fired when the menu item is selected. + * + * Prevent default behavior of selection with `event.preventDefault()`. */ - onSelect?: () => void; + onSelect?: (event: Event) => void; }>; export type MenuItemProps = MenuItemPropsWithoutHTML & Without; export type MenuCheckboxItemPropsWithoutHTML = Omit & { + /** + * The checked state of the checkbox item. + * + * Supports two-way binding with `bind:checked`. + */ checked?: boolean | "indeterminate"; + + /** + * A callback that is fired when the checked state changes. + */ onCheckedChange?: OnChangeFn; } & { children?: Snippet<[{ checked: boolean | "indeterminate" }]>; }; -export type MenucheckboxItemProps = MenuCheckboxItemPropsWithoutHTML & +export type MenuCheckboxItemProps = MenuCheckboxItemPropsWithoutHTML & Without; export type MenuTriggerPropsWithoutHTML = WithAsChild<{ @@ -99,15 +110,49 @@ export type MenuSubPropsWithoutHTML = { }; export type MenuSubContentPropsWithoutHTML = WithAsChild; - export type MenuSubContentProps = MenuSubContentPropsWithoutHTML & Without; export type MenuSeparatorPropsWithoutHTML = WithAsChild<{}>; - export type MenuSeparatorProps = MenuSeparatorPropsWithoutHTML & Without; export type MenuArrowPropsWithoutHTML = ArrowPropsWithoutHTML; - export type MenuArrowProps = ArrowProps; + +export type MenuGroupPropsWithoutHTML = WithAsChild<{}>; +export type MenuGroupProps = MenuGroupPropsWithoutHTML & + Without; + +export type MenuLabelPropsWithoutHTML = WithAsChild<{}>; +export type MenuLabelProps = MenuLabelPropsWithoutHTML & + Without; + +export type MenuRadioGroupPropsWithoutHTML = WithAsChild<{ + /** + * The value of the selected radio item. + * + * Supports two-way binding with `bind:value`. + */ + value?: string; + + /** + * A callback that is fired when the selected radio item changes. + */ + onValueChange?: OnChangeFn; +}>; + +export type MenuRadioGroupProps = MenuRadioGroupPropsWithoutHTML & + Without; + +export type MenuRadioItemPropsWithoutHTML = Omit & { + /** + * The value of the radio item. + */ + value: string; +} & { + children?: Snippet<[{ checked: boolean }]>; +}; + +export type MenuRadioItemProps = MenuRadioItemPropsWithoutHTML & + Without; diff --git a/sites/docs/src/lib/components/demos/dropdown-menu-demo.svelte b/sites/docs/src/lib/components/demos/dropdown-menu-demo.svelte index b31897c33..1a7239ac1 100644 --- a/sites/docs/src/lib/components/demos/dropdown-menu-demo.svelte +++ b/sites/docs/src/lib/components/demos/dropdown-menu-demo.svelte @@ -10,6 +10,7 @@ } from "$icons/index.js"; let checked = $state(false); + let invited = $state(""); @@ -116,78 +117,104 @@ class="w-full max-w-[209px] rounded-xl border border-muted bg-background px-1 py-1.5 shadow-popover !ring-0 !ring-transparent" sideOffset={10} > - - + - - HJ - - @huntabyte - - - + + HJ + + @huntabyte + {#if checked} + × + {/if} + {/snippet} + + - - PS - - @pavel_stianko - - - + + PS + + @pavel_stianko + {#if checked} + × + {/if} + {/snippet} + + - - CK - - @cokakoala_ - - - + + CK + + @cokakoala_ + {#if checked} + × + {/if} + {/snippet} + + - - TL - - @thomasglopes - + {#snippet children({ checked })} + + + TL + + @thomasglopes + {#if checked} + × + {/if} + {/snippet} + +