vectis-ui 0.10.3 → 0.11.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/components/VBadge/VBadge.d.ts +1 -1
- package/dist/components/VButton/VButtonGroup.css +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +4 -4
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/icons/first_page.d.ts +5 -0
- package/dist/components/VIcon/icons/first_page.js +9 -0
- package/dist/components/VIcon/icons/first_page.js.map +1 -0
- package/dist/components/VIcon/icons/index.d.ts +12 -2
- package/dist/components/VIcon/icons/index.js +70 -66
- package/dist/components/VIcon/icons/index.js.map +1 -1
- package/dist/components/VIcon/icons/last_page.d.ts +5 -0
- package/dist/components/VIcon/icons/last_page.js +9 -0
- package/dist/components/VIcon/icons/last_page.js.map +1 -0
- package/dist/components/VIcon/icons/names.d.ts +2 -2
- package/dist/components/VIcon/icons/names.js +1 -1
- package/dist/components/VIcon/icons/names.js.map +1 -1
- package/dist/components/VMenu/VMenu.d.ts +6 -3
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js +34 -31
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.css +1 -1
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +70 -67
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.css +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +7 -0
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.d.ts +6 -0
- package/dist/components/VMenu/context.js +4 -1
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.css +1 -1
- package/dist/components/VPagination/VPagination.d.ts +30 -6
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +125 -75
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +5 -5
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js +2 -0
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons.js +36 -34
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"names.js","names":[],"sources":["../../../../src/components/VIcon/icons/names.ts"],"sourcesContent":["/*\n * GENERATED FILE — do not edit by hand.\n * Regenerate: pnpm icons · Source: scripts/build-icons.ts\n *\n * Material Symbols Rounded (wght 400 · GRAD 0 · opsz 24)\n * google/material-design-icons @ 528cb964c01fb2b09bc3b9208f82b6d8f8c1c1e2\n * Apache-2.0 licence © Google.\n */\n\nconst NAMES = [\n 'arrow_downward',\n 'arrow_downward_alt',\n 'arrow_drop_down',\n 'arrow_drop_up',\n 'arrow_left_alt',\n 'arrow_right_alt',\n 'arrow_upward',\n 'arrow_upward_alt',\n 'attach_file',\n 'audio_file',\n 'calendar_today',\n 'check',\n 'check_circle',\n 'chevron_left',\n 'chevron_right',\n 'close',\n 'cloud_upload',\n 'code',\n 'description',\n 'error',\n 'expand_less',\n 'expand_more',\n 'folder_zip',\n 'image',\n 'info',\n 'more_horiz',\n 'notifications',\n 'picture_as_pdf',\n 'schedule',\n 'search',\n 'swap_vert',\n 'table_chart',\n 'video_file',\n 'warning',\n] as const\n\n/** The icon names the DS renders itself: the contract of a consumer resolver. */\nexport type IconName = (typeof NAMES)[number]\n\n/**\n * The names alone, carrying no drawing at all. `classIconResolver` in `strict` mode\n * asks nothing more than \"does the design system ship this name?\", and a module of\n * its own is what stops a consumer who wired in their OWN icon library from\n * downloading
|
|
1
|
+
{"version":3,"file":"names.js","names":[],"sources":["../../../../src/components/VIcon/icons/names.ts"],"sourcesContent":["/*\n * GENERATED FILE — do not edit by hand.\n * Regenerate: pnpm icons · Source: scripts/build-icons.ts\n *\n * Material Symbols Rounded (wght 400 · GRAD 0 · opsz 24)\n * google/material-design-icons @ 528cb964c01fb2b09bc3b9208f82b6d8f8c1c1e2\n * Apache-2.0 licence © Google.\n */\n\nconst NAMES = [\n 'arrow_downward',\n 'arrow_downward_alt',\n 'arrow_drop_down',\n 'arrow_drop_up',\n 'arrow_left_alt',\n 'arrow_right_alt',\n 'arrow_upward',\n 'arrow_upward_alt',\n 'attach_file',\n 'audio_file',\n 'calendar_today',\n 'check',\n 'check_circle',\n 'chevron_left',\n 'chevron_right',\n 'close',\n 'cloud_upload',\n 'code',\n 'description',\n 'error',\n 'expand_less',\n 'expand_more',\n 'first_page',\n 'folder_zip',\n 'image',\n 'info',\n 'last_page',\n 'more_horiz',\n 'notifications',\n 'picture_as_pdf',\n 'schedule',\n 'search',\n 'swap_vert',\n 'table_chart',\n 'video_file',\n 'warning',\n] as const\n\n/** The icon names the DS renders itself: the contract of a consumer resolver. */\nexport type IconName = (typeof NAMES)[number]\n\n/**\n * The names alone, carrying no drawing at all. `classIconResolver` in `strict` mode\n * asks nothing more than \"does the design system ship this name?\", and a module of\n * its own is what stops a consumer who wired in their OWN icon library from\n * downloading 36 Material paths to answer that one question.\n */\nexport const builtinIconNames: ReadonlySet<string> = /* @__PURE__ */ new Set(NAMES)\n"],"mappings":"AAyDA,IAAa,oBAAwD,IAAI,IAAI,gBAhD/D,0aAgD+D,CAAK"}
|
|
@@ -26,15 +26,18 @@ interface MenuProps {
|
|
|
26
26
|
}
|
|
27
27
|
type __VLS_Props = MenuProps;
|
|
28
28
|
/**
|
|
29
|
-
* What the trigger has to carry: the link to the panel it opens,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* What the trigger has to carry: the link to the panel it opens, the attributes telling
|
|
30
|
+
* assistive technology that a menu is attached to this button and whether it is currently
|
|
31
|
+
* open, and the anchor name the panel is positioned against.
|
|
32
32
|
*/
|
|
33
33
|
export type MenuTriggerProps = {
|
|
34
34
|
popovertarget: string;
|
|
35
35
|
'aria-haspopup': 'menu';
|
|
36
36
|
'aria-expanded': boolean;
|
|
37
37
|
'aria-controls': string;
|
|
38
|
+
style: {
|
|
39
|
+
'anchor-name': string;
|
|
40
|
+
};
|
|
38
41
|
};
|
|
39
42
|
type __VLS_Slots = {
|
|
40
43
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenu.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal and anchoring. JavaScript supplies the model bridge
|
|
1
|
+
{"version":3,"file":"VMenu.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal, and CSS anchoring places them. JavaScript supplies\n * the model bridge and ARIA menu focus management.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuAnchor, menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, the attributes telling\n * assistive technology that a menu is attached to this button and whether it is currently\n * open, and the anchor name the panel is positioned against.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n style: { 'anchor-name': string }\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\nconst anchor = menuAnchor(menuId)\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n style: { 'anchor-name': anchor },\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the trigger when it\n// closes. Keeping the state in step alone would leave a keyboard user stranded at the top of\n// the page every time a menu opened or closed.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal; a click outside, Escape; leaves the focus nowhere, on the\n // page body. Only then is it handed back to the trigger: if the focus has already moved\n // somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * The position comes from the trigger's anchor name. Naming the trigger as `source` still gives\n * the opening the same invoker a click would.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * Relay the imperative panel API so programmatic opening can move focus immediately without\n * waiting for a model render.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :anchor=\"anchor\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":""}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { usePopoverModel as e } from "../../composables/usePopover.js";
|
|
2
2
|
import { isKeyboardFocus as t } from "../../utils/focus.js";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { Fragment as
|
|
3
|
+
import { menuAnchor as n, menuInvoker as r, menuKey as i } from "./context.js";
|
|
4
|
+
import a from "./VMenuPanel.js";
|
|
5
|
+
import { Fragment as o, computed as s, createElementBlock as c, createVNode as l, defineComponent as u, mergeModels as d, openBlock as f, provide as p, ref as m, renderSlot as h, unref as g, useId as _, useModel as v, withCtx as y } from "vue";
|
|
6
6
|
//#region src/components/VMenu/VMenu.vue?vue&type=script&setup=true&lang.ts
|
|
7
|
-
var
|
|
7
|
+
var b = /*@__PURE__*/ u({
|
|
8
8
|
__name: "VMenu",
|
|
9
|
-
props: /*@__PURE__*/
|
|
9
|
+
props: /*@__PURE__*/ d({
|
|
10
10
|
placement: { default: "bottom-start" },
|
|
11
11
|
size: { default: "sm" },
|
|
12
12
|
compact: {
|
|
@@ -26,43 +26,46 @@ var y = /*@__PURE__*/ l({
|
|
|
26
26
|
openModifiers: {}
|
|
27
27
|
}),
|
|
28
28
|
emits: ["update:open"],
|
|
29
|
-
setup(
|
|
30
|
-
let
|
|
31
|
-
popovertarget:
|
|
29
|
+
setup(u, { expose: d }) {
|
|
30
|
+
let b = v(u, "open"), x = m(null), S = _(), C = n(S), w = s(() => ({
|
|
31
|
+
popovertarget: S,
|
|
32
32
|
"aria-haspopup": "menu",
|
|
33
|
-
"aria-expanded":
|
|
34
|
-
"aria-controls":
|
|
33
|
+
"aria-expanded": b.value,
|
|
34
|
+
"aria-controls": S,
|
|
35
|
+
style: { "anchor-name": C }
|
|
35
36
|
}));
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
if (
|
|
37
|
+
p(i, { closeAll: () => x.value?.close() });
|
|
38
|
+
function T(e) {
|
|
39
|
+
if (b.value = e, e) t(document.activeElement) ? x.value?.focusFirst() : x.value?.focusPanel();
|
|
39
40
|
else {
|
|
40
41
|
let e = document.activeElement;
|
|
41
|
-
(!e || e === document.body ||
|
|
42
|
+
(!e || e === document.body || x.value?.el?.contains(e)) && r(S)?.focus();
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
45
|
+
function E() {
|
|
46
|
+
x.value?.show(r(S) ?? void 0);
|
|
46
47
|
}
|
|
47
|
-
return e(
|
|
48
|
-
show:
|
|
49
|
-
close: () =>
|
|
50
|
-
el:
|
|
51
|
-
}), (e, t) => (
|
|
52
|
-
id:
|
|
48
|
+
return e(b, () => x.value?.shown ?? !1, E, () => x.value?.close()), d({
|
|
49
|
+
show: E,
|
|
50
|
+
close: () => x.value?.close(),
|
|
51
|
+
el: s(() => x.value?.el ?? null)
|
|
52
|
+
}), (e, t) => (f(), c(o, null, [h(e.$slots, "trigger", { triggerProps: w.value }), l(a, {
|
|
53
|
+
id: g(S),
|
|
53
54
|
ref_key: "panelRef",
|
|
54
|
-
ref:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
ref: x,
|
|
56
|
+
anchor: g(C),
|
|
57
|
+
placement: u.placement,
|
|
58
|
+
size: u.size,
|
|
59
|
+
compact: u.compact,
|
|
60
|
+
width: u.width,
|
|
61
|
+
"match-trigger": u.matchTrigger,
|
|
62
|
+
onToggle: T
|
|
61
63
|
}, {
|
|
62
|
-
default:
|
|
64
|
+
default: y(() => [h(e.$slots, "default")]),
|
|
63
65
|
_: 3
|
|
64
66
|
}, 8, [
|
|
65
67
|
"id",
|
|
68
|
+
"anchor",
|
|
66
69
|
"placement",
|
|
67
70
|
"size",
|
|
68
71
|
"compact",
|
|
@@ -72,6 +75,6 @@ var y = /*@__PURE__*/ l({
|
|
|
72
75
|
}
|
|
73
76
|
});
|
|
74
77
|
//#endregion
|
|
75
|
-
export {
|
|
78
|
+
export { b as default };
|
|
76
79
|
|
|
77
80
|
//# sourceMappingURL=VMenu.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenu.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal and anchoring. JavaScript supplies the model bridge
|
|
1
|
+
{"version":3,"file":"VMenu.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenu.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Native auto popovers own light dismissal, and CSS anchoring places them. JavaScript supplies\n * the model bridge and ARIA menu focus management.\n */\n\nimport { computed, provide, ref, useId } from 'vue'\n\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuAnchor, menuInvoker, menuKey } from './context'\nimport type { MenuPlacement, MenuSize } from './context'\nimport { usePopoverModel } from '../../composables/usePopover'\nimport { isKeyboardFocus } from '../../utils/focus'\n\ninterface MenuProps {\n /**\n * Where the panel opens relative to its trigger. The browser moves it to another\n * side by itself when there is not enough room.\n */\n placement?: MenuPlacement\n /**\n * How tall the rows are: 32, 40 or 48 pixels. Submenus inherit it, so it is set\n * once on the menu as a whole.\n */\n size?: MenuSize\n /** Takes 4px off the height of every row, submenus included. */\n compact?: boolean\n /**\n * A width for the panel: a number is read as pixels, a string as any CSS length or\n * keyword (`16rem`, `max-content`). It applies to the menu itself; submenus keep the\n * default width.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than the button that opened it, while leaving\n * it free to grow wider for its content. Submenus are unaffected.\n */\n matchTrigger?: boolean\n}\n\nwithDefaults(defineProps<MenuProps>(), {\n placement: 'bottom-start',\n size: 'sm',\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\n/**\n * Whether the menu is showing. It starts closed and is fed BY the panel, so the browser's\n * own dismissal (a click outside, Escape, choosing a command) writes back to it.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\n/**\n * What the trigger has to carry: the link to the panel it opens, the attributes telling\n * assistive technology that a menu is attached to this button and whether it is currently\n * open, and the anchor name the panel is positioned against.\n */\nexport type MenuTriggerProps = {\n popovertarget: string\n 'aria-haspopup': 'menu'\n 'aria-expanded': boolean\n 'aria-controls': string\n style: { 'anchor-name': string }\n}\n\ndefineSlots<{\n /**\n * The button that opens the menu. Bind the `triggerProps` it receives onto it: that\n * is what wires the two together.\n */\n trigger(props: { triggerProps: MenuTriggerProps }): unknown\n /** The contents of the menu: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelRef = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst menuId = useId()\nconst anchor = menuAnchor(menuId)\n\nconst triggerProps = computed<MenuTriggerProps>(() => ({\n popovertarget: menuId,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open.value,\n 'aria-controls': menuId,\n style: { 'anchor-name': anchor },\n}))\n\n// Closing this panel closes every submenu with it: they are rendered inside it, and\n// the browser closes a stack of popovers from the outside in.\nprovide(menuKey, { closeAll: () => panelRef.value?.close() })\n\n// @a11y\n// The focus half of the bridge: into the panel when it opens, back to the trigger when it\n// closes. Keeping the state in step alone would leave a keyboard user stranded at the top of\n// the page every time a menu opened or closed.\nfunction onToggle(value: boolean) {\n open.value = value\n if (value) {\n if (isKeyboardFocus(document.activeElement)) panelRef.value?.focusFirst()\n else panelRef.value?.focusPanel()\n } else {\n // The browser's own dismissal; a click outside, Escape; leaves the focus nowhere, on the\n // page body. Only then is it handed back to the trigger: if the focus has already moved\n // somewhere else deliberately, it must be left alone.\n const active = document.activeElement\n if (!active || active === document.body || panelRef.value?.el?.contains(active)) {\n menuInvoker(menuId)?.focus()\n }\n }\n}\n\n/*\n * The position comes from the trigger's anchor name. Naming the trigger as `source` still gives\n * the opening the same invoker a click would.\n */\nfunction openAtTrigger() {\n panelRef.value?.show(menuInvoker(menuId) ?? undefined)\n}\n\n// The open state is the panel's, read back from it rather than copied here: the panel has\n// already recorded the browser's answer by the time it reports the toggle.\nusePopoverModel(\n open,\n () => panelRef.value?.shown ?? false,\n openAtTrigger,\n () => panelRef.value?.close(),\n)\n\n/*\n * Relay the imperative panel API so programmatic opening can move focus immediately without\n * waiting for a model render.\n */\ndefineExpose({\n /** Opens the menu at once, without waiting for the model to come round. */\n show: openAtTrigger,\n /** Closes it at once, submenus included. */\n close: () => panelRef.value?.close(),\n /** The panel element, for what neither of the two above covers. */\n el: computed(() => panelRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <VMenuPanel\n :id=\"menuId\"\n ref=\"panelRef\"\n :anchor=\"anchor\"\n :placement=\"placement\"\n :size=\"size\"\n :compact=\"compact\"\n :width=\"width\"\n :match-trigger=\"matchTrigger\"\n @toggle=\"onToggle\"\n >\n <slot />\n </VMenuPanel>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDA,IAAM,IAAO,EAAoB,GAAC,MAA0B,GAyBtD,IAAW,EAA4C,IAAI,GAC3D,IAAS,EAAM,GACf,IAAS,EAAW,CAAM,GAE1B,IAAe,SAAkC;GACrD,eAAe;GACf,iBAAiB;GACjB,iBAAiB,EAAK;GACtB,iBAAiB;GACjB,OAAO,EAAE,eAAe,EAAO;EACjC,EAAE;EAIF,EAAQ,GAAS,EAAE,gBAAgB,EAAS,OAAO,MAAM,EAAE,CAAC;EAM5D,SAAS,EAAS,GAAgB;GAEhC,IADA,EAAK,QAAQ,GACT,GACF,AAAI,EAAgB,SAAS,aAAa,IAAG,EAAS,OAAO,WAAW,IACnE,EAAS,OAAO,WAAW;QAC3B;IAIL,IAAM,IAAS,SAAS;IACxB,CAAI,CAAC,KAAU,MAAW,SAAS,QAAQ,EAAS,OAAO,IAAI,SAAS,CAAM,MAC5E,EAAY,CAAM,CAAC,EAAE,MAAM;GAE/B;EACF;EAMA,SAAS,IAAgB;GACvB,EAAS,OAAO,KAAK,EAAY,CAAM,KAAK,KAAA,CAAS;EACvD;SAIA,EACE,SACM,EAAS,OAAO,SAAS,IAC/B,SACM,EAAS,OAAO,MAAM,CAC9B,GAMA,EAAa;GAEX,MAAM;GAEN,aAAa,EAAS,OAAO,MAAM;GAEnC,IAAI,QAAe,EAAS,OAAO,MAAM,IAAI;EAC/C,CAAC,+BAIC,EAAqD,EAAA,QAAA,WAAA,EAA/B,cAAe,EAAA,MAAY,CAAA,GACjD,EAYa,GAAA;GAXV,IAAI,EAAA,CAAA;YACD;GAAJ,KAAI;GACH,QAAQ,EAAA,CAAA;GACR,WAAW,EAAA;GACX,MAAM,EAAA;GACN,SAAS,EAAA;GACT,OAAO,EAAA;GACP,iBAAe,EAAA;GACP;;oBAED,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-menu-item{align-items:center;gap:var(--control-gap);inline-size:100%;min-block-size:var(--control-height);padding-block:var(--vectis-space-1);padding-inline:var(--control-padding-inline);color:var(--vectis-color-text);border-radius:min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));font-family:inherit;font-size:var(--control-font-size);line-height:var(--vectis-text-body-md-leading);text-align:start;cursor:pointer;background:0 0;border:none;text-decoration:none;display:flex}.v-menu-item-content{flex-direction:column;flex:1;min-inline-size:0;display:flex}.v-menu-item-label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.v-menu-item-sublabel{font-size:var(--vectis-text-caption-size);color:var(--vectis-color-text-muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.v-menu-item-icon,.v-menu-item-chevron{color:var(--vectis-color-text-muted);flex:none}.v-menu-item:hover:not(:disabled,[aria-disabled=true]),.v-menu-item:focus,.v-menu-item[aria-expanded=true]{background:var(--vectis-color-surface-muted);outline:none}.v-menu-item[data-selected]{background:var(--vectis-color-accent-surface);color:var(--vectis-color-accent-text)}.v-menu-item[data-selected]:hover:not(:disabled,[aria-disabled=true]),.v-menu-item[data-selected]:focus,.v-menu-item[data-selected][aria-expanded=true]{background:color-mix(in oklab, var(--vectis-color-accent-surface), var(--vectis-color-accent-text) 8%)}.v-menu-item[data-tone=danger]{color:var(--vectis-color-danger-text)}.v-menu-item[data-tone=danger]:hover:not(:disabled,[aria-disabled=true]),.v-menu-item[data-tone=danger]:focus,.v-menu-item[data-tone=danger][aria-expanded=true]{background:var(--vectis-color-danger-surface)}.v-menu-item:disabled,.v-menu-item[aria-disabled=true]{color:var(--vectis-color-text-subtle);cursor:not-allowed;background:0 0}.v-menu-item:is([data-selected],[data-tone=danger],:disabled,[aria-disabled=true]) :is(.v-menu-item-icon,.v-menu-item-sublabel){color:inherit}@media (forced-colors:active){.v-menu-item:focus,.v-menu-item[aria-expanded=true]{outline:var(--vectis-focus-ring-width) solid Highlight;outline-offset:calc(-1 * var(--vectis-focus-ring-width))}.v-menu-item.v-menu-item[data-selected]{forced-color-adjust:none;color:highlighttext;background-color:highlight}}}
|
|
2
|
+
@layer vectis.components{.v-menu-item{align-items:center;gap:var(--control-gap);inline-size:100%;min-block-size:var(--control-height);padding-block:var(--vectis-space-1);padding-inline:var(--control-padding-inline);color:var(--vectis-color-text);border-radius:min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));font-family:inherit;font-size:var(--control-font-size);line-height:var(--vectis-text-body-md-leading);text-align:start;cursor:pointer;background:0 0;border:none;text-decoration:none;display:flex}.v-menu-item[aria-haspopup]{anchor-name:var(--menu-anchor)}.v-menu-item-content{flex-direction:column;flex:1;min-inline-size:0;display:flex}.v-menu-item-label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.v-menu-item-sublabel{font-size:var(--vectis-text-caption-size);color:var(--vectis-color-text-muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.v-menu-item-icon,.v-menu-item-chevron{color:var(--vectis-color-text-muted);flex:none}.v-menu-item:hover:not(:disabled,[aria-disabled=true]),.v-menu-item:focus,.v-menu-item[aria-expanded=true]{background:var(--vectis-color-surface-muted);outline:none}.v-menu-item[data-selected]{background:var(--vectis-color-accent-surface);color:var(--vectis-color-accent-text)}.v-menu-item[data-selected]:hover:not(:disabled,[aria-disabled=true]),.v-menu-item[data-selected]:focus,.v-menu-item[data-selected][aria-expanded=true]{background:color-mix(in oklab, var(--vectis-color-accent-surface), var(--vectis-color-accent-text) 8%)}.v-menu-item[data-tone=danger]{color:var(--vectis-color-danger-text)}.v-menu-item[data-tone=danger]:hover:not(:disabled,[aria-disabled=true]),.v-menu-item[data-tone=danger]:focus,.v-menu-item[data-tone=danger][aria-expanded=true]{background:var(--vectis-color-danger-surface)}.v-menu-item:disabled,.v-menu-item[aria-disabled=true]{color:var(--vectis-color-text-subtle);cursor:not-allowed;background:0 0}.v-menu-item:is([data-selected],[data-tone=danger],:disabled,[aria-disabled=true]) :is(.v-menu-item-icon,.v-menu-item-sublabel){color:inherit}@media (forced-colors:active){.v-menu-item:focus,.v-menu-item[aria-expanded=true]{outline:var(--vectis-focus-ring-width) solid Highlight;outline-offset:calc(-1 * var(--vectis-focus-ring-width))}.v-menu-item.v-menu-item[data-selected]{forced-color-adjust:none;color:highlighttext;background-color:highlight}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenuItem.js","names":[],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// When a submenu is opened from code rather than by a click, the item must be handed to the\n// browser as the source of that opening. The implicit anchor is only established natively, on\n// click; without it the panel has nothing to position itself against and lands at the corner of\n// the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VMenuItem.js","names":[],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuAnchor, menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\n// Hover and the keyboard open the submenu from code: see the `anchor` prop of VMenuPanel.\nconst subAnchor = menuAnchor(subId)\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n :style=\"hasSubmenu() ? { '--menu-anchor': subAnchor } : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n :anchor=\"subAnchor\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n /* The name is unique per item and read by the submenu panel's `position-anchor`. */\n .v-menu-item[aria-haspopup] {\n anchor-name: var(--menu-anchor);\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -4,14 +4,14 @@ import { useInertLink as n } from "../../composables/useInertLink.js";
|
|
|
4
4
|
import { chevron_right as r } from "../VIcon/icons/chevron_right.js";
|
|
5
5
|
import { isRtl as i } from "../../utils/direction.js";
|
|
6
6
|
import { useTimer as a } from "../../composables/useTimer.js";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { Fragment as
|
|
7
|
+
import { menuAnchor as o, menuKey as s } from "./context.js";
|
|
8
|
+
import c from "./VMenuPanel.js";
|
|
9
|
+
import { Fragment as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createTextVNode as m, defineComponent as h, inject as g, mergeProps as _, normalizeProps as v, openBlock as y, ref as b, renderSlot as x, resolveDynamicComponent as S, toDisplayString as C, unref as w, useAttrs as T, useId as E, useSlots as D, withCtx as O } from "vue";
|
|
10
10
|
//#region src/components/VMenu/VMenuItem.vue?vue&type=script&setup=true&lang.ts
|
|
11
|
-
var
|
|
11
|
+
var k = { class: "v-menu-item-content" }, A = { class: "v-menu-item-label" }, j = {
|
|
12
12
|
key: 0,
|
|
13
13
|
class: "v-menu-item-sublabel"
|
|
14
|
-
},
|
|
14
|
+
}, M = /*@__PURE__*/ h({
|
|
15
15
|
inheritAttrs: !1,
|
|
16
16
|
__name: "VMenuItem",
|
|
17
17
|
props: {
|
|
@@ -31,81 +31,82 @@ var O = { class: "v-menu-item-content" }, k = { class: "v-menu-item-label" }, A
|
|
|
31
31
|
href: { default: void 0 }
|
|
32
32
|
},
|
|
33
33
|
emits: ["select"],
|
|
34
|
-
setup(
|
|
35
|
-
let
|
|
36
|
-
function F() {
|
|
37
|
-
return !!P.submenu;
|
|
38
|
-
}
|
|
34
|
+
setup(h, { emit: M }) {
|
|
35
|
+
let N = h, P = M, F = D();
|
|
39
36
|
function I() {
|
|
40
|
-
return
|
|
37
|
+
return !!F.submenu;
|
|
38
|
+
}
|
|
39
|
+
function L() {
|
|
40
|
+
return !I() && N.href !== void 0 ? "a" : "button";
|
|
41
41
|
}
|
|
42
|
-
let
|
|
43
|
-
href: () =>
|
|
44
|
-
inert: () =>
|
|
45
|
-
attrs: () =>
|
|
46
|
-
}),
|
|
47
|
-
function
|
|
48
|
-
|
|
42
|
+
let R = T(), z = n({
|
|
43
|
+
href: () => I() ? void 0 : N.href,
|
|
44
|
+
inert: () => N.disabled,
|
|
45
|
+
attrs: () => R
|
|
46
|
+
}), B = g(s, null);
|
|
47
|
+
function V() {
|
|
48
|
+
N.disabled || I() || (P("select"), B?.closeAll());
|
|
49
49
|
}
|
|
50
|
-
let
|
|
51
|
-
function
|
|
50
|
+
let H = E(), U = o(H), W = b(!1), G = b(null), K = b(null);
|
|
51
|
+
function q(e) {
|
|
52
52
|
if (!(e.altKey || e.ctrlKey || e.metaKey)) {
|
|
53
|
-
if (!
|
|
54
|
-
e.key === " " &&
|
|
53
|
+
if (!I()) {
|
|
54
|
+
e.key === " " && L() === "a" && (e.preventDefault(), K.value?.click());
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
i(
|
|
57
|
+
N.disabled || [
|
|
58
|
+
i(K.value) ? "ArrowLeft" : "ArrowRight",
|
|
59
59
|
"Enter",
|
|
60
60
|
" "
|
|
61
|
-
].includes(e.key) && (e.preventDefault(),
|
|
61
|
+
].includes(e.key) && (e.preventDefault(), G.value?.show(K.value ?? void 0), G.value?.focusFirst());
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
let
|
|
65
|
-
function
|
|
66
|
-
|
|
64
|
+
let J = a();
|
|
65
|
+
function Y(e) {
|
|
66
|
+
N.disabled || e.pointerType === "touch" || (K.value?.focus({ preventScroll: !0 }), I() && J.start(() => G.value?.show(K.value ?? void 0), 150));
|
|
67
67
|
}
|
|
68
|
-
function
|
|
69
|
-
!
|
|
70
|
-
|
|
68
|
+
function X(e) {
|
|
69
|
+
!I() || e.pointerType === "touch" || J.start(() => {
|
|
70
|
+
G.value?.el?.contains(document.activeElement) || G.value?.close();
|
|
71
71
|
}, 150);
|
|
72
72
|
}
|
|
73
|
-
return (n, i) => (
|
|
73
|
+
return (n, i) => (y(), f(l, null, [(y(), u(S(L()), _({
|
|
74
74
|
ref_key: "itemEl",
|
|
75
|
-
ref:
|
|
76
|
-
"aria-disabled":
|
|
77
|
-
"aria-current":
|
|
78
|
-
"aria-haspopup":
|
|
79
|
-
"aria-expanded":
|
|
80
|
-
"aria-controls":
|
|
81
|
-
},
|
|
75
|
+
ref: K,
|
|
76
|
+
"aria-disabled": w(z).isInertLink.value ? "true" : void 0,
|
|
77
|
+
"aria-current": h.selected ? "true" : void 0,
|
|
78
|
+
"aria-haspopup": I() ? "menu" : void 0,
|
|
79
|
+
"aria-expanded": I() ? W.value : void 0,
|
|
80
|
+
"aria-controls": I() ? w(H) : void 0
|
|
81
|
+
}, w(z).attrs.value, {
|
|
82
82
|
role: "menuitem",
|
|
83
83
|
tabindex: "-1",
|
|
84
84
|
class: "v-menu-item",
|
|
85
|
-
type:
|
|
86
|
-
disabled:
|
|
87
|
-
href:
|
|
88
|
-
"data-tone":
|
|
89
|
-
"data-selected":
|
|
90
|
-
popovertarget:
|
|
91
|
-
popovertargetaction:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
type: L() === "button" ? "button" : void 0,
|
|
86
|
+
disabled: L() === "button" ? h.disabled : void 0,
|
|
87
|
+
href: w(z).linkHref.value,
|
|
88
|
+
"data-tone": h.tone,
|
|
89
|
+
"data-selected": h.selected ? "" : void 0,
|
|
90
|
+
popovertarget: I() ? w(H) : void 0,
|
|
91
|
+
popovertargetaction: I() ? "show" : void 0,
|
|
92
|
+
style: I() ? { "--menu-anchor": w(U) } : void 0,
|
|
93
|
+
onClick: V,
|
|
94
|
+
onKeydown: q,
|
|
95
|
+
onPointerenter: Y,
|
|
96
|
+
onPointerleave: X
|
|
96
97
|
}), {
|
|
97
|
-
default:
|
|
98
|
-
|
|
98
|
+
default: O(() => [
|
|
99
|
+
x(n.$slots, "start", {}, () => [h.iconStart ? (y(), u(t, _({
|
|
99
100
|
key: 0,
|
|
100
101
|
class: "v-menu-item-icon"
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
}, w(e)(h.iconStart)), null, 16)) : d("", !0)]),
|
|
103
|
+
p("span", k, [p("span", A, [x(n.$slots, "default", {}, () => [m(C(h.label), 1)])]), h.sublabel !== void 0 || n.$slots.sublabel ? (y(), f("span", j, [x(n.$slots, "sublabel", {}, () => [m(C(h.sublabel), 1)])])) : d("", !0)]),
|
|
104
|
+
I() ? (y(), u(t, {
|
|
104
105
|
key: 0,
|
|
105
|
-
name:
|
|
106
|
+
name: w(r),
|
|
106
107
|
class: "v-menu-item-chevron",
|
|
107
108
|
mirrored: ""
|
|
108
|
-
}, null, 8, ["name"])) :
|
|
109
|
+
}, null, 8, ["name"])) : x(n.$slots, "end", {}, () => [h.iconEnd ? (y(), u(t, v(_({ key: 0 }, w(e)(h.iconEnd))), null, 16)) : d("", !0)], void 0, 1)
|
|
109
110
|
]),
|
|
110
111
|
_: 3
|
|
111
112
|
}, 16, [
|
|
@@ -120,24 +121,26 @@ var O = { class: "v-menu-item-content" }, k = { class: "v-menu-item-label" }, A
|
|
|
120
121
|
"data-tone",
|
|
121
122
|
"data-selected",
|
|
122
123
|
"popovertarget",
|
|
123
|
-
"popovertargetaction"
|
|
124
|
-
|
|
124
|
+
"popovertargetaction",
|
|
125
|
+
"style"
|
|
126
|
+
])), I() ? (y(), u(c, {
|
|
125
127
|
key: 0,
|
|
126
|
-
id:
|
|
128
|
+
id: w(H),
|
|
127
129
|
ref_key: "subPanel",
|
|
128
|
-
ref:
|
|
130
|
+
ref: G,
|
|
129
131
|
placement: "right-start",
|
|
132
|
+
anchor: w(U),
|
|
130
133
|
submenu: "",
|
|
131
|
-
onToggle: i[0] ||= (e) =>
|
|
132
|
-
onPointerenter: i[1] ||= (e) =>
|
|
133
|
-
onPointerleave:
|
|
134
|
+
onToggle: i[0] ||= (e) => W.value = e,
|
|
135
|
+
onPointerenter: i[1] ||= (e) => w(J).cancel(),
|
|
136
|
+
onPointerleave: X
|
|
134
137
|
}, {
|
|
135
|
-
default:
|
|
138
|
+
default: O(() => [x(n.$slots, "submenu")]),
|
|
136
139
|
_: 3
|
|
137
|
-
}, 8, ["id"])) :
|
|
140
|
+
}, 8, ["id", "anchor"])) : d("", !0)], 64));
|
|
138
141
|
}
|
|
139
142
|
});
|
|
140
143
|
//#endregion
|
|
141
|
-
export {
|
|
144
|
+
export { M as default };
|
|
142
145
|
|
|
143
146
|
//# sourceMappingURL=VMenuItem.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenuItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\n// When a submenu is opened from code rather than by a click, the item must be handed to the\n// browser as the source of that opening. The implicit anchor is only established natively, on\n// click; without it the panel has nothing to position itself against and lands at the corner of\n// the viewport, with no error anywhere.\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VMenuItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VMenu/VMenuItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Menu commands use native buttons or links. JavaScript handles inert anchors and submenu hover\n * intent; the panel owns roving focus and dismissal.\n */\n\nimport { inject, ref, useAttrs, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { useInertLink } from '../../composables/useInertLink'\nimport { isRtl } from '../../utils/direction'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport type { IconSource } from '../VIcon/types'\nimport VMenuPanel from './VMenuPanel.vue'\nimport { menuAnchor, menuKey, SUBMENU_HOVER_DELAY } from './context'\n\nimport { useTimer } from '../../composables/useTimer'\n\n/**\n * What a command means, in colour. It is the subset of VButton's tones a menu row can\n * carry: an ordinary command, or a destructive one.\n */\nexport type MenuItemTone = 'neutral' | 'danger'\n\ninterface MenuItemProps {\n /** What the command says. The default slot replaces it. */\n label?: string\n /** A second line under the label, for a shortcut or a short explanation. */\n sublabel?: string\n /**\n * An icon before the label: an icon name, an image address, or an explicit render.\n * The `#start` slot replaces it.\n */\n iconStart?: IconSource\n /** An icon after the label, in the same forms. The `#end` slot replaces it. */\n iconEnd?: IconSource\n /**\n * Marks this item as the one currently in effect: the chosen sort order, the active\n * view. It is coloured and announced as such.\n */\n selected?: boolean\n /**\n * What the command means, in colour. `danger` marks it destructive (deleting something\n * belongs there), and `neutral`, the default, is every other command.\n */\n tone?: MenuItemTone\n disabled?: boolean\n /**\n * Turns the item into a link pointing at this address, for a menu that navigates\n * rather than acts. A disabled link is made inert by hand.\n */\n href?: string\n}\n\n// With a submenu this component renders two elements; the item and the nested panel; so there\n// is no single root for Vue to put the consumer's attributes on. They are placed on the item\n// explicitly.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MenuItemProps>(), {\n label: undefined,\n sublabel: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n selected: false,\n tone: 'neutral',\n disabled: false,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * The command was chosen, by click or by keyboard. It is emitted before the menu\n * closes, so the handler runs while the menu is still open.\n */\n select: []\n}>()\n\nconst slots = defineSlots<{\n /** The label, replacing the `label` prop. */\n default?(): unknown\n /** The second line, replacing the `sublabel` prop. */\n sublabel?(): unknown\n /** Free content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Free content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n /**\n * The contents of a submenu: items, groups and separators, this component included,\n * so menus may nest as deep as needed.\n */\n submenu?(): unknown\n}>()\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasSubmenu() {\n return !!slots.submenu\n}\nfunction tag() {\n return !hasSubmenu() && props.href !== undefined ? ('a' as const) : ('button' as const)\n}\n\nconst attrs = useAttrs()\nconst link = useInertLink({\n href: () => (hasSubmenu() ? undefined : props.href),\n inert: () => props.disabled,\n attrs: () => attrs,\n})\n\nconst menu = inject(menuKey, null)\n\nfunction onClick() {\n if (props.disabled || hasSubmenu()) return\n emit('select')\n menu?.closeAll()\n}\n\nconst subId = useId()\n// Hover and the keyboard open the submenu from code: see the `anchor` prop of VMenuPanel.\nconst subAnchor = menuAnchor(subId)\nconst subOpen = ref(false)\nconst subPanel = ref<InstanceType<typeof VMenuPanel> | null>(null)\nconst itemEl = ref<HTMLElement | null>(null)\n\n// @keyboard\n// Opening a submenu from the keyboard, which the browser's own toggle does not cover; it only\n// reacts to a click.\nfunction onKeydown(event: KeyboardEvent) {\n if (event.altKey || event.ctrlKey || event.metaKey) return\n // @keyboard @a11y\n // A menu item is activated by Space as well as Enter, and a LINK answers Enter alone:\n // Space would scroll the page instead.\n if (!hasSubmenu()) {\n if (event.key === ' ' && tag() === 'a') {\n event.preventDefault()\n itemEl.value?.click()\n }\n return\n }\n if (props.disabled) return\n // The submenu opens towards the end of the line, so the arrow that points at it is the\n // one that opens it: the left one in a right-to-left page.\n const toward = isRtl(itemEl.value) ? 'ArrowLeft' : 'ArrowRight'\n if (![toward, 'Enter', ' '].includes(event.key)) return\n // The button's native activation has to be stopped: it would fire a click of its\n // own, and that click would toggle the panel shut again right behind the opening\n // below.\n event.preventDefault()\n subPanel.value?.show(itemEl.value ?? undefined)\n subPanel.value?.focusFirst()\n}\n\n// A submenu opens on hover, but only once the pointer has stayed long enough to show it was\n// meant; otherwise every panel crossed on the way to another one would flash open. ONE timer\n// serves both directions, since opening and closing are mutually exclusive: arming either\n// always cancels the other.\nconst hoverTimer = useTimer()\n\n// @a11y @core\n// A TOUCH tap sends `pointerenter` and `pointerleave` before its `click`, so the leave below\n// armed the close and the submenu the tap opened shut 150 ms later. Hover intent means nothing\n// to a finger: only a pointer that can hover takes this route, the tap opening the submenu\n// natively through the click.\nfunction onPointerEnter(event: PointerEvent) {\n if (props.disabled || event.pointerType === 'touch') return\n // Hovering also moves the focus, so that the mouse and the keyboard never highlight two\n // different items at once; in a menu there is only ever one current item.\n itemEl.value?.focus({ preventScroll: true })\n if (!hasSubmenu()) return\n hoverTimer.start(() => subPanel.value?.show(itemEl.value ?? undefined), SUBMENU_HOVER_DELAY)\n}\n\n// @a11y @core\n// The test on where the focus currently is, is the accessibility half:\n// a pointer drifting off the item must not close a submenu a keyboard user is\n// standing inside.\nfunction onPointerLeave(event: PointerEvent) {\n if (!hasSubmenu() || event.pointerType === 'touch') return\n hoverTimer.start(() => {\n if (subPanel.value?.el?.contains(document.activeElement)) return\n subPanel.value?.close()\n }, SUBMENU_HOVER_DELAY)\n}\n</script>\n\n<template>\n <component\n :is=\"tag()\"\n ref=\"itemEl\"\n :aria-disabled=\"link.isInertLink.value ? 'true' : undefined\"\n :aria-current=\"selected ? 'true' : undefined\"\n :aria-haspopup=\"hasSubmenu() ? 'menu' : undefined\"\n :aria-expanded=\"hasSubmenu() ? subOpen : undefined\"\n :aria-controls=\"hasSubmenu() ? subId : undefined\"\n v-bind=\"link.attrs.value\"\n role=\"menuitem\"\n tabindex=\"-1\"\n class=\"v-menu-item\"\n :type=\"tag() === 'button' ? 'button' : undefined\"\n :disabled=\"tag() === 'button' ? disabled : undefined\"\n :href=\"link.linkHref.value\"\n :data-tone=\"tone\"\n :data-selected=\"selected ? '' : undefined\"\n :popovertarget=\"hasSubmenu() ? subId : undefined\"\n :popovertargetaction=\"hasSubmenu() ? 'show' : undefined\"\n :style=\"hasSubmenu() ? { '--menu-anchor': subAnchor } : undefined\"\n @click=\"onClick\"\n @keydown=\"onKeydown\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"start\">\n <VIcon v-if=\"iconStart\" class=\"v-menu-item-icon\" v-bind=\"iconProps(iconStart)\" />\n </slot>\n <span class=\"v-menu-item-content\">\n <span class=\"v-menu-item-label\"\n ><slot>{{ label }}</slot></span\n >\n <span v-if=\"sublabel !== undefined || $slots.sublabel\" class=\"v-menu-item-sublabel\">\n <slot name=\"sublabel\">{{ sublabel }}</slot>\n </span>\n </span>\n <!-- An item opening a submenu always shows the chevron announcing it, and never\n the end icon: the sideways opening is what the reader needs to be told -->\n <VIcon v-if=\"hasSubmenu()\" :name=\"chevronRightIcon\" class=\"v-menu-item-chevron\" mirrored />\n <slot v-else name=\"end\">\n <VIcon v-if=\"iconEnd\" v-bind=\"iconProps(iconEnd)\" />\n </slot>\n </component>\n <VMenuPanel\n v-if=\"hasSubmenu()\"\n :id=\"subId\"\n ref=\"subPanel\"\n placement=\"right-start\"\n :anchor=\"subAnchor\"\n submenu\n @toggle=\"subOpen = $event\"\n @pointerenter=\"hoverTimer.cancel()\"\n @pointerleave=\"onPointerLeave\"\n >\n <slot name=\"submenu\" />\n </VMenuPanel>\n</template>\n\n<style>\n@layer vectis.components {\n .v-menu-item {\n /*\n * Every dimension comes from the `--control-*` variables the root panel sets and this row\n * inherits; one size table for the whole design system. The icons follow with nothing\n * written for them, their own variables belonging to that same block.\n */\n display: flex;\n align-items: center;\n gap: var(--control-gap);\n inline-size: 100%;\n min-block-size: var(--control-height);\n padding-block: var(--vectis-space-1);\n padding-inline: var(--control-padding-inline);\n border: none;\n background: transparent;\n color: var(--vectis-color-text);\n border-radius: min(var(--vectis-radius-interactive), calc(var(--control-height) / 2));\n font-family: inherit;\n font-size: var(--control-font-size);\n line-height: var(--vectis-text-body-md-leading);\n text-align: start;\n text-decoration: none;\n cursor: pointer;\n }\n\n /* The name is unique per item and read by the submenu panel's `position-anchor`. */\n .v-menu-item[aria-haspopup] {\n anchor-name: var(--menu-anchor);\n }\n\n .v-menu-item-content {\n flex: 1;\n min-inline-size: 0;\n display: flex;\n flex-direction: column;\n }\n\n .v-menu-item-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .v-menu-item-sublabel {\n overflow: hidden;\n font-size: var(--vectis-text-caption-size);\n color: var(--vectis-color-text-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* The start icon is muted, as on every row of the design system (VSideNavigationItem,\n VAccordionItem): the label carries the command, the icon only helps find it. */\n .v-menu-item-icon,\n .v-menu-item-chevron {\n flex: none;\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * In a menu the focus IS the highlight, so it is drawn on `:focus` and not on\n * `:focus-visible`: the focus is moved from code; by the arrows, and by hovering; and the\n * browser would not call that a keyboard focus worth showing. While a submenu is open its\n * parent item keeps the highlight too, so the path followed through the levels stays visible.\n */\n .v-menu-item:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n background: var(--vectis-color-surface-muted);\n outline: none;\n }\n\n .v-menu-item[data-selected] {\n background: var(--vectis-color-accent-surface);\n color: var(--vectis-color-accent-text);\n }\n\n .v-menu-item[data-selected]:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-selected]:focus,\n .v-menu-item[data-selected][aria-expanded='true'] {\n /* The selected row is already tinted, so its hover deepens that tint rather than\n replacing it with the neutral highlight. */\n background: color-mix(\n in oklab,\n var(--vectis-color-accent-surface),\n var(--vectis-color-accent-text) 8%\n );\n }\n\n .v-menu-item[data-tone='danger'] {\n color: var(--vectis-color-danger-text);\n }\n\n .v-menu-item[data-tone='danger']:hover:not(:disabled, [aria-disabled='true']),\n .v-menu-item[data-tone='danger']:focus,\n .v-menu-item[data-tone='danger'][aria-expanded='true'] {\n background: var(--vectis-color-danger-surface);\n }\n\n /* Both forms have to be matched: `:disabled` only ever applies to a `<button>`, and\n an item rendered as a link is made inert through `aria-disabled` instead. */\n .v-menu-item:disabled,\n .v-menu-item[aria-disabled='true'] {\n background: transparent;\n color: var(--vectis-color-text-subtle);\n cursor: not-allowed;\n }\n\n /* On a row painted in a colour of its own, the start icon and the second line take the\n label's colour rather than the muted grey: muted would clash with the selected and the\n danger tints, and on a disabled row it is DARKER than the subtle grey the label takes,\n so they would outshine it. Each `:is()` counts for its most specific member, which\n keeps the rule at (0,3,0), above the two (0,1,0) base rules. */\n .v-menu-item:is([data-selected], [data-tone='danger'], :disabled, [aria-disabled='true'])\n :is(.v-menu-item-icon, .v-menu-item-sublabel) {\n color: inherit;\n }\n\n /*\n * The highlight is a background, which the mode forces to Canvas: the row under the focus\n * would look exactly like the others, and a keyboard user would lose their place (WCAG\n * 2.4.7). An outline inside the row takes its place.\n */\n @media (forced-colors: active) {\n .v-menu-item:focus,\n .v-menu-item[aria-expanded='true'] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n\n .v-menu-item.v-menu-item[data-selected] {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-menu{min-inline-size:var(--vectis-control-size-menu-min);max-inline-size:min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)))}.v-menu:focus{outline:none}.v-menu .v-menu[data-placement=right-start]{margin-block-start:calc(-1 * (var(--vectis-space-1) + 1px))}.v-menu[data-width]{min-inline-size:0;inline-size:var(--menu-width)}.v-menu[data-match-trigger]{min-inline-size:anchor-size(width)}}
|
|
2
|
+
@layer vectis.components{.v-menu{position-anchor:var(--menu-anchor);min-inline-size:var(--vectis-control-size-menu-min);max-inline-size:min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)))}.v-menu:focus{outline:none}.v-menu .v-menu[data-placement=right-start]{margin-block-start:calc(-1 * (var(--vectis-space-1) + 1px))}.v-menu[data-width]{min-inline-size:0;inline-size:var(--menu-width)}.v-menu[data-match-trigger]{min-inline-size:anchor-size(width)}}
|
|
@@ -9,6 +9,13 @@ interface MenuPanelProps {
|
|
|
9
9
|
* the row-height settings off it, so that it inherits its parent's instead.
|
|
10
10
|
*/
|
|
11
11
|
submenu?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The anchor name carried by whatever opens the panel: the trigger, or the parent item.
|
|
14
|
+
* The implicit anchor is not enough: an opening from code relies on `showPopover({ source })`,
|
|
15
|
+
* and browser extensions that wrap `showPopover` (password managers do) drop that option,
|
|
16
|
+
* leaving the panel at the corner of the viewport.
|
|
17
|
+
*/
|
|
18
|
+
anchor: string;
|
|
12
19
|
/**
|
|
13
20
|
* The row height, set by the ROOT panel only. Submenus receive no value and inherit
|
|
14
21
|
* it through CSS instead.
|