@nextcloud/vue
Version:
Nextcloud vue components
435 lines (434 loc) • 13.3 kB
JavaScript
import '../assets/NcPopover-CZ3pMU6Y.css';
import { options, Dropdown } from "floating-vue";
import { createFocusTrap } from "focus-trap";
import { defineComponent, warn, resolveComponent, createBlock, openBlock, withCtx, createVNode, renderSlot, normalizeProps, guardReactiveProps } from "vue";
import { g as getTrapStack } from "./focusTrap-HJQ4pqHV.mjs";
import { l as logger } from "./logger-D3RVzcfQ.mjs";
import { i as isRtl } from "./rtl-v0UOPAM7.mjs";
import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.mjs";
const _sfc_main$1 = defineComponent({
name: "NcPopoverTriggerProvider",
provide() {
return {
"NcPopover:trigger:shown": () => this.shown,
"NcPopover:trigger:attrs": () => this.triggerAttrs
};
},
props: {
/**
* Is the popover currently shown
*/
shown: {
type: Boolean,
required: true
},
/**
* ARIA Role of the popup
*/
popupRole: {
type: String,
default: void 0
}
},
computed: {
triggerAttrs() {
return {
"aria-haspopup": this.popupRole,
"aria-expanded": this.shown.toString()
};
}
},
render() {
return this.$slots.default?.({
attrs: this.triggerAttrs
});
}
});
const ncPopover = "_ncPopover_wpltc_20";
const style0 = {
"material-design-icon": "_material-design-icon_wpltc_12",
ncPopover
};
const theme = "nc-popover-9";
options.themes[theme] = structuredClone(options.themes.dropdown);
const _sfc_main = {
name: "NcPopover",
components: {
Dropdown,
NcPopoverTriggerProvider: _sfc_main$1
},
props: {
/**
* Element to use for calculating the popper boundary (size and position).
* Either a query string or the actual HTMLElement.
*/
boundary: {
type: [String, Object],
default: ""
},
/**
* Automatically hide the popover on click outside.
*
* @deprecated Use `no-close-on-click-outside` instead (inverted value)
*/
closeOnClickOutside: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: true
},
/**
* Disable the automatic popover hide on click outside.
*/
noCloseOnClickOutside: {
type: Boolean,
default: false
},
/**
* Container where to mount the popover.
* Either a select query or `false` to mount to the parent node.
*/
container: {
type: [Boolean, String],
default: "body"
},
/**
* Delay for showing or hiding the popover.
*
* Can either be a number or an object to configure different delays (`{ show: number, hide: number }`).
*/
delay: {
type: [Number, Object],
default: 0
},
/**
* Disable the popover focus trap.
*/
noFocusTrap: {
type: Boolean,
default: false
},
/**
* Where to place the popover.
*
* This consists of the vertical placement and the horizontal placement.
* E.g. `bottom` will place the popover on the bottom of the trigger (horizontally centered),
* while `buttom-start` will horizontally align the popover on the logical start (e.g. for LTR layout on the left.).
* The `start` or `end` placement will align the popover on the left or right side or the trigger element.
*
* @type {'auto'|'auto-start'|'auto-end'|'top'|'top-start'|'top-end'|'bottom'|'bottom-start'|'bottom-end'|'start'|'end'}
*/
placement: {
type: String,
default: "bottom"
},
/**
* Class to be applied to the popover base
*/
popoverBaseClass: {
type: String,
default: ""
},
/**
* Events that trigger the popover on the popover container itself.
* This is useful if you set `triggers` to `hover` and also want the popover to stay open while hovering the popover itself.
*
* It is possible to also pass an object to define different triggers for hide and show `{ show: ['hover'], hide: ['click'] }`.
*/
popoverTriggers: {
type: [Array, Object],
default: null
},
/**
* Popup role
*
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup#values
*/
popupRole: {
type: String,
default: void 0,
validator: (value) => ["menu", "listbox", "tree", "grid", "dialog", "true"].includes(value)
},
/**
* Set element to return focus to after focus trap deactivation
*
* @type {SetReturnFocus}
*/
setReturnFocus: {
default: void 0,
type: [Boolean, HTMLElement, SVGElement, String, Function]
},
/**
* Show or hide the popper
*/
shown: {
type: Boolean,
default: false
},
/**
* Events that trigger the popover.
*
* If you pass an empty array then only the `shown` prop can control the popover state.
* Following events are available:
* - `'hover'`
* - `'click'`
* - `'focus'`
* - `'touch'`
*
* It is also possible to pass an object to have different events for show and hide:
* `{ hide: ['click'], show: ['click', 'hover'] }`
*/
triggers: {
type: [Array, Object],
default: () => ["click"]
}
},
emits: [
"afterShow",
"afterHide",
"update:shown"
],
setup() {
return {
theme
};
},
data() {
return {
internalShown: this.shown
};
},
computed: {
popperTriggers() {
if (this.popoverTriggers && Array.isArray(this.popoverTriggers)) {
return this.popoverTriggers;
}
return void 0;
},
popperHideTriggers() {
if (this.popoverTriggers && typeof this.popoverTriggers === "object") {
return this.popoverTriggers.hide;
}
return void 0;
},
popperShowTriggers() {
if (this.popoverTriggers && typeof this.popoverTriggers === "object") {
return this.popoverTriggers.show;
}
return void 0;
},
internalTriggers() {
if (this.triggers && Array.isArray(this.triggers)) {
return this.triggers;
}
return void 0;
},
hideTriggers() {
if (this.triggers && typeof this.triggers === "object") {
return this.triggers.hide;
}
return void 0;
},
showTriggers() {
if (this.triggers && typeof this.triggers === "object") {
return this.triggers.show;
}
return void 0;
},
internalPlacement() {
if (this.placement === "start") {
return isRtl ? "right" : "left";
} else if (this.placement === "end") {
return isRtl ? "left" : "right";
}
return this.placement;
}
},
watch: {
shown(value) {
this.internalShown = value;
},
internalShown(value) {
this.$emit("update:shown", value);
}
},
mounted() {
this.checkTriggerA11y();
},
beforeUnmount() {
this.clearFocusTrap();
this.clearEscapeStopPropagation();
},
methods: {
/**
* Check if the trigger has all required a11y attributes.
* Important to check custom trigger button.
*/
checkTriggerA11y() {
if (window.OC?.debug) {
const triggerContainer = this.getPopoverTriggerContainerElement();
const requiredTriggerButton = triggerContainer.querySelector("[aria-expanded]");
if (!requiredTriggerButton) {
warn("It looks like you are using a custom button as a <NcPopover> or other popover #trigger. If you are not using <NcButton> as a trigger, you need to bind attrs from the #trigger slot props to your custom button. See <NcPopover> docs for an example.");
}
}
},
/**
* Remove incorrect aria-describedby attribute from the trigger.
*
* @see https://github.com/Akryum/floating-vue/blob/8d4f7125aae0e3ea00ba4093d6d2001ab15058f1/packages/floating-vue/src/components/Popper.ts#L734
*/
removeFloatingVueAriaDescribedBy() {
const triggerContainer = this.getPopoverTriggerContainerElement();
const triggerElements = triggerContainer.querySelectorAll("[data-popper-shown]");
for (const el of triggerElements) {
el.removeAttribute("aria-describedby");
}
},
/**
* @return {HTMLElement|undefined}
*/
getPopoverContentElement() {
return this.$refs.popover?.$refs.popperContent?.$el;
},
/**
* @return {HTMLElement|undefined}
*/
getPopoverTriggerContainerElement() {
return this.$refs.popover?.$refs.popper?.$refs.reference;
},
/**
* Add focus trap for accessibility.
*/
async useFocusTrap() {
await this.$nextTick();
if (this.noFocusTrap) {
return;
}
const el = this.getPopoverContentElement();
el.tabIndex = -1;
if (!el) {
return;
}
this.$focusTrap = createFocusTrap(el, {
// Prevents to lose focus using esc key
// Focus will be release when popover be hide
escapeDeactivates: false,
allowOutsideClick: true,
setReturnFocus: this.setReturnFocus,
trapStack: getTrapStack(),
fallBackFocus: el
});
this.$focusTrap.activate();
},
/**
* Remove focus trap
*
* @param {object} options The configuration options for focusTrap
*/
clearFocusTrap(options2 = {}) {
try {
this.$focusTrap?.deactivate(options2);
this.$focusTrap = null;
} catch (error) {
logger.warn("[NcPopover] Failed to clear focus trap", { error });
}
},
/**
* Add stopPropagation for Escape.
* It prevents global Escape handling after closing popover.
*
* Manual event handling is used here instead of v-on because there is no direct access to the node.
* Alternative - wrap <template #popover> in a div wrapper.
*/
addEscapeStopPropagation() {
const el = this.getPopoverContentElement();
el?.addEventListener("keydown", this.stopKeydownEscapeHandler);
},
/**
* Remove stop Escape handler
*/
clearEscapeStopPropagation() {
const el = this.getPopoverContentElement();
el?.removeEventListener("keydown", this.stopKeydownEscapeHandler);
},
/**
* @param {KeyboardEvent} event - native keydown event
*/
stopKeydownEscapeHandler(event) {
if (event.type === "keydown" && event.key === "Escape") {
event.stopPropagation();
}
},
async afterShow() {
this.getPopoverContentElement().addEventListener("transitionend", () => {
this.$emit("afterShow");
}, { once: true, passive: true });
this.removeFloatingVueAriaDescribedBy();
await this.$nextTick();
await this.useFocusTrap();
this.addEscapeStopPropagation();
},
afterHide() {
this.getPopoverContentElement()?.addEventListener("transitionend", () => {
this.$emit("afterHide");
}, { once: true, passive: true });
this.clearFocusTrap();
this.clearEscapeStopPropagation();
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_NcPopoverTriggerProvider = resolveComponent("NcPopoverTriggerProvider");
const _component_Dropdown = resolveComponent("Dropdown");
return openBlock(), createBlock(_component_Dropdown, {
ref: "popover",
shown: $data.internalShown,
"onUpdate:shown": [
_cache[0] || (_cache[0] = ($event) => $data.internalShown = $event),
_cache[1] || (_cache[1] = ($event) => $data.internalShown = $event)
],
"arrow-padding": 10,
"auto-hide": !$props.noCloseOnClickOutside && $props.closeOnClickOutside,
boundary: $props.boundary || void 0,
container: $props.container,
delay: $props.delay,
distance: 10,
"handle-resize": "",
"no-auto-focus": true,
placement: $options.internalPlacement,
"popper-class": [_ctx.$style.ncPopover, $props.popoverBaseClass],
"popper-triggers": $options.popperTriggers,
"popper-hide-triggers": $options.popperHideTriggers,
"popper-show-triggers": $options.popperShowTriggers,
theme: $setup.theme,
triggers: $options.internalTriggers,
"hide-triggers": $options.hideTriggers,
"show-triggers": $options.showTriggers,
onApplyShow: $options.afterShow,
onApplyHide: $options.afterHide
}, {
popper: withCtx((slotProps) => [
renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps)))
]),
default: withCtx(() => [
createVNode(_component_NcPopoverTriggerProvider, {
shown: $data.internalShown,
"popup-role": $props.popupRole
}, {
default: withCtx((slotProps) => [
renderSlot(_ctx.$slots, "trigger", normalizeProps(guardReactiveProps(slotProps)))
]),
_: 3
}, 8, ["shown", "popup-role"])
]),
_: 3
}, 8, ["shown", "auto-hide", "boundary", "container", "delay", "placement", "popper-class", "popper-triggers", "popper-hide-triggers", "popper-show-triggers", "theme", "triggers", "hide-triggers", "show-triggers", "onApplyShow", "onApplyHide"]);
}
const cssModules = {
"$style": style0
};
const NcPopover = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__cssModules", cssModules]]);
export {
NcPopover as N
};
//# sourceMappingURL=NcPopover-C-MTaPCs.mjs.map