@skdx/docs-kit 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/angular-inputs.json +1 -1
- package/dist/data/api-manifest.json +1 -1
- package/dist/shared/contracts/accordion.js +1 -1
- package/dist/shared/contracts/alert.js +1 -1
- package/dist/shared/contracts/autocomplete.d.ts +1 -1
- package/dist/shared/contracts/autocomplete.js +1 -1
- package/dist/shared/contracts/avatar.js +1 -1
- package/dist/shared/contracts/breadcrumb.js +1 -1
- package/dist/shared/contracts/button.js +1 -1
- package/dist/shared/contracts/calendar.js +1 -1
- package/dist/shared/contracts/card.js +1 -1
- package/dist/shared/contracts/carousel.js +1 -1
- package/dist/shared/contracts/checkbox-card.js +1 -1
- package/dist/shared/contracts/checkbox-group.js +1 -1
- package/dist/shared/contracts/collapsible.js +1 -1
- package/dist/shared/contracts/combobox.js +1 -1
- package/dist/shared/contracts/command.js +1 -1
- package/dist/shared/contracts/dashboard-layout.js +1 -1
- package/dist/shared/contracts/date-picker.js +1 -1
- package/dist/shared/contracts/dialog.d.ts +1 -1
- package/dist/shared/contracts/dialog.js +1 -1
- package/dist/shared/contracts/drawer.d.ts +1 -1
- package/dist/shared/contracts/drawer.js +1 -1
- package/dist/shared/contracts/field.js +1 -1
- package/dist/shared/contracts/file-upload.js +1 -1
- package/dist/shared/contracts/floating-panel.js +1 -1
- package/dist/shared/contracts/form.js +1 -1
- package/dist/shared/contracts/image.js +1 -1
- package/dist/shared/contracts/listbox.js +1 -1
- package/dist/shared/contracts/loading-overlay.js +1 -1
- package/dist/shared/contracts/mentions.js +1 -1
- package/dist/shared/contracts/menu.js +1 -1
- package/dist/shared/contracts/menubar.js +1 -1
- package/dist/shared/contracts/navigation-menu.js +1 -1
- package/dist/shared/contracts/pagination.js +1 -1
- package/dist/shared/contracts/password-input.js +1 -1
- package/dist/shared/contracts/popover.js +1 -1
- package/dist/shared/contracts/progress.js +1 -1
- package/dist/shared/contracts/qr-code.js +1 -1
- package/dist/shared/contracts/radio-card.js +1 -1
- package/dist/shared/contracts/radio-group.js +1 -1
- package/dist/shared/contracts/rating.js +1 -1
- package/dist/shared/contracts/resizable.js +1 -1
- package/dist/shared/contracts/scroll-area.js +1 -1
- package/dist/shared/contracts/select.js +1 -1
- package/dist/shared/contracts/slider.js +1 -1
- package/dist/shared/contracts/speed-dial.js +1 -1
- package/dist/shared/contracts/split-button.js +1 -1
- package/dist/shared/contracts/stepper.js +1 -1
- package/dist/shared/contracts/switch.js +1 -1
- package/dist/shared/contracts/table.js +3 -1
- package/dist/shared/contracts/tabs.js +1 -1
- package/dist/shared/contracts/tag-group.js +1 -1
- package/dist/shared/contracts/tags-input.js +1 -1
- package/dist/shared/contracts/text-field.js +1 -1
- package/dist/shared/contracts/textarea.js +1 -1
- package/dist/shared/contracts/timeline.js +1 -1
- package/dist/shared/contracts/toast.js +9 -1
- package/dist/shared/contracts/toolbar.js +1 -1
- package/dist/shared/contracts/tour.js +1 -1
- package/dist/shared/contracts/transfer.js +1 -1
- package/dist/shared/contracts/tree-select.js +1 -1
- package/dist/shared/contracts/tree.js +1 -1
- package/dist/shared/contracts/types.d.ts +2 -0
- package/dist/shared/contracts/workspace-switcher.js +1 -1
- package/dist/shared/core/field-core.d.ts +2 -0
- package/dist/shared/core/field-core.js +1 -1
- package/dist/skins-src/_foundation-fallback.css +2 -1
- package/dist/skins-src/components/_actuator.css +1 -1
- package/dist/skins-src/components/_card-control.css +1 -1
- package/dist/skins-src/components/_collection.css +2 -2
- package/dist/skins-src/components/_overlay.css +1 -1
- package/dist/skins-src/components/_trigger.css +1 -1
- package/dist/skins-src/components/accordion.css +1 -1
- package/dist/skins-src/components/alert.css +2 -2
- package/dist/skins-src/components/autocomplete.css +1 -1
- package/dist/skins-src/components/avatar.css +1 -1
- package/dist/skins-src/components/badge.css +1 -1
- package/dist/skins-src/components/button.css +1 -1
- package/dist/skins-src/components/calendar.css +8 -6
- package/dist/skins-src/components/card.css +1 -1
- package/dist/skins-src/components/carousel.css +1 -1
- package/dist/skins-src/components/chat-composer.css +1 -1
- package/dist/skins-src/components/chat-message.css +1 -1
- package/dist/skins-src/components/checkbox-card.css +2 -2
- package/dist/skins-src/components/checkbox.css +1 -1
- package/dist/skins-src/components/clipboard.css +2 -2
- package/dist/skins-src/components/color-picker.css +4 -4
- package/dist/skins-src/components/combobox.css +2 -2
- package/dist/skins-src/components/command.css +1 -1
- package/dist/skins-src/components/dashboard-layout.css +1 -1
- package/dist/skins-src/components/date-field.css +2 -2
- package/dist/skins-src/components/dialog.css +4 -4
- package/dist/skins-src/components/drawer.css +9 -9
- package/dist/skins-src/components/editable.css +3 -3
- package/dist/skins-src/components/file-upload.css +4 -4
- package/dist/skins-src/components/floating-panel.css +1 -1
- package/dist/skins-src/components/forgot-password-page.css +1 -1
- package/dist/skins-src/components/form-section.css +1 -1
- package/dist/skins-src/components/form.css +12 -2
- package/dist/skins-src/components/image.css +1 -1
- package/dist/skins-src/components/kbd.css +1 -1
- package/dist/skins-src/components/link.css +1 -1
- package/dist/skins-src/components/listbox.css +1 -1
- package/dist/skins-src/components/mentions.css +1 -1
- package/dist/skins-src/components/menubar.css +2 -2
- package/dist/skins-src/components/navigation-menu.css +1 -1
- package/dist/skins-src/components/number-field.css +1 -1
- package/dist/skins-src/components/onboarding-checklist.css +1 -1
- package/dist/skins-src/components/pagination.css +2 -2
- package/dist/skins-src/components/password-input.css +80 -16
- package/dist/skins-src/components/pin-input.css +2 -2
- package/dist/skins-src/components/popover.css +1 -1
- package/dist/skins-src/components/qr-code.css +2 -2
- package/dist/skins-src/components/radio-card.css +1 -1
- package/dist/skins-src/components/reset-password-page.css +1 -1
- package/dist/skins-src/components/select.css +3 -3
- package/dist/skins-src/components/sign-in-page.css +1 -1
- package/dist/skins-src/components/sign-up-page.css +1 -1
- package/dist/skins-src/components/skeleton.css +1 -1
- package/dist/skins-src/components/skip-link.css +1 -1
- package/dist/skins-src/components/speed-dial.css +3 -3
- package/dist/skins-src/components/stat-card.css +6 -1
- package/dist/skins-src/components/stepper.css +3 -1
- package/dist/skins-src/components/switch.css +21 -1
- package/dist/skins-src/components/table.css +4 -4
- package/dist/skins-src/components/tags-input.css +2 -2
- package/dist/skins-src/components/text-field.css +26 -21
- package/dist/skins-src/components/text.css +1 -1
- package/dist/skins-src/components/textarea.css +1 -1
- package/dist/skins-src/components/time-field.css +2 -2
- package/dist/skins-src/components/toast.css +2 -2
- package/dist/skins-src/components/toggle-group.css +3 -3
- package/dist/skins-src/components/toggle.css +1 -1
- package/dist/skins-src/components/toolbar.css +2 -2
- package/dist/skins-src/components/tree-select.css +2 -2
- package/dist/skins-src/components/tree.css +1 -1
- package/dist/skins-src/components/usage-card.css +1 -1
- package/dist/skins-src/components/user-menu.css +1 -1
- package/dist/skins-src/components/verification-page.css +1 -1
- package/dist/skins-src/components/workspace-switcher.css +2 -2
- package/package.json +14 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var a={name:"Image",purpose:`A native <img> with a required alt and a Fallback part shown on error. Loading status is read from the mounted <img>'s own native load/error events, not Avatar's off-DOM probe, which would double-fetch and defeat loading="lazy".`,props:[{name:"src",type:"string",required:!1,description:"Image source, passed straight through to the native src."},{name:"alt",type:"string",required:!0,description:'Accessible name for the image, required at the type level. alt="" is the decorative opt-out; omitting it announces the filename instead.'},{name:"srcSet",type:"string",required:!1,description:"Responsive candidate list, passed through to the native srcset."},{name:"sizes",type:"string",required:!1,description:"Layout size hints, passed through to the native sizes."},{name:"loading",type:"'eager' | 'lazy'",defaultValue:"'lazy'",required:!1,description:"Load timing, passed straight through to the native loading."},{name:"decoding",type:"'sync' | 'async' | 'auto'",defaultValue:"'async'",required:!1,description:"Decode hint, passed straight through to the native decoding."},{name:"fetchPriority",type:"'high' | 'low' | 'auto'",defaultValue:"'auto'",required:!1,description:"Fetch priority, passed through to the native fetchpriority."},{name:"fit",type:"'contain' | 'cover' | 'fill' | 'none' | 'scale-down'",required:!1,description:"How the image fills its box, emitted as object-fit."},{name:"position",type:"string",required:!1,description:"Image alignment within its box, emitted as object-position."},{name:"fallback",type:"unknown",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:"Vue and Web Components take this as a named slot rather than a prop \u2014 Vue because a renderable region is a slot in Vue's own idiom, Web Components because a custom element attribute carries a string and cannot carry a node. Both slot spellings are declared under slots.",description:"Rendered in the Fallback part once loading fails; a prop in React/Svelte/Angular, a slot in Vue and Web Components."},{name:"crossOrigin",type:"string",required:!1,description:"CORS mode, passed straight through to the native crossorigin."},{name:"referrerPolicy",type:"string",required:!1,description:"Referrer policy, passed through to the native referrerpolicy."}],events:[{name:"loading-status-change",detail:"'idle' | 'loading' | 'loaded' | 'error'",description:"Fires the new loading status ('idle' | 'loading' | 'loaded' | 'error') on every transition, sourced from the <img>'s own native load/error events, not Avatar's off-DOM probe image."}],parts:[{name:"Fallback",description:"Renders in place of the <img> once loading status is 'error'. A node or slot, not a second load machine."}],slots:[{name:"fallback",description:"Content shown in place of the <img> once loading status is 'error'."}],states:["idle","loading","loaded","error"],controlledUncontrolled:"Stateless \u2014 Image tracks its own image-loading status internally, not consumer-driven.",formBehavior:"Not applicable \u2014 Image does not participate in forms.",validationBehavior:"Not applicable \u2014 Image carries no value and participates in no validation.",accessibility:{role:"img (native <img>; the Fallback part carries whatever role its own content provides once it renders)",keyboard:[],ariaAttributes:["alt is required at the type level, so it cannot be omitted the way a lint rule can be silenced",'alt="" is the legal decorative opt-out, distinct from an absent alt attribute, which announces the filename instead'],focusManagement:"Not focusable; not part of the tab order.",namingRequirement:`Named by the required alt prop. alt="" opts a purely decorative image out of the accessibility tree's name; alt itself cannot be omitted.`},styling:{cssParts:["root","fallback"],dataAttributes:[...e,"data-loading-status"],cssCustomProperties:["--skdx-image-radius"]},publicMethods:[]};export{a as imageContract};
|
|
1
|
+
import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var a={name:"Image",purpose:`A native <img> with a required alt and a Fallback part shown on error. Loading status is read from the mounted <img>'s own native load/error events, not Avatar's off-DOM probe, which would double-fetch and defeat loading="lazy".`,props:[{name:"src",type:"string",required:!1,description:"Image source, passed straight through to the native src."},{name:"alt",type:"string",required:!0,description:'Accessible name for the image, required at the type level. alt="" is the decorative opt-out; omitting it announces the filename instead.'},{name:"srcSet",type:"string",required:!1,description:"Responsive candidate list, passed through to the native srcset."},{name:"sizes",type:"string",required:!1,description:"Layout size hints, passed through to the native sizes."},{name:"loading",type:"'eager' | 'lazy'",defaultValue:"'lazy'",required:!1,description:"Load timing, passed straight through to the native loading."},{name:"decoding",type:"'sync' | 'async' | 'auto'",defaultValue:"'async'",required:!1,description:"Decode hint, passed straight through to the native decoding."},{name:"fetchPriority",type:"'high' | 'low' | 'auto'",defaultValue:"'auto'",required:!1,description:"Fetch priority, passed through to the native fetchpriority."},{name:"fit",type:"'contain' | 'cover' | 'fill' | 'none' | 'scale-down'",required:!1,description:"How the image fills its box, emitted as object-fit."},{name:"position",type:"string",required:!1,description:"Image alignment within its box, emitted as object-position."},{name:"fallback",type:"unknown",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:"Vue and Web Components take this as a named slot rather than a prop \u2014 Vue because a renderable region is a slot in Vue's own idiom, Web Components because a custom element attribute carries a string and cannot carry a node. Both slot spellings are declared under slots.",description:"Rendered in the Fallback part once loading fails; a prop in React/Svelte/Angular, a slot in Vue and Web Components."},{name:"crossOrigin",type:"string",required:!1,description:"CORS mode, passed straight through to the native crossorigin."},{name:"referrerPolicy",type:"string",required:!1,description:"Referrer policy, passed through to the native referrerpolicy."}],events:[{name:"loading-status-change",detail:"'idle' | 'loading' | 'loaded' | 'error'",description:"Fires the new loading status ('idle' | 'loading' | 'loaded' | 'error') on every transition, sourced from the <img>'s own native load/error events, not Avatar's off-DOM probe image."}],parts:[{name:"Fallback",description:"Renders in place of the <img> once loading status is 'error'. A node or slot, not a second load machine."}],slots:[{name:"fallback",description:"Content shown in place of the <img> once loading status is 'error'."}],states:["idle","loading","loaded","error"],controlledUncontrolled:"Stateless \u2014 Image tracks its own image-loading status internally, not consumer-driven.",formBehavior:"Not applicable \u2014 Image does not participate in forms.",validationBehavior:"Not applicable \u2014 Image carries no value and participates in no validation.",accessibility:{role:"img (native <img>; the Fallback part carries whatever role its own content provides once it renders)",keyboard:[],ariaAttributes:["alt is required at the type level, so it cannot be omitted the way a lint rule can be silenced",'alt="" is the legal decorative opt-out, distinct from an absent alt attribute, which announces the filename instead'],focusManagement:"Not focusable; not part of the tab order.",namingRequirement:`Named by the required alt prop. alt="" opts a purely decorative image out of the accessibility tree's name; alt itself cannot be omitted.`},styling:{cssParts:["root","fallback"],dataAttributes:[...e,"data-loading-status"],cssCustomProperties:["--skdx-image-radius"]},publicMethods:[],frameworkExtensions:[{framework:"webComponents",kind:"slot",name:"default",type:"Content projection",description:"default content projected by image. This framework spells the content customization as a native named slot/snippet on this part; consumers of other wrappers compose the corresponding part or use their documented rendering property."},{framework:"webComponents",kind:"slot",name:"default",type:"Content projection",description:"default content projected by image. This framework spells the content customization as a native named slot/snippet on this part; consumers of other wrappers compose the corresponding part or use their documented rendering property."}]};export{a as imageContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changeDescription as i,changeDetail as e}from"./change-reason.js";import{SELECTION_ATTRIBUTE_RULE as t,SELECTION_INPUT_RULE as o,SELECTION_OUTPUT_RULE as a}from"./selection-value.js";import{ROOT_DATA_ATTRIBUTES as n}from"./styling-hooks.js";var d={name:"Listbox",purpose:"An always-visible list of selectable options with one tab stop, roving focus, typeahead, and single or multiple selection \u2014 the selection list a Select wraps in a popup, usable on its own. Given options and no hand-composed ListboxItem/ListboxGroup children, the root derives one ListboxItem per entry (grouped where options nests a group) instead of the consumer writing the loop.",props:[{name:"options",type:"ReadonlyArray<{ value: string; label?: string; disabled?: boolean } | { label: string; options: Array<{ value: string; label?: string; disabled?: boolean }> }>",required:!1,description:"Flat or grouped option data to render automatically as ListboxItems (and, for a group, a ListboxGroup/ListboxLabel pair), instead of hand composing them as children. Ignored when children declares its own ListboxItem or ListboxGroup. Property-only on <skdx-listbox> (@property({ attribute: false })): an HTML attribute cannot carry an array of objects."},{name:"renderOption",type:"(option: { value: string; label?: string; disabled?: boolean }, state: { selected: boolean }) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:'Web Components uses a named slot (`slot="option"`) since HTML attributes cannot carry functions. Vue uses a scoped slot (`#option`) instead of a prop. Angular uses a TemplateRef input.',description:"Customises how a derived option renders inside its ListboxItem. Receives the option and whether it is currently selected. Only read while options is deriving the anatomy."},{name:"value",type:"string | string[]",required:!1,description:`Selected option value(s), for controlled usage. Pair with a value-change event/emit/callback. ${o} ${a} ${t}`},{name:"defaultValue",type:"string | string[]",required:!1,description:`Initially selected value(s), for uncontrolled usage. Same shape as \`value\`. ${o} ${t}`},{name:"multiple",type:"boolean",defaultValue:"false",required:!1,description:'Allows more than one option to be selected. Sets `aria-multiselectable="true"`, makes choosing an option toggle it instead of replacing, and enables range-selection keys: Shift+ArrowDown/ArrowUp, Shift+Space, Control+Shift+Home/End, and Control+A.'},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables every option at once, exposed as aria-disabled/data-disabled on the list and on each option. An individual Item can still be disabled on its own; the two OR together."},{name:"allowSelectAll",type:"boolean",defaultValue:"true",required:!1,description:"Whether Control+A selects every enabled option while `multiple`, or clears if all are already selected. Inert while single-select. Default true (normative for multi-select lists); disable if Control+A should stay with the page (e.g. in an editor pane)."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'vertical'",required:!1,description:"Layout axis of the option list. Reflected as aria-orientation and data-orientation, and it is what picks the arrow-key pair: a vertical list roves on ArrowDown/ArrowUp, a horizontal one on ArrowRight/ArrowLeft. This can never be a skin-only attribute \u2014 it changes the keyboard, so it is a component property that the skin then reads for its flex-direction. Home/End, PageUp/PageDown, typeahead and every selection key are unaffected: they name positions in the list rather than directions on screen."},{name:"focusOnHover",type:"boolean",defaultValue:"false",required:!1,description:"Whether moving the pointer over an option moves roving focus onto it. Off by default since Listbox roves real DOM focus; hovering would move document.activeElement. Never moves focus onto a disabled option, never selects."},{name:"disallowEmptySelection",type:"boolean",defaultValue:"false",required:!1,description:"Whether the last selected option refuses to be deselected, so the list always holds at least one value once it holds any. Only reachable while `multiple`. Prevents deselecting via toggle, Control+A clear, or keyboard range operations. Never creates a selection \u2014 a list that starts empty stays empty until the user picks something."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."},{name:"shape",type:"'square' | 'rounded' | (string & {})",required:!1,description:"Corner rounding rung, written to data-shape on the root; the data-shape attribute may be set directly instead."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the root; the data-size attribute may be set directly instead."}],events:[{name:"value-change",description:`${i("the selection")} Carries the whole selection, not the single option that was toggled; selectedValue names that option. ${a}`,detail:e("string[]","selectedValue: string | null")},{name:"value-change-detail",frameworks:["angular"],exception:"event-shape",frameworksReason:"Angular's `valueChange` emits the scalar so `[(value)]` round-trips the value itself; this second output carries the detail object the other four hand to `value-change`.",description:"Fires with valueChange, carrying the previous selection, the change reason and the toggled option.",detail:e("string[]","selectedValue: string | null")}],parts:[{name:"Item",description:`One selectable option (role="option"), holding the list's single tab stop while it is the active one.`,props:[{name:"value",type:"string",required:!0,description:"Identifier committed to the root's value when this option is chosen."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Excludes this option from selection and from arrow/typeahead navigation. It stays in the DOM and keeps `aria-selected`, so it is still announced as part of the list."},{name:"textValue",type:"string",required:!1,description:"Text typeahead matches this option against, published as `data-text-value`. Required in practice whenever the option renders more than its label \u2014 an ItemIndicator glyph, an icon, a second line \u2014 because the fallback is the option's whole rendered text content, glyph included."},{name:"posInSet",type:"number | undefined",required:!1,description:`This option's 1-based position in the whole option set, emitted as aria-posinset. Only needed when the rendered options are a window onto a longer list \u2014 a virtualized listbox, or one that pages in more rows as the user scrolls \u2014 where a screen reader counting the DOM announces "3 of 20" for row 4,003 of a million. Absent by default, so the browser's own count stands for an ordinary list. This is the one piece of virtualized-listbox accessibility that belongs inside a package: the consumer owns the windowing, but only the option can carry the attribute.`},{name:"setSize",type:"number | undefined",required:!1,description:'Size of the whole option set this option belongs to, emitted as aria-setsize. Set together with `posInSet`: a position without a size announces "item 4 of an unknown number", which is worse than the DOM count it replaces.'}]},{name:"ItemIndicator",description:"Selected-state glyph inside an Item, rendered only while that option is selected. `aria-hidden`: the option's `aria-selected` already carries the state.",props:[{name:"forceMount",type:"boolean",defaultValue:"false",required:!1,description:"Keep the indicator mounted even while its option isn't selected."}]},{name:"Group",description:'A labelled group of related Items (role="group"), pointing `aria-labelledby` at its own Label.'},{name:"Label",description:"The heading naming its Group, carrying the id the Group references. Not an option, and not focusable."}],slots:[{name:"default",description:"Listbox content \u2014 Item, Group, and Label parts. Given options and no ListboxItem/ListboxGroup here, the root derives this content itself instead."},{name:"item",description:"An Item's content \u2014 its label, and optionally an ItemIndicator."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"listbox.css"},{attribute:"data-shape",values:["square","rounded"],stylesheet:"listbox.css"},{attribute:"data-size",values:["sm","lg"],stylesheet:"listbox.css"}],states:["disabled","multiple","selected","active"],controlledUncontrolled:"The selected value supports controlled (value + value-change) and uncontrolled (defaultValue) usage. There is no open state to control \u2014 the list is always visible. In Angular the pair is split: `valueChange` emits the scalar for `[(value)]`, `valueChangeDetail` carries the detail.",formBehavior:"None. No `name`/hidden-input form participation. Listbox is a picker whose result the consumer commits; wrap it in a Field and submit the value you hold.",validationBehavior:"None. No `required`/`invalid` flag or constraint validation.",accessibility:{role:"listbox (root); group (each Group); option (each Item)",keyboard:[{key:"Tab",behavior:"Moves into the list, landing on its single tab stop \u2014 the selected option, or the first enabled one \u2014 and back out again. Focus is never trapped."},{key:"ArrowDown / ArrowUp",behavior:"Moves roving focus to the next/previous enabled option (no wrap) while `orientation` is `vertical`. A `horizontal` list answers ArrowRight/ArrowLeft instead, and leaves the vertical pair to the page \u2014 one axis, one owner."},{key:"ArrowRight / ArrowLeft",behavior:"Moves roving focus to the next/previous enabled option (no wrap) while `orientation` is `horizontal`. Inert on a vertical list, which is the default."},{key:"Home / End",behavior:"Moves roving focus to the first/last enabled option."},{key:"PageDown / PageUp",behavior:"Moves roving focus ten enabled options down/up, clamped at the ends. Ten is a literal count rather than a measured screenful: a package has no rendered row height to divide a scroll box by. The listbox pattern does not specify these two keys at all, and the alternative reading \u2014 aliasing them to Home/End, which is what the one reference documenting them does \u2014 would leave the user with no coarse move at all. Ten is what the combobox pattern makes normative for its listbox, so Select already jumps ten, and two components in the same family presenting the same list must answer the same key the same way."},{key:"Printable characters",behavior:"Typeahead: moves focus to the next option whose text starts with what was typed, matched against `textValue` when given. A 1s buffer accumulates characters into one search; repeating a single letter cycles through the options starting with it, as a native `<select>` does. Typeahead moves focus only \u2014 it never selects, since selection follows Enter/Space."},{key:"Enter or Space",behavior:"Selects the focused option \u2014 replacing the selection, or toggling it when `multiple`. Space is claimed by the option, so it never starts a typeahead search and never scrolls the list."},{key:"Shift+ArrowDown / Shift+ArrowUp",behavior:"While `multiple`: moves roving focus to the next/previous enabled option **and toggles that option**, so a run can be extended without leaving the keyboard. The toggle is literal, as APG states it \u2014 walking down selects each option arrived at, and walking back up deselects them again, which is how an overshot run is shrunk. Clamped like the plain arrows: at either end there is nothing to extend onto, so nothing moves and nothing is selected. While single-select these are the plain arrows, since a list with one selected option has no selection to extend."},{key:"Shift+Space",behavior:'While `multiple`: selects every enabled option between the selection anchor and the focused one, inclusive, in either direction \u2014 APG\'s "Selects contiguous items from the most recently selected item to the focused item." Additive: it selects what it spans and leaves the rest of the selection alone. The anchor is the option the user last acted on directly: every non-range commit (click, Enter, Space, and the Shift+Arrow toggle) moves it, whether that commit selected or deselected the option; a range commit deliberately leaves it where it is, so repeated Shift+Space grows one range from a fixed origin instead of dragging the origin along behind the focus; and losing the selection entirely drops it, since with nothing selected there is no "most recently selected item" to name \u2014 a Shift+Space then selects only the focused option. Unlike Enter/Space this key is read on the list rather than on the option, because a range needs the whole option list and the anchor, which only the list has. While single-select it is the plain Space the option claims.'},{key:"Control+Shift+Home / Control+Shift+End",behavior:'While `multiple`: selects every enabled option from the focused one to the first (Home) or last (End) option of the list, inclusive \u2014 APG\'s "Selects the focused option and all options up to the first option" and its downward twin. Additive, exactly like Shift+Space: the span is added to the selection rather than replacing it. Focus deliberately does not move \u2014 APG makes that half optional, and leaving it put is what keeps these two agreeing with Shift+Space, which also selects a span without moving. A range commit, so the selection anchor stays where the last non-range commit left it and a following Shift+Space still runs from there rather than from the end of the list. Inert while single-select.'},{key:"Control+A",behavior:"While `multiple` and `allowSelectAll`: selects every enabled option, and \u2014 once every enabled option is already selected \u2014 clears them again. The toggle is the optional half of the normative rule and the half that makes the key usable, since without it the only way back out of a select-all is to click every option. Disabled options are never swept in: they are not a selection target by any other route, and one added here could not be removed from the keyboard. Inert while single-select, and left entirely to the page when `allowSelectAll` is off."}],ariaAttributes:['aria-multiselectable="true" on the list while `multiple` (absent otherwise, never "false")','aria-orientation on the list, always present and always matching `orientation` \u2014 unlike aria-multiselectable this is not a presence flag, since "horizontal" and "vertical" are both real answers',"aria-disabled on the list and on each disabled option","aria-selected on every option, selected or not","aria-posinset and aria-setsize on an option that declares them, and on no other \u2014 the browser counts the DOM correctly for a list that is entirely present, and a partial or contradictory pair is worse than none","aria-labelledby on each Group, pointing at its Label"],focusManagement:'Real roving focus, not virtual: exactly one option carries tabindex="0" (the selected one, else the first enabled one) and every other carries tabindex="-1", so document.activeElement is always the option being announced. Focusing an option moves the tab stop onto it. Disabled options are never the tab stop and are skipped by the arrows. Deliberately unlike Combobox, which keeps DOM focus on its input and tracks the highlighted option with aria-activedescendant \u2014 there is no input here to keep focus in, which is exactly what the collection-focus rule turns on.',namingRequirement:'role="listbox" takes no accessible name from its content, so the consumer must supply one with aria-label or aria-labelledby. Nothing in the component can do this for it: unlike Select, there is no trigger to point at. Each Group is named by its own Label automatically.'},styling:{cssParts:["listbox","item","item-indicator","group","label"],dataAttributes:[...n,"data-multiple","data-orientation","data-disabled","data-state","data-value","data-text-value","data-density","data-shape","data-size",{name:"data-skdx-listbox-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[]};export{d as listboxContract};
|
|
1
|
+
import{changeDescription as i,changeDetail as e}from"./change-reason.js";import{SELECTION_ATTRIBUTE_RULE as t,SELECTION_INPUT_RULE as o,SELECTION_OUTPUT_RULE as a}from"./selection-value.js";import{ROOT_DATA_ATTRIBUTES as n}from"./styling-hooks.js";var d={name:"Listbox",purpose:"An always-visible list of selectable options with one tab stop, roving focus, typeahead, and single or multiple selection \u2014 the selection list a Select wraps in a popup, usable on its own. Given options and no hand-composed ListboxItem/ListboxGroup children, the root derives one ListboxItem per entry (grouped where options nests a group) instead of the consumer writing the loop.",props:[{name:"options",type:"ReadonlyArray<{ value: string; label?: string; disabled?: boolean } | { label: string; options: Array<{ value: string; label?: string; disabled?: boolean }> }>",required:!1,description:"Flat or grouped option data to render automatically as ListboxItems (and, for a group, a ListboxGroup/ListboxLabel pair), instead of hand composing them as children. Ignored when children declares its own ListboxItem or ListboxGroup. Property-only on <skdx-listbox> (@property({ attribute: false })): an HTML attribute cannot carry an array of objects."},{name:"renderOption",type:"(option: { value: string; label?: string; disabled?: boolean }, state: { selected: boolean }) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:'Web Components uses a named slot (`slot="option"`) since HTML attributes cannot carry functions. Vue uses a scoped slot (`#option`) instead of a prop. Angular uses a TemplateRef input.',description:"Customises how a derived option renders inside its ListboxItem. Receives the option and whether it is currently selected. Only read while options is deriving the anatomy."},{name:"value",type:"string | string[]",required:!1,description:`Selected option value(s), for controlled usage. Pair with a value-change event/emit/callback. ${o} ${a} ${t}`},{name:"defaultValue",type:"string | string[]",required:!1,description:`Initially selected value(s), for uncontrolled usage. Same shape as \`value\`. ${o} ${t}`},{name:"multiple",type:"boolean",defaultValue:"false",required:!1,description:'Allows more than one option to be selected. Sets `aria-multiselectable="true"`, makes choosing an option toggle it instead of replacing, and enables range-selection keys: Shift+ArrowDown/ArrowUp, Shift+Space, Control+Shift+Home/End, and Control+A.'},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables every option at once, exposed as aria-disabled/data-disabled on the list and on each option. An individual Item can still be disabled on its own; the two OR together."},{name:"allowSelectAll",type:"boolean",defaultValue:"true",required:!1,description:"Whether Control+A selects every enabled option while `multiple`, or clears if all are already selected. Inert while single-select. Default true (normative for multi-select lists); disable if Control+A should stay with the page (e.g. in an editor pane)."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'vertical'",required:!1,description:"Layout axis of the option list. Reflected as aria-orientation and data-orientation, and it is what picks the arrow-key pair: a vertical list roves on ArrowDown/ArrowUp, a horizontal one on ArrowRight/ArrowLeft. This can never be a skin-only attribute \u2014 it changes the keyboard, so it is a component property that the skin then reads for its flex-direction. Home/End, PageUp/PageDown, typeahead and every selection key are unaffected: they name positions in the list rather than directions on screen."},{name:"focusOnHover",type:"boolean",defaultValue:"false",required:!1,description:"Whether moving the pointer over an option moves roving focus onto it. Off by default since Listbox roves real DOM focus; hovering would move document.activeElement. Never moves focus onto a disabled option, never selects."},{name:"disallowEmptySelection",type:"boolean",defaultValue:"false",required:!1,description:"Whether the last selected option refuses to be deselected, so the list always holds at least one value once it holds any. Only reachable while `multiple`. Prevents deselecting via toggle, Control+A clear, or keyboard range operations. Never creates a selection \u2014 a list that starts empty stays empty until the user picks something."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."},{name:"shape",type:"'square' | 'rounded' | (string & {})",required:!1,description:"Corner rounding rung, written to data-shape on the root; the data-shape attribute may be set directly instead."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the root; the data-size attribute may be set directly instead."}],events:[{name:"value-change",description:`${i("the selection")} Carries the whole selection, not the single option that was toggled; selectedValue names that option. ${a}`,detail:e("string[]","selectedValue: string | null")},{name:"value-change-detail",frameworks:["angular"],exception:"event-shape",frameworksReason:"Angular's `valueChange` emits the scalar so `[(value)]` round-trips the value itself; this second output carries the detail object the other four hand to `value-change`.",description:"Fires with valueChange, carrying the previous selection, the change reason and the toggled option.",detail:e("string[]","selectedValue: string | null")}],parts:[{name:"Item",description:`One selectable option (role="option"), holding the list's single tab stop while it is the active one.`,props:[{name:"value",type:"string",required:!0,description:"Identifier committed to the root's value when this option is chosen."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Excludes this option from selection and from arrow/typeahead navigation. It stays in the DOM and keeps `aria-selected`, so it is still announced as part of the list."},{name:"textValue",type:"string",required:!1,description:"Text typeahead matches this option against, published as `data-text-value`. Required in practice whenever the option renders more than its label \u2014 an ItemIndicator glyph, an icon, a second line \u2014 because the fallback is the option's whole rendered text content, glyph included."},{name:"posInSet",type:"number | undefined",required:!1,description:`This option's 1-based position in the whole option set, emitted as aria-posinset. Only needed when the rendered options are a window onto a longer list \u2014 a virtualized listbox, or one that pages in more rows as the user scrolls \u2014 where a screen reader counting the DOM announces "3 of 20" for row 4,003 of a million. Absent by default, so the browser's own count stands for an ordinary list. This is the one piece of virtualized-listbox accessibility that belongs inside a package: the consumer owns the windowing, but only the option can carry the attribute.`},{name:"setSize",type:"number | undefined",required:!1,description:'Size of the whole option set this option belongs to, emitted as aria-setsize. Set together with `posInSet`: a position without a size announces "item 4 of an unknown number", which is worse than the DOM count it replaces.'}],slots:[{name:"default",description:"An Item's content \u2014 its label, and optionally an ItemIndicator."}]},{name:"ItemIndicator",description:"Selected-state glyph inside an Item, rendered only while that option is selected. `aria-hidden`: the option's `aria-selected` already carries the state.",props:[{name:"forceMount",type:"boolean",defaultValue:"false",required:!1,description:"Keep the indicator mounted even while its option isn't selected."}]},{name:"Group",description:'A labelled group of related Items (role="group"), pointing `aria-labelledby` at its own Label.'},{name:"Label",description:"The heading naming its Group, carrying the id the Group references. Not an option, and not focusable."}],slots:[{name:"default",description:"Listbox content \u2014 Item, Group, and Label parts. Given options and no ListboxItem/ListboxGroup here, the root derives this content itself instead."},{name:"option",frameworks:["vue","webComponents"],exception:"render-prop",frameworksReason:"vue, webComponents expose option as native content projection for the renderOption rendering capability. Other frameworks pass the documented renderOption render prop or template instead of a named slot with this spelling.",description:"Customises how a derived option renders inside its ListboxItem. Receives the option and whether it is currently selected. Only read while options is deriving the anatomy."},{name:"render-option",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-option as native content projection for the renderOption rendering capability. Other frameworks pass the documented renderOption render prop or template instead of a named slot with this spelling.",description:"Customises how a derived option renders inside its ListboxItem. Receives the option and whether it is currently selected. Only read while options is deriving the anatomy."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"listbox.css"},{attribute:"data-shape",values:["square","rounded"],stylesheet:"listbox.css"},{attribute:"data-size",values:["sm","lg"],stylesheet:"listbox.css"}],states:["disabled","multiple","selected","active"],controlledUncontrolled:"The selected value supports controlled (value + value-change) and uncontrolled (defaultValue) usage. There is no open state to control \u2014 the list is always visible. In Angular the pair is split: `valueChange` emits the scalar for `[(value)]`, `valueChangeDetail` carries the detail.",formBehavior:"None. No `name`/hidden-input form participation. Listbox is a picker whose result the consumer commits; wrap it in a Field and submit the value you hold.",validationBehavior:"None. No `required`/`invalid` flag or constraint validation.",accessibility:{role:"listbox (root); group (each Group); option (each Item)",keyboard:[{key:"Tab",behavior:"Moves into the list, landing on its single tab stop \u2014 the selected option, or the first enabled one \u2014 and back out again. Focus is never trapped."},{key:"ArrowDown / ArrowUp",behavior:"Moves roving focus to the next/previous enabled option (no wrap) while `orientation` is `vertical`. A `horizontal` list answers ArrowRight/ArrowLeft instead, and leaves the vertical pair to the page \u2014 one axis, one owner."},{key:"ArrowRight / ArrowLeft",behavior:"Moves roving focus to the next/previous enabled option (no wrap) while `orientation` is `horizontal`. Inert on a vertical list, which is the default."},{key:"Home / End",behavior:"Moves roving focus to the first/last enabled option."},{key:"PageDown / PageUp",behavior:"Moves roving focus ten enabled options down/up, clamped at the ends. Ten is a literal count rather than a measured screenful: a package has no rendered row height to divide a scroll box by. The listbox pattern does not specify these two keys at all, and the alternative reading \u2014 aliasing them to Home/End, which is what the one reference documenting them does \u2014 would leave the user with no coarse move at all. Ten is what the combobox pattern makes normative for its listbox, so Select already jumps ten, and two components in the same family presenting the same list must answer the same key the same way."},{key:"Printable characters",behavior:"Typeahead: moves focus to the next option whose text starts with what was typed, matched against `textValue` when given. A 1s buffer accumulates characters into one search; repeating a single letter cycles through the options starting with it, as a native `<select>` does. Typeahead moves focus only \u2014 it never selects, since selection follows Enter/Space."},{key:"Enter or Space",behavior:"Selects the focused option \u2014 replacing the selection, or toggling it when `multiple`. Space is claimed by the option, so it never starts a typeahead search and never scrolls the list."},{key:"Shift+ArrowDown / Shift+ArrowUp",behavior:"While `multiple`: moves roving focus to the next/previous enabled option **and toggles that option**, so a run can be extended without leaving the keyboard. The toggle is literal, as APG states it \u2014 walking down selects each option arrived at, and walking back up deselects them again, which is how an overshot run is shrunk. Clamped like the plain arrows: at either end there is nothing to extend onto, so nothing moves and nothing is selected. While single-select these are the plain arrows, since a list with one selected option has no selection to extend."},{key:"Shift+Space",behavior:'While `multiple`: selects every enabled option between the selection anchor and the focused one, inclusive, in either direction \u2014 APG\'s "Selects contiguous items from the most recently selected item to the focused item." Additive: it selects what it spans and leaves the rest of the selection alone. The anchor is the option the user last acted on directly: every non-range commit (click, Enter, Space, and the Shift+Arrow toggle) moves it, whether that commit selected or deselected the option; a range commit deliberately leaves it where it is, so repeated Shift+Space grows one range from a fixed origin instead of dragging the origin along behind the focus; and losing the selection entirely drops it, since with nothing selected there is no "most recently selected item" to name \u2014 a Shift+Space then selects only the focused option. Unlike Enter/Space this key is read on the list rather than on the option, because a range needs the whole option list and the anchor, which only the list has. While single-select it is the plain Space the option claims.'},{key:"Control+Shift+Home / Control+Shift+End",behavior:'While `multiple`: selects every enabled option from the focused one to the first (Home) or last (End) option of the list, inclusive \u2014 APG\'s "Selects the focused option and all options up to the first option" and its downward twin. Additive, exactly like Shift+Space: the span is added to the selection rather than replacing it. Focus deliberately does not move \u2014 APG makes that half optional, and leaving it put is what keeps these two agreeing with Shift+Space, which also selects a span without moving. A range commit, so the selection anchor stays where the last non-range commit left it and a following Shift+Space still runs from there rather than from the end of the list. Inert while single-select.'},{key:"Control+A",behavior:"While `multiple` and `allowSelectAll`: selects every enabled option, and \u2014 once every enabled option is already selected \u2014 clears them again. The toggle is the optional half of the normative rule and the half that makes the key usable, since without it the only way back out of a select-all is to click every option. Disabled options are never swept in: they are not a selection target by any other route, and one added here could not be removed from the keyboard. Inert while single-select, and left entirely to the page when `allowSelectAll` is off."}],ariaAttributes:['aria-multiselectable="true" on the list while `multiple` (absent otherwise, never "false")','aria-orientation on the list, always present and always matching `orientation` \u2014 unlike aria-multiselectable this is not a presence flag, since "horizontal" and "vertical" are both real answers',"aria-disabled on the list and on each disabled option","aria-selected on every option, selected or not","aria-posinset and aria-setsize on an option that declares them, and on no other \u2014 the browser counts the DOM correctly for a list that is entirely present, and a partial or contradictory pair is worse than none","aria-labelledby on each Group, pointing at its Label"],focusManagement:'Real roving focus, not virtual: exactly one option carries tabindex="0" (the selected one, else the first enabled one) and every other carries tabindex="-1", so document.activeElement is always the option being announced. Focusing an option moves the tab stop onto it. Disabled options are never the tab stop and are skipped by the arrows. Deliberately unlike Combobox, which keeps DOM focus on its input and tracks the highlighted option with aria-activedescendant \u2014 there is no input here to keep focus in, which is exactly what the collection-focus rule turns on.',namingRequirement:'role="listbox" takes no accessible name from its content, so the consumer must supply one with aria-label or aria-labelledby. Nothing in the component can do this for it: unlike Select, there is no trigger to point at. Each Group is named by its own Label automatically.'},styling:{cssParts:["listbox","item","item-indicator","group","label"],dataAttributes:[...n,"data-multiple","data-orientation","data-disabled","data-state","data-value","data-text-value","data-density","data-shape","data-size",{name:"data-skdx-listbox-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[]};export{d as listboxContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var n={name:"LoadingOverlay",purpose:"A busy-state overlay for a region or the whole viewport, with delay to suppress flash-of-spinner on fast responses and aria-busy on the covered content. Composed of root, Backdrop, and Content parts.",props:[{name:"visible",type:"boolean",required:!1,description:"Controlled busy state \u2014 whether work is in flight. Pair with the visible-change event."},{name:"fullScreen",type:"boolean",defaultValue:"false",required:!1,description:"Cover the viewport instead of the root's box (position: fixed; inset: 0) and lock body scroll while visible. An inline overlay never locks the body \u2014 freezing the whole page for a spinner over one card is a bug, not a feature."},{name:"loadedLabel",type:"string",required:!1,description:"Text announced when the overlay hides, closing the appearance announcement loop. Optional (default undefined) \u2014 announcing every completion on a page full of small loads would be excessive, so this is opt-in. When omitted the overlay still clears its message from the live region on hide, which lets two consecutive loads announce twice."},{name:"loadingLabel",type:"string",defaultValue:"'Loading'",required:!1,description:'Text announced when the overlay appears, and the default indicator label inside Content. Name the work ("Loading invoices") when more than one region can be busy.'},{name:"delay",type:"number",defaultValue:"0",required:!1,description:"Milliseconds to wait after visible turns true before rendering. Responses arriving inside the window never render an overlay at all, which stops the flash-of-spinner on fast requests. The timer cancels if visible flips back or on unmount. Default 0 (show immediately) so nothing is hidden from a slow connection."},{name:"emphasis",type:"'low' | 'medium' | 'high' | (string & {})",required:!1,description:"Scrim strength rung forwarded to the Backdrop's data-emphasis; the attribute may be written on Backdrop directly instead."}],events:[{name:"visible-change",detail:"boolean",description:"Fired when the overlay actually appears or disappears (after delay). Reports the effective transition rather than echoing the prop back, because the moment a delayed overlay paints is what the caller cannot derive. With delay 0 the two coincide."}],parts:[{name:"Backdrop",description:'The scrim layer, present only while visible. Decorative and aria-hidden \u2014 the busy state comes from aria-busy and the announcement, not the dimmed rectangle. A CSS exit transition on hide is React-only: React alone keeps the node painted with `data-state="hidden"` until its running animation ends, Vue and Svelte unmount the instant `visible` turns false, and Angular and Web Components hide the node with `hidden`, which resolves to `display: none` and cannot be transitioned. Every framework reports the effective transition through `visible-change`, so a caller that needs to sequence work against the hide has that signal in all five.',props:[{name:"emphasis",type:"'low' | 'medium' | 'high' | (string & {})",required:!1,description:"Scrim strength rung, written to data-emphasis on the backdrop; the data-emphasis attribute may be set directly instead."}]},{name:"Content",description:"The centered indicator layer, present only while visible on the same terms as Backdrop, exit transition included. It sits inside the inert wrapper, so nothing here is focusable or announced \u2014 announcements come from the root instead."}],slots:[{name:"default",description:"The covered content plus Backdrop and Content layers. It stays in the DOM and reading order \u2014 busy, not gone."}],appearance:[{attribute:"data-emphasis",values:["low","medium","high"],stylesheet:"loading-overlay.css"}],states:["hidden","visible","full-screen"],controlledUncontrolled:"Controlled. The caller owns visibility through visible and hears the effective transition through visible-change. There is nothing for a user to act on (no trigger, dismissal, or gesture), so uncontrolled mode has no event to change state from. The delay sits between the requested state and the rendered one, so parts mount against the effective visibility rather than the prop.",formBehavior:"Not applicable \u2014 LoadingOverlay participates in no form.",validationBehavior:"Not applicable \u2014 LoadingOverlay collects no value.",accessibility:{role:"none \u2014 the root is a generic element carrying aria-busy, not a landmark or a live region of its own",keyboard:[],ariaAttributes:["aria-busy on the root while visible, which tells assistive technology the content underneath is being updated. The root is never itself inert, or this attribute would be removed from the accessibility tree and the busy state would reach nobody","inert on the covered wrapper \u2014 never on the root \u2014 while visible, so the content and layers are out of the tab order and accessibility tree while the element naming the state stays in it","The loadingLabel is announced through the shared live announcer, deliberately rendered outside the inert subtree: a live region inside an inert subtree announces nothing. Announced after the delay, so finished work is never announced as started","aria-hidden on the Backdrop, which is decoration","loadedLabel when supplied is announced on the hide transition; when omitted the overlay clears its message from the shared region, so a second identical loadingLabel still registers as a mutation and is announced"],focusManagement:"Focus is neither moved nor trapped. The covered region is inert while visible, so focus cannot land inside it, but nothing is pulled away from wherever the user is: a busy region is not modal.",namingRequirement:'loadingLabel is the accessible name of the busy state. Default "Loading" is enough for one region; name the work when there is more than one, since identical announcements tell the user nothing about which finished.'},styling:{cssParts:["root","covered","backdrop","content"],dataAttributes:[...e,"data-state","data-full-screen","data-emphasis",{name:"data-skdx-loading-overlay-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[]};export{n as loadingOverlayContract};
|
|
1
|
+
import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var n={name:"LoadingOverlay",purpose:"A busy-state overlay for a region or the whole viewport, with delay to suppress flash-of-spinner on fast responses and aria-busy on the covered content. Composed of root, Backdrop, and Content parts.",props:[{name:"visible",type:"boolean",required:!1,description:"Controlled busy state \u2014 whether work is in flight. Pair with the visible-change event."},{name:"fullScreen",type:"boolean",defaultValue:"false",required:!1,description:"Cover the viewport instead of the root's box (position: fixed; inset: 0) and lock body scroll while visible. An inline overlay never locks the body \u2014 freezing the whole page for a spinner over one card is a bug, not a feature."},{name:"loadedLabel",type:"string",required:!1,description:"Text announced when the overlay hides, closing the appearance announcement loop. Optional (default undefined) \u2014 announcing every completion on a page full of small loads would be excessive, so this is opt-in. When omitted the overlay still clears its message from the live region on hide, which lets two consecutive loads announce twice."},{name:"loadingLabel",type:"string",defaultValue:"'Loading'",required:!1,description:'Text announced when the overlay appears, and the default indicator label inside Content. Name the work ("Loading invoices") when more than one region can be busy.'},{name:"delay",type:"number",defaultValue:"0",required:!1,description:"Milliseconds to wait after visible turns true before rendering. Responses arriving inside the window never render an overlay at all, which stops the flash-of-spinner on fast requests. The timer cancels if visible flips back or on unmount. Default 0 (show immediately) so nothing is hidden from a slow connection."},{name:"emphasis",type:"'low' | 'medium' | 'high' | (string & {})",required:!1,description:"Scrim strength rung forwarded to the Backdrop's data-emphasis; the attribute may be written on Backdrop directly instead."}],events:[{name:"visible-change",detail:"boolean",description:"Fired when the overlay actually appears or disappears (after delay). Reports the effective transition rather than echoing the prop back, because the moment a delayed overlay paints is what the caller cannot derive. With delay 0 the two coincide."}],parts:[{name:"Backdrop",description:'The scrim layer, present only while visible. Decorative and aria-hidden \u2014 the busy state comes from aria-busy and the announcement, not the dimmed rectangle. A CSS exit transition on hide is React-only: React alone keeps the node painted with `data-state="hidden"` until its running animation ends, Vue and Svelte unmount the instant `visible` turns false, and Angular and Web Components hide the node with `hidden`, which resolves to `display: none` and cannot be transitioned. Every framework reports the effective transition through `visible-change`, so a caller that needs to sequence work against the hide has that signal in all five.',props:[{name:"emphasis",type:"'low' | 'medium' | 'high' | (string & {})",required:!1,description:"Scrim strength rung, written to data-emphasis on the backdrop; the data-emphasis attribute may be set directly instead."}]},{name:"Content",description:"The centered indicator layer, present only while visible on the same terms as Backdrop, exit transition included. It sits inside the inert wrapper, so nothing here is focusable or announced \u2014 announcements come from the root instead."}],slots:[{name:"default",description:"The covered content plus Backdrop and Content layers. It stays in the DOM and reading order \u2014 busy, not gone."}],appearance:[{attribute:"data-emphasis",values:["low","medium","high"],stylesheet:"loading-overlay.css"}],states:["hidden","visible","full-screen"],controlledUncontrolled:"Controlled. The caller owns visibility through visible and hears the effective transition through visible-change. There is nothing for a user to act on (no trigger, dismissal, or gesture), so uncontrolled mode has no event to change state from. The delay sits between the requested state and the rendered one, so parts mount against the effective visibility rather than the prop.",formBehavior:"Not applicable \u2014 LoadingOverlay participates in no form.",validationBehavior:"Not applicable \u2014 LoadingOverlay collects no value.",accessibility:{role:"none \u2014 the root is a generic element carrying aria-busy, not a landmark or a live region of its own",keyboard:[],ariaAttributes:["aria-busy on the root while visible, which tells assistive technology the content underneath is being updated. The root is never itself inert, or this attribute would be removed from the accessibility tree and the busy state would reach nobody","inert on the covered wrapper \u2014 never on the root \u2014 while visible, so the content and layers are out of the tab order and accessibility tree while the element naming the state stays in it","The loadingLabel is announced through the shared live announcer, deliberately rendered outside the inert subtree: a live region inside an inert subtree announces nothing. Announced after the delay, so finished work is never announced as started","aria-hidden on the Backdrop, which is decoration","loadedLabel when supplied is announced on the hide transition; when omitted the overlay clears its message from the shared region, so a second identical loadingLabel still registers as a mutation and is announced"],focusManagement:"Focus is neither moved nor trapped. The covered region is inert while visible, so focus cannot land inside it, but nothing is pulled away from wherever the user is: a busy region is not modal.",namingRequirement:'loadingLabel is the accessible name of the busy state. Default "Loading" is enough for one region; name the work when there is more than one, since identical announcements tell the user nothing about which finished.'},styling:{cssParts:["root","covered","backdrop","content"],dataAttributes:[...e,"data-state","data-full-screen","data-emphasis",{name:"data-skdx-loading-overlay-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[],frameworkExtensions:[{framework:"webComponents",name:"effectiveVisible",type:"boolean",description:"The effective visibility \u2014 `visible` after `delay` has elapsed \u2014 that the parts render against. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings.",kind:"prop"},{framework:"webComponents",name:"effectiveVisible",type:"boolean",description:"The effective visibility \u2014 `visible` after `delay` has elapsed \u2014 that the parts render against. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings."}]};export{n as loadingOverlayContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changeDescription as n,changeDetail as e}from"./change-reason.js";import{constraintValidationMethods as i,setCustomValidityMethod as r}from"./form-validity.js";import{dismissalEvents as o,forceMountProp as t,openChangeCompleteEvent as s,popperContentProps as l,popperMeasurementProperties as d,portalPart as h,portalPropsSansForceMount as p}from"./overlay-parts.js";import{VIRTUAL_FOCUS_RULE as u}from"./rules.js";import{PART_DATA_ATTRIBUTES as c}from"./styling-hooks.js";var a="mention: { trigger: string; value: string; text: string; start: number; end: number } | null",w={name:"Mentions",purpose:`A multi-line text field where a trigger character such as "@" opens a floating listbox that filters a collection, and choosing an item replaces only the typed token rather than the whole value. Real DOM focus stays on the textarea for the entire interaction; the highlighted item is virtual focus (aria-activedescendant), and the popup is anchored to the caret rather than to the field. Unlike a Combobox, the component's value is the free text the user wrote and the query is a substring inside it.`,props:[{name:"value",type:"string",required:!1,description:"The field's full text, for controlled usage. Pair it with the value-change event. This is the whole textarea value, not the mention that was just picked; the picked item rides along in the event detail."},{name:"defaultValue",type:"string",defaultValue:"''",required:!1,description:"Initial text of the field, for uncontrolled usage, and the text a form reset restores."},{name:"triggers",type:"string | readonly string[]",defaultValue:"'@'",required:!1,description:`Characters that open a mention when typed at a word boundary. More than one is the common requirement \u2014 "@" for people beside "#" for issues \u2014 so a string is read one character at a time ("@#" configures both) and an array says the same thing where an attribute would be awkward. Each entry is exactly one character; multi-character triggers are not supported. The active character is reported on every query-change event, published as data-trigger, and matched against each Item's own trigger, so each trigger addresses its own collection without a second collection prop.`},{name:"options",type:"Array<{ value: string; label?: string; trigger?: string; disabled?: boolean }>",required:!1,description:"Declares the mentionable collection as data instead of hand-composed Item children, and the derived Content renders one Item per entry. An entry with no `trigger` belongs to the first configured trigger. The list is flat: mention popups are short and ranked, so there is no group form here. Property-only on `<skdx-mentions>` (`@property({ attribute: false })`): an array of objects has no attribute form."},{name:"filter",type:"'manual' | ((itemText: string, query: string) => boolean | number)",defaultValue:"'manual'",required:!1,description:"How Items are narrowed against the active query \u2014 the substring between the trigger character and the caret, never the whole field value. `'manual'` (default): the consumer renders only matching Items, which is what an async lookup keyed on query-change needs. A predicate moves filtering inside: every Item is measured by `textValue` or its rendered text, and rejected ones stop being options. Return a boolean to filter or a number to rank (0 excludes, positive keeps and orders by score). With `options` supplied and no `filter`, a case-insensitive match on the query narrows the derived list. On `<skdx-mentions>` this is property-only (`el.filter = \u2026`): attributes carry strings, not predicates."},{name:"serialize",type:"(item: { value: string; textValue: string; trigger: string }) => string",defaultValue:"(item) => `${item.trigger}${item.textValue}`",required:!1,description:'Builds the text that replaces the typed token when an item is chosen. The default writes the trigger character followed by the item\'s text, so picking Alice under "@ali" leaves "@Alice". Return anything the field should contain instead \u2014 a bare name, a wiki link, a "user:1234" token \u2014 and the same string reaches the value-change detail as `text`. Property-only on `<skdx-mentions>`: attributes cannot carry functions.'},{name:"appendSpace",type:"boolean",defaultValue:"true",required:!1,description:"Whether a single space follows an inserted mention so typing continues cleanly, skipped when the text already carries on with whitespace. Set false where a mention is normally followed by punctuation."},{name:"allowedSpaces",type:"number",defaultValue:"0",required:!1,description:"How many spaces a query may contain before the mention is abandoned. Zero means the first space ends it, which is what a handle needs; raise it for a collection whose labels are full names. A line break always ends a mention regardless of this number."},{name:"requireWordBoundary",type:"boolean",defaultValue:"true",required:!1,description:'Whether a trigger only opens a mention at the start of the value or directly after a non-word character, which is what keeps "user@example.com" from opening a popup mid-address. Set false to accept a trigger anywhere.'},{name:"open",type:"boolean",required:!1,description:"Controlled open state of the popup. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state of the popup, for uncontrolled usage."},{name:"autoHighlight",type:"boolean",defaultValue:"true",required:!1,description:"Keeps the first item highlighted whenever the filtered list changes while open, so Enter always has a target. On by default, the opposite of Combobox: there the draft text is itself a valid answer, while a mention popup exists only to be committed and an unhighlighted list would make Enter insert a line break in the middle of a half-typed handle."},{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Wraps arrow navigation around the ends of the item list. On by default; set false to clamp at the ends."},{name:"loading",type:"boolean",defaultValue:"false",required:!1,description:'Marks items as in flight, exposed as `aria-busy="true"` plus `data-loading` on Input and Content, and suppresses the result-count announcement \u2014 a count measured mid-fetch would announce "0 results available" for every keystroke.'},{name:"autoResize",type:"boolean",defaultValue:"false",required:!1,description:"Grows the field to fit its content instead of showing a scrollbar, on the same measurement Textarea uses. A scrolling field moves the caret under the popup, so a long message reads better with this on."},{name:"minRows",type:"number",defaultValue:"2",required:!1,description:"The field's opening size, and the floor auto-resize will not shrink below. Rendered as the native rows attribute when autoResize is off, and applied as a minimum on the measured height when it is on, so the same number means the same thing either way."},{name:"maxRows",type:"number",required:!1,description:"Caps auto-resize growth to this many rows, based on computed line-height. Where minRows and maxRows disagree the cap wins, matching Textarea."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables the field so no mention can ever open. Distinct from readOnly, which still allows focus and text selection."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Makes the field read-only, exposed as the native readonly attribute plus aria-readonly/data-readonly. Trigger detection is suspended, since nothing can be inserted."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field as invalid, exposed as aria-invalid and data-invalid on the Input."},{name:"name",type:"string",required:!1,description:"Form field name. The full text of the field is submitted under it; without a name the component submits nothing at all."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Id of the `<form>` this field submits into, so it can live outside that form's subtree. Web Components names this `formId`, not `form`: the element already publishes a readonly `form` accessor."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field required for native constraint validation: valueMissing while the text is empty. It says nothing about mentions \u2014 a message with no mention in it is still a valid message."},{name:"getResultCountLabel",type:"(count: number) => string",defaultValue:"defaultGetResultCountLabel",required:!1,description:'Formats the message announced through the shared LiveAnnouncer whenever the rendered item count changes while the popup is open. Return an empty string to announce nothing. The default singularises: "1 result available" for a count of one, "3 results available" otherwise. Property-only on `<skdx-mentions>`, like `filter` and `serialize`.'},{name:"placeholder",type:"string",required:!1,description:"Placeholder text forwarded to the derived Input. Ignored under full manual composition, where the Input is written by hand."},{name:"renderOption",type:"(option: MentionsOptionData, state: { highlighted: boolean }) => ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:'Web Components uses a named light-DOM slot (`slot="option"`, keyed by `data-value`), the platform-native form of the hook, and Vue uses a scoped slot (`#option`). Both are that framework\'s own idiom for the same customisation, and neither is visible to a prop extractor.',description:"Custom rendering for an `options`-derived Item \u2014 an avatar beside a handle, a secondary line of context. Angular takes a `TemplateRef` instead of a function. Ignored for hand-composed Item children."},{name:"aria-label",type:"string",required:!1,frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"React forwards `aria-label` literally; Vue and Svelte declare it as `ariaLabel`, which the extractor folds back; Angular takes `ariaLabel`. The derived name resolves to `aria-label ?? placeholder` in all five. Web Components declares no property at all \u2014 it claims the native attribute off the host and relocates it onto the derived control, which is the platform mechanism for the same capability.",description:"Convenience accessible name for the derived Input, and for Content, which takes no name from its own contents. Equivalent to naming Input directly under manual composition."},{name:"emptyMessage",type:"string",required:!1,frameworks:["angular","svelte"],exception:"derived-anatomy",frameworksReason:"Angular and Svelte fold the Empty part into their derived anatomy behind this prop. React, Vue and Web Components publish the same part for the consumer to place by hand, so the prop would only restate what writing the part already says.",description:"Content for the derived Empty part, shown when the query matches no items. Where this prop is absent the package renders a hardcoded placeholder string instead."}],events:[{name:"value-change",description:`${n("the field's text")} Carries the whole field value, never the mention alone; \`mention\` names the item that was just inserted and the range it replaced, and is null for an ordinary keystroke.`,detail:e("string",a)},{name:"value-change-detail",frameworks:["angular"],exception:"event-shape",frameworksReason:"Angular's `valueChange` emits the scalar so `[(value)]` round-trips the text itself; this second output carries the detail object the other four hand to `value-change`.",description:"Fires with valueChange, carrying the previous text, the change reason and the inserted mention.",detail:e("string",a)},{name:"query-change",detail:"{ trigger: string; query: string; start: number; end: number } | null",description:"Fires whenever the token under the caret changes, carrying the active trigger, the query substring and the offsets the token occupies \u2014 and null once no mention is being typed. This is the hook an async collection is loaded from: there is no `query` prop, because the query is derived from the text and the caret together and cannot be set without also placing the caret."},{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},{name:"highlight-change",detail:"{ value: string | null; previousValue: string | null; reason: 'keyboard' | 'pointer' | 'auto' }",description:"Fires whenever virtual focus moves, carrying the highlighted item value, the value it replaced, and what moved it. Read-only: no `highlight` or `defaultHighlight` prop exists, since a programmatic highlight could name an unrendered item and make aria-activedescendant point at nothing. `value` is the item's value, never its element id. `reason` is not a ChangeReason: `'auto'` covers filtering, re-seeding and the highlight `autoHighlight` assigns on open."},s("popup"),...o("popup")],parts:[{name:"Input",lightDom:!0,description:'The multi-line text field the mentions are typed into \u2014 a real `<textarea>`, so wrapping, line breaks, autosize and the native form value are the platform\'s. Keeps real DOM focus for the whole interaction and carries aria-haspopup="listbox" with aria-controls, aria-autocomplete="list" and the aria-activedescendant naming the highlighted item. HTML permits no role attribute on a textarea, so the field keeps its native textbox role and the popup relationship is announced through haspopup; the field\'s value stays the free text the user wrote.'},{...h,props:[...p,{...t,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components never unmounts a closed overlay (it only hides it), so forceMount has no unmount to prevent. Angular ships no Portal part on any overlay family \u2014 it portals with the [skdxPortal] attribute directive instead."}]},{name:"Content",description:'The floating listbox (role="listbox") of mentionable items, positioned below and start-aligned. Its anchor is the caret rather than the field: each adapter measures the caret rectangle and positions Content against a zero-width virtual anchor standing on it, so the popup follows the mention through wrapped lines instead of parking under the whole textarea. One consequence for skins: `--skdx-mentions-trigger-width` measures that anchor and is therefore always 0, so size Content from `--skdx-mentions-available-width` rather than matching the anchor as a dropdown would.',props:[...l({align:"'start'",sideOffset:"4"}),{...t,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components never unmounts a closed overlay (it only hides it). Angular keeps this part in the tree and toggles it with [hidden], so neither ships forceMount."}]},{name:"Item",description:'One mentionable entry (role="option"). Highlighted virtually, never focused, so the caret stays exactly where the user left it. An Item whose trigger differs from the active one is not an option at all: unreachable through aria-activedescendant, uncounted in the announcement, and invisible to the Empty part\'s emptiness check \u2014 the same treatment a rejected `filter` result gets. Removed from the DOM in React/Vue/Svelte/Angular; hidden in Web Components, which cannot un-author consumer tags.',props:[{name:"value",type:"string",required:!0,description:"Identifier for this entry, published as data-value and carried on the value-change detail when the entry is inserted. It is not what gets written into the text; `serialize` decides that."},{name:"trigger",type:"string",required:!1,description:"Which trigger character this entry belongs to, so one field can address several collections. Defaults to the first configured trigger, which makes the prop unnecessary for a single-trigger field."},{name:"textValue",type:"string",required:!1,description:"Text the `filter` predicate matches and the default `serialize` writes into the field. Falls back to the entry's rendered text, which is wrong only when the entry renders more than its label \u2014 an avatar, a secondary line \u2014 so supply it then."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Excludes this entry from insertion and from pointer or keyboard highlight; exposed as aria-disabled and data-disabled."}]},{name:"Empty",description:"Shown in place of the item list when the active query matches nothing \u2014 because the consumer rendered no Items for it, or because the `filter` predicate rejected every one."}],slots:[{name:"default",description:"Content's body \u2014 the Item entries and an Empty state."}],states:["closed","open","highlighted","disabled","readonly","invalid","loading"],controlledUncontrolled:"value and open are independent controlled/uncontrolled axes, each starting from its default* prop and switching to controlled when the matching prop is passed. The active token is neither: it is derived from the text and the caret on every input, selection and composition-end event, so there is no query prop to control and no token state to go stale. In Angular the pair is split: `valueChange` emits the scalar for `[(value)]`, `valueChangeDetail` carries the detail.",formBehavior:"Form-associated: with a name, the field's full text is submitted under it and a required field with empty text is valueMissing. React, Vue, Svelte and Angular stamp the name on the real `<textarea>` the Input renders, so submission and validation are the platform's own; `<skdx-mentions>` keeps its textarea in a shadow root and is form-associated through ElementInternals, publishing the same value and validity. A form reset restores `defaultValue` \u2014 falling back to the empty string \u2014 in all five packages, and closes any open popup. Mentions are submitted as the text `serialize` produced; there is no second hidden control carrying a structured list.",validationBehavior:"invalid is presentational only (aria-invalid/data-invalid); required, minlength and maxlength participate in native constraint validation on the underlying textarea, reached through checkValidity()/reportValidity(). No membership rule exists and none is planned: text around a mention is the normal case here, and an unresolved handle is a consumer-side concern rather than a constraint the component can state. A field marked invalid must also be pointed at the text explaining why, through aria-describedby; wrapping it in a Field does that wiring automatically.",accessibility:{role:"textbox (Input, the native <textarea>), listbox (Content), option (Item), presentation (Empty)",keyboard:[{key:"Typing",behavior:"Ordinary text entry. Typing a trigger character at a word boundary opens the popup with an empty query; typing on narrows it; typing a space, a line break or a second trigger ends the mention and closes the popup."},{key:"Typing (IME composition)",behavior:"Trigger detection is suspended between compositionstart and compositionend and runs once on the composition-end value. Detecting mid-composition would read the half-formed romaji or pinyin buffer as a query, reopen and re-filter the popup on every intermediate keystroke, and let Enter \u2014 which commits the composition \u2014 insert a mention the user never chose. Enter, ArrowUp and ArrowDown are left entirely to the IME while a composition is active."},{key:"ArrowDown / ArrowUp",behavior:'While the popup is open, moves the highlight by one and prevents the default, wrapping around the ends unless `loop` is false. While it is closed neither key is intercepted: in a multi-line field they are the caret\'s line keys, and stealing them would strand a keyboard user in the first line. Neither key opens the popup, which is the difference from Combobox \u2014 there is no "show me the list" gesture when the list belongs to a token that has not been typed yet.'},{key:"Home / End / PageUp / PageDown",behavior:"Never intercepted, open or closed. These are the caret keys of a multi-line field and a user needs them far more than a jump to the ends of a short mention list, which arrow keys reach in a few presses anyway."},{key:"Enter",behavior:"While the popup is open with an item highlighted, inserts that mention and prevents the default, so no line break is added. With the popup closed, or open with nothing highlighted, it is the textarea's own Enter and inserts a line break."},{key:"Tab",behavior:"While the popup is open with an item highlighted, commits that mention and prevents the default \u2014 the gesture every mention field ships. Otherwise Tab is untouched and moves focus, and Escape always closes the popup first, so the field is never a keyboard trap."},{key:"Escape",behavior:"Closes the popup and leaves the text and the caret exactly as they are. The dismissed token stays closed while it is being edited, so continuing to type does not reopen it; the next trigger character opens a fresh one."},{key:"Backspace",behavior:"Ordinary deletion. Deleting back past the trigger character ends the mention and closes the popup; an inserted mention is plain text and is deleted one character at a time, not as an atom."}],ariaAttributes:['aria-haspopup="listbox" with aria-controls and aria-autocomplete="list" on the Input, which is a real <textarea>; HTML permits no role attribute there, so the field keeps its native textbox role and the popup is announced through haspopup rather than aria-expanded',"aria-activedescendant on the Input naming the highlighted item \u2014 the highlight is virtual, so DOM focus never leaves the field and the caret never moves","Content's id matches the Input's aria-controls, which resolves exactly while the popup is open and is absent while it is closed, since an IDREF resolving to nothing is worse than no IDREF","data-trigger on Content and on the Input naming the trigger character of the active token, and on every Item naming the trigger it belongs to",'aria-busy="true" and data-loading on Input and Content while `loading`',"data-highlighted on the item holding virtual focus, and aria-disabled with data-disabled on a disabled item, which highlight and insertion both skip","the number of rendered items is announced through the shared LiveAnnouncer polite region while the popup is open, formatted by getResultCountLabel \u2014 filtering is otherwise silent to assistive technology \u2014 and suppressed entirely while `loading`","aria-invalid and aria-readonly on the Input, mirroring the matching props, with aria-describedby carrying the text that explains an invalid field",'the highlighted item is scrolled into view on every keyboard move with scrollIntoView({ block: "nearest" }); a highlight assigned while the popup is opening scrolls the popup itself, since floating-ui renders at the document origin before measuring'],focusManagement:`${u} Here the anchor is the Input, a textarea whose caret offset is part of the interaction: the popup must never take focus, and pointer interactions inside Content cancel their own mousedown so both DOM focus and the text selection survive a click. When filtering removes the highlighted item the highlight moves to the first survivor rather than disappearing, and aria-activedescendant is dropped only when no item is left. Inserting a mention puts the caret just past the inserted text \u2014 past the appended space where one was added \u2014 and the adapter sets it in the same task as the value write, so the field never flashes the caret at the end of the value. Blur closes the popup and inserts nothing: an unconfirmed mention is just the text the user typed.`,namingRequirement:"The Input must be named by a <label>, aria-label, or aria-labelledby; a textbox takes no name from its content, and Content is a sibling rather than an ancestor. Wrapping the field in the Field family satisfies this automatically. A placeholder is not a name \u2014 it disappears as soon as the field has text."},styling:{cssParts:["input","content","item","empty"],dataAttributes:[...c,"data-state","data-side","data-align","data-trigger","data-highlighted","data-disabled","data-readonly","data-invalid","data-loading","data-value",{name:"data-skdx-mentions-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:d("mentions")},publicMethods:[...i,r,{name:"focus",signature:"(): void",description:"Moves keyboard focus to the text field, which is where this component keeps real DOM focus.",frameworks:["vue","webComponents"],exception:"element-handoff",frameworksReason:"Vue hides the Input inside the root component instance, and the custom element forwards focus() to its shadow textarea. React, Svelte and Angular hand the consumer the Input element itself."}]};export{w as mentionsContract};
|
|
1
|
+
import{changeDescription as n,changeDetail as e}from"./change-reason.js";import{constraintValidationMethods as i,setCustomValidityMethod as r}from"./form-validity.js";import{dismissalEvents as o,forceMountProp as t,openChangeCompleteEvent as s,popperContentProps as l,popperMeasurementProperties as d,portalPart as h,portalPropsSansForceMount as p}from"./overlay-parts.js";import{VIRTUAL_FOCUS_RULE as u}from"./rules.js";import{PART_DATA_ATTRIBUTES as c}from"./styling-hooks.js";var a="mention: { trigger: string; value: string; text: string; start: number; end: number } | null",w={name:"Mentions",purpose:`A multi-line text field where a trigger character such as "@" opens a floating listbox that filters a collection, and choosing an item replaces only the typed token rather than the whole value. Real DOM focus stays on the textarea for the entire interaction; the highlighted item is virtual focus (aria-activedescendant), and the popup is anchored to the caret rather than to the field. Unlike a Combobox, the component's value is the free text the user wrote and the query is a substring inside it.`,props:[{name:"value",type:"string",required:!1,description:"The field's full text, for controlled usage. Pair it with the value-change event. This is the whole textarea value, not the mention that was just picked; the picked item rides along in the event detail."},{name:"defaultValue",type:"string",defaultValue:"''",required:!1,description:"Initial text of the field, for uncontrolled usage, and the text a form reset restores."},{name:"triggers",type:"string | readonly string[]",defaultValue:"'@'",required:!1,description:`Characters that open a mention when typed at a word boundary. More than one is the common requirement \u2014 "@" for people beside "#" for issues \u2014 so a string is read one character at a time ("@#" configures both) and an array says the same thing where an attribute would be awkward. Each entry is exactly one character; multi-character triggers are not supported. The active character is reported on every query-change event, published as data-trigger, and matched against each Item's own trigger, so each trigger addresses its own collection without a second collection prop.`},{name:"options",type:"Array<{ value: string; label?: string; trigger?: string; disabled?: boolean }>",required:!1,description:"Declares the mentionable collection as data instead of hand-composed Item children, and the derived Content renders one Item per entry. An entry with no `trigger` belongs to the first configured trigger. The list is flat: mention popups are short and ranked, so there is no group form here. Property-only on `<skdx-mentions>` (`@property({ attribute: false })`): an array of objects has no attribute form."},{name:"filter",type:"'manual' | ((itemText: string, query: string) => boolean | number)",defaultValue:"'manual'",required:!1,description:"How Items are narrowed against the active query \u2014 the substring between the trigger character and the caret, never the whole field value. `'manual'` (default): the consumer renders only matching Items, which is what an async lookup keyed on query-change needs. A predicate moves filtering inside: every Item is measured by `textValue` or its rendered text, and rejected ones stop being options. Return a boolean to filter or a number to rank (0 excludes, positive keeps and orders by score). With `options` supplied and no `filter`, a case-insensitive match on the query narrows the derived list. On `<skdx-mentions>` this is property-only (`el.filter = \u2026`): attributes carry strings, not predicates."},{name:"serialize",type:"(item: { value: string; textValue: string; trigger: string }) => string",defaultValue:"(item) => `${item.trigger}${item.textValue}`",required:!1,description:'Builds the text that replaces the typed token when an item is chosen. The default writes the trigger character followed by the item\'s text, so picking Alice under "@ali" leaves "@Alice". Return anything the field should contain instead \u2014 a bare name, a wiki link, a "user:1234" token \u2014 and the same string reaches the value-change detail as `text`. Property-only on `<skdx-mentions>`: attributes cannot carry functions.'},{name:"appendSpace",type:"boolean",defaultValue:"true",required:!1,description:"Whether a single space follows an inserted mention so typing continues cleanly, skipped when the text already carries on with whitespace. Set false where a mention is normally followed by punctuation."},{name:"allowedSpaces",type:"number",defaultValue:"0",required:!1,description:"How many spaces a query may contain before the mention is abandoned. Zero means the first space ends it, which is what a handle needs; raise it for a collection whose labels are full names. A line break always ends a mention regardless of this number."},{name:"requireWordBoundary",type:"boolean",defaultValue:"true",required:!1,description:'Whether a trigger only opens a mention at the start of the value or directly after a non-word character, which is what keeps "user@example.com" from opening a popup mid-address. Set false to accept a trigger anywhere.'},{name:"open",type:"boolean",required:!1,description:"Controlled open state of the popup. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state of the popup, for uncontrolled usage."},{name:"autoHighlight",type:"boolean",defaultValue:"true",required:!1,description:"Keeps the first item highlighted whenever the filtered list changes while open, so Enter always has a target. On by default, the opposite of Combobox: there the draft text is itself a valid answer, while a mention popup exists only to be committed and an unhighlighted list would make Enter insert a line break in the middle of a half-typed handle."},{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Wraps arrow navigation around the ends of the item list. On by default; set false to clamp at the ends."},{name:"loading",type:"boolean",defaultValue:"false",required:!1,description:'Marks items as in flight, exposed as `aria-busy="true"` plus `data-loading` on Input and Content, and suppresses the result-count announcement \u2014 a count measured mid-fetch would announce "0 results available" for every keystroke.'},{name:"autoResize",type:"boolean",defaultValue:"false",required:!1,description:"Grows the field to fit its content instead of showing a scrollbar, on the same measurement Textarea uses. A scrolling field moves the caret under the popup, so a long message reads better with this on."},{name:"minRows",type:"number",defaultValue:"2",required:!1,description:"The field's opening size, and the floor auto-resize will not shrink below. Rendered as the native rows attribute when autoResize is off, and applied as a minimum on the measured height when it is on, so the same number means the same thing either way."},{name:"maxRows",type:"number",required:!1,description:"Caps auto-resize growth to this many rows, based on computed line-height. Where minRows and maxRows disagree the cap wins, matching Textarea."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables the field so no mention can ever open. Distinct from readOnly, which still allows focus and text selection."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Makes the field read-only, exposed as the native readonly attribute plus aria-readonly/data-readonly. Trigger detection is suspended, since nothing can be inserted."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field as invalid, exposed as aria-invalid and data-invalid on the Input."},{name:"name",type:"string",required:!1,description:"Form field name. The full text of the field is submitted under it; without a name the component submits nothing at all."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Id of the `<form>` this field submits into, so it can live outside that form's subtree. Web Components names this `formId`, not `form`: the element already publishes a readonly `form` accessor."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field required for native constraint validation: valueMissing while the text is empty. It says nothing about mentions \u2014 a message with no mention in it is still a valid message."},{name:"getResultCountLabel",type:"(count: number) => string",defaultValue:"defaultGetResultCountLabel",required:!1,description:'Formats the message announced through the shared LiveAnnouncer whenever the rendered item count changes while the popup is open. Return an empty string to announce nothing. The default singularises: "1 result available" for a count of one, "3 results available" otherwise. Property-only on `<skdx-mentions>`, like `filter` and `serialize`.'},{name:"placeholder",type:"string",required:!1,description:"Placeholder text forwarded to the derived Input. Ignored under full manual composition, where the Input is written by hand."},{name:"renderOption",type:"(option: MentionsOptionData, state: { highlighted: boolean }) => ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:'Web Components uses a named light-DOM slot (`slot="option"`, keyed by `data-value`), the platform-native form of the hook, and Vue uses a scoped slot (`#option`). Both are that framework\'s own idiom for the same customisation, and neither is visible to a prop extractor.',description:"Custom rendering for an `options`-derived Item \u2014 an avatar beside a handle, a secondary line of context. Angular takes a `TemplateRef` instead of a function. Ignored for hand-composed Item children."},{name:"aria-label",type:"string",required:!1,frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"React forwards `aria-label` literally; Vue and Svelte declare it as `ariaLabel`, which the extractor folds back; Angular takes `ariaLabel`. The derived name resolves to `aria-label ?? placeholder` in all five. Web Components declares no property at all \u2014 it claims the native attribute off the host and relocates it onto the derived control, which is the platform mechanism for the same capability.",description:"Convenience accessible name for the derived Input, and for Content, which takes no name from its own contents. Equivalent to naming Input directly under manual composition."},{name:"emptyMessage",type:"string",required:!1,frameworks:["angular","svelte"],exception:"derived-anatomy",frameworksReason:"Angular and Svelte fold the Empty part into their derived anatomy behind this prop. React, Vue and Web Components publish the same part for the consumer to place by hand, so the prop would only restate what writing the part already says.",description:"Content for the derived Empty part, shown when the query matches no items. Where this prop is absent the package renders a hardcoded placeholder string instead."}],events:[{name:"value-change",description:`${n("the field's text")} Carries the whole field value, never the mention alone; \`mention\` names the item that was just inserted and the range it replaced, and is null for an ordinary keystroke.`,detail:e("string",a)},{name:"value-change-detail",frameworks:["angular"],exception:"event-shape",frameworksReason:"Angular's `valueChange` emits the scalar so `[(value)]` round-trips the text itself; this second output carries the detail object the other four hand to `value-change`.",description:"Fires with valueChange, carrying the previous text, the change reason and the inserted mention.",detail:e("string",a)},{name:"query-change",detail:"{ trigger: string; query: string; start: number; end: number } | null",description:"Fires whenever the token under the caret changes, carrying the active trigger, the query substring and the offsets the token occupies \u2014 and null once no mention is being typed. This is the hook an async collection is loaded from: there is no `query` prop, because the query is derived from the text and the caret together and cannot be set without also placing the caret."},{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},{name:"highlight-change",detail:"{ value: string | null; previousValue: string | null; reason: 'keyboard' | 'pointer' | 'auto' }",description:"Fires whenever virtual focus moves, carrying the highlighted item value, the value it replaced, and what moved it. Read-only: no `highlight` or `defaultHighlight` prop exists, since a programmatic highlight could name an unrendered item and make aria-activedescendant point at nothing. `value` is the item's value, never its element id. `reason` is not a ChangeReason: `'auto'` covers filtering, re-seeding and the highlight `autoHighlight` assigns on open."},s("popup"),...o("popup")],parts:[{name:"Input",lightDom:!0,description:'The multi-line text field the mentions are typed into \u2014 a real `<textarea>`, so wrapping, line breaks, autosize and the native form value are the platform\'s. Keeps real DOM focus for the whole interaction and carries aria-haspopup="listbox" with aria-controls, aria-autocomplete="list" and the aria-activedescendant naming the highlighted item. HTML permits no role attribute on a textarea, so the field keeps its native textbox role and the popup relationship is announced through haspopup; the field\'s value stays the free text the user wrote.'},{...h,props:[...p,{...t,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components never unmounts a closed overlay (it only hides it), so forceMount has no unmount to prevent. Angular ships no Portal part on any overlay family \u2014 it portals with the [skdxPortal] attribute directive instead."}]},{name:"Content",description:'The floating listbox (role="listbox") of mentionable items, positioned below and start-aligned. Its anchor is the caret rather than the field: each adapter measures the caret rectangle and positions Content against a zero-width virtual anchor standing on it, so the popup follows the mention through wrapped lines instead of parking under the whole textarea. One consequence for skins: `--skdx-mentions-trigger-width` measures that anchor and is therefore always 0, so size Content from `--skdx-mentions-available-width` rather than matching the anchor as a dropdown would.',props:[...l({align:"'start'",sideOffset:"4"}),{...t,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components never unmounts a closed overlay (it only hides it). Angular keeps this part in the tree and toggles it with [hidden], so neither ships forceMount."}]},{name:"Item",description:'One mentionable entry (role="option"). Highlighted virtually, never focused, so the caret stays exactly where the user left it. An Item whose trigger differs from the active one is not an option at all: unreachable through aria-activedescendant, uncounted in the announcement, and invisible to the Empty part\'s emptiness check \u2014 the same treatment a rejected `filter` result gets. Removed from the DOM in React/Vue/Svelte/Angular; hidden in Web Components, which cannot un-author consumer tags.',props:[{name:"value",type:"string",required:!0,description:"Identifier for this entry, published as data-value and carried on the value-change detail when the entry is inserted. It is not what gets written into the text; `serialize` decides that."},{name:"trigger",type:"string",required:!1,description:"Which trigger character this entry belongs to, so one field can address several collections. Defaults to the first configured trigger, which makes the prop unnecessary for a single-trigger field."},{name:"textValue",type:"string",required:!1,description:"Text the `filter` predicate matches and the default `serialize` writes into the field. Falls back to the entry's rendered text, which is wrong only when the entry renders more than its label \u2014 an avatar, a secondary line \u2014 so supply it then."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Excludes this entry from insertion and from pointer or keyboard highlight; exposed as aria-disabled and data-disabled."}]},{name:"Empty",description:"Shown in place of the item list when the active query matches nothing \u2014 because the consumer rendered no Items for it, or because the `filter` predicate rejected every one."}],slots:[{name:"default",description:"Content's body \u2014 the Item entries and an Empty state."},{name:"option",frameworks:["vue","webComponents"],exception:"render-prop",frameworksReason:"vue, webComponents expose option as native content projection for the renderOption rendering capability. Other frameworks pass the documented renderOption render prop or template instead of a named slot with this spelling.",description:"Custom rendering for an `options`-derived Item \u2014 an avatar beside a handle, a secondary line of context. Angular takes a `TemplateRef` instead of a function. Ignored for hand-composed Item children."},{name:"render-option",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-option as native content projection for the renderOption rendering capability. Other frameworks pass the documented renderOption render prop or template instead of a named slot with this spelling.",description:"Custom rendering for an `options`-derived Item \u2014 an avatar beside a handle, a secondary line of context. Angular takes a `TemplateRef` instead of a function. Ignored for hand-composed Item children."}],states:["closed","open","highlighted","disabled","readonly","invalid","loading"],controlledUncontrolled:"value and open are independent controlled/uncontrolled axes, each starting from its default* prop and switching to controlled when the matching prop is passed. The active token is neither: it is derived from the text and the caret on every input, selection and composition-end event, so there is no query prop to control and no token state to go stale. In Angular the pair is split: `valueChange` emits the scalar for `[(value)]`, `valueChangeDetail` carries the detail.",formBehavior:"Form-associated: with a name, the field's full text is submitted under it and a required field with empty text is valueMissing. React, Vue, Svelte and Angular stamp the name on the real `<textarea>` the Input renders, so submission and validation are the platform's own; `<skdx-mentions>` keeps its textarea in a shadow root and is form-associated through ElementInternals, publishing the same value and validity. A form reset restores `defaultValue` \u2014 falling back to the empty string \u2014 in all five packages, and closes any open popup. Mentions are submitted as the text `serialize` produced; there is no second hidden control carrying a structured list.",validationBehavior:"invalid is presentational only (aria-invalid/data-invalid); required, minlength and maxlength participate in native constraint validation on the underlying textarea, reached through checkValidity()/reportValidity(). No membership rule exists and none is planned: text around a mention is the normal case here, and an unresolved handle is a consumer-side concern rather than a constraint the component can state. A field marked invalid must also be pointed at the text explaining why, through aria-describedby; wrapping it in a Field does that wiring automatically.",accessibility:{role:"textbox (Input, the native <textarea>), listbox (Content), option (Item), presentation (Empty)",keyboard:[{key:"Typing",behavior:"Ordinary text entry. Typing a trigger character at a word boundary opens the popup with an empty query; typing on narrows it; typing a space, a line break or a second trigger ends the mention and closes the popup."},{key:"Typing (IME composition)",behavior:"Trigger detection is suspended between compositionstart and compositionend and runs once on the composition-end value. Detecting mid-composition would read the half-formed romaji or pinyin buffer as a query, reopen and re-filter the popup on every intermediate keystroke, and let Enter \u2014 which commits the composition \u2014 insert a mention the user never chose. Enter, ArrowUp and ArrowDown are left entirely to the IME while a composition is active."},{key:"ArrowDown / ArrowUp",behavior:'While the popup is open, moves the highlight by one and prevents the default, wrapping around the ends unless `loop` is false. While it is closed neither key is intercepted: in a multi-line field they are the caret\'s line keys, and stealing them would strand a keyboard user in the first line. Neither key opens the popup, which is the difference from Combobox \u2014 there is no "show me the list" gesture when the list belongs to a token that has not been typed yet.'},{key:"Home / End / PageUp / PageDown",behavior:"Never intercepted, open or closed. These are the caret keys of a multi-line field and a user needs them far more than a jump to the ends of a short mention list, which arrow keys reach in a few presses anyway."},{key:"Enter",behavior:"While the popup is open with an item highlighted, inserts that mention and prevents the default, so no line break is added. With the popup closed, or open with nothing highlighted, it is the textarea's own Enter and inserts a line break."},{key:"Tab",behavior:"While the popup is open with an item highlighted, commits that mention and prevents the default \u2014 the gesture every mention field ships. Otherwise Tab is untouched and moves focus, and Escape always closes the popup first, so the field is never a keyboard trap."},{key:"Escape",behavior:"Closes the popup and leaves the text and the caret exactly as they are. The dismissed token stays closed while it is being edited, so continuing to type does not reopen it; the next trigger character opens a fresh one."},{key:"Backspace",behavior:"Ordinary deletion. Deleting back past the trigger character ends the mention and closes the popup; an inserted mention is plain text and is deleted one character at a time, not as an atom."}],ariaAttributes:['aria-haspopup="listbox" with aria-controls and aria-autocomplete="list" on the Input, which is a real <textarea>; HTML permits no role attribute there, so the field keeps its native textbox role and the popup is announced through haspopup rather than aria-expanded',"aria-activedescendant on the Input naming the highlighted item \u2014 the highlight is virtual, so DOM focus never leaves the field and the caret never moves","Content's id matches the Input's aria-controls, which resolves exactly while the popup is open and is absent while it is closed, since an IDREF resolving to nothing is worse than no IDREF","data-trigger on Content and on the Input naming the trigger character of the active token, and on every Item naming the trigger it belongs to",'aria-busy="true" and data-loading on Input and Content while `loading`',"data-highlighted on the item holding virtual focus, and aria-disabled with data-disabled on a disabled item, which highlight and insertion both skip","the number of rendered items is announced through the shared LiveAnnouncer polite region while the popup is open, formatted by getResultCountLabel \u2014 filtering is otherwise silent to assistive technology \u2014 and suppressed entirely while `loading`","aria-invalid and aria-readonly on the Input, mirroring the matching props, with aria-describedby carrying the text that explains an invalid field",'the highlighted item is scrolled into view on every keyboard move with scrollIntoView({ block: "nearest" }); a highlight assigned while the popup is opening scrolls the popup itself, since floating-ui renders at the document origin before measuring'],focusManagement:`${u} Here the anchor is the Input, a textarea whose caret offset is part of the interaction: the popup must never take focus, and pointer interactions inside Content cancel their own mousedown so both DOM focus and the text selection survive a click. When filtering removes the highlighted item the highlight moves to the first survivor rather than disappearing, and aria-activedescendant is dropped only when no item is left. Inserting a mention puts the caret just past the inserted text \u2014 past the appended space where one was added \u2014 and the adapter sets it in the same task as the value write, so the field never flashes the caret at the end of the value. Blur closes the popup and inserts nothing: an unconfirmed mention is just the text the user typed.`,namingRequirement:"The Input must be named by a <label>, aria-label, or aria-labelledby; a textbox takes no name from its content, and Content is a sibling rather than an ancestor. Wrapping the field in the Field family satisfies this automatically. A placeholder is not a name \u2014 it disappears as soon as the field has text."},styling:{cssParts:["input","content","item","empty"],dataAttributes:[...c,"data-state","data-side","data-align","data-trigger","data-highlighted","data-disabled","data-readonly","data-invalid","data-loading","data-value",{name:"data-skdx-mentions-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:d("mentions")},publicMethods:[...i,r,{name:"focus",signature:"(): void",description:"Moves keyboard focus to the text field, which is where this component keeps real DOM focus.",frameworks:["vue","webComponents"],exception:"element-handoff",frameworksReason:"Vue hides the Input inside the root component instance, and the custom element forwards focus() to its shadow textarea. React, Svelte and Angular hand the consumer the Input element itself."}]};export{w as mentionsContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changeDetail as a}from"./change-reason.js";import{arrowPart as o,closeAutoFocusEvent as i,dismissalEvents as r,forceMountProp as n,menuSubParts as s,openChangeCompleteEvent as l,popperContentProps as d,popperMeasurementProperties as h,portalPart as c,shortcutPart as u,textValueProp as t}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as p}from"./styling-hooks.js";import{withoutScope as m}from"./types.js";var w={name:"Menu",purpose:'An accessible menu of actions, opened by a trigger button and positioned via floating-ui (side/align/collision avoidance) or \u2014 under openOn="contextmenu" \u2014 opened by right-click or long-press at the pointer and clamped to the viewport. Full arrow-key/typeahead navigation, nested submenus, and checkbox/radio item support throughout. Composed of a root plus Trigger, Portal, Content, Arrow, Item, CheckboxItem, RadioGroup, RadioItem, ItemIndicator, Group, Label, Separator, and Sub/SubTrigger/SubContent parts.',props:[{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:'Initial open state, for uncontrolled usage. Under openOn="contextmenu" with no point set and no pointer event to place the panel at, it opens at the default (0, 0) point \u2014 clamped to the top-left of the viewport by collisionPadding \u2014 so pair it with point to open somewhere meaningful.'},{name:"openOn",type:"'click' | 'contextmenu'",defaultValue:"'click'",required:!1,description:'Which gesture opens the menu. "click" anchors Content to the Trigger button and honours side, align and the offsets. "contextmenu" turns the Trigger into a right-clickable region: right-click or a 500ms long-press opens Content at the pointer (or at point) with the native browser menu suppressed, the panel is clamped by collisionPadding instead of anchored, side/align/sideOffset/alignOffset are ignored, and modal defaults to false.'},{name:"point",type:"{ x: number; y: number }",required:!1,description:'Viewport coordinates for Content placement under openOn="contextmenu", overriding the pointer position so a programmatic open (defaultOpen, controlled, a custom gesture) lands somewhere meaningful. Still clamped by collisionPadding, and ignored under openOn="click". Property-only on Web Components \u2014 a pair of numbers, not a string.'},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:'Whether the open menu takes the page out of use: everything outside its portal is marked inert and body scroll is locked, on the same reference-counted mechanism as Dialog and Drawer. Set false to leave the page interactive. Tab closes the menu per WAI-ARIA, not trapped in either mode. Defaults to true under openOn="click" and to false under openOn="contextmenu", where the region the menu was invoked from is usually still the thing being worked on.'},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether selecting an item closes the menu. Default inherited by Item/RadioItem but not CheckboxItem (which toggles and stays open by default). Independent of select event preventDefault; either veto is enough."},{name:"trigger",type:"ReactElement | Snippet | TemplateRef<unknown>",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`React, Svelte and Angular all take this as a typed prop. Vue takes the identical content through its named #trigger slot and Web Components through light-DOM children marked slot="trigger", neither of which is a declared prop an extractor can see; see the trigger slot entry below for that half. Only in React is the derived trigger a whole interactive element, merged onto the consumer's own via asChild; the other four render their own button and take this as only that button's inner content.`,description:"Trigger for the derived anatomy; none rendered if omitted. Content model differs by package: React is a whole interactive element (via asChild); Svelte/Angular supply inner content of a button the recipe renders."},{name:"items",type:"MenuEntry[]",required:!1,description:"Entries the derived menu renders: Item, Separator, CheckboxItem, RadioGroup, Sub, or Group; recursive for submenus. Supplying it draws the whole anatomy from data; manual composition uses primitives/menu and is the only route to per-item closeOnSelect."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:`Disables the trigger, forwarded to the derived Trigger's own disabled prop. Under openOn="contextmenu" this stands the right-clickable region down and lets the native browser menu through.`},{name:"side",type:"'top' | 'right' | 'bottom' | 'left' | 'start' | 'end'",defaultValue:"'bottom'",required:!1,description:`Which side of the trigger the derived menu opens on \u2014 forwarded to the underlying Content part. Set Content's own side directly under full manual composition. Ignored under openOn="contextmenu", where the panel sits at the pointer and has no anchor to take a side of.`},{name:"align",type:"'start' | 'center' | 'end'",defaultValue:"'start'",required:!1,description:'Alignment of the derived menu along side \u2014 forwarded to the underlying Content part, and start rather than center because a menu panel lines up with the edge of its trigger. Ignored under openOn="contextmenu", for the same reason side is.'},{name:"renderItem",type:"(item: MenuEntry) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue expresses the same customisation as a scoped #item slot and Web Components as a light-DOM slot="item" child keyed by data-value \u2014 each framework's own idiomatic mechanism for the hook, and neither a declared prop an extractor can see. Web Components additionally has no property-only equivalent, since an HTML attribute cannot carry a function; a Web Components entry with no matching slotted node falls back to its plain label, which is what the other ports render when their hook returns nothing. Angular ships the same name as a TemplateRef input.`,description:"Customises what renders *inside* a derived item, not the item itself: role, id, attributes, keyboard, and disabled state stay the library's. Never called for radio options ({ label, value }). Only read while items derives anatomy."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to Content's and SubContent's data-size; the attribute may be written on either part directly instead."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},l("menu"),...r("menu"),i("menu"),{name:"select",detail:"MenuEntry | null",description:"Fired by an Item/CheckboxItem/RadioItem when activated. Preventing default keeps the menu open. Under the derived anatomy the root re-fires it carrying the MenuEntry entry that was activated, and only for a plain or checkbox entry: a radio pick reports through its own radio-group entry's onValueChange, because the payload is typed as the item model and a radio option ({ label, value }) is not one. The entry's own onSelect runs first and the root's after it; neither cancels the other."},{name:"item-select",frameworks:["webComponents"],exception:"event-shape",frameworksReason:"React, Vue, Svelte and Angular spell the recipe-level callback select (onSelect / @select / (select)) on the root. <skdx-menu> cannot: its Item part already dispatches a bubbling select through the same host with a different detail, and two same-named events carrying different payloads on one node is a real ambiguity rather than a naming preference, so the recipe's is renamed here alone.",description:"Recipe-level select \u2014 dispatched with the MenuEntry entry that was activated, for plain and checkbox entries only.",detail:"MenuEntry"},{name:"checked-change",detail:"boolean",description:"Fired by a CheckboxItem with the new checked state when selected."},{name:"value-change",detail:a("string"),description:"Fired by a RadioGroup with a RadioItem's value when it is selected."}],parts:[{name:"Trigger",description:'The button that opens the menu and anchors its position. Under openOn="contextmenu" it is instead a focusable region (role="group", tabindex="0") that opens the panel at the pointer on right-click or on a 500ms touch long-press, suppressing the native browser menu; moving more than 10px, lifting the finger, or a browser cancel disarms the press, and touch-action: none applies only while armed so the region scrolls normally.',props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:'Stands the trigger down so it opens nothing. Under openOn="contextmenu" the native browser context menu is let through instead.'}]},c,{name:"Content",description:`The floating menu panel (role="menu"). Owns positioning, roving focus, and the dismissal interceptors the root declares as escape-key-down / pointer-down-outside / focus-outside / interact-outside. Under openOn="contextmenu" it is placed at the pointer (or at the root's point) and clamped to the viewport by collisionPadding, and its side, align, sideOffset and alignOffset props go unread \u2014 there is no anchor element to position against.`,props:[...d({align:"'start'"}),n,{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether Up/Down navigation wraps from the last item back to the first."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},o,{name:"Item",description:'One actionable entry (role="menuitem"). Selecting it fires select and closes the menu unless the handler prevents default or closeOnSelect is false.',props:[{name:"as",type:"PolymorphicTag",defaultValue:"'div'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular has no `as` input. Its element-choice mechanism is an attribute directive the consumer puts on their own element \u2014 `<section skdx-ng-card>` \u2014 which is the same capability spelled the way Angular's template compiler already reads, and eleven such directives predate this one.",description:"Element name to render instead of the default <div>, from the closed allowlist in POLYMORPHIC_TAGS. A tag name only, never a component: the list has to be closed for Lit, which needs a static-html literal per tag and cannot build one from a runtime string. React additionally reaches this through asChild, which takes a component and wins when both are set."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",required:!1,description:"Whether selecting this item closes the menu, overriding the root's closeOnSelect for this item alone. Unset, it inherits the root's value (true by default). Independent of the select event's preventDefault, which keeps the menu open for one activation."},{name:"href",type:"string",required:!1,frameworks:["svelte","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Vue take href as a plain attribute on the element `as` chose, and Angular puts its item directive on an <a href> the consumer wrote, so none of the three needs a declared member. Svelte and Web Components render the anchor themselves \u2014 Web Components inside a shadow root the attribute cannot reach \u2014 so the value has to arrive as a declared property.",description:'URL for an item that navigates, applied to the anchor the item renders when `as` is "a". Web Components additionally withholds it while the item is disabled, so a disabled entry is not still a working link.'},t]},{name:"CheckboxItem",description:'Toggleable item (role="menuitemcheckbox") whose aria-checked and data-state mirror its checked state. Selecting it reports through checked-change and leaves the menu open unless closeOnSelect says otherwise. Wrap an ItemIndicator for a checked-only glyph.',props:[{name:"checked",type:"boolean",defaultValue:"false",required:!1,description:"The checked state, always externally controlled: the item reports the state it wants through checked-change and never flips itself."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",defaultValue:"false",required:!1,description:"Whether selecting this item closes the menu. Unlike Item and RadioItem this does not inherit the root's closeOnSelect: a checkbox item exists to be toggled, often more than once in a visit, so it stays open unless asked otherwise. Set it true to opt this item into closing."},t]},{name:"RadioGroup",description:'Coordinates a set of mutually-exclusive RadioItems (role="group"), holding the selected value and reporting a new one through value-change. Named by a Label rendered inside it on exactly the same wiring as Group, which it also shadows: a Label inside a RadioGroup nested in a Group names the RadioGroup, not the outer Group.',props:[{name:"value",type:"string",required:!1,description:"The selected RadioItem's value, always externally controlled. Pair with the value-change event."}]},{name:"RadioItem",description:`A single option inside a RadioGroup (role="menuitemradio") whose aria-checked and data-state (checked/unchecked) reflect whether its value matches the group's selected value. Selecting it reports the group's new value through the value-change event and closes the menu. Wrap an ItemIndicator inside it for a glyph shown only while selected.`,props:[{name:"value",type:"string",required:!0,description:"This option's value. Compared against the enclosing RadioGroup's value to decide checked state, and reported through value-change when the option is selected."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",required:!1,description:"Whether selecting this item closes the menu, overriding the root's closeOnSelect for this item alone. Unset, it inherits the root's value (true by default). Independent of the select event's preventDefault, which keeps the menu open for one activation."},t]},{name:"ItemIndicator",description:"The checked-state glyph inside a CheckboxItem or RadioItem, rendered only when checked.",props:[m(n)]},{name:"Group",description:'Groups related items (role="group"). A Label rendered inside it names it: the Group generates an id, gives it to the Label it contains, and points its own aria-labelledby at it \u2014 so the group announces as "Actions, group" rather than as an unnamed one. The attribute is emitted only while a Label is actually there, because a dangling IDREF is a broken relationship rather than a weaker one, and an author-supplied aria-label or aria-labelledby wins over the generated link. A Label rendered outside a Group (the flat "heading then items" arrangement) still renders and still names nothing.'},{name:"Label",description:"A non-interactive heading for a Group \u2014 not itself a menu item and never focusable. Inside a Group or RadioGroup it takes the id that group points aria-labelledby at, which is the only way either of them gets an accessible name; outside one it is presentational text."},{name:"Separator",description:'A horizontal rule between item groups (role="separator", aria-orientation="horizontal").'},u,...s().map(e=>e.name==="SubContent"?{...e,props:[...e.props??[],{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the submenu panel; the data-size attribute may be set directly instead."}]}:e)],slots:[{name:"trigger",description:`Trigger's content \u2014 the button that opens the menu and anchors its position, or under openOn="contextmenu" the right-clickable region, which is itself a tab stop so Shift+F10 and the Menu key can reach it.`},{name:"default",description:"Content's body \u2014 Item/CheckboxItem/RadioGroup/Group/Label/Separator entries."}],appearance:[{attribute:"data-size",values:["sm","lg"],stylesheet:"menu.css"}],states:["closed","open","checked","unchecked","disabled"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultOpen and manages its own state internally. Passing open switches to controlled. CheckboxItem checked and RadioGroup value are always externally controlled (no internal state).",formBehavior:'Not applicable \u2014 Menu participates in no form; Trigger is type="button".',validationBehavior:"Not applicable.",accessibility:{role:'menu (Content), menuitem (Item), menuitemcheckbox (CheckboxItem), menuitemradio (RadioItem), group (Group/RadioGroup), separator (Separator); under openOn="contextmenu" the Trigger is a group rather than a button \u2014 the neutral container role, claiming no widget behaviour the region does not implement and, unlike button, not making its arbitrary wrapped content presentational',keyboard:[{key:"Enter / Space / ArrowDown",behavior:"Opens the trigger and moves focus to the first item."},{key:"Shift+F10 / Menu key (on Trigger)",behavior:`Under openOn="contextmenu", opens Content at the Trigger's own box and moves focus to the first item \u2014 the keyboard equivalent of right-clicking, since the browser fires contextmenu for these keys only on the focused element.`},{key:"ArrowUp (on Trigger)",behavior:"Opens the trigger and moves focus to the *last* item, so a user reaching upward for the bottom of the menu gets there in one keystroke."},{key:"ArrowUp / ArrowDown",behavior:"Moves focus between items, wrapping unless loop is false."},{key:"Home / End",behavior:"Jumps focus to the first/last item."},{key:"Typing a letter",behavior:"Jumps focus to the next item matching that text (typeahead)."},{key:"Enter / Space (on an item)",behavior:"Selects the focused item."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down), refocuses Trigger."},{key:"Alt+ArrowUp (inside the menu)",behavior:"Closes the menu and returns focus to Trigger, the mirror of the Alt+ArrowDown/ArrowDown that opened it. Unlike Escape it is not cancelable \u2014 it is a navigation key, not a dismissal \u2014 and the plain ArrowUp meaning above applies only without the modifier."},{key:"Tab",behavior:"Closes the menu and returns focus to Trigger; the key is not consumed, so the browser then continues the tab sequence from the Trigger \u2014 to the next tabbable element after it, or the previous one under Shift+Tab. Per the WAI-ARIA menu pattern, Tab moves focus out of a menu rather than being trapped by it."},{key:"ArrowRight (on a SubTrigger)",behavior:'Opens the submenu and moves focus to its first item. Mirrored to ArrowLeft under dir="rtl", in all five packages.'},{key:"ArrowLeft (inside a SubContent)",behavior:'Closes the submenu and returns focus to its SubTrigger, leaving the parent menu open. Mirrored to ArrowRight under dir="rtl", in all five packages.'},{key:"Escape (inside a SubContent)",behavior:"Closes only the submenu and returns focus to its SubTrigger."}],ariaAttributes:['aria-haspopup="menu", aria-expanded, and aria-controls on Trigger',`Under openOn="contextmenu" the Trigger carries tabindex="0", role="group", and \u2014 whenever the panel is mounted \u2014 aria-controls plus aria-owns naming it, and deliberately no aria-expanded and no aria-haspopup: role="group" supports neither. aria-expanded is absent from group's supported *and* inherited lists in ARIA 1.2 and in the 1.3 editor's draft alike, and aria-haspopup is inherited there only as "deprecated on this role in ARIA 1.2". aria-owns is the supported substitute \u2014 it places the open panel inside the region in the accessibility tree \u2014 and the open/closed state is announced a second way by focus moving into a role="menu" named after the region and back out again. Neither attribute is emitted while the panel is absent from the DOM: a dangling IDREF is a broken relationship rather than a weaker one. Where the closed panel stays mounted but hidden, the Trigger keeps aria-controls published throughout.`,'aria-haspopup="menu", aria-expanded, and aria-controls on SubTrigger, which is itself a role="menuitem" in the parent menu',`Content's id matches Trigger's aria-controls; aria-orientation="vertical" on Content`,`Content's aria-labelledby points back at Trigger's id, and SubContent's at its SubTrigger's id \u2014 a role="menu" takes no name from its own items, so without this every panel announces as a bare "menu". An author-supplied aria-label wins: the default is only emitted when none is set.`,"aria-checked on CheckboxItem/RadioItem reflecting checked state","aria-disabled and data-disabled on a disabled item, which stays reachable by the arrow keys and typeahead \u2014 APG keeps a disabled menu item focusable so it is discoverable \u2014 but cannot be selected",'aria-labelledby on Group and RadioGroup pointing at the id of the Label they contain, and emitted only while that Label is in the DOM \u2014 a role="group" takes no name from its own items, so an unnamed one announces as a bare "group". An author-supplied aria-label or aria-labelledby wins.'],focusManagement:`Roving DOM focus, not virtual, and a popup in front of a trigger does not change that: the collection-focus rule reaches for virtual focus only when a text input or a role="combobox" element has to keep DOM focus, and a menu trigger has neither obligation \u2014 APG's menu pattern moves real focus into the menu, so the item under the arrow keys is the element assistive technology reports as focused. Focus moves into the open menu \u2014 the first item, or the last when ArrowUp on the Trigger opened it \u2014 and returns to Trigger when closed via Escape/Tab/an item selection. Tab additionally leaves its default action intact, so focus continues out of the Trigger into the page tab sequence rather than resting on it. Under openOn='contextmenu' the entry gesture differs and the rest does not: right-click suppresses the native browser menu and opens Content at the pointer with focus on the first item, a 500ms touch long-press does the same at the pressed coordinates, and Shift+F10 or the Menu key on the focused Trigger region does it at the region's own box. Opening a submenu by keyboard or click moves focus to its first item; closing it returns focus to its SubTrigger. Hover-opening a submenu does not move focus, and pointer-leaving both SubTrigger and SubContent closes it after a short grace delay. While a submenu is hover-open and focus is still on its SubTrigger, the parent panel's own arrow keys move only among the parent's own items, never into the submenu. While modal (the default) everything outside the menu's portal container is inert and body scroll is locked for the menu's lifetime, so the page behind it is out of reach of pointer, keyboard and assistive technology alike; the inertness is lifted before focus is handed back, so Escape, Tab and selecting an item still land on the Trigger. An Item must not contain its own interactive elements \u2014 no button, link, checkbox or input inside a menuitem. The roving-focus layer moves DOM focus to the [role^=menuitem] elements and to nothing inside them, so a nested control is unreachable by the arrow keys and reachable by Tab only for the instant before Tab closes the menu; a screen reader meanwhile reads the control's own name as part of the item's. Where a row needs two actions, use two items.`},styling:{cssParts:["trigger","content","arrow","item","group","separator","label","checkbox-item","radio-group","radio-item","item-indicator","shortcut","sub-trigger","sub-content"],dataAttributes:[...p,"data-state","data-side","data-align","data-disabled","data-text-value","data-size",{name:"data-skdx-menu-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:h("menu")},publicMethods:[]};export{w as menuContract};
|
|
1
|
+
import{changeDetail as a}from"./change-reason.js";import{arrowPart as o,closeAutoFocusEvent as r,dismissalEvents as i,forceMountProp as n,menuSubParts as s,openChangeCompleteEvent as l,popperContentProps as d,popperMeasurementProperties as h,portalPart as c,shortcutPart as u,textValueProp as t}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as p}from"./styling-hooks.js";import{withoutScope as m}from"./types.js";var w={name:"Menu",purpose:'An accessible menu of actions, opened by a trigger button and positioned via floating-ui (side/align/collision avoidance) or \u2014 under openOn="contextmenu" \u2014 opened by right-click or long-press at the pointer and clamped to the viewport. Full arrow-key/typeahead navigation, nested submenus, and checkbox/radio item support throughout. Composed of a root plus Trigger, Portal, Content, Arrow, Item, CheckboxItem, RadioGroup, RadioItem, ItemIndicator, Group, Label, Separator, and Sub/SubTrigger/SubContent parts.',props:[{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:'Initial open state, for uncontrolled usage. Under openOn="contextmenu" with no point set and no pointer event to place the panel at, it opens at the default (0, 0) point \u2014 clamped to the top-left of the viewport by collisionPadding \u2014 so pair it with point to open somewhere meaningful.'},{name:"openOn",type:"'click' | 'contextmenu'",defaultValue:"'click'",required:!1,description:'Which gesture opens the menu. "click" anchors Content to the Trigger button and honours side, align and the offsets. "contextmenu" turns the Trigger into a right-clickable region: right-click or a 500ms long-press opens Content at the pointer (or at point) with the native browser menu suppressed, the panel is clamped by collisionPadding instead of anchored, side/align/sideOffset/alignOffset are ignored, and modal defaults to false.'},{name:"point",type:"{ x: number; y: number }",required:!1,description:'Viewport coordinates for Content placement under openOn="contextmenu", overriding the pointer position so a programmatic open (defaultOpen, controlled, a custom gesture) lands somewhere meaningful. Still clamped by collisionPadding, and ignored under openOn="click". Property-only on Web Components \u2014 a pair of numbers, not a string.'},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:'Whether the open menu takes the page out of use: everything outside its portal is marked inert and body scroll is locked, on the same reference-counted mechanism as Dialog and Drawer. Set false to leave the page interactive. Tab closes the menu per WAI-ARIA, not trapped in either mode. Defaults to true under openOn="click" and to false under openOn="contextmenu", where the region the menu was invoked from is usually still the thing being worked on.'},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether selecting an item closes the menu. Default inherited by Item/RadioItem but not CheckboxItem (which toggles and stays open by default). Independent of select event preventDefault; either veto is enough."},{name:"trigger",type:"ReactElement | Snippet | TemplateRef<unknown>",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`React, Svelte and Angular all take this as a typed prop. Vue takes the identical content through its named #trigger slot and Web Components through light-DOM children marked slot="trigger", neither of which is a declared prop an extractor can see; see the trigger slot entry below for that half. Only in React is the derived trigger a whole interactive element, merged onto the consumer's own via asChild; the other four render their own button and take this as only that button's inner content.`,description:"Trigger for the derived anatomy; none rendered if omitted. Content model differs by package: React is a whole interactive element (via asChild); Svelte/Angular supply inner content of a button the recipe renders."},{name:"items",type:"MenuEntry[]",required:!1,description:"Entries the derived menu renders: Item, Separator, CheckboxItem, RadioGroup, Sub, or Group; recursive for submenus. Supplying it draws the whole anatomy from data; manual composition uses primitives/menu and is the only route to per-item closeOnSelect."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:`Disables the trigger, forwarded to the derived Trigger's own disabled prop. Under openOn="contextmenu" this stands the right-clickable region down and lets the native browser menu through.`},{name:"side",type:"'top' | 'right' | 'bottom' | 'left' | 'start' | 'end'",defaultValue:"'bottom'",required:!1,description:`Which side of the trigger the derived menu opens on \u2014 forwarded to the underlying Content part. Set Content's own side directly under full manual composition. Ignored under openOn="contextmenu", where the panel sits at the pointer and has no anchor to take a side of.`},{name:"align",type:"'start' | 'center' | 'end'",defaultValue:"'start'",required:!1,description:'Alignment of the derived menu along side \u2014 forwarded to the underlying Content part, and start rather than center because a menu panel lines up with the edge of its trigger. Ignored under openOn="contextmenu", for the same reason side is.'},{name:"renderItem",type:"(item: MenuEntry) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue expresses the same customisation as a scoped #item slot and Web Components as a light-DOM slot="item" child keyed by data-value \u2014 each framework's own idiomatic mechanism for the hook, and neither a declared prop an extractor can see. Web Components additionally has no property-only equivalent, since an HTML attribute cannot carry a function; a Web Components entry with no matching slotted node falls back to its plain label, which is what the other ports render when their hook returns nothing. Angular ships the same name as a TemplateRef input.`,description:"Customises what renders *inside* a derived item, not the item itself: role, id, attributes, keyboard, and disabled state stay the library's. Never called for radio options ({ label, value }). Only read while items derives anatomy."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to Content's and SubContent's data-size; the attribute may be written on either part directly instead."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},l("menu"),...i("menu"),r("menu"),{name:"select",detail:"MenuEntry | null",description:"Fired by an Item/CheckboxItem/RadioItem when activated. Preventing default keeps the menu open. Under the derived anatomy the root re-fires it carrying the MenuEntry entry that was activated, and only for a plain or checkbox entry: a radio pick reports through its own radio-group entry's onValueChange, because the payload is typed as the item model and a radio option ({ label, value }) is not one. The entry's own onSelect runs first and the root's after it; neither cancels the other."},{name:"item-select",frameworks:["webComponents"],exception:"event-shape",frameworksReason:"React, Vue, Svelte and Angular spell the recipe-level callback select (onSelect / @select / (select)) on the root. <skdx-menu> cannot: its Item part already dispatches a bubbling select through the same host with a different detail, and two same-named events carrying different payloads on one node is a real ambiguity rather than a naming preference, so the recipe's is renamed here alone.",description:"Recipe-level select \u2014 dispatched with the MenuEntry entry that was activated, for plain and checkbox entries only.",detail:"MenuEntry"},{name:"checked-change",detail:"boolean",description:"Fired by a CheckboxItem with the new checked state when selected."},{name:"value-change",detail:a("string"),description:"Fired by a RadioGroup with a RadioItem's value when it is selected."}],parts:[{name:"Trigger",description:'The button that opens the menu and anchors its position. Under openOn="contextmenu" it is instead a focusable region (role="group", tabindex="0") that opens the panel at the pointer on right-click or on a 500ms touch long-press, suppressing the native browser menu; moving more than 10px, lifting the finger, or a browser cancel disarms the press, and touch-action: none applies only while armed so the region scrolls normally.',props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:'Stands the trigger down so it opens nothing. Under openOn="contextmenu" the native browser context menu is let through instead.'}]},c,{name:"Content",description:`The floating menu panel (role="menu"). Owns positioning, roving focus, and the dismissal interceptors the root declares as escape-key-down / pointer-down-outside / focus-outside / interact-outside. Under openOn="contextmenu" it is placed at the pointer (or at the root's point) and clamped to the viewport by collisionPadding, and its side, align, sideOffset and alignOffset props go unread \u2014 there is no anchor element to position against.`,props:[...d({align:"'start'"}),n,{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether Up/Down navigation wraps from the last item back to the first."},{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},o,{name:"Item",description:'One actionable entry (role="menuitem"). Selecting it fires select and closes the menu unless the handler prevents default or closeOnSelect is false.',props:[{name:"as",type:"PolymorphicTag",defaultValue:"'div'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular has no `as` input. Its element-choice mechanism is an attribute directive the consumer puts on their own element \u2014 `<section skdx-ng-card>` \u2014 which is the same capability spelled the way Angular's template compiler already reads, and eleven such directives predate this one.",description:"Element name to render instead of the default <div>, from the closed allowlist in POLYMORPHIC_TAGS. A tag name only, never a component: the list has to be closed for Lit, which needs a static-html literal per tag and cannot build one from a runtime string. React additionally reaches this through asChild, which takes a component and wins when both are set."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",required:!1,description:"Whether selecting this item closes the menu, overriding the root's closeOnSelect for this item alone. Unset, it inherits the root's value (true by default). Independent of the select event's preventDefault, which keeps the menu open for one activation."},{name:"href",type:"string",required:!1,frameworks:["svelte","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Vue take href as a plain attribute on the element `as` chose, and Angular puts its item directive on an <a href> the consumer wrote, so none of the three needs a declared member. Svelte and Web Components render the anchor themselves \u2014 Web Components inside a shadow root the attribute cannot reach \u2014 so the value has to arrive as a declared property.",description:'URL for an item that navigates, applied to the anchor the item renders when `as` is "a". Web Components additionally withholds it while the item is disabled, so a disabled entry is not still a working link.'},t]},{name:"CheckboxItem",description:'Toggleable item (role="menuitemcheckbox") whose aria-checked and data-state mirror its checked state. Selecting it reports through checked-change and leaves the menu open unless closeOnSelect says otherwise. Wrap an ItemIndicator for a checked-only glyph.',props:[{name:"checked",type:"boolean",defaultValue:"false",required:!1,description:"The checked state, always externally controlled: the item reports the state it wants through checked-change and never flips itself."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",defaultValue:"false",required:!1,description:"Whether selecting this item closes the menu. Unlike Item and RadioItem this does not inherit the root's closeOnSelect: a checkbox item exists to be toggled, often more than once in a visit, so it stays open unless asked otherwise. Set it true to opt this item into closing."},t]},{name:"RadioGroup",description:'Coordinates a set of mutually-exclusive RadioItems (role="group"), holding the selected value and reporting a new one through value-change. Named by a Label rendered inside it on exactly the same wiring as Group, which it also shadows: a Label inside a RadioGroup nested in a Group names the RadioGroup, not the outer Group.',props:[{name:"value",type:"string",required:!1,description:"The selected RadioItem's value, always externally controlled. Pair with the value-change event."}]},{name:"RadioItem",description:`A single option inside a RadioGroup (role="menuitemradio") whose aria-checked and data-state (checked/unchecked) reflect whether its value matches the group's selected value. Selecting it reports the group's new value through the value-change event and closes the menu. Wrap an ItemIndicator inside it for a glyph shown only while selected.`,props:[{name:"value",type:"string",required:!0,description:"This option's value. Compared against the enclosing RadioGroup's value to decide checked state, and reported through value-change when the option is selected."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks the item aria-disabled and blocks selection. The item stays in the roving order and remains focusable, so it is discoverable rather than invisible."},{name:"closeOnSelect",type:"boolean",required:!1,description:"Whether selecting this item closes the menu, overriding the root's closeOnSelect for this item alone. Unset, it inherits the root's value (true by default). Independent of the select event's preventDefault, which keeps the menu open for one activation."},t]},{name:"ItemIndicator",description:"The checked-state glyph inside a CheckboxItem or RadioItem, rendered only when checked.",props:[m(n)]},{name:"Group",description:'Groups related items (role="group"). A Label rendered inside it names it: the Group generates an id, gives it to the Label it contains, and points its own aria-labelledby at it \u2014 so the group announces as "Actions, group" rather than as an unnamed one. The attribute is emitted only while a Label is actually there, because a dangling IDREF is a broken relationship rather than a weaker one, and an author-supplied aria-label or aria-labelledby wins over the generated link. A Label rendered outside a Group (the flat "heading then items" arrangement) still renders and still names nothing.'},{name:"Label",description:"A non-interactive heading for a Group \u2014 not itself a menu item and never focusable. Inside a Group or RadioGroup it takes the id that group points aria-labelledby at, which is the only way either of them gets an accessible name; outside one it is presentational text."},{name:"Separator",description:'A horizontal rule between item groups (role="separator", aria-orientation="horizontal").'},u,...s().map(e=>e.name==="SubContent"?{...e,props:[...e.props??[],{name:"size",type:"'sm' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the submenu panel; the data-size attribute may be set directly instead."}]}:e)],slots:[{name:"trigger",description:`Trigger's content \u2014 the button that opens the menu and anchors its position, or under openOn="contextmenu" the right-clickable region, which is itself a tab stop so Shift+F10 and the Menu key can reach it.`},{name:"default",description:"Content's body \u2014 Item/CheckboxItem/RadioGroup/Group/Label/Separator entries."},{name:"item",frameworks:["vue","webComponents"],exception:"render-prop",frameworksReason:"vue, webComponents expose item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customises what renders *inside* a derived item, not the item itself: role, id, attributes, keyboard, and disabled state stay the library's. Never called for radio options ({ label, value }). Only read while items derives anatomy."},{name:"render-item",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customises what renders *inside* a derived item, not the item itself: role, id, attributes, keyboard, and disabled state stay the library's. Never called for radio options ({ label, value }). Only read while items derives anatomy."}],appearance:[{attribute:"data-size",values:["sm","lg"],stylesheet:"menu.css"}],states:["closed","open","checked","unchecked","disabled"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultOpen and manages its own state internally. Passing open switches to controlled. CheckboxItem checked and RadioGroup value are always externally controlled (no internal state).",formBehavior:'Not applicable \u2014 Menu participates in no form; Trigger is type="button".',validationBehavior:"Not applicable.",accessibility:{role:'menu (Content), menuitem (Item), menuitemcheckbox (CheckboxItem), menuitemradio (RadioItem), group (Group/RadioGroup), separator (Separator); under openOn="contextmenu" the Trigger is a group rather than a button \u2014 the neutral container role, claiming no widget behaviour the region does not implement and, unlike button, not making its arbitrary wrapped content presentational',keyboard:[{key:"Enter / Space / ArrowDown",behavior:"Opens the trigger and moves focus to the first item."},{key:"Shift+F10 / Menu key (on Trigger)",behavior:`Under openOn="contextmenu", opens Content at the Trigger's own box and moves focus to the first item \u2014 the keyboard equivalent of right-clicking, since the browser fires contextmenu for these keys only on the focused element.`},{key:"ArrowUp (on Trigger)",behavior:"Opens the trigger and moves focus to the *last* item, so a user reaching upward for the bottom of the menu gets there in one keystroke."},{key:"ArrowUp / ArrowDown",behavior:"Moves focus between items, wrapping unless loop is false."},{key:"Home / End",behavior:"Jumps focus to the first/last item."},{key:"Typing a letter",behavior:"Jumps focus to the next item matching that text (typeahead)."},{key:"Enter / Space (on an item)",behavior:"Selects the focused item."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down), refocuses Trigger."},{key:"Alt+ArrowUp (inside the menu)",behavior:"Closes the menu and returns focus to Trigger, the mirror of the Alt+ArrowDown/ArrowDown that opened it. Unlike Escape it is not cancelable \u2014 it is a navigation key, not a dismissal \u2014 and the plain ArrowUp meaning above applies only without the modifier."},{key:"Tab",behavior:"Closes the menu and returns focus to Trigger; the key is not consumed, so the browser then continues the tab sequence from the Trigger \u2014 to the next tabbable element after it, or the previous one under Shift+Tab. Per the WAI-ARIA menu pattern, Tab moves focus out of a menu rather than being trapped by it."},{key:"ArrowRight (on a SubTrigger)",behavior:'Opens the submenu and moves focus to its first item. Mirrored to ArrowLeft under dir="rtl", in all five packages.'},{key:"ArrowLeft (inside a SubContent)",behavior:'Closes the submenu and returns focus to its SubTrigger, leaving the parent menu open. Mirrored to ArrowRight under dir="rtl", in all five packages.'},{key:"Escape (inside a SubContent)",behavior:"Closes only the submenu and returns focus to its SubTrigger."}],ariaAttributes:['aria-haspopup="menu", aria-expanded, and aria-controls on Trigger',`Under openOn="contextmenu" the Trigger carries tabindex="0", role="group", and \u2014 whenever the panel is mounted \u2014 aria-controls plus aria-owns naming it, and deliberately no aria-expanded and no aria-haspopup: role="group" supports neither. aria-expanded is absent from group's supported *and* inherited lists in ARIA 1.2 and in the 1.3 editor's draft alike, and aria-haspopup is inherited there only as "deprecated on this role in ARIA 1.2". aria-owns is the supported substitute \u2014 it places the open panel inside the region in the accessibility tree \u2014 and the open/closed state is announced a second way by focus moving into a role="menu" named after the region and back out again. Neither attribute is emitted while the panel is absent from the DOM: a dangling IDREF is a broken relationship rather than a weaker one. Where the closed panel stays mounted but hidden, the Trigger keeps aria-controls published throughout.`,'aria-haspopup="menu", aria-expanded, and aria-controls on SubTrigger, which is itself a role="menuitem" in the parent menu',`Content's id matches Trigger's aria-controls; aria-orientation="vertical" on Content`,`Content's aria-labelledby points back at Trigger's id, and SubContent's at its SubTrigger's id \u2014 a role="menu" takes no name from its own items, so without this every panel announces as a bare "menu". An author-supplied aria-label wins: the default is only emitted when none is set.`,"aria-checked on CheckboxItem/RadioItem reflecting checked state","aria-disabled and data-disabled on a disabled item, which stays reachable by the arrow keys and typeahead \u2014 APG keeps a disabled menu item focusable so it is discoverable \u2014 but cannot be selected",'aria-labelledby on Group and RadioGroup pointing at the id of the Label they contain, and emitted only while that Label is in the DOM \u2014 a role="group" takes no name from its own items, so an unnamed one announces as a bare "group". An author-supplied aria-label or aria-labelledby wins.'],focusManagement:`Roving DOM focus, not virtual, and a popup in front of a trigger does not change that: the collection-focus rule reaches for virtual focus only when a text input or a role="combobox" element has to keep DOM focus, and a menu trigger has neither obligation \u2014 APG's menu pattern moves real focus into the menu, so the item under the arrow keys is the element assistive technology reports as focused. Focus moves into the open menu \u2014 the first item, or the last when ArrowUp on the Trigger opened it \u2014 and returns to Trigger when closed via Escape/Tab/an item selection. Tab additionally leaves its default action intact, so focus continues out of the Trigger into the page tab sequence rather than resting on it. Under openOn='contextmenu' the entry gesture differs and the rest does not: right-click suppresses the native browser menu and opens Content at the pointer with focus on the first item, a 500ms touch long-press does the same at the pressed coordinates, and Shift+F10 or the Menu key on the focused Trigger region does it at the region's own box. Opening a submenu by keyboard or click moves focus to its first item; closing it returns focus to its SubTrigger. Hover-opening a submenu does not move focus, and pointer-leaving both SubTrigger and SubContent closes it after a short grace delay. While a submenu is hover-open and focus is still on its SubTrigger, the parent panel's own arrow keys move only among the parent's own items, never into the submenu. While modal (the default) everything outside the menu's portal container is inert and body scroll is locked for the menu's lifetime, so the page behind it is out of reach of pointer, keyboard and assistive technology alike; the inertness is lifted before focus is handed back, so Escape, Tab and selecting an item still land on the Trigger. An Item must not contain its own interactive elements \u2014 no button, link, checkbox or input inside a menuitem. The roving-focus layer moves DOM focus to the [role^=menuitem] elements and to nothing inside them, so a nested control is unreachable by the arrow keys and reachable by Tab only for the instant before Tab closes the menu; a screen reader meanwhile reads the control's own name as part of the item's. Where a row needs two actions, use two items.`},styling:{cssParts:["trigger","content","arrow","item","group","separator","label","checkbox-item","radio-group","radio-item","item-indicator","shortcut","sub-trigger","sub-content"],dataAttributes:[...p,"data-state","data-side","data-align","data-disabled","data-text-value","data-size",{name:"data-skdx-menu-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:h("menu")},publicMethods:[]};export{w as menuContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changeDetail as a}from"./change-reason.js";import{arrowPart as n,closeAutoFocusEvent as r,dismissalEvents as o,forceMountProp as t,menuSubParts as i,openChangeCompleteEvent as s,popperContentProps as l,popperMeasurementProperties as d,portalPart as u,shortcutPart as c,textValueProp as e}from"./overlay-parts.js";import{ROOT_DATA_ATTRIBUTES as m}from"./styling-hooks.js";import{withoutScope as p}from"./types.js";var y={name:"Menubar",purpose:"A WAI-ARIA menu bar: triggers in roving-focus rows, each opening a floating-positioned menu. Switches between menus by hover/focus once one is open, matching native desktop menu bars.",props:[{name:"menu",type:"string",required:!1,description:"Controlled currently-open menu. Pair with menu-change. Named menu (not value) since menu state is not a selection and differs semantically from RadioGroup's value-change."},{name:"defaultMenu",type:"string",defaultValue:"''",required:!1,description:"Initially open menu's value, for uncontrolled usage."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'horizontal'",required:!1,description:"Axis triggers lay out on, reflected as aria-orientation. Horizontal roves with ArrowLeft/ArrowRight, vertical with ArrowUp/ArrowDown. Open menus stay vertical either way."},{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether trigger focus wraps at the ends. Between-menu navigation always wraps."},{name:"menus",type:"{ label: string; items: MenuEntry[]; value?: string; disabled?: boolean }[]",required:!1,description:"Data array for derived anatomy: one Menu/Trigger/Portal/Content per entry. label is trigger text, value its identifier (defaults to index), disabled the Trigger's state."},{name:"renderItem",type:"(item: MenuEntry) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue uses a scoped #item slot, Web Components a light-DOM slot="item" child, Angular a TemplateRef input \u2014 each framework's idiomatic hook. When no slot is filled, it falls back to the plain label.`,description:"Customizes item content only, never the item's role/id/data attributes. Skipped for radio options, only used during derived anatomy render."},{name:"density",type:"'compact' | 'comfortable' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the root; the data-size attribute may be set directly instead."}],events:[{name:"menu-change",detail:"string",description:"Fired with the open menu value (or an empty string when closed), always. Distinct from RadioGroup value-change so listeners can tell the two apart."},...o("menu"),r("menu"),{name:"select",detail:"MenuEntry | null",description:"Fired when Item/CheckboxItem/RadioItem is activated; preventDefault keeps menu open. Radio picks report via RadioGroup.value-change instead. In derived anatomy, root re-fires with the MenuEntry."},{name:"item-select",frameworks:["webComponents"],exception:"event-shape",frameworksReason:"Other frameworks use select with different payloads causing ambiguity. <skdx-menubar> renames it to item-select to disambiguate.",description:"Recipe-level select \u2014 dispatched with MenuEntry for plain and checkbox entries only.",detail:"MenuEntry"},{name:"checked-change",detail:"boolean",description:"Fired by a CheckboxItem with the new checked state when selected."}],parts:[{name:"Menu",description:"One top-level entry: a Trigger paired with Portal/Content.",props:[{name:"value",type:"string",required:!0,description:"This menu's identifier. Required \u2014 the root addresses menus by value and needs a stable key."}],events:[s("menu")]},{name:"Trigger",description:"A top-level menu's button. Opens its menu on click, hover once any menu is open.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Blocks menu opening, data-disabled reflection. React uses native passthrough; Vue/Svelte/Angular/Web Components declare it explicitly."}]},u,{name:"Content",description:'The floating menu panel (role="menu"): positioning, roving focus, dismissal.',props:[...l({align:"'start'"}),t,{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether Up/Down navigation wraps at the ends."}]},n,{name:"Item",description:'An actionable menu entry (role="menuitem"): fires select, closes menu unless prevented.',props:[{name:"as",type:"PolymorphicTag",defaultValue:"'div'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular has no `as` input. Its element-choice mechanism is an attribute directive the consumer puts on their own element \u2014 `<section skdx-ng-card>` \u2014 which is the same capability spelled the way Angular's template compiler already reads, and eleven such directives predate this one.",description:"Element name to render instead of the default <div>, from the closed allowlist in POLYMORPHIC_TAGS. A tag name only, never a component: the list has to be closed for Lit, which needs a static-html literal per tag and cannot build one from a runtime string. React additionally reaches this through asChild, which takes a component and wins when both are set."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether selecting this item closes the menu. Independent of the select event's preventDefault, which keeps the menu open for one activation alone."},e]},{name:"CheckboxItem",description:'A toggleable item (role="menuitemcheckbox"): aria-checked/data-state mirror checked. Reports new state via checked-change; wrap ItemIndicator for checked glyph.',props:[{name:"checked",type:"boolean",defaultValue:"false",required:!1,description:"Externally controlled checked state. Item reports via checked-change, never toggles itself."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},e]},{name:"RadioGroup",description:"Mutually-exclusive RadioItems. Emits value-change \u2014 distinct from root's menu-change.",props:[{name:"value",type:"string",required:!1,description:"Selected RadioItem's value, externally controlled. Pair with this part's value-change."}],events:[{name:"value-change",description:"Fired with selected RadioItem's value.",detail:a("string")}]},{name:"RadioItem",description:'A RadioGroup option (role="menuitemradio"): aria-checked/data-state reflect group selection. Wrap ItemIndicator for selected glyph.',props:[{name:"value",type:"string",required:!0,description:"This option's value. Compared against group's value for checked state, reported via group's value-change when selected."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},e]},{name:"ItemIndicator",description:"Checked-state glyph in CheckboxItem or RadioItem, rendered only when checked.",props:[p(t)]},{name:"Group",description:'Groups related items (role="group"). Interior Label names it via aria-labelledby. Group generates id, gives to Label, points aria-labelledby at it. Consumer aria-label/aria-labelledby wins over generated link.'},{name:"Label",description:"Non-interactive heading for a Group or RadioGroup, providing their accessible name via aria-labelledby. Presentational text outside a group."},{name:"Separator",description:'A horizontal rule between item groups (role="separator", aria-orientation="horizontal").'},c,...i()],slots:[{name:"default",description:'Root content \u2014 one Menu per top-level entry (e.g. "File", "Edit").'}],appearance:[{attribute:"data-density",values:["compact","comfortable"],stylesheet:"menubar.css"},{attribute:"data-size",values:["sm","md","lg"],stylesheet:"menubar.css"}],states:["closed","open","checked","unchecked","disabled"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultMenu and manages which menu is open internally. Passing menu switches to controlled. CheckboxItem checked and RadioGroup value are always externally controlled.",formBehavior:'Not applicable \u2014 Menubar participates in no form; triggers are type="button".',validationBehavior:"Not applicable.",accessibility:{role:"menubar (root), menuitem (Trigger, Item), menuitemcheckbox (CheckboxItem), menuitemradio (RadioItem), menu (Content), group (Group/RadioGroup), separator (Separator)",keyboard:[{key:"ArrowLeft / ArrowRight",behavior:"Moves focus between triggers, wrapping unless loop is false, and switches the open menu if one is already open. ArrowUp/ArrowDown take this role instead when orientation is vertical."},{key:"Home / End",behavior:"Jumps focus to the first/last trigger."},{key:"Typing a letter",behavior:"Jumps focus to the next trigger matching that text (typeahead)."},{key:"Enter / Space / ArrowDown (on a trigger)",behavior:"Opens that trigger's menu and moves focus to its first item."},{key:"ArrowUp (on a trigger)",behavior:"Opens that trigger's menu and moves focus to its *last* item, APG's optional Up Arrow rule. In a vertical bar the key roves between triggers instead, and only Enter/Space/ArrowRight open a menu."},{key:"ArrowUp / ArrowDown (within an open menu)",behavior:"Moves focus between items, wrapping unless loop is false."},{key:"ArrowRight / ArrowLeft (on an item with no submenu)",behavior:`Closes the current menu and opens the adjacent one, wrapping: focus moves to that menu's trigger and then to its panel's first item. Mirrored under dir="rtl", where ArrowLeft moves to the next menu and ArrowRight to the previous. An item that *does* have a submenu keeps the submenu meaning below.`},{key:"Enter / Space (on an item)",behavior:"Selects the focused item."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down), refocuses the trigger."},{key:"Tab",behavior:"Closes every open menu and submenu and returns focus to the trigger, which restores the bar to its single tab stop; the key is not consumed, so focus continues out of the bar to the next element in the page tab sequence, per the WAI-ARIA menu pattern."},{key:"ArrowRight (on a SubTrigger)",behavior:'Opens the submenu and moves focus to its first item \u2014 it does not move to the next menubar trigger. Mirrored to ArrowLeft under dir="rtl", in all five packages.'},{key:"ArrowLeft (inside a SubContent)",behavior:'Closes the submenu and returns focus to its SubTrigger, leaving the parent menu open. Mirrored to ArrowRight under dir="rtl", in all five packages.'},{key:"Escape (inside a SubContent)",behavior:"Closes only the submenu and returns focus to its SubTrigger."}],ariaAttributes:['role="menubar" on the root','aria-orientation on the root, mirroring the orientation prop \u2014 "horizontal" by default, and the only way a vertical bar tells assistive technology which arrows move along it','aria-label or aria-labelledby on the root, supplied by the consumer \u2014 a menubar takes no name from its menuitem children, so an unnamed one announces as a bare "menu bar" and two on a page are indistinguishable. No prop is needed: passthrough already carries both.',"aria-labelledby on Content pointing at its Trigger's id, and on SubContent pointing at its SubTrigger's id \u2014 APG requires a menu to name itself after the control that opens it. An author-supplied aria-label wins: the default is only emitted when none is set.",'aria-haspopup="menu", aria-expanded, and aria-controls on SubTrigger, which is itself a role="menuitem" in the menu that contains it','aria-haspopup="menu", aria-expanded, and aria-controls on each Trigger',`a menu's Content id matches its Trigger's aria-controls; aria-orientation="vertical" on Content`,"aria-checked on CheckboxItem/RadioItem reflecting checked state","aria-disabled and data-disabled on a disabled item, which stays reachable by the arrow keys and typeahead \u2014 APG keeps a disabled menu item focusable so it is discoverable \u2014 but cannot be selected, and on a disabled Trigger, which is skipped by the bar's own roving focus and cannot open its menu",`aria-labelledby on Group and RadioGroup pointing at the id of the Label they contain, and emitted only while that Label is in the DOM \u2014 a role="group" takes no name from its own items, so an unnamed one announces as a bare "group". Identical wiring to Menu's, and an author-supplied aria-label or aria-labelledby wins over it.`],focusManagement:"Single tab stop into the menubar (roving tabindex across triggers). Opening a menu moves focus to its first item \u2014 to its *last* item when ArrowUp opened it from the trigger of a horizontal bar; closing via Escape/Tab/an item selection returns focus to that menu's trigger. Hovering or focusing a sibling trigger while any menu is open switches which one is open. Opening a submenu by keyboard or click moves focus to its first item; closing it returns focus to its SubTrigger. Hover-opening a submenu does not move focus, and pointer-leaving both SubTrigger and SubContent closes it after a short grace delay. While a submenu is hover-open and focus is still on its SubTrigger, the parent panel's own arrow keys move only among the parent's own items, never into the submenu. An Item must not contain its own interactive elements \u2014 no button, link, checkbox or input inside a menuitem. The roving-focus layer moves DOM focus to the [role^=menuitem] elements and to nothing inside them, so a nested control is unreachable by the arrow keys and reachable by Tab only for the instant before Tab closes the menu; a screen reader meanwhile reads the control's own name as part of the item's. Where a row needs two actions, use two items."},styling:{cssParts:["trigger","content","arrow","item","group","separator","root","label","checkbox-item","radio-group","radio-item","item-indicator","shortcut","sub-trigger","sub-content"],dataAttributes:[...m,"data-state","data-side","data-align","data-disabled","data-skdx-menubar-trigger","data-text-value","data-density","data-size",{name:"data-skdx-menubar-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:d("menubar")},publicMethods:[]};export{y as menubarContract};
|
|
1
|
+
import{changeDetail as a}from"./change-reason.js";import{arrowPart as n,closeAutoFocusEvent as o,dismissalEvents as r,forceMountProp as t,menuSubParts as i,openChangeCompleteEvent as s,popperContentProps as l,popperMeasurementProperties as d,portalPart as c,shortcutPart as p,textValueProp as e}from"./overlay-parts.js";import{ROOT_DATA_ATTRIBUTES as m}from"./styling-hooks.js";import{withoutScope as u}from"./types.js";var v={name:"Menubar",purpose:"A WAI-ARIA menu bar: triggers in roving-focus rows, each opening a floating-positioned menu. Switches between menus by hover/focus once one is open, matching native desktop menu bars.",props:[{name:"menu",type:"string",required:!1,description:"Controlled currently-open menu. Pair with menu-change. Named menu (not value) since menu state is not a selection and differs semantically from RadioGroup's value-change."},{name:"defaultMenu",type:"string",defaultValue:"''",required:!1,description:"Initially open menu's value, for uncontrolled usage."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'horizontal'",required:!1,description:"Axis triggers lay out on, reflected as aria-orientation. Horizontal roves with ArrowLeft/ArrowRight, vertical with ArrowUp/ArrowDown. Open menus stay vertical either way."},{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether trigger focus wraps at the ends. Between-menu navigation always wraps."},{name:"menus",type:"{ label: string; items: MenuEntry[]; value?: string; disabled?: boolean }[]",required:!1,description:"Data array for derived anatomy: one Menu/Trigger/Portal/Content per entry. label is trigger text, value its identifier (defaults to index), disabled the Trigger's state."},{name:"renderItem",type:"(item: MenuEntry) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue uses a scoped #item slot, Web Components a light-DOM slot="item" child, Angular a TemplateRef input \u2014 each framework's idiomatic hook. When no slot is filled, it falls back to the plain label.`,description:"Customizes item content only, never the item's role/id/data attributes. Skipped for radio options, only used during derived anatomy render."},{name:"density",type:"'compact' | 'comfortable' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the root; the data-size attribute may be set directly instead."}],events:[{name:"menu-change",detail:"string",description:"Fired with the open menu value (or an empty string when closed), always. Distinct from RadioGroup value-change so listeners can tell the two apart."},...r("menu"),o("menu"),{name:"select",detail:"MenuEntry | null",description:"Fired when Item/CheckboxItem/RadioItem is activated; preventDefault keeps menu open. Radio picks report via RadioGroup.value-change instead. In derived anatomy, root re-fires with the MenuEntry."},{name:"item-select",frameworks:["webComponents"],exception:"event-shape",frameworksReason:"Other frameworks use select with different payloads causing ambiguity. <skdx-menubar> renames it to item-select to disambiguate.",description:"Recipe-level select \u2014 dispatched with MenuEntry for plain and checkbox entries only.",detail:"MenuEntry"},{name:"checked-change",detail:"boolean",description:"Fired by a CheckboxItem with the new checked state when selected."}],parts:[{name:"Menu",description:"One top-level entry: a Trigger paired with Portal/Content.",props:[{name:"value",type:"string",required:!0,description:"This menu's identifier. Required \u2014 the root addresses menus by value and needs a stable key."}],events:[s("menu")]},{name:"Trigger",description:"A top-level menu's button. Opens its menu on click, hover once any menu is open.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Blocks menu opening, data-disabled reflection. React uses native passthrough; Vue/Svelte/Angular/Web Components declare it explicitly."}]},c,{name:"Content",description:'The floating menu panel (role="menu"): positioning, roving focus, dismissal.',props:[...l({align:"'start'"}),t,{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether Up/Down navigation wraps at the ends."}]},n,{name:"Item",description:'An actionable menu entry (role="menuitem"): fires select, closes menu unless prevented.',props:[{name:"as",type:"PolymorphicTag",defaultValue:"'div'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular has no `as` input. Its element-choice mechanism is an attribute directive the consumer puts on their own element \u2014 `<section skdx-ng-card>` \u2014 which is the same capability spelled the way Angular's template compiler already reads, and eleven such directives predate this one.",description:"Element name to render instead of the default <div>, from the closed allowlist in POLYMORPHIC_TAGS. A tag name only, never a component: the list has to be closed for Lit, which needs a static-html literal per tag and cannot build one from a runtime string. React additionally reaches this through asChild, which takes a component and wins when both are set."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether selecting this item closes the menu. Independent of the select event's preventDefault, which keeps the menu open for one activation alone."},e]},{name:"CheckboxItem",description:'A toggleable item (role="menuitemcheckbox"): aria-checked/data-state mirror checked. Reports new state via checked-change; wrap ItemIndicator for checked glyph.',props:[{name:"checked",type:"boolean",defaultValue:"false",required:!1,description:"Externally controlled checked state. Item reports via checked-change, never toggles itself."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},e]},{name:"RadioGroup",description:"Mutually-exclusive RadioItems. Emits value-change \u2014 distinct from root's menu-change.",props:[{name:"value",type:"string",required:!1,description:"Selected RadioItem's value, externally controlled. Pair with this part's value-change."}],events:[{name:"value-change",description:"Fired with selected RadioItem's value.",detail:a("string")}]},{name:"RadioItem",description:'A RadioGroup option (role="menuitemradio"): aria-checked/data-state reflect group selection. Wrap ItemIndicator for selected glyph.',props:[{name:"value",type:"string",required:!0,description:"This option's value. Compared against group's value for checked state, reported via group's value-change when selected."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Marks aria-disabled, blocks selection. Stays focusable in roving order for discoverability."},e]},{name:"ItemIndicator",description:"Checked-state glyph in CheckboxItem or RadioItem, rendered only when checked.",props:[u(t)]},{name:"Group",description:'Groups related items (role="group"). Interior Label names it via aria-labelledby. Group generates id, gives to Label, points aria-labelledby at it. Consumer aria-label/aria-labelledby wins over generated link.'},{name:"Label",description:"Non-interactive heading for a Group or RadioGroup, providing their accessible name via aria-labelledby. Presentational text outside a group."},{name:"Separator",description:'A horizontal rule between item groups (role="separator", aria-orientation="horizontal").'},p,...i()],slots:[{name:"default",description:'Root content \u2014 one Menu per top-level entry (e.g. "File", "Edit").'},{name:"item",frameworks:["vue","webComponents"],exception:"render-prop",frameworksReason:"vue, webComponents expose item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customizes item content only, never the item's role/id/data attributes. Skipped for radio options, only used during derived anatomy render."},{name:"render-item",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customizes item content only, never the item's role/id/data attributes. Skipped for radio options, only used during derived anatomy render."}],appearance:[{attribute:"data-density",values:["compact","comfortable"],stylesheet:"menubar.css"},{attribute:"data-size",values:["sm","md","lg"],stylesheet:"menubar.css"}],states:["closed","open","checked","unchecked","disabled"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultMenu and manages which menu is open internally. Passing menu switches to controlled. CheckboxItem checked and RadioGroup value are always externally controlled.",formBehavior:'Not applicable \u2014 Menubar participates in no form; triggers are type="button".',validationBehavior:"Not applicable.",accessibility:{role:"menubar (root), menuitem (Trigger, Item), menuitemcheckbox (CheckboxItem), menuitemradio (RadioItem), menu (Content), group (Group/RadioGroup), separator (Separator)",keyboard:[{key:"ArrowLeft / ArrowRight",behavior:"Moves focus between triggers, wrapping unless loop is false, and switches the open menu if one is already open. ArrowUp/ArrowDown take this role instead when orientation is vertical."},{key:"Home / End",behavior:"Jumps focus to the first/last trigger."},{key:"Typing a letter",behavior:"Jumps focus to the next trigger matching that text (typeahead)."},{key:"Enter / Space / ArrowDown (on a trigger)",behavior:"Opens that trigger's menu and moves focus to its first item."},{key:"ArrowUp (on a trigger)",behavior:"Opens that trigger's menu and moves focus to its *last* item, APG's optional Up Arrow rule. In a vertical bar the key roves between triggers instead, and only Enter/Space/ArrowRight open a menu."},{key:"ArrowUp / ArrowDown (within an open menu)",behavior:"Moves focus between items, wrapping unless loop is false."},{key:"ArrowRight / ArrowLeft (on an item with no submenu)",behavior:`Closes the current menu and opens the adjacent one, wrapping: focus moves to that menu's trigger and then to its panel's first item. Mirrored under dir="rtl", where ArrowLeft moves to the next menu and ArrowRight to the previous. An item that *does* have a submenu keeps the submenu meaning below.`},{key:"Enter / Space (on an item)",behavior:"Selects the focused item."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down), refocuses the trigger."},{key:"Tab",behavior:"Closes every open menu and submenu and returns focus to the trigger, which restores the bar to its single tab stop; the key is not consumed, so focus continues out of the bar to the next element in the page tab sequence, per the WAI-ARIA menu pattern."},{key:"ArrowRight (on a SubTrigger)",behavior:'Opens the submenu and moves focus to its first item \u2014 it does not move to the next menubar trigger. Mirrored to ArrowLeft under dir="rtl", in all five packages.'},{key:"ArrowLeft (inside a SubContent)",behavior:'Closes the submenu and returns focus to its SubTrigger, leaving the parent menu open. Mirrored to ArrowRight under dir="rtl", in all five packages.'},{key:"Escape (inside a SubContent)",behavior:"Closes only the submenu and returns focus to its SubTrigger."}],ariaAttributes:['role="menubar" on the root','aria-orientation on the root, mirroring the orientation prop \u2014 "horizontal" by default, and the only way a vertical bar tells assistive technology which arrows move along it','aria-label or aria-labelledby on the root, supplied by the consumer \u2014 a menubar takes no name from its menuitem children, so an unnamed one announces as a bare "menu bar" and two on a page are indistinguishable. No prop is needed: passthrough already carries both.',"aria-labelledby on Content pointing at its Trigger's id, and on SubContent pointing at its SubTrigger's id \u2014 APG requires a menu to name itself after the control that opens it. An author-supplied aria-label wins: the default is only emitted when none is set.",'aria-haspopup="menu", aria-expanded, and aria-controls on SubTrigger, which is itself a role="menuitem" in the menu that contains it','aria-haspopup="menu", aria-expanded, and aria-controls on each Trigger',`a menu's Content id matches its Trigger's aria-controls; aria-orientation="vertical" on Content`,"aria-checked on CheckboxItem/RadioItem reflecting checked state","aria-disabled and data-disabled on a disabled item, which stays reachable by the arrow keys and typeahead \u2014 APG keeps a disabled menu item focusable so it is discoverable \u2014 but cannot be selected, and on a disabled Trigger, which is skipped by the bar's own roving focus and cannot open its menu",`aria-labelledby on Group and RadioGroup pointing at the id of the Label they contain, and emitted only while that Label is in the DOM \u2014 a role="group" takes no name from its own items, so an unnamed one announces as a bare "group". Identical wiring to Menu's, and an author-supplied aria-label or aria-labelledby wins over it.`],focusManagement:"Single tab stop into the menubar (roving tabindex across triggers). Opening a menu moves focus to its first item \u2014 to its *last* item when ArrowUp opened it from the trigger of a horizontal bar; closing via Escape/Tab/an item selection returns focus to that menu's trigger. Hovering or focusing a sibling trigger while any menu is open switches which one is open. Opening a submenu by keyboard or click moves focus to its first item; closing it returns focus to its SubTrigger. Hover-opening a submenu does not move focus, and pointer-leaving both SubTrigger and SubContent closes it after a short grace delay. While a submenu is hover-open and focus is still on its SubTrigger, the parent panel's own arrow keys move only among the parent's own items, never into the submenu. An Item must not contain its own interactive elements \u2014 no button, link, checkbox or input inside a menuitem. The roving-focus layer moves DOM focus to the [role^=menuitem] elements and to nothing inside them, so a nested control is unreachable by the arrow keys and reachable by Tab only for the instant before Tab closes the menu; a screen reader meanwhile reads the control's own name as part of the item's. Where a row needs two actions, use two items."},styling:{cssParts:["trigger","content","arrow","item","group","separator","root","label","checkbox-item","radio-group","radio-item","item-indicator","shortcut","sub-trigger","sub-content"],dataAttributes:[...m,"data-state","data-side","data-align","data-disabled","data-skdx-menubar-trigger","data-text-value","data-density","data-size",{name:"data-skdx-menubar-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:d("menubar")},publicMethods:[],frameworkExtensions:[{framework:"webComponents",part:"Menu",kind:"method",name:"close",type:"() => void",description:"Closes the open menu (which may be a sibling), since a menubar has exactly one open menu at a time. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",part:"Menu",kind:"method",name:"openMenu",type:"() => void",description:"Opens this menu (sets the ancestor menubar's value to this menu's value). Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",part:"RadioGroup",kind:"method",name:"setValue",type:"(value: string) => void",description:"Sets `value` and dispatches `value-change`. The value it already holds is a no-op. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",kind:"method",name:"setMenu",type:"(menu: string) => void",description:"Sets `menu` and dispatches `menu-change` plus a non-bubbling `skdx-menubar-internal-update`, a no-op if already that value. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",part:"Menu",kind:"method",name:"close",type:"() => void",description:"Closes the open menu (which may be a sibling), since a menubar has exactly one open menu at a time. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",part:"Menu",kind:"method",name:"openMenu",type:"() => void",description:"Opens this menu (sets the ancestor menubar's value to this menu's value). Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",part:"RadioGroup",kind:"method",name:"setValue",type:"(value: string) => void",description:"Sets `value` and dispatches `value-change`. The value it already holds is a no-op. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."},{framework:"webComponents",kind:"method",name:"setMenu",type:"(menu: string) => void",description:"Sets `menu` and dispatches `menu-change` plus a non-bubbling `skdx-menubar-internal-update`, a no-op if already that value. Web Components exposes this documented state/action on its element for native JavaScript and composed elements. Other wrappers coordinate their parts through context, injected state or callback props; this instance helper is separate from the common interaction contract."}]};export{v as menubarContract};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changeDetail as n}from"./change-reason.js";import{forceMountProp as e}from"./overlay-parts.js";import{ROOT_DATA_ATTRIBUTES as a}from"./styling-hooks.js";import{withoutScope as t}from"./types.js";var l={name:"NavigationMenu",purpose:"A hover/click-driven navigation menu. The root owns which item is open and hands hover-intent timers to its items.",props:[{name:"value",type:"string | null",defaultValue:"null",required:!1,description:"Value of the currently open item, for controlled usage. `null` means nothing is open."},{name:"defaultValue",type:"string | null",defaultValue:"null",required:!1,description:"Initially open item value, for uncontrolled usage. `null` (the default) starts closed."},{name:"delayDuration",type:"number",defaultValue:"200",required:!1,description:"Delay before hovering a trigger opens its content (ms)."},{name:"skipDelayDuration",type:"number",defaultValue:"300",required:!1,description:"Window after closing during which re-hovering a sibling trigger opens instantly (ms)."},{name:"closeDelayDuration",type:"number",defaultValue:"150",required:!1,description:"Grace period after pointer leaves trigger or panel before close (ms)."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether Link activation closes the panel."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'horizontal'",required:!1,description:"Axis the menu runs along. Sets data-orientation on the root and the list, and switches roving focus between Left/Right and Up/Down."},{name:"items",type:"NavigationMenuItem[]",required:!1,description:"Menu entries the root derives; omit to hand-compose."},{name:"renderItem",type:"(item: NavigationMenuItem) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue expresses the same customisation as a scoped #item slot and Web Components as a light-DOM slot="item" child keyed by data-value \u2014 each framework's own idiomatic mechanism for the hook, and neither a declared prop an extractor can see. Web Components additionally has no property-only equivalent, since an HTML attribute cannot carry a function; a Web Components entry with no matching slotted node falls back to its plain label, which is what the other ports render when their hook returns nothing. Angular ships the same name as a TemplateRef input.`,description:"Customize what renders inside derived entry (trigger body, link content)."}],events:[{name:"value-change",detail:n("string | null"),description:"Fires with the new open value whenever it changes, and with `null` when everything closes."}],parts:[{name:"List",description:"The top-level `<ul>` of triggers and links, and the roving-focus ring the arrow keys move within.",props:[{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether the arrow keys wrap from the last top-level item back to the first, and vice versa."}]},{name:"Item",description:"One top-level entry, pairing a Trigger and Content (or a bare Link) under a shared value.",props:[{name:"value",type:"string",required:!0,description:"This item's identifier, handed down through context to its own Trigger and Content so the menu knows which panel the item owns."}]},{name:"Content",description:"The panel an Item's Trigger opens, holding the links and layout for that top-level entry.",props:[t(e)]},{name:"Indicator",description:"Optional arrow or highlight that tracks the open item along the List, rendered outside it.",props:[t(e)]},{name:"Link",description:"A plain navigation link inside the menu or an open content panel.",props:[{name:"active",type:"boolean",required:!1,description:'Marks the link as the current page: sets data-active and aria-current="page".'},{name:"href",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte type their Link props on the anchor attribute set itself (AnchorHTMLAttributes, HTMLAnchorAttributes) and spread the rest onto the element, so href arrives as the native attribute with no named prop for an extractor to read. Vue, Angular and Web Components render the anchor from a template and must name it to bind it.",type:"string",required:!1,description:"The link's destination, written onto the anchor this part renders."},{name:"target",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte take target off their anchor attribute types and spread it onto the element, so it reaches the anchor natively without a named prop. Vue, Angular and Web Components bind it from a template and name it to read it back for the rel guard.",type:"string",required:!1,description:"Native target attribute for the anchor, such as _blank to open a new browsing context."},{name:"rel",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte take rel off their anchor attribute types rather than declaring it \u2014 Svelte still resolves it against target with the shared helper, React writes through whatever the caller passes. Vue, Angular and Web Components name it because their templates bind the resolved value rather than the raw one.",type:"string",required:!1,description:"Native rel attribute for the anchor; resolves to noopener noreferrer when target is _blank and no explicit value is given."}]}],slots:[{name:"default",description:"Root content \u2014 a NavigationMenuList and optional NavigationMenuIndicator."}],states:["closed","open"],controlledUncontrolled:"Supports both: `value` for controlled, `defaultValue` for uncontrolled. The closed sentinel is `null`.",formBehavior:"Not applicable \u2014 NavigationMenu does not participate in forms.",validationBehavior:"Not applicable \u2014 NavigationMenu carries no value and participates in no validation.",accessibility:{role:"nav (unnamed unless the consumer supplies aria-label \u2014 see the note below)",keyboard:[{key:"ArrowLeft/ArrowRight (horizontal) or ArrowUp/ArrowDown (vertical)",behavior:"Move focus between top-level triggers/links, wrapping at ends."},{key:"Enter/Space",behavior:"Toggle the focused trigger immediately, with no open delay: it opens a closed panel and closes the one it just opened. Trigger is a disclosure button carrying aria-expanded, which APG requires to toggle."},{key:"ArrowDown (on open trigger)",behavior:"Move focus into trigger content panel."},{key:"Escape",behavior:"Close open content and refocuses its trigger."},{key:"Home",behavior:"Move focus to first top-level trigger/link."},{key:"End",behavior:"Moves focus to the last top-level trigger or link. No state change and no event."}],ariaAttributes:["trigger's aria-expanded reflects whether its content is open","trigger's aria-controls points at its content's id","content's aria-labelledby points back at its trigger's id","aria-label on the root landmark is a **required authoring input** for any localised application and for any page carrying more than one navigation landmark, and the root emits no attribute at all when the consumer supplies none. An untranslated English default baked into a library is an i18n leak, and a generic name masks the WCAG failure instead of surfacing it \u2014 a footer or breadcrumb nav announced under a generic default is worse than an unnamed one, while a single nav landmark needs no accessible name to begin with. Unnamed is the honest default: it leaves the omission visible to an audit rather than papering over it.","`aria-label` and `href` on the Link part are host accommodations rather than canonical properties \u2014 a component host must declare any attribute it means to reflect \u2014 so they are excluded from the canonical property table."],focusManagement:"Natural tab order \u2014 every top-level trigger and link is its own tab stop, per the APG disclosure-navigation pattern. The axis arrows move focus between them without changing the tab order, and the ring is scoped to the list's direct `<li>` children so an open panel's own links stay out of it. ArrowDown on an open trigger is the keyboard route *into* that panel, landing on its first tabbable element; Escape is the route back out, returning focus to the trigger that owned the closed content. Moving focus out of an open panel, by Shift+Tab or otherwise, closes it."},styling:{cssParts:["root","list","item","trigger","content","link","indicator"],dataAttributes:[...a,"data-state","data-active","data-orientation","data-skdx-nav-trigger","data-value",{name:"data-skdx-navigation-menu-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:["--skdx-navigation-menu-indicator-offset","--skdx-navigation-menu-indicator-size"]},publicMethods:[]};export{l as navigationMenuContract};
|
|
1
|
+
import{changeDetail as n}from"./change-reason.js";import{forceMountProp as e}from"./overlay-parts.js";import{ROOT_DATA_ATTRIBUTES as a}from"./styling-hooks.js";import{withoutScope as t}from"./types.js";var l={name:"NavigationMenu",purpose:"A hover/click-driven navigation menu. The root owns which item is open and hands hover-intent timers to its items.",props:[{name:"value",type:"string | null",defaultValue:"null",required:!1,description:"Value of the currently open item, for controlled usage. `null` means nothing is open."},{name:"defaultValue",type:"string | null",defaultValue:"null",required:!1,description:"Initially open item value, for uncontrolled usage. `null` (the default) starts closed."},{name:"delayDuration",type:"number",defaultValue:"200",required:!1,description:"Delay before hovering a trigger opens its content (ms)."},{name:"skipDelayDuration",type:"number",defaultValue:"300",required:!1,description:"Window after closing during which re-hovering a sibling trigger opens instantly (ms)."},{name:"closeDelayDuration",type:"number",defaultValue:"150",required:!1,description:"Grace period after pointer leaves trigger or panel before close (ms)."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether Link activation closes the panel."},{name:"orientation",type:"'horizontal' | 'vertical'",defaultValue:"'horizontal'",required:!1,description:"Axis the menu runs along. Sets data-orientation on the root and the list, and switches roving focus between Left/Right and Up/Down."},{name:"items",type:"NavigationMenuItem[]",required:!1,description:"Menu entries the root derives; omit to hand-compose."},{name:"renderItem",type:"(item: NavigationMenuItem) => React.ReactNode",required:!1,frameworks:["react","svelte","angular"],exception:"render-prop",frameworksReason:`Vue expresses the same customisation as a scoped #item slot and Web Components as a light-DOM slot="item" child keyed by data-value \u2014 each framework's own idiomatic mechanism for the hook, and neither a declared prop an extractor can see. Web Components additionally has no property-only equivalent, since an HTML attribute cannot carry a function; a Web Components entry with no matching slotted node falls back to its plain label, which is what the other ports render when their hook returns nothing. Angular ships the same name as a TemplateRef input.`,description:"Customize what renders inside derived entry (trigger body, link content)."}],events:[{name:"value-change",detail:n("string | null"),description:"Fires with the new open value whenever it changes, and with `null` when everything closes."}],parts:[{name:"List",description:"The top-level `<ul>` of triggers and links, and the roving-focus ring the arrow keys move within.",props:[{name:"loop",type:"boolean",defaultValue:"true",required:!1,description:"Whether the arrow keys wrap from the last top-level item back to the first, and vice versa."}]},{name:"Item",description:"One top-level entry, pairing a Trigger and Content (or a bare Link) under a shared value.",props:[{name:"value",type:"string",required:!0,description:"This item's identifier, handed down through context to its own Trigger and Content so the menu knows which panel the item owns."}]},{name:"Content",description:"The panel an Item's Trigger opens, holding the links and layout for that top-level entry.",props:[t(e)]},{name:"Indicator",description:"Optional arrow or highlight that tracks the open item along the List, rendered outside it.",props:[t(e)]},{name:"Link",description:"A plain navigation link inside the menu or an open content panel.",props:[{name:"active",type:"boolean",required:!1,description:'Marks the link as the current page: sets data-active and aria-current="page".'},{name:"href",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte type their Link props on the anchor attribute set itself (AnchorHTMLAttributes, HTMLAnchorAttributes) and spread the rest onto the element, so href arrives as the native attribute with no named prop for an extractor to read. Vue, Angular and Web Components render the anchor from a template and must name it to bind it.",type:"string",required:!1,description:"The link's destination, written onto the anchor this part renders."},{name:"target",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte take target off their anchor attribute types and spread it onto the element, so it reaches the anchor natively without a named prop. Vue, Angular and Web Components bind it from a template and name it to read it back for the rel guard.",type:"string",required:!1,description:"Native target attribute for the anchor, such as _blank to open a new browsing context."},{name:"rel",frameworks:["vue","angular","webComponents"],exception:"attribute-passthrough",frameworksReason:"React and Svelte take rel off their anchor attribute types rather than declaring it \u2014 Svelte still resolves it against target with the shared helper, React writes through whatever the caller passes. Vue, Angular and Web Components name it because their templates bind the resolved value rather than the raw one.",type:"string",required:!1,description:"Native rel attribute for the anchor; resolves to noopener noreferrer when target is _blank and no explicit value is given."}]}],slots:[{name:"default",description:"Root content \u2014 a NavigationMenuList and optional NavigationMenuIndicator."},{name:"item",frameworks:["vue","webComponents"],exception:"render-prop",frameworksReason:"vue, webComponents expose item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customize what renders inside derived entry (trigger body, link content)."},{name:"render-item",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-item as native content projection for the renderItem rendering capability. Other frameworks pass the documented renderItem render prop or template instead of a named slot with this spelling.",description:"Customize what renders inside derived entry (trigger body, link content)."}],states:["closed","open"],controlledUncontrolled:"Supports both: `value` for controlled, `defaultValue` for uncontrolled. The closed sentinel is `null`.",formBehavior:"Not applicable \u2014 NavigationMenu does not participate in forms.",validationBehavior:"Not applicable \u2014 NavigationMenu carries no value and participates in no validation.",accessibility:{role:"nav (unnamed unless the consumer supplies aria-label \u2014 see the note below)",keyboard:[{key:"ArrowLeft/ArrowRight (horizontal) or ArrowUp/ArrowDown (vertical)",behavior:"Move focus between top-level triggers/links, wrapping at ends."},{key:"Enter/Space",behavior:"Toggle the focused trigger immediately, with no open delay: it opens a closed panel and closes the one it just opened. Trigger is a disclosure button carrying aria-expanded, which APG requires to toggle."},{key:"ArrowDown (on open trigger)",behavior:"Move focus into trigger content panel."},{key:"Escape",behavior:"Close open content and refocuses its trigger."},{key:"Home",behavior:"Move focus to first top-level trigger/link."},{key:"End",behavior:"Moves focus to the last top-level trigger or link. No state change and no event."}],ariaAttributes:["trigger's aria-expanded reflects whether its content is open","trigger's aria-controls points at its content's id","content's aria-labelledby points back at its trigger's id","aria-label on the root landmark is a **required authoring input** for any localised application and for any page carrying more than one navigation landmark, and the root emits no attribute at all when the consumer supplies none. An untranslated English default baked into a library is an i18n leak, and a generic name masks the WCAG failure instead of surfacing it \u2014 a footer or breadcrumb nav announced under a generic default is worse than an unnamed one, while a single nav landmark needs no accessible name to begin with. Unnamed is the honest default: it leaves the omission visible to an audit rather than papering over it.","`aria-label` and `href` on the Link part are host accommodations rather than canonical properties \u2014 a component host must declare any attribute it means to reflect \u2014 so they are excluded from the canonical property table."],focusManagement:"Natural tab order \u2014 every top-level trigger and link is its own tab stop, per the APG disclosure-navigation pattern. The axis arrows move focus between them without changing the tab order, and the ring is scoped to the list's direct `<li>` children so an open panel's own links stay out of it. ArrowDown on an open trigger is the keyboard route *into* that panel, landing on its first tabbable element; Escape is the route back out, returning focus to the trigger that owned the closed content. Moving focus out of an open panel, by Shift+Tab or otherwise, closes it."},styling:{cssParts:["root","list","item","trigger","content","link","indicator"],dataAttributes:[...a,"data-state","data-active","data-orientation","data-skdx-nav-trigger","data-value",{name:"data-skdx-navigation-menu-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:["--skdx-navigation-menu-indicator-offset","--skdx-navigation-menu-indicator-size"]},publicMethods:[]};export{l as navigationMenuContract};
|