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-part
attribute 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) => void
Point
boolean
PositioningOptions
boolean
string
boolean
(details: OpenChangeDetails) => void
boolean
boolean
boolean
(details: NavigateDetails) => void
"ltr" | "rtl"
string
() => ShadowRoot | Node | Document
(event: KeyboardEvent) => void
(event: PointerDownOutsideEvent) => void
(event: FocusOutsideEvent) => void
(event: InteractOutsideEvent) => void
Machine API
The menu api
exposes the following methods:
boolean
(open: boolean) => void
string
(value: string) => void
(parent: Service) => void
(child: Service) => void
(options?: Partial<PositioningOptions>) => void
(props: OptionItemProps) => OptionItemState
(props: ItemProps) => ItemState
Data Attributes
Trigger
data-scope
data-part
data-placement
data-state
Indicator
data-scope
data-part
data-state
Content
data-scope
data-part
data-state
data-placement
Item
data-scope
data-part
data-disabled
data-highlighted
data-valuetext
Option Item
data-scope
data-part
data-type
data-value
data-state
data-disabled
data-highlighted
data-valuetext
Item Indicator
data-scope
data-part
data-disabled
data-highlighted
data-state
Item Text
data-scope
data-part
data-disabled
data-highlighted
data-state
Accessibility
Keyboard Interaction
Space
Enter
ArrowDown
ArrowUp
ArrowRight/ArrowLeft
Esc