163 lines
5.2 KiB
JavaScript
163 lines
5.2 KiB
JavaScript
import { mergeProps as _mergeProps, createVNode as _createVNode } from "vue";
|
|
// Styles
|
|
import "./VMenu.css";
|
|
|
|
// Components
|
|
import { VDialogTransition } from "../transitions/index.mjs";
|
|
import { VDefaultsProvider } from "../VDefaultsProvider/index.mjs";
|
|
import { VOverlay } from "../VOverlay/index.mjs";
|
|
import { makeVOverlayProps } from "../VOverlay/VOverlay.mjs"; // Composables
|
|
import { forwardRefs } from "../../composables/forwardRefs.mjs";
|
|
import { useProxiedModel } from "../../composables/proxiedModel.mjs";
|
|
import { useScopeId } from "../../composables/scopeId.mjs"; // Utilities
|
|
import { computed, inject, mergeProps, nextTick, provide, ref, shallowRef, watch } from 'vue';
|
|
import { VMenuSymbol } from "./shared.mjs";
|
|
import { focusableChildren, focusChild, genericComponent, getNextElement, getUid, omit, propsFactory, useRender } from "../../util/index.mjs"; // Types
|
|
export const makeVMenuProps = propsFactory({
|
|
// TODO
|
|
// disableKeys: Boolean,
|
|
id: String,
|
|
...omit(makeVOverlayProps({
|
|
closeDelay: 250,
|
|
closeOnContentClick: true,
|
|
locationStrategy: 'connected',
|
|
openDelay: 300,
|
|
scrim: false,
|
|
scrollStrategy: 'reposition',
|
|
transition: {
|
|
component: VDialogTransition
|
|
}
|
|
}), ['absolute'])
|
|
}, 'VMenu');
|
|
export const VMenu = genericComponent()({
|
|
name: 'VMenu',
|
|
props: makeVMenuProps(),
|
|
emits: {
|
|
'update:modelValue': value => true
|
|
},
|
|
setup(props, _ref) {
|
|
let {
|
|
slots
|
|
} = _ref;
|
|
const isActive = useProxiedModel(props, 'modelValue');
|
|
const {
|
|
scopeId
|
|
} = useScopeId();
|
|
const uid = getUid();
|
|
const id = computed(() => props.id || `v-menu-${uid}`);
|
|
const overlay = ref();
|
|
const parent = inject(VMenuSymbol, null);
|
|
const openChildren = shallowRef(0);
|
|
provide(VMenuSymbol, {
|
|
register() {
|
|
++openChildren.value;
|
|
},
|
|
unregister() {
|
|
--openChildren.value;
|
|
},
|
|
closeParents() {
|
|
setTimeout(() => {
|
|
if (!openChildren.value) {
|
|
isActive.value = false;
|
|
parent?.closeParents();
|
|
}
|
|
}, 40);
|
|
}
|
|
});
|
|
async function onFocusIn(e) {
|
|
const before = e.relatedTarget;
|
|
const after = e.target;
|
|
await nextTick();
|
|
if (isActive.value && before !== after && overlay.value?.contentEl &&
|
|
// We're the topmost menu
|
|
overlay.value?.globalTop &&
|
|
// It isn't the document or the menu body
|
|
![document, overlay.value.contentEl].includes(after) &&
|
|
// It isn't inside the menu body
|
|
!overlay.value.contentEl.contains(after)) {
|
|
const focusable = focusableChildren(overlay.value.contentEl);
|
|
focusable[0]?.focus();
|
|
}
|
|
}
|
|
watch(isActive, val => {
|
|
if (val) {
|
|
parent?.register();
|
|
document.addEventListener('focusin', onFocusIn, {
|
|
once: true
|
|
});
|
|
} else {
|
|
parent?.unregister();
|
|
document.removeEventListener('focusin', onFocusIn);
|
|
}
|
|
});
|
|
function onClickOutside() {
|
|
parent?.closeParents();
|
|
}
|
|
function onKeydown(e) {
|
|
if (props.disabled) return;
|
|
if (e.key === 'Tab') {
|
|
const nextElement = getNextElement(focusableChildren(overlay.value?.contentEl, false), e.shiftKey ? 'prev' : 'next', el => el.tabIndex >= 0);
|
|
if (!nextElement) {
|
|
isActive.value = false;
|
|
overlay.value?.activatorEl?.focus();
|
|
}
|
|
}
|
|
}
|
|
function onActivatorKeydown(e) {
|
|
if (props.disabled) return;
|
|
const el = overlay.value?.contentEl;
|
|
if (el && isActive.value) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
focusChild(el, 'next');
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
focusChild(el, 'prev');
|
|
}
|
|
} else if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
isActive.value = true;
|
|
e.preventDefault();
|
|
setTimeout(() => setTimeout(() => onActivatorKeydown(e)));
|
|
}
|
|
}
|
|
const activatorProps = computed(() => mergeProps({
|
|
'aria-haspopup': 'menu',
|
|
'aria-expanded': String(isActive.value),
|
|
'aria-owns': id.value,
|
|
onKeydown: onActivatorKeydown
|
|
}, props.activatorProps));
|
|
useRender(() => {
|
|
const overlayProps = VOverlay.filterProps(props);
|
|
return _createVNode(VOverlay, _mergeProps({
|
|
"ref": overlay,
|
|
"id": id.value,
|
|
"class": ['v-menu', props.class],
|
|
"style": props.style
|
|
}, overlayProps, {
|
|
"modelValue": isActive.value,
|
|
"onUpdate:modelValue": $event => isActive.value = $event,
|
|
"absolute": true,
|
|
"activatorProps": activatorProps.value,
|
|
"onClick:outside": onClickOutside,
|
|
"onKeydown": onKeydown
|
|
}, scopeId), {
|
|
activator: slots.activator,
|
|
default: function () {
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
return _createVNode(VDefaultsProvider, {
|
|
"root": "VMenu"
|
|
}, {
|
|
default: () => [slots.default?.(...args)]
|
|
});
|
|
}
|
|
});
|
|
});
|
|
return forwardRefs({
|
|
id,
|
|
ΨopenChildren: openChildren
|
|
}, overlay);
|
|
}
|
|
});
|
|
//# sourceMappingURL=VMenu.mjs.map
|