Content Menu
An accessible dropdown and context menu that is used to display a list of actions or options that a user can choose when a trigger element is right-clicked or long pressed.
Features
Install
Install the component from your command line.
Anatomy
Import all parts and piece them together.
<script setup lang="ts">import * as menu from '@destyler/menu'import { normalizeProps, useMachine } from '@destyler/vue'import { computed, ref, useId } from 'vue'
const [state, send] = useMachine(menu.machine({ 'id': useId(), 'aria-label': 'File',}))const api = computed(() => menu.connect(state.value, send, normalizeProps))</script>
<template> <div> <button v-bind="api.getContextTriggerProps()" /> <Teleport to="body"> <div v-if="api.open" v-bind="api.getPositionerProps()"> <ul v-bind="api.getContentProps()"> <li v-bind="api.getItemProps({ value: 'item1-1' })" /> <li v-bind="api.getItemProps({ value: 'item1-2' })" /> <li v-bind="api.getItemProps({ value: 'item1-3' })" /> <li v-bind="api.getItemProps({ value: 'item1-4' })" /> <li v-bind="api.getItemProps({ value: 'item1-5' })" /> </ul> <ul v-bind="api.getContentProps()"> <li v-bind="api.getItemProps({ value: 'item2-1' })" /> <li v-bind="api.getItemProps({ value: 'item2-2' })" /> <li v-bind="api.getItemProps({ value: 'item2-3' })" /> <li v-bind="api.getItemProps({ value: 'item2-4' })" /> <li v-bind="api.getItemProps({ value: 'item2-5' })" /> </ul> </div> </Teleport> </div></template>import * as menu from '@destyler/menu'import { normalizeProps, useMachine } from '@destyler/react'import { useId } from 'react'import { createPortal } from 'react-dom'
export default function Menu() {
const [state, send] = useMachine(menu.machine({ 'id': useId(), 'aria-label': 'File', })) const api = menu.connect(state, send, normalizeProps)
return ( <div> <button {...api.getContextTriggerProps()}></button> {api.open && createPortal( <div {...api.getPositionerProps()}> <ul {...api.getContentProps()} > <li {...api.getItemProps({ value: 'item1-1' })} /> <li {...api.getItemProps({ value: 'item1-2' })} /> <li {...api.getItemProps({ value: 'item1-3' })} /> <li {...api.getItemProps({ value: 'item1-4' })} /> <li {...api.getItemProps({ value: 'item1-5' })} /> </ul> <ul {...api.getContentProps()} > <li {...api.getItemProps({ value: 'item2-1' })} /> <li {...api.getItemProps({ value: 'item2-2' })} /> <li {...api.getItemProps({ value: 'item2-3' })} /> <li {...api.getItemProps({ value: 'item2-4' })} /> <li {...api.getItemProps({ value: 'item2-5' })} /> </ul> </div>, document.body, )} </div> )}<script lang="ts"> import * as menu from '@destyler/menu' import { normalizeProps, useMachine, portal } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(menu.machine({ id, 'aria-label': 'File', }))
const api = $derived(menu.connect(state, send, normalizeProps))</script>
<div> <button {...api.getContextTriggerProps()} ></button>
{#if api.open} <div use:portal> <div {...api.getPositionerProps()}> <ul {...api.getContentProps()}> <li {...api.getItemProps({ value: 'item1-1' })}></li> <li {...api.getItemProps({ value: 'item1-2' })}></li> <li {...api.getItemProps({ value: 'item1-3' })}></li> <li {...api.getItemProps({ value: 'item1-4' })}></li> <li {...api.getItemProps({ value: 'item1-5' })}></li> </ul> <ul {...api.getContentProps()}> <li {...api.getItemProps({ value: 'item2-1' })}></li> <li {...api.getItemProps({ value: 'item2-2' })}></li> <li {...api.getItemProps({ value: 'item2-3' })}></li> <li {...api.getItemProps({ value: 'item2-4' })}></li> <li {...api.getItemProps({ value: 'item2-5' })}></li> </ul> </div> </div>
{/if}</div>import * as menu from '@destyler/menu'import { normalizeProps, useMachine } from '@destyler/solid'import { createMemo, createUniqueId } from 'solid-js'import { Portal } from 'solid-js/web'
export default function Menu() { const [state, send] = useMachine(menu.machine({ 'id': createUniqueId(), 'aria-label': 'File', })) const api = createMemo(() => menu.connect(state, send, normalizeProps))
return ( <div> <button {...api().getContextTriggerProps()} ></button> {api().open && ( <Portal> <div {...api().getPositionerProps()}> <ul {...api().getContentProps()}> <li {...api().getItemProps({ value: 'item1-1' })} ></li> <li {...api().getItemProps({ value: 'item1-2' })} ></li> <li {...api().getItemProps({ value: 'item1-3' })} ></li> <li {...api().getItemProps({ value: 'item1-4' })} ></li> <li {...api().getItemProps({ value: 'item1-5' })} ></li> </ul> <ul {...api().getContentProps()}> <li {...api().getItemProps({ value: 'item2-1' })} ></li> <li {...api().getItemProps({ value: 'item2-2' })} ></li> <li {...api().getItemProps({ value: 'item2-3' })} ></li> <li {...api().getItemProps({ value: 'item2-4' })} ></li> <li {...api().getItemProps({ value: 'item2-5' })} ></li> </ul> </div> </Portal> )} </div> )}Styling guide
Earlier, we mentioned that each QR Code part has a
data-partattribute added to them to select and style them in the DOM.
Open and closed state
When the menu is open or closed, the content and trigger parts will have the data-state attribute.
[data-part="content"][data-state="open|closed"] { /* styles for open or closed state */}
[data-part="trigger"][data-state="open|closed"] { /* styles for open or closed state */}Highlighted item state
When an item is highlighted, via keyboard navigation or pointer, it is given a data-highlighted attribute.
[data-part="item"][data-highlighted] { /* styles for highlighted state */}
[data-part="item"][data-type="radio|checkbox"][data-highlighted] { /* styles for highlighted state */}Disabled item state
When an item or an option item is disabled, it is given a data-disabled attribute.
[data-part="item"][data-disabled] { /* styles for disabled state */}
[data-part="item"][data-type="radio|checkbox"][data-disabled] { /* styles for disabled state */}Using arrows
When using arrows within the menu, you can style it using css variables.
[data-part="arrow"] { --arrow-size: 20px; --arrow-background: red;}Checked option item state
When an option item is checked, it is given a data-state attribute.
[data-part="item"][data-type="radio|checkbox"][data-state="checked"] { /* styles for checked state */}Methods and Properties
Machine Context
The Menu machine exposes the following context properties:
Partial<{ trigger: string; contextTrigger: string; content: string; groupLabel: (id: string) => string; group: (id: string) => string; positioner: string; arrow: string; }>string(details: HighlightChangeDetails) => void(details: SelectionDetails) => voidPointbooleanPositioningOptionsbooleanstringboolean(details: OpenChangeDetails) => voidbooleanbooleanboolean(details: NavigateDetails) => void"ltr" | "rtl"string() => ShadowRoot | Node | Document(event: KeyboardEvent) => void(event: PointerDownOutsideEvent) => void(event: FocusOutsideEvent) => void(event: InteractOutsideEvent) => voidMachine API
The menu api exposes the following methods:
boolean(open: boolean) => voidstring(value: string) => void(parent: Service) => void(child: Service) => void(options?: Partial<PositioningOptions>) => void(props: OptionItemProps) => OptionItemState(props: ItemProps) => ItemStateData Attributes
Trigger
data-scopedata-partdata-placementdata-stateIndicator
data-scopedata-partdata-stateContent
data-scopedata-partdata-statedata-placementItem
data-scopedata-partdata-disableddata-highlighteddata-valuetextOption Item
data-scopedata-partdata-typedata-valuedata-statedata-disableddata-highlighteddata-valuetextItem Indicator
data-scopedata-partdata-disableddata-highlighteddata-stateItem Text
data-scopedata-partdata-disableddata-highlighteddata-stateAccessibility
Keyboard Interaction
SpaceEnterArrowDownArrowUpArrowRight/ArrowLeftEsc