@dialpad/dialtone
Version:
Dialpad's Dialtone design system monorepo
403 lines (402 loc) • 12.4 kB
JavaScript
import KeyboardNavigation from "../../common/mixins/keyboard_list_navigation.js";
import { DROPDOWN_PADDING_CLASSES } from "./dropdown_constants.js";
import { getUniqueString } from "../../common/utils.js";
import { EVENT_KEYNAMES } from "../../common/constants.js";
import SrOnlyCloseButtonMixin from "../../common/mixins/sr_only_close_button.js";
import SrOnlyCloseButton from "../../common/sr_only_close_button.vue.js";
import normalizeComponent from "../../_virtual/_plugin-vue2_normalizer.js";
import DtPopover from "../popover/popover.vue.js";
import { LIST_ITEM_NAVIGATION_TYPES } from "../list_item/list_item_constants.js";
import { POPOVER_APPEND_TO_VALUES } from "../popover/popover_constants.js";
const _sfc_main = {
name: "DtDropdown",
components: {
DtPopover,
SrOnlyCloseButton
},
mixins: [
KeyboardNavigation({
indexKey: "highlightIndex",
idKey: "highlightId",
listElementKey: "getListElement",
listItemRole: "menuitem",
afterHighlightMethod: "afterHighlight",
beginningOfListMethod: "beginningOfListMethod",
endOfListMethod: "endOfListMethod",
activeItemKey: "activeItemEl",
focusOnKeyboardNavigation: true
}),
SrOnlyCloseButtonMixin
],
props: {
/**
* Controls whether the dropdown is shown. Leaving this null will have the dropdown trigger on click by default.
* If you set this value, the default trigger behavior will be disabled and you can control it as you need.
* Supports .sync modifier
*/
open: {
type: Boolean,
default: null
},
/**
* Opens the dropdown on right click (context menu). If you set this value to `true`,
* the default trigger behavior will be disabled.
*/
openOnContext: {
type: Boolean,
default: false
},
/**
* Vertical padding size around the list element.
* @values none, small, large
*/
padding: {
type: String,
default: "small",
validator: (padding) => {
return Object.keys(DROPDOWN_PADDING_CLASSES).some((item) => item === padding);
}
},
/**
* Determines modal state, dropdown has a modal overlay preventing interaction with elements
* below it, but it is invisible.
*/
modal: {
type: Boolean,
default: true
},
/**
* Width configuration for the popover content. When its value is 'anchor',
* the popover content will have the same width as the anchor.
* @values null, anchor
*/
contentWidth: {
type: String,
default: null
},
/**
* Determines maximum height for the popover before overflow.
* Possible units rem|px|em
*/
maxHeight: {
type: String,
default: ""
},
/**
* Determines maximum width for the popover before overflow.
* Possible units rem|px|%|em
*/
maxWidth: {
type: String,
default: ""
},
/**
* Sets an ID on the list element of the component. Used by several aria attributes
* as well as when deriving the IDs for each item.
*/
listId: {
type: String,
default() {
return getUniqueString();
}
},
/**
* The type of navigation that this component should support.
* - "arrow-keys" for items that are navigated with UP/DOWN keys.
* - "tab" for items that are navigated using the TAB key.
* - "none" for static items that are not interactive.
* @values arrow-keys, tab, none
*/
navigationType: {
type: String,
default: LIST_ITEM_NAVIGATION_TYPES.ARROW_KEYS,
validator: (t) => Object.values(LIST_ITEM_NAVIGATION_TYPES).includes(t)
},
/**
* If the dropdown does not fit in the direction described by "placement",
* it will attempt to change it's direction to the "fallbackPlacements".
*
* @values top, top-start, top-end,
* right, right-start, right-end,
* left, left-start, left-end,
* bottom, bottom-start, bottom-end,
* auto, auto-start, auto-end
* */
fallbackPlacements: {
type: Array,
default: () => {
return ["auto"];
}
},
/**
* The direction the dropdown displays relative to the anchor.
*/
placement: {
type: String,
default: "bottom"
},
/**
* A method that will be called when the selection goes past the beginning of the list.
*/
onBeginningOfList: {
type: Function,
default: null
},
/**
* A method that will be called when the selection goes past the end of the list.
*/
onEndOfList: {
type: Function,
default: null
},
/**
* Additional class for the wrapper list element.
*/
listClass: {
type: [String, Array, Object],
default: ""
},
/**
* Sets the element to which the popover is going to append to.
* 'body' will append to the nearest body (supports shadow DOM).
* @values 'body', 'parent', HTMLElement,
*/
appendTo: {
type: [HTMLElement, String],
default: "body",
validator: (appendTo) => {
return POPOVER_APPEND_TO_VALUES.includes(appendTo) || appendTo instanceof HTMLElement;
}
},
/**
* If set to false the dialog will display over top of the anchor when there is insufficient space.
* If set to true it will never move from its position relative to the anchor and will clip instead.
* <a
* class="d-link"
* href="https://popper.js.org/docs/v2/modifiers/prevent-overflow/#tether"
* target="_blank"
* >
* Popper.js docs
* </a>
* @values true, false
*/
tether: {
type: Boolean,
default: true
},
/**
* Named transition when the content display is toggled.
* @see DtLazyShow
*/
transition: {
type: String,
default: "fade"
}
},
emits: [
/**
* Event fired when the highlight changes
*
* @event highlight
* @type {Number}
*/
"highlight",
/**
* Event fired when dropdown is shown or hidden
*
* @event opened
* @type {Boolean | Array}
*/
"opened",
/**
* Event fired to sync the open prop with the parent component
* @event update:open
*/
"update:open"
],
data() {
return {
LIST_ITEM_NAVIGATION_TYPES,
DROPDOWN_PADDING_CLASSES,
EVENT_KEYNAMES,
openedWithKeyboard: false,
isOpen: null
};
},
computed: {
dropdownListeners() {
return {
...this.$listeners,
opened: (isPopoverOpen) => {
this.updateInitialHighlightIndex(isPopoverOpen);
},
keydown: (event) => {
const eventCode = event.code;
switch (eventCode) {
case EVENT_KEYNAMES.up:
case EVENT_KEYNAMES.arrowup:
this.onUpKeyPress(event);
event.stopPropagation();
event.preventDefault();
break;
case EVENT_KEYNAMES.down:
case EVENT_KEYNAMES.arrowdown:
this.onDownKeyPress(event);
event.stopPropagation();
event.preventDefault();
break;
case EVENT_KEYNAMES.space:
case EVENT_KEYNAMES.spacebar:
this.onSpaceKey();
break;
case EVENT_KEYNAMES.enter:
this.onEnterKey();
break;
case EVENT_KEYNAMES.home:
this.onHomeKeyPress(event);
event.stopPropagation();
event.preventDefault();
break;
case EVENT_KEYNAMES.end:
this.onEndKeyPress(event);
event.stopPropagation();
event.preventDefault();
break;
default:
this.onKeyPress(event);
break;
}
this.$emit("keydown", event);
}
};
},
beginningOfListMethod() {
return this.onBeginningOfList || this.jumpToEnd;
},
endOfListMethod() {
return this.onEndOfList || this.jumpToBeginning;
},
activeItemEl() {
return this.getListElement().querySelector("#" + this.highlightId);
},
isArrowKeyNav() {
return this.navigationType === this.LIST_ITEM_NAVIGATION_TYPES.ARROW_KEYS;
},
listClasses() {
return [
"d-dropdown-list",
DROPDOWN_PADDING_CLASSES[this.padding],
this.listClass,
{ "d-context-menu-list": this.openOnContext }
];
},
shouldOpenWithArrowKeys() {
return !this.openOnContext;
}
},
methods: {
onMouseHighlight(e) {
const liElement = e.target.closest("li");
if (liElement && liElement.role && this.highlightId !== liElement.id) {
this.setHighlightId(liElement.id);
liElement.focus();
}
},
getListElement() {
return this.$refs.listWrapper;
},
clearHighlightIndex() {
this.setHighlightIndex(-1);
},
afterHighlight() {
if (this.visuallyHiddenClose && this.highlightIndex === this._itemsLength() - 1) {
return;
}
this.$emit("highlight", this.highlightIndex);
},
updateInitialHighlightIndex(isPopoverOpen) {
this.isOpen = isPopoverOpen;
if (isPopoverOpen) {
if (this.openedWithKeyboard && this.isArrowKeyNav) {
this.setHighlightIndex(0);
}
this.$emit("opened", true);
} else {
this.clearHighlightIndex();
this.openedWithKeyboard = false;
this.$emit("opened", false);
}
},
onSpaceKey() {
if (!this.open) {
this.openedWithKeyboard = true;
}
},
onEnterKey() {
if (!this.open) {
this.openedWithKeyboard = true;
}
},
onUpKeyPress() {
if (!this.isOpen) {
this.openedWithKeyboard = true;
return;
}
if (this.isArrowKeyNav) {
return this.onUpKey();
}
},
onDownKeyPress() {
if (!this.isOpen) {
this.openedWithKeyboard = true;
return;
}
if (this.isArrowKeyNav) {
return this.onDownKey();
}
},
onHomeKeyPress() {
if (!this.isOpen || !this.isArrowKeyNav) {
return;
}
return this.onHomeKey();
},
onEndKeyPress() {
if (!this.isOpen || !this.isArrowKeyNav) {
return;
}
return this.onEndKey();
},
onKeyPress(e) {
if (!this.isOpen || !this.isArrowKeyNav || !this.isValidLetter(e.key)) {
return;
}
e.stopPropagation();
e.preventDefault();
return this.onNavigationKey(e.key);
}
}
};
var _sfc_render = function render() {
var _vm = this, _c = _vm._self._c;
return _c("dt-popover", _vm._g({ ref: "popover", attrs: { "content-width": _vm.contentWidth, "open": _vm.open, "placement": _vm.placement, "initial-focus-element": _vm.openedWithKeyboard ? "first" : "dialog", "fallback-placements": _vm.fallbackPlacements, "padding": "none", "role": "menu", "append-to": _vm.appendTo, "modal": _vm.modal, "max-height": _vm.maxHeight, "max-width": _vm.maxWidth, "open-with-arrow-keys": _vm.shouldOpenWithArrowKeys, "open-on-context": _vm.openOnContext, "tether": _vm.tether, "transition": _vm.transition }, scopedSlots: _vm._u([{ key: "anchor", fn: function({ attrs }) {
return [_vm._t("anchor", null, null, attrs)];
} }, { key: "content", fn: function({ close }) {
return [_c("ul", { ref: "listWrapper", class: _vm.listClasses, attrs: { "id": _vm.listId, "data-qa": "dt-dropdown-list-wrapper" }, on: { "mouseleave": _vm.clearHighlightIndex, "!mousemove": function($event) {
return _vm.onMouseHighlight.apply(null, arguments);
} } }, [_vm._t("list", null, { "close": close }), _vm.showVisuallyHiddenClose ? _c("sr-only-close-button", { attrs: { "visually-hidden-close-label": _vm.visuallyHiddenCloseLabel, "tabindex": _vm.isArrowKeyNav ? -1 : 0 }, on: { "close": close } }) : _vm._e()], 2)];
} }, { key: "footerContent", fn: function({ close }) {
return [_vm._t("footer", null, { "close": close })];
} }], null, true) }, _vm.dropdownListeners));
};
var _sfc_staticRenderFns = [];
var __component__ = /* @__PURE__ */ normalizeComponent(
_sfc_main,
_sfc_render,
_sfc_staticRenderFns
);
const DtDropdown = __component__.exports;
export {
DtDropdown as default
};
//# sourceMappingURL=dropdown.vue.js.map