775 lines
26 KiB
Vue
775 lines
26 KiB
Vue
|
<script lang="ts">
|
||
|
// import NcButton from '../NcButton/index.js'
|
||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||
|
// import NcPopover from '../NcPopover/index.js'
|
||
|
import GenRandomId from '../utils/GenRandomId';
|
||
|
// import { t } from '../../l10n.js'
|
||
|
|
||
|
import { computed } from 'vue';
|
||
|
import DotsHorizontal from '@/Components/Icons/DotsHorizontal.vue';
|
||
|
|
||
|
const focusableSelector = '.focusable';
|
||
|
|
||
|
/**
|
||
|
* The Actions component can be used to display one ore more actions.
|
||
|
* If only a single action is provided, it will be rendered as an inline icon.
|
||
|
* For more, a menu indicator will be shown and a popovermenu containing the
|
||
|
* actions will be opened on click.
|
||
|
*
|
||
|
* @since 0.10.0
|
||
|
*/
|
||
|
export default {
|
||
|
name: 'NcActions',
|
||
|
|
||
|
components: {
|
||
|
// NcButton,
|
||
|
BaseButton,
|
||
|
DotsHorizontal,
|
||
|
// NcPopover,
|
||
|
},
|
||
|
|
||
|
provide() {
|
||
|
return {
|
||
|
/**
|
||
|
* NcActions can be used as:
|
||
|
* - Application menu (has menu role)
|
||
|
* - Navigation (has no specific role, should be used an element with navigation role)
|
||
|
* - Popover with plain text or text inputs (has no specific role)
|
||
|
* Depending on the usage (used items), the menu and its items should have different roles for a11y.
|
||
|
* Provide the role for NcAction* components in the NcActions content.
|
||
|
* @type {import('vue').ComputedRef<boolean>}
|
||
|
*/
|
||
|
'NcActions:isSemanticMenu': computed(() => this.isSemanticMenu),
|
||
|
};
|
||
|
},
|
||
|
|
||
|
props: {
|
||
|
/**
|
||
|
* Specify the open state of the popover menu
|
||
|
*/
|
||
|
open: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This disables the internal open management,
|
||
|
* so the actions menu only respects the `open` prop.
|
||
|
* This is e.g. necessary for the NcAvatar component
|
||
|
* to only open the actions menu after loading it's entries has finished.
|
||
|
*/
|
||
|
manualOpen: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Force the actions to display in a three dot menu
|
||
|
*/
|
||
|
forceMenu: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Force the name to show for single actions
|
||
|
*/
|
||
|
forceName: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Specify the menu name
|
||
|
*/
|
||
|
menuName: {
|
||
|
type: String,
|
||
|
default: null,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Apply primary styling for this menu
|
||
|
*/
|
||
|
primary: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Specifies the button type used for trigger and single actions buttons
|
||
|
* Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
|
||
|
* the default button style will be applied.
|
||
|
*/
|
||
|
type: {
|
||
|
type: String,
|
||
|
validator(value: string) {
|
||
|
return (
|
||
|
[
|
||
|
'primary',
|
||
|
'secondary',
|
||
|
'tertiary',
|
||
|
'tertiary-no-background',
|
||
|
'tertiary-on-primary',
|
||
|
'error',
|
||
|
'warning',
|
||
|
'success',
|
||
|
].indexOf(value) !== -1
|
||
|
);
|
||
|
},
|
||
|
default: null,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Icon to show for the toggle menu button
|
||
|
* when more than one action is inside the actions component.
|
||
|
* Only replace the default three-dot icon if really necessary.
|
||
|
*/
|
||
|
defaultIcon: {
|
||
|
type: String,
|
||
|
default: '',
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Aria label for the actions menu.
|
||
|
*
|
||
|
* If `menuName` is defined this will not be used to prevent
|
||
|
* any accessible name conflicts. This ensures that the
|
||
|
* element can be activated via voice input.
|
||
|
*/
|
||
|
ariaLabel: {
|
||
|
type: String,
|
||
|
default: 'Actions',
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
|
||
|
* @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
|
||
|
* otherwise root element will inherit incorrect aria-hidden.
|
||
|
*/
|
||
|
ariaHidden: {
|
||
|
type: Boolean,
|
||
|
default: null,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Wanted direction of the menu
|
||
|
*/
|
||
|
placement: {
|
||
|
type: String,
|
||
|
default: 'bottom',
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* DOM element for the actions' popover boundaries
|
||
|
*/
|
||
|
boundariesElement: {
|
||
|
type: Element,
|
||
|
default: () => document.querySelector('body'),
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Selector for the actions' popover container
|
||
|
*/
|
||
|
container: {
|
||
|
type: [String, Object, Element, Boolean],
|
||
|
default: 'body',
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Disabled state of the main button (single action or menu toggle)
|
||
|
*/
|
||
|
disabled: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Display x items inline out of the dropdown menu
|
||
|
* Will be ignored if `forceMenu` is set
|
||
|
*/
|
||
|
inline: {
|
||
|
type: Number,
|
||
|
default: 0,
|
||
|
},
|
||
|
},
|
||
|
|
||
|
emits: ['open', 'update:open', 'close', 'focus', 'blur'],
|
||
|
|
||
|
data() {
|
||
|
return {
|
||
|
opened: this.open,
|
||
|
focusIndex: 0,
|
||
|
randomId: `menu-${GenRandomId()}`,
|
||
|
isSemanticMenu: false,
|
||
|
isSemanticNavigation: false,
|
||
|
isSemanticPopoverLike: false,
|
||
|
};
|
||
|
},
|
||
|
|
||
|
computed: {
|
||
|
triggerBtnType() {
|
||
|
// If requested, we use a primary button
|
||
|
return (
|
||
|
this.type ||
|
||
|
(this.primary
|
||
|
? 'primary'
|
||
|
: // If it has a name, we use a secondary button
|
||
|
this.menuName
|
||
|
? 'secondary'
|
||
|
: 'tertiary')
|
||
|
);
|
||
|
},
|
||
|
},
|
||
|
|
||
|
watch: {
|
||
|
// Watch parent prop
|
||
|
open(state) {
|
||
|
if (state === this.opened) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.opened = state;
|
||
|
},
|
||
|
},
|
||
|
|
||
|
methods: {
|
||
|
/**
|
||
|
* Do we have exactly one Action and
|
||
|
* is it allowed as a standalone element?
|
||
|
*
|
||
|
* @param {Array} action The action to check
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
isValidSingleAction(action) {
|
||
|
const componentName = action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
|
||
|
return ['NcActionButton', 'NcActionLink', 'NcActionRouter'].includes(componentName);
|
||
|
},
|
||
|
|
||
|
// MENU STATE MANAGEMENT
|
||
|
openMenu() {
|
||
|
if (this.opened) {
|
||
|
return;
|
||
|
}
|
||
|
this.opened = true;
|
||
|
|
||
|
/**
|
||
|
* Event emitted when the popover menu open state is changed
|
||
|
*
|
||
|
* @type {boolean}
|
||
|
*/
|
||
|
this.$emit('update:open', true);
|
||
|
|
||
|
/**
|
||
|
* Event emitted when the popover menu is opened
|
||
|
*/
|
||
|
this.$emit('open');
|
||
|
},
|
||
|
closeMenu(returnFocus = true) {
|
||
|
if (!this.opened) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.opened = false;
|
||
|
|
||
|
this.$refs.popover.clearFocusTrap({ returnFocus });
|
||
|
|
||
|
/**
|
||
|
* Event emitted when the popover menu open state is changed
|
||
|
*
|
||
|
* @type {boolean}
|
||
|
*/
|
||
|
this.$emit('update:open', false);
|
||
|
|
||
|
/**
|
||
|
* Event emitted when the popover menu is closed
|
||
|
*/
|
||
|
this.$emit('close');
|
||
|
|
||
|
// close everything
|
||
|
this.focusIndex = 0;
|
||
|
|
||
|
// focus back the menu button
|
||
|
this.$refs.menuButton.$el.focus();
|
||
|
},
|
||
|
|
||
|
onOpen(event) {
|
||
|
this.$nextTick(() => {
|
||
|
this.focusFirstAction(event);
|
||
|
});
|
||
|
},
|
||
|
|
||
|
// MENU KEYS & FOCUS MANAGEMENT
|
||
|
// focus nearest focusable item on mouse move
|
||
|
// DO NOT change the focus if the target is already focused
|
||
|
// this will prevent issues with input being unfocused
|
||
|
// on mouse move
|
||
|
onMouseFocusAction(event) {
|
||
|
if (document.activeElement === event.target) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const menuItem = event.target.closest('li');
|
||
|
if (menuItem && this.$refs.menu.contains(menuItem)) {
|
||
|
const focusableItem = menuItem.querySelector(focusableSelector);
|
||
|
if (focusableItem) {
|
||
|
const focusList = this.$refs.menu.querySelectorAll(focusableSelector);
|
||
|
const focusIndex = [...focusList].indexOf(focusableItem);
|
||
|
if (focusIndex > -1) {
|
||
|
this.focusIndex = focusIndex;
|
||
|
this.focusAction();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
/**
|
||
|
* Dispatches the keydown listener to different handlers
|
||
|
*
|
||
|
* @param {object} event The keydown event
|
||
|
*/
|
||
|
onKeydown(event) {
|
||
|
if (event.key === 'Tab' && !this.isSemanticPopoverLike) {
|
||
|
this.closeMenu(false);
|
||
|
}
|
||
|
|
||
|
if (event.key === 'ArrowUp') {
|
||
|
this.focusPreviousAction(event);
|
||
|
}
|
||
|
|
||
|
if (event.key === 'ArrowDown') {
|
||
|
this.focusNextAction(event);
|
||
|
}
|
||
|
|
||
|
if (event.key === 'PageUp') {
|
||
|
this.focusFirstAction(event);
|
||
|
}
|
||
|
|
||
|
if (event.key === 'PageDown') {
|
||
|
this.focusLastAction(event);
|
||
|
}
|
||
|
|
||
|
if (event.key === 'Escape') {
|
||
|
this.closeMenu();
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
},
|
||
|
removeCurrentActive() {
|
||
|
const currentActiveElement = this.$refs.menu.querySelector('li.active');
|
||
|
if (currentActiveElement) {
|
||
|
currentActiveElement.classList.remove('active');
|
||
|
}
|
||
|
},
|
||
|
focusAction() {
|
||
|
// TODO: have a global disabled state for non input elements
|
||
|
const focusElement = this.$refs.menu.querySelectorAll(focusableSelector)[this.focusIndex];
|
||
|
if (focusElement) {
|
||
|
this.removeCurrentActive();
|
||
|
const liMenuParent = focusElement.closest('li.action');
|
||
|
focusElement.focus();
|
||
|
if (liMenuParent) {
|
||
|
liMenuParent.classList.add('active');
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
focusPreviousAction(event) {
|
||
|
if (this.opened) {
|
||
|
if (this.focusIndex === 0) {
|
||
|
this.focusLastAction(event);
|
||
|
} else {
|
||
|
this.preventIfEvent(event);
|
||
|
this.focusIndex = this.focusIndex - 1;
|
||
|
}
|
||
|
this.focusAction();
|
||
|
}
|
||
|
},
|
||
|
focusNextAction(event) {
|
||
|
if (this.opened) {
|
||
|
const indexLength = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
|
||
|
if (this.focusIndex === indexLength) {
|
||
|
this.focusFirstAction(event);
|
||
|
} else {
|
||
|
this.preventIfEvent(event);
|
||
|
this.focusIndex = this.focusIndex + 1;
|
||
|
}
|
||
|
this.focusAction();
|
||
|
}
|
||
|
},
|
||
|
focusFirstAction(event) {
|
||
|
if (this.opened) {
|
||
|
this.preventIfEvent(event);
|
||
|
// In case a button is considered aria-selected we will use this one as a initial focus
|
||
|
const firstSelectedIndex = [...this.$refs.menu.querySelectorAll(focusableSelector)].findIndex((button) => {
|
||
|
return button.parentElement.getAttribute('aria-selected');
|
||
|
});
|
||
|
this.focusIndex = firstSelectedIndex > -1 ? firstSelectedIndex : 0;
|
||
|
this.focusAction();
|
||
|
}
|
||
|
},
|
||
|
focusLastAction(event) {
|
||
|
if (this.opened) {
|
||
|
this.preventIfEvent(event);
|
||
|
this.focusIndex = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
|
||
|
this.focusAction();
|
||
|
}
|
||
|
},
|
||
|
preventIfEvent(event) {
|
||
|
if (event) {
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
}
|
||
|
},
|
||
|
onFocus(event) {
|
||
|
this.$emit('focus', event);
|
||
|
},
|
||
|
onBlur(event) {
|
||
|
this.$emit('blur', event);
|
||
|
},
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The render function to display the component
|
||
|
*
|
||
|
* @param {Function} h The function to create VNodes
|
||
|
* @return {object|undefined} The created VNode
|
||
|
*/
|
||
|
render(h) {
|
||
|
/**
|
||
|
* Filter the Actions, so that we only get allowed components.
|
||
|
* This also ensure that we don't get 'text' elements, which would
|
||
|
* become problematic later on.
|
||
|
*/
|
||
|
const actions = ([this.$slots.default] || []).filter(
|
||
|
(action) => action?.componentOptions?.tag || action?.componentOptions?.Ctor?.extendOptions?.name,
|
||
|
);
|
||
|
|
||
|
const getActionName = (action) => action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
|
||
|
|
||
|
const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio'];
|
||
|
const textInputActions = ['NcActionInput', 'NcActionTextEditable'];
|
||
|
const linkActions = ['NcActionLink', 'NcActionRouter'];
|
||
|
|
||
|
const hasTextInputAction = actions.some((action) => textInputActions.includes(getActionName(action)));
|
||
|
const hasMenuItemAction = actions.some((action) => menuItemsActions.includes(getActionName(action)));
|
||
|
const hasLinkAction = actions.some((action) => linkActions.includes(getActionName(action)));
|
||
|
|
||
|
// We consider the NcActions to have role="menu" if it consists some button-like action and not text inputs
|
||
|
this.isSemanticMenu = hasMenuItemAction && !hasTextInputAction;
|
||
|
// We consider the NcActions to be navigation if it consists some link-like action
|
||
|
this.isSemanticNavigation = hasLinkAction && !hasMenuItemAction && !hasTextInputAction;
|
||
|
// If it is no a manu and not a navigation, it is a popover with items: a form or just a text
|
||
|
this.isSemanticPopoverLike = !this.isSemanticMenu && !this.isSemanticNavigation;
|
||
|
|
||
|
/**
|
||
|
* Filter and list actions that are allowed to be displayed inline
|
||
|
*/
|
||
|
let inlineActions = actions.filter(this.isValidSingleAction);
|
||
|
if (this.forceMenu && inlineActions.length > 0 && this.inline > 0) {
|
||
|
// Vue.util.warn('Specifying forceMenu will ignore any inline actions rendering.');
|
||
|
inlineActions = [];
|
||
|
}
|
||
|
|
||
|
// Check that we have at least one action
|
||
|
if (actions.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render the provided action
|
||
|
*
|
||
|
* @param {import('vue').VNode} action the action to render
|
||
|
* @return {Function} the vue render function
|
||
|
*/
|
||
|
const renderInlineAction = (action) => {
|
||
|
const icon =
|
||
|
action?.data?.scopedSlots?.icon()?.[0] || h('span', { class: ['icon', action?.componentOptions?.propsData?.icon] });
|
||
|
const attrs = action?.data?.attrs || {};
|
||
|
const clickListener = action?.componentOptions?.listeners?.click;
|
||
|
|
||
|
const text = action?.componentOptions?.children?.[0]?.text?.trim?.();
|
||
|
const ariaLabel = action?.componentOptions?.propsData?.ariaLabel || text;
|
||
|
const buttonText = this.forceName ? text : '';
|
||
|
|
||
|
let title = action?.componentOptions?.propsData?.title;
|
||
|
// Show a default title for single actions if none is present
|
||
|
if (!(this.forceName || title)) {
|
||
|
title = text;
|
||
|
}
|
||
|
|
||
|
return h(
|
||
|
'BaseButton',
|
||
|
{
|
||
|
class: ['action-item action-item--single', action?.data?.staticClass, action?.data?.class],
|
||
|
attrs: {
|
||
|
...attrs,
|
||
|
'aria-label': ariaLabel,
|
||
|
title,
|
||
|
},
|
||
|
ref: action?.data?.ref,
|
||
|
props: {
|
||
|
// If it has a menuName, we use a secondary button
|
||
|
type: this.type || (buttonText ? 'secondary' : 'tertiary'),
|
||
|
disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
|
||
|
...action?.componentOptions?.propsData,
|
||
|
},
|
||
|
on: {
|
||
|
focus: this.onFocus,
|
||
|
blur: this.onBlur,
|
||
|
// If we have a click listener,
|
||
|
// we bind it to execute on click and forward the click event
|
||
|
...(!!clickListener && {
|
||
|
click: (event) => {
|
||
|
if (clickListener) {
|
||
|
clickListener(event);
|
||
|
}
|
||
|
},
|
||
|
}),
|
||
|
},
|
||
|
},
|
||
|
[h('template', { slot: 'icon' }, [icon]), buttonText],
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Render the actions popover
|
||
|
*
|
||
|
* @param {Array} actions the actions to render within
|
||
|
* @return {Function} the vue render function
|
||
|
*/
|
||
|
const renderActionsPopover = (actions) => {
|
||
|
const triggerIcon =
|
||
|
this.$slots.icon?.[0] ||
|
||
|
(this.defaultIcon
|
||
|
? h('span', { class: ['icon', this.defaultIcon] })
|
||
|
: h('DotsHorizontal', {
|
||
|
props: {
|
||
|
size: 20,
|
||
|
},
|
||
|
}));
|
||
|
return h(
|
||
|
'NcPopover',
|
||
|
{
|
||
|
ref: 'popover',
|
||
|
props: {
|
||
|
delay: 0,
|
||
|
handleResize: true,
|
||
|
shown: this.opened,
|
||
|
placement: this.placement,
|
||
|
boundary: this.boundariesElement,
|
||
|
container: this.container,
|
||
|
popoverBaseClass: 'action-item__popper',
|
||
|
// Menu and navigation should not have focus trap
|
||
|
// Tab should close the menu and move focus to the next UI element
|
||
|
setReturnFocus: !this.isSemanticPopoverLike ? null : this.$refs.menuButton?.$el,
|
||
|
focusTrap: this.isSemanticPopoverLike,
|
||
|
},
|
||
|
// For some reason the popover component
|
||
|
// does not react to props given under the 'props' key,
|
||
|
// so we use both 'attrs' and 'props'
|
||
|
attrs: {
|
||
|
delay: 0,
|
||
|
handleResize: true,
|
||
|
shown: this.opened,
|
||
|
placement: this.placement,
|
||
|
boundary: this.boundariesElement,
|
||
|
container: this.container,
|
||
|
...(this.manualOpen && { triggers: [] }),
|
||
|
},
|
||
|
on: {
|
||
|
'show': this.openMenu,
|
||
|
'after-show': this.onOpen,
|
||
|
'hide': this.closeMenu,
|
||
|
},
|
||
|
},
|
||
|
[
|
||
|
h(
|
||
|
'BaseButton',
|
||
|
{
|
||
|
class: 'action-item__menutoggle',
|
||
|
props: {
|
||
|
type: this.triggerBtnType,
|
||
|
disabled: this.disabled,
|
||
|
},
|
||
|
slot: 'trigger',
|
||
|
ref: 'menuButton',
|
||
|
attrs: {
|
||
|
'aria-haspopup': this.isSemanticMenu ? 'menu' : null,
|
||
|
'aria-label': this.menuName ? null : this.ariaLabel,
|
||
|
'aria-controls': this.opened ? this.randomId : null,
|
||
|
'aria-expanded': this.opened ? 'true' : 'false',
|
||
|
},
|
||
|
on: {
|
||
|
focus: this.onFocus,
|
||
|
blur: this.onBlur,
|
||
|
},
|
||
|
},
|
||
|
[h('template', { slot: 'icon' }, [triggerIcon]), this.menuName],
|
||
|
),
|
||
|
h(
|
||
|
'div',
|
||
|
{
|
||
|
class: {
|
||
|
open: this.opened,
|
||
|
},
|
||
|
attrs: {
|
||
|
tabindex: '-1',
|
||
|
},
|
||
|
on: {
|
||
|
keydown: this.onKeydown,
|
||
|
mousemove: this.onMouseFocusAction,
|
||
|
},
|
||
|
ref: 'menu',
|
||
|
},
|
||
|
[
|
||
|
h(
|
||
|
'ul',
|
||
|
{
|
||
|
attrs: {
|
||
|
id: this.randomId,
|
||
|
tabindex: '-1',
|
||
|
role: this.isSemanticMenu ? 'menu' : undefined,
|
||
|
},
|
||
|
},
|
||
|
[actions],
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
],
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* If we have a single action only and didn't force a menu,
|
||
|
* we render the action as a standalone button
|
||
|
*/
|
||
|
if (actions.length === 1 && inlineActions.length === 1 && !this.forceMenu) {
|
||
|
return renderInlineAction(inlineActions[0]);
|
||
|
}
|
||
|
|
||
|
// If we completely re-render the children
|
||
|
// we need to focus the first action again
|
||
|
// Mostly used when clicking a menu item
|
||
|
this.$nextTick(() => {
|
||
|
if (this.opened && this.$refs.menu) {
|
||
|
const isAnyActive = this.$refs.menu.querySelector('li.active') || [];
|
||
|
if (isAnyActive.length === 0) {
|
||
|
this.focusFirstAction();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* If we some inline actions to render, render them, then the menu
|
||
|
*/
|
||
|
if (inlineActions.length > 0 && this.inline > 0) {
|
||
|
const renderedInlineActions = inlineActions.slice(0, this.inline);
|
||
|
// Filter already rendered actions
|
||
|
const menuActions = actions.filter((action) => !renderedInlineActions.includes(action));
|
||
|
return h(
|
||
|
'div',
|
||
|
{
|
||
|
class: ['action-items', `action-item--${this.triggerBtnType}`],
|
||
|
},
|
||
|
[
|
||
|
// Render inline actions
|
||
|
...renderedInlineActions.map(renderInlineAction),
|
||
|
// render the rest within the popover menu
|
||
|
menuActions.length > 0
|
||
|
? h(
|
||
|
'div',
|
||
|
{
|
||
|
class: [
|
||
|
'action-item',
|
||
|
{
|
||
|
'action-item--open': this.opened,
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
[renderActionsPopover(menuActions)],
|
||
|
)
|
||
|
: null,
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Otherwise, we render the actions in a popover
|
||
|
*/
|
||
|
return h(
|
||
|
'div',
|
||
|
{
|
||
|
class: [
|
||
|
'action-item action-item--default-popover',
|
||
|
`action-item--${this.triggerBtnType}`,
|
||
|
{
|
||
|
'action-item--open': this.opened,
|
||
|
},
|
||
|
],
|
||
|
},
|
||
|
[renderActionsPopover(actions)],
|
||
|
);
|
||
|
},
|
||
|
};
|
||
|
</script>
|
||
|
|
||
|
<style lang="css" scoped>
|
||
|
/* Inline buttons */
|
||
|
.action-items {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
|
||
|
/* Spacing between buttons */
|
||
|
/* &>button {
|
||
|
margin-right: math.div(28, 2);
|
||
|
} */
|
||
|
}
|
||
|
|
||
|
.action-item {
|
||
|
/* --open-background-color: var(--color-background-hover, $action-background-hover); */
|
||
|
position: relative;
|
||
|
display: inline-block;
|
||
|
|
||
|
&.action-item--primary {
|
||
|
--open-background-color: var(--color-primary-element-hover);
|
||
|
}
|
||
|
|
||
|
&.action-item--secondary {
|
||
|
--open-background-color: var(--color-primary-element-light-hover);
|
||
|
}
|
||
|
|
||
|
&.action-item--error {
|
||
|
--open-background-color: var(--color-error-hover);
|
||
|
}
|
||
|
|
||
|
&.action-item--warning {
|
||
|
--open-background-color: var(--color-warning-hover);
|
||
|
}
|
||
|
|
||
|
&.action-item--success {
|
||
|
--open-background-color: var(--color-success-hover);
|
||
|
}
|
||
|
|
||
|
&.action-item--tertiary-no-background {
|
||
|
--open-background-color: transparent;
|
||
|
}
|
||
|
|
||
|
&.action-item--open .action-item__menutoggle {
|
||
|
background-color: var(--open-background-color);
|
||
|
}
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
<style lang="css">
|
||
|
/* // We overwrote the popover base class, so we can style
|
||
|
// the popover__inner for actions only. */
|
||
|
.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper {
|
||
|
border-radius: var(--border-radius-large);
|
||
|
overflow: hidden;
|
||
|
|
||
|
.v-popper__inner {
|
||
|
border-radius: var(--border-radius-large);
|
||
|
padding: 4px;
|
||
|
max-height: calc(50vh - 16px);
|
||
|
overflow: auto;
|
||
|
}
|
||
|
}
|
||
|
</style>
|