@dialpad/dialtone
Version:
Dialpad's Dialtone design system monorepo
669 lines (668 loc) • 21.1 kB
JavaScript
import DtRecipeComboboxWithPopover from "../combobox_with_popover/combobox_with_popover.vue.js";
import DtInput from "../../../components/input/input.vue.js";
import DtChip from "../../../components/chip/chip.vue.js";
import DtValidationMessages from "../../../components/validation_messages/validation_messages.vue.js";
import { validationMessageValidator } from "../../../common/validators.js";
import { hasSlotContent, returnFirstEl } from "../../../common/utils.js";
import { POPOVER_APPEND_TO_VALUES } from "../../../components/popover/popover_constants.js";
import { MULTI_SELECT_SIZES, CHIP_SIZES, CHIP_TOP_POSITION } from "./combobox_multi_select_constants.js";
import SrOnlyCloseButtonMixin from "../../../common/mixins/sr_only_close_button.js";
import { resolveComponent, openBlock, createBlock, createSlots, withCtx, createElementVNode, normalizeClass, createElementBlock, Fragment, renderList, mergeProps, toHandlers, withKeys, createTextVNode, toDisplayString, createVNode, withModifiers, renderSlot } from "vue";
import _export_sfc from "../../../_virtual/_plugin-vue_export-helper.js";
const _sfc_main = {
compatConfig: { MODE: 3 },
name: "DtRecipeComboboxMultiSelect",
components: {
DtRecipeComboboxWithPopover,
DtInput,
DtChip,
DtValidationMessages
},
mixins: [SrOnlyCloseButtonMixin],
props: {
/**
* String to use for the input label.
*/
label: {
type: String,
required: true
},
/**
* Determines visibility of input label.
* @values true, false
*/
labelVisible: {
type: Boolean,
default: true
},
/**
* Description for the input
*/
description: {
type: String,
default: ""
},
/**
* Input placeholder
*/
placeholder: {
type: String,
default: "Select one or start typing"
},
/**
* Input validation messages
*/
inputMessages: {
type: Array,
default: () => [],
validator: (inputMessages) => {
return validationMessageValidator(inputMessages);
}
},
/**
* Show input validation message
*/
showInputMessages: {
type: Boolean,
default: true
},
// @TODO: https://dialpad.atlassian.net/browse/DP-52324
// type: {
// type: String,
// values: ['input', 'select'],
// default: 'select',
// },
/**
* Determines if the list is loading
*/
loading: {
type: Boolean,
default: false
},
/**
* The message when the list is loading
*/
loadingMessage: {
type: String,
default: "loading..."
},
/**
* Determines when to show the list element and also controls the aria-expanded attribute.
* Leaving this null will have the combobox trigger on input focus by default.
* If you set this value, the default trigger behavior will be disabled and you can
* control it as you need.
*/
showList: {
type: Boolean,
default: null
},
/**
* Determines maximum height for the popover before overflow.
* Possible units rem|px|em
*/
listMaxHeight: {
type: String,
default: "300px"
},
/**
* The selected items
*/
selectedItems: {
type: Array,
default: function() {
return [];
}
},
/**
* Would be the maximum number of selections you can make. 0 is unlimited
*/
maxSelected: {
type: Number,
default: 0
},
/**
* Max select message when the max selections is exceeded with the structure:
* `[{"message": string, "type": VALIDATION_MESSAGE_TYPES }]`
*/
maxSelectedMessage: {
type: Array,
default: function() {
return [];
}
},
/**
* Displays the list when the combobox is focused, before the user has typed anything.
* When this is enabled the list will not close after selection.
*/
hasSuggestionList: {
type: Boolean,
default: true
},
/**
* Size of the chip, one of `xs`, `sm`, `md`
*/
size: {
type: String,
default: "md",
validator: (t) => Object.values(MULTI_SELECT_SIZES).includes(t)
},
/**
* 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;
}
},
/**
* Named transition when the content display is toggled.
* @see DtLazyShow
*/
transition: {
type: String,
default: "fade"
},
/**
* Determines whether the combobox should collapse to a single when losing focus.
* @type {boolean}
*/
collapseOnFocusOut: {
type: Boolean,
default: false
},
/**
* Determines maximum width for the popover before overflow.
* Possible units rem|px|em
*/
listMaxWidth: {
type: String,
default: ""
},
/**
* Amount of reserved space (in px) on the right side of the input
* before the chips and the input caret jump to the next line.
* default is 64
*/
reservedRightSpace: {
type: Number,
default: 64
},
/**
* Determines the maximum width of a single chip. If the text within this chip exceeds the value
* it will be truncated with ellipses.
* Possible units rem|px|em
*/
chipMaxWidth: {
type: String,
default: ""
},
/**
* Additional class name for the input element.
* Can accept String, Object, and Array, i.e. has the
* same API as Vue's built-in handling of the class attribute.
*/
inputClass: {
type: [String, Object, Array],
default: ""
},
/**
* Additional class name for the input wrapper element.
* Can accept all of String, Object, and Array, i.e. has the
* same api as Vue's built-in handling of the class attribute.
*/
inputWrapperClass: {
type: [String, Object, Array],
default: ""
}
},
emits: [
/**
* Native input event
*
* @event input
* @type {String }
*/
"input",
/**
* Event fired when item selected
*
* @event select
* @type {Number}
*/
"select",
/**
* Event fired when item removed
*
* @event remove
* @type {String}
*/
"remove",
/**
* Event fired when max selected items limit is reached
*
* @event max-selected
* @type {Object}
*/
"max-selected",
/**
* Native keyup event
*
* @event keyup
* @type {KeyboardEvent}
*/
"keyup",
/**
* Event fired when combobox item is highlighted
*
* @event combobox-highlight
* @type {Object}
*/
"combobox-highlight"
],
data() {
return {
value: "",
popoverOffset: [0, 4],
showValidationMessages: false,
resizeWindowObserver: null,
initialInputHeight: null,
CHIP_SIZES,
hasSlotContent,
inputFocused: false,
hideInputText: false
};
},
computed: {
inputPlaceHolder() {
var _a;
return ((_a = this.selectedItems) == null ? void 0 : _a.length) > 0 ? "" : this.placeholder;
},
chipListeners() {
return {
keyup: (event) => {
this.onChipKeyup(event);
this.$emit("keyup", event);
}
};
},
inputListeners() {
return {
input: (event) => {
this.$emit("input", event);
if (this.hasSuggestionList) {
this.showComboboxList();
}
},
keyup: (event) => {
this.onInputKeyup(event);
this.$emit("keyup", event);
},
click: (event) => {
if (this.hasSuggestionList) {
this.showComboboxList();
}
}
};
},
chipWrapperClass() {
return {
[`d-recipe-combobox-multi-select__chip-wrapper-${this.size}--collapsed`]: !this.inputFocused && this.collapseOnFocusOut
};
}
},
watch: {
selectedItems: {
deep: true,
handler: async function() {
this.initSelectedItems();
}
},
chipMaxWidth: {
async handler() {
this.initSelectedItems();
}
},
async label() {
await this.$nextTick();
this.setChipsTopPosition();
},
async description() {
await this.$nextTick();
this.setChipsTopPosition();
},
size: {
async handler() {
await this.$nextTick();
const input = this.getInput();
this.revertInputPadding(input);
this.initialInputHeight = input.getBoundingClientRect().height;
this.setInputPadding();
this.setChipsTopPosition();
}
}
},
mounted() {
this.setInitialInputHeight();
this.resizeWindowObserver = new ResizeObserver(async () => {
this.setChipsTopPosition();
this.setInputPadding();
});
this.resizeWindowObserver.observe(document.body);
this.initSelectedItems();
},
beforeUnmount() {
var _a;
(_a = this.resizeWindowObserver) == null ? void 0 : _a.unobserve(document.body);
},
methods: {
comboboxHighlight(highlightIndex) {
this.$emit("combobox-highlight", highlightIndex);
},
async initSelectedItems() {
await this.$nextTick();
this.setInputPadding();
this.setChipsTopPosition();
this.setInputMinWidth();
this.checkMaxSelected();
},
onChipRemove(item) {
var _a;
this.$emit("remove", item);
(_a = this.$refs.input) == null ? void 0 : _a.focus();
},
onComboboxSelect(i) {
if (this.loading) return;
this.value = "";
this.$emit("select", i);
},
showComboboxList() {
var _a;
if (this.showList != null) {
return;
}
(_a = this.$refs.comboboxWithPopover) == null ? void 0 : _a.showComboboxList();
},
closeComboboxList() {
var _a;
if (this.showList != null) {
return;
}
(_a = this.$refs.comboboxWithPopover) == null ? void 0 : _a.closeComboboxList();
},
getChipButtons() {
return this.$refs.chips && this.$refs.chips.map((chip) => returnFirstEl(chip.$el).querySelector("button"));
},
getChips() {
return this.$refs.chips && this.$refs.chips.map((chip) => returnFirstEl(chip.$el));
},
getLastChipButton() {
return this.$refs.chips && this.getChipButtons()[this.getChipButtons().length - 1];
},
getLastChip() {
return this.$refs.chips && this.getChips()[this.getChips().length - 1];
},
getFirstChip() {
return this.$refs.chips && this.getChips()[0];
},
getInput() {
var _a;
return (_a = this.$refs.input) == null ? void 0 : _a.$refs.input;
},
onChipKeyup(event) {
var _a;
const key = (_a = event.code) == null ? void 0 : _a.toLowerCase();
if (key === "arrowleft") {
this.navigateBetweenChips(event.target, true);
} else if (key === "arrowright") {
if (event.target.id === this.getLastChipButton().id) {
this.moveFromChipToInput();
} else {
this.navigateBetweenChips(event.target, false);
}
}
},
onInputKeyup(event) {
var _a;
const key = (_a = event.code) == null ? void 0 : _a.toLowerCase();
if (this.selectedItems.length > 0 && event.target.selectionStart === 0) {
if (key === "backspace" || key === "arrowleft") {
this.moveFromInputToChip();
}
}
},
moveFromInputToChip() {
var _a;
this.getLastChipButton().focus();
(_a = this.$refs.input) == null ? void 0 : _a.blur();
this.closeComboboxList();
},
moveFromChipToInput() {
var _a;
this.getLastChipButton().blur();
(_a = this.$refs.input) == null ? void 0 : _a.focus();
this.showComboboxList();
},
navigateBetweenChips(target, toLeft) {
var _a;
const from = this.getChipButtons().indexOf(target);
const to = toLeft ? from - 1 : from + 1;
if (to < 0 || to >= ((_a = this.$refs.chips) == null ? void 0 : _a.length)) {
return;
}
this.getChipButtons()[from].blur();
this.getChipButtons()[to].focus();
this.closeComboboxList();
},
setChipsTopPosition() {
const input = this.getInput();
if (!input) return;
const inputSlotWrapper = this.$refs.inputSlotWrapper;
const top = input.getBoundingClientRect().top - inputSlotWrapper.getBoundingClientRect().top;
const chipsWrapper = this.$refs.chipsWrapper;
chipsWrapper.style.top = top - CHIP_TOP_POSITION[this.size] + "px";
},
setInputPadding() {
const lastChip = this.getLastChip();
const input = this.getInput();
const chipsWrapper = this.$refs.chipsWrapper;
if (!input) return;
this.revertInputPadding(input);
this.popoverOffset = [0, 4];
if (!lastChip) return;
if (this.collapseOnFocusOut && !this.inputFocused) return;
const left = lastChip.offsetLeft + this.getFullWidth(lastChip);
const spaceLeft = input.getBoundingClientRect().width - left;
if (spaceLeft > this.reservedRightSpace) {
input.style.paddingLeft = left + "px";
} else {
input.style.paddingLeft = "4px";
}
const chipsWrapperHeight = chipsWrapper.getBoundingClientRect().height - 4;
const lastChipHeight = lastChip.getBoundingClientRect().height - 4;
const top = spaceLeft > this.reservedRightSpace ? lastChip.offsetTop + 2 : chipsWrapperHeight + lastChipHeight - 9;
input.style.paddingTop = `${top}px`;
},
revertInputPadding(input) {
input.style.paddingLeft = "";
input.style.paddingTop = "";
input.style.paddingBottom = "";
},
getFullWidth(el) {
const styles = window.getComputedStyle(el);
return el.offsetWidth + parseInt(styles.marginLeft) + parseInt(styles.marginRight);
},
setInputMinWidth() {
const firstChip = this.getFirstChip();
const input = this.getInput();
if (!input) return;
if (firstChip) {
input.style.minWidth = this.getFullWidth(firstChip) + 4 + "px";
} else {
input.style.minWidth = "";
}
},
checkMaxSelected() {
if (this.maxSelected === 0) return;
if (this.selectedItems.length > this.maxSelected) {
this.showValidationMessages = true;
this.$emit("max-selected");
} else {
this.showValidationMessages = false;
}
},
setInitialInputHeight() {
const input = this.getInput();
if (!input) return;
this.initialInputHeight = input.getBoundingClientRect().height;
},
async handleInputFocusIn() {
this.inputFocused = true;
if (this.collapseOnFocusOut) {
await this.$nextTick();
this.setInputPadding();
this.hideInputText = false;
}
},
async handleInputFocusOut() {
this.inputFocused = false;
if (this.collapseOnFocusOut) {
const input = this.getInput();
if (!input) return;
if (!input.style.paddingTop) {
return;
}
this.hideInputText = true;
this.revertInputPadding(input);
}
}
}
};
const _hoisted_1 = { ref: "header" };
const _hoisted_2 = {
key: 1,
class: "d-recipe-combobox-multi-select__list--loading"
};
const _hoisted_3 = { ref: "footer" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_dt_chip = resolveComponent("dt-chip");
const _component_dt_input = resolveComponent("dt-input");
const _component_dt_validation_messages = resolveComponent("dt-validation-messages");
const _component_dt_recipe_combobox_with_popover = resolveComponent("dt-recipe-combobox-with-popover");
return openBlock(), createBlock(_component_dt_recipe_combobox_with_popover, {
ref: "comboboxWithPopover",
label: $props.label,
"show-list": $props.showList,
"max-height": $props.listMaxHeight,
"max-width": $props.listMaxWidth,
"popover-offset": $data.popoverOffset,
"has-suggestion-list": $props.hasSuggestionList,
"visually-hidden-close-label": _ctx.visuallyHiddenCloseLabel,
"visually-hidden-close": _ctx.visuallyHiddenClose,
"content-width": "anchor",
"append-to": $props.appendTo,
transition: $props.transition,
onSelect: $options.onComboboxSelect,
onHighlight: $options.comboboxHighlight
}, createSlots({
input: withCtx(({ onInput }) => [
createElementVNode("span", {
ref: "inputSlotWrapper",
class: "d-recipe-combobox-multi-select__input-wrapper",
onFocusin: _cache[1] || (_cache[1] = (...args) => $options.handleInputFocusIn && $options.handleInputFocusIn(...args)),
onFocusout: _cache[2] || (_cache[2] = (...args) => $options.handleInputFocusOut && $options.handleInputFocusOut(...args))
}, [
createElementVNode("span", {
ref: "chipsWrapper",
class: normalizeClass(["d-recipe-combobox-multi-select__chip-wrapper", $options.chipWrapperClass])
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList($props.selectedItems, (item) => {
return openBlock(), createBlock(_component_dt_chip, mergeProps({
ref_for: true,
ref: "chips",
key: item,
"label-class": ["d-chip__label"],
class: [
"d-recipe-combobox-multi-select__chip",
{ "d-recipe-combobox-multi-select__chip--truncate": !!$props.chipMaxWidth }
],
style: { maxWidth: $props.chipMaxWidth },
"close-button-props": { ariaLabel: "close" },
size: $data.CHIP_SIZES[$props.size]
}, toHandlers($options.chipListeners), {
onKeyup: withKeys(($event) => $options.onChipRemove(item), ["backspace"]),
onClose: ($event) => $options.onChipRemove(item)
}), {
default: withCtx(() => [
createTextVNode(toDisplayString(item), 1)
]),
_: 2
}, 1040, ["class", "style", "size", "onKeyup", "onClose"]);
}), 128))
], 2),
createVNode(_component_dt_input, mergeProps({
ref: "input",
modelValue: $data.value,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => $data.value = $event),
class: "d-recipe-combobox-multi-select__input",
"input-class": [
$props.inputClass,
{
"d-recipe-combobox-multi-select__input--hidden": $data.hideInputText
}
],
"input-wrapper-class": $props.inputWrapperClass,
"aria-label": $props.label,
label: $props.labelVisible ? $props.label : "",
description: $props.description,
placeholder: $options.inputPlaceHolder,
"show-messages": $props.showInputMessages,
messages: $props.inputMessages,
size: $props.size
}, toHandlers($options.inputListeners), { onInput }), null, 16, ["modelValue", "input-class", "input-wrapper-class", "aria-label", "label", "description", "placeholder", "show-messages", "messages", "size", "onInput"]),
createVNode(_component_dt_validation_messages, {
"validation-messages": $props.maxSelectedMessage,
"show-messages": $data.showValidationMessages
}, null, 8, ["validation-messages", "show-messages"])
], 544)
]),
list: withCtx(() => [
createElementVNode("div", {
ref: "list",
class: "d-recipe-combobox-multi-select__list",
onMousedown: _cache[3] || (_cache[3] = withModifiers(() => {
}, ["prevent"]))
}, [
!$props.loading ? renderSlot(_ctx.$slots, "list", { key: 0 }) : (openBlock(), createElementBlock("div", _hoisted_2, toDisplayString($props.loadingMessage), 1))
], 544)
]),
_: 2
}, [
$data.hasSlotContent(_ctx.$slots.header) ? {
name: "header",
fn: withCtx(() => [
createElementVNode("div", _hoisted_1, [
renderSlot(_ctx.$slots, "header")
], 512)
]),
key: "0"
} : void 0,
$data.hasSlotContent(_ctx.$slots.footer) ? {
name: "footer",
fn: withCtx(() => [
createElementVNode("div", _hoisted_3, [
renderSlot(_ctx.$slots, "footer")
], 512)
]),
key: "1"
} : void 0
]), 1032, ["label", "show-list", "max-height", "max-width", "popover-offset", "has-suggestion-list", "visually-hidden-close-label", "visually-hidden-close", "append-to", "transition", "onSelect", "onHighlight"]);
}
const combobox_multi_select = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
export {
combobox_multi_select as default
};
//# sourceMappingURL=combobox_multi_select.vue.js.map