UNPKG

@nextcloud/vue

Version:
435 lines (434 loc) 13.3 kB
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