UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

410 lines (409 loc) 14.8 kB
import { ref, computed, watch, nextTick, onMounted, onUnmounted, openBlock, createElementBlock, createElementVNode, toDisplayString, Fragment, renderList, withDirectives, createCommentVNode, unref, vShow, normalizeClass } from "vue"; import { emojisGrouped } from "@dialpad/dialtone-emojis"; import { CDN_URL, ARROW_KEYS } from "../emoji_picker_constants.js"; import { useKeyboardNavigation } from "../composables/useKeyboardNavigation.js"; const _hoisted_1 = { class: "d-emoji-picker__selector" }; const _hoisted_2 = { key: 0, class: "d-emoji-picker__search-label d-emoji-picker__alignment" }; const _hoisted_3 = { key: 0 }; const _hoisted_4 = { class: "d-emoji-picker__tab" }; const _hoisted_5 = ["aria-label", "onClick", "onFocusin", "onMouseover", "onKeydown"]; const _hoisted_6 = ["alt", "aria-label", "title", "src"]; const _hoisted_7 = { key: 2, class: "d-emoji-picker__alignment" }; const _hoisted_8 = { class: "d-emoji-picker__tab", "data-qa": "filtered-emojis" }; const _hoisted_9 = ["aria-label", "onClick", "onFocusin", "onMouseover", "onKeydown"]; const _hoisted_10 = ["alt", "aria-label", "title", "src"]; const _sfc_main = { __name: "emoji_selector", props: { /** * The filter to apply to the emoji list * @type {String} * @default '' */ emojiFilter: { type: String, default: "" }, /** * The skin tone to apply to the emoji list * @type {String} * @required */ skinTone: { type: String, required: true }, /** * The labels for the tabset * @type {Array} * @required */ tabsetLabels: { type: Array, required: true }, selectedTabset: { type: Object, required: true }, /** * The label for the search results tab * @type {String} * @required */ searchResultsLabel: { type: String, required: true }, searchNoResultsLabel: { type: String, required: true }, /** * The list of recently used emojis * @type {Array} */ recentlyUsedEmojis: { type: Array, default: () => [] } }, emits: [ /** * Emitted when the user hover over an emoji * @event highlighted-emoji * @param {Object} emoji - The emoji data that was hovered */ "highlighted-emoji", /** * Emitted when the user select an emoji * @event selected-emoji * @param {Object} emoji - The emoji data that was selected */ "selected-emoji", /** * Emitted when the user scroll into an emoji tab * @event scroll-into-tab * @param {Number} tab-index - The tab that was scrolled into */ "scroll-into-tab", /** * Emitted when the user reach the end of the emoji list * @event focus-skin-selector */ "focus-skin-selector", /** * Emitted when the user shift tab in first tab of emoji selector * @event focus-search-input */ "focus-search-input" ], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emits = __emit; const { emojiFilteredRefs, isFiltering, hoverFirstEmoji, setEmojiRef, setFilteredRef, focusEmoji, handleArrowNavigationFiltered, handleArrowNavigation } = useKeyboardNavigation(); const tabCategoryRef = ref(null); const listRef = ref(null); const tabLabelObserver = ref(null); const TABS_DATA = ["Recently used", "People", "Nature", "Food", "Activity", "Travel", "Objects", "Symbols", "Flags"]; const tabLabels = computed(() => { return props.recentlyUsedEmojis.length ? props.tabsetLabels.map((label) => ({ label, ref: ref(null) })) : props.tabsetLabels.slice(1).map((label) => ({ label, ref: ref(null) })); }); const fixedLabel = ref(tabLabels.value[0].label); const tabs = computed(() => { return props.recentlyUsedEmojis.length ? TABS_DATA : TABS_DATA.slice(1); }); const filteredEmojis = ref([]); const currentEmojis = computed(() => { return [ ...emojisGrouped[`People${props.skinTone}`], ...emojisGrouped.Nature, ...emojisGrouped.Food, ...emojisGrouped[`Activity${props.skinTone}`], ...emojisGrouped.Travel, ...emojisGrouped[`Objects${props.skinTone}`], ...emojisGrouped.Symbols, ...emojisGrouped.Flags ]; }); const debouncedSearch = debounce(() => { emojiFilteredRefs.value = []; searchByNameAndKeywords(); }); watch(currentEmojis, () => { searchByNameAndKeywords(); }, { immediate: true }); watch( () => props.recentlyUsedEmojis, () => { emojisGrouped["Recently used"] = props.recentlyUsedEmojis; }, { immediate: true } ); watch(() => props.emojiFilter, () => { resetScroll(); if (props.emojiFilter) { isFiltering.value = true; } else { isFiltering.value = false; highlightEmoji(null); } debouncedSearch(); }); watch( () => props.selectedTabset, (tab) => { scrollToTab(tab.tabId); }, { deep: true } ); function hoverEmoji(emoji, isFirst = false) { hoverFirstEmoji.value = isFirst; emits("highlighted-emoji", emoji); } function searchByNameAndKeywords() { const searchStr = props.emojiFilter.toLowerCase(); filteredEmojis.value = currentEmojis.value.filter((obj) => { const nameIncludesSearchStr = obj.name.toLowerCase().includes(searchStr); const keywordsIncludeSearchStr = obj.keywords.some((keyword) => keyword.toLowerCase().includes(searchStr)); return nameIncludesSearchStr || keywordsIncludeSearchStr; }); nextTick(() => { if (searchStr) { hoverEmoji(filteredEmojis.value[0], true); } }); } function debounce(fn, delay = 300) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } function getImgSrc(emoji) { return `${CDN_URL + emoji}.png`; } function handleImageError(event) { event.target.parentNode.style.display = "none"; } function scrollToTab(tabIndex, focusFirstEmoji = true) { const tabLabel = tabLabels.value[tabIndex - 1]; const tabElement = tabLabel.ref.value[0]; nextTick(() => { const container = listRef.value; const offsetTop = tabIndex === 1 ? 0 : tabElement.offsetTop - 15; container.scrollTop = offsetTop; if (focusFirstEmoji) { focusEmoji(tabIndex - 1, 0); } }); } function resetScroll() { const container = listRef.value; container.scrollTop = 0; } function setTabLabelObserver() { tabLabelObserver.value = new IntersectionObserver(async (entries) => { entries.forEach((entry) => { var _a, _b, _c, _d, _e; const { target } = entry; const index = parseInt(target.dataset.index); if (entry.isIntersecting && target.offsetTop <= tabCategoryRef.value.offsetTop + 50) { fixedLabel.value = ((_a = tabLabels.value[index - 1]) == null ? void 0 : _a.label) ?? ((_b = tabLabels.value[0]) == null ? void 0 : _b.label); emits("scroll-into-tab", index - 1); } else if (entry.boundingClientRect.bottom <= ((_c = tabCategoryRef.value) == null ? void 0 : _c.getBoundingClientRect().bottom)) { emits("scroll-into-tab", index); fixedLabel.value = (_d = tabLabels.value[index]) == null ? void 0 : _d.label; } else if (index === 1) { emits("scroll-into-tab", index); fixedLabel.value = (_e = tabLabels.value[0]) == null ? void 0 : _e.label; } }); }); tabLabelObserver.value.observe(tabCategoryRef.value); Array.from(listRef.value.children).forEach((child, index) => { tabLabelObserver.value.observe(child); child.dataset.index = index; }); } const handleKeyDownFilteredEmojis = (event, indexEmoji, emoji) => { event.preventDefault(); if (Object.values(ARROW_KEYS).includes(event.key)) { handleArrowNavigationFiltered(event.key, indexEmoji); return; } switch (event.key) { case "Tab": emits("focus-skin-selector"); break; case "Enter": selectEmoji(emoji, event); break; } }; const handleKeyDown = (event, indexTab, indexEmoji, emoji) => { event.preventDefault(); if (Object.values(ARROW_KEYS).includes(event.key)) { handleArrowNavigation(event.key, indexTab, indexEmoji); return; } switch (event.key) { case "Tab": if (event.shiftKey) { if (focusEmoji(indexTab, 0) && indexTab > 0) { scrollToTab(indexTab, true); } else { scrollToTab(1, false); emits("focus-search-input"); } } else { if (focusEmoji(indexTab + 1, 0)) { scrollToTab(indexTab + 1 + 1, false); } else { emits("focus-skin-selector"); } } break; case "Enter": selectEmoji(emoji, event); break; } }; function selectEmoji(emoji, event) { emits("selected-emoji", { ...emoji, shift_key: event.shiftKey }); } function highlightEmoji(emoji) { emits("highlighted-emoji", emoji); } function focusEmojiSelector() { focusEmoji(0, 0); } function focusLastEmoji() { scrollToTab(tabs.value.length, true); } onMounted(() => { setTabLabelObserver(); }); onUnmounted(() => { tabLabelObserver.value.disconnect(); }); __expose({ focusEmojiSelector, focusLastEmoji }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", _hoisted_1, [ createElementVNode("div", { id: "d-emoji-picker-list", ref_key: "listRef", ref: listRef, class: "d-emoji-picker__list" }, [ __props.emojiFilter ? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString(filteredEmojis.value.length > 0 ? __props.searchResultsLabel : __props.searchNoResultsLabel), 1)) : (openBlock(), createElementBlock("div", { key: 1, ref_key: "tabCategoryRef", ref: tabCategoryRef, class: "d-emoji-picker__category d-emoji-picker__alignment" }, [ createElementVNode("p", null, toDisplayString(fixedLabel.value), 1) ], 512)), (openBlock(true), createElementBlock(Fragment, null, renderList(tabLabels.value, (tabLabel, indexTab) => { return withDirectives((openBlock(), createElementBlock("div", { key: indexTab, ref_for: true, ref: tabLabel.ref, class: "d-emoji-picker__alignment" }, [ indexTab ? (openBlock(), createElementBlock("p", _hoisted_3, toDisplayString(tabLabel.label), 1)) : createCommentVNode("", true), createElementVNode("div", _hoisted_4, [ (openBlock(true), createElementBlock(Fragment, null, renderList(unref(emojisGrouped)[tabs.value[indexTab] + __props.skinTone] ? unref(emojisGrouped)[tabs.value[indexTab] + __props.skinTone] : unref(emojisGrouped)[tabs.value[indexTab]], (emoji, indexEmoji) => { return openBlock(), createElementBlock("button", { key: emoji.shortname, ref_for: true, ref: (el) => { if (el) unref(setEmojiRef)(el, indexTab, indexEmoji); }, type: "button", "aria-label": emoji.name, onClick: (event) => selectEmoji(emoji, event), onFocusin: ($event) => highlightEmoji(emoji), onFocusout: _cache[0] || (_cache[0] = ($event) => highlightEmoji(null)), onMouseover: ($event) => highlightEmoji(emoji), onMouseleave: _cache[1] || (_cache[1] = ($event) => highlightEmoji(null)), onKeydown: (event) => handleKeyDown(event, indexTab, indexEmoji, emoji) }, [ createElementVNode("img", { class: "d-icon d-icon--size-500", alt: emoji.name, "aria-label": emoji.name, title: emoji.name, src: getImgSrc(emoji.unicode_character), onError: handleImageError }, null, 40, _hoisted_6) ], 40, _hoisted_5); }), 128)) ]) ])), [ [vShow, !__props.emojiFilter] ]); }), 128)), __props.emojiFilter ? (openBlock(), createElementBlock("div", _hoisted_7, [ createElementVNode("div", _hoisted_8, [ (openBlock(true), createElementBlock(Fragment, null, renderList(filteredEmojis.value, (emoji, index) => { return openBlock(), createElementBlock("button", { key: emoji.shortname, ref_for: true, ref: (el) => { if (el) unref(setFilteredRef)(el, index); }, type: "button", "aria-label": emoji.name, class: normalizeClass({ "hover-emoji": index === 0 && unref(hoverFirstEmoji) }), onClick: (event) => selectEmoji(emoji, event), onFocusin: ($event) => highlightEmoji(emoji), onFocusout: _cache[2] || (_cache[2] = ($event) => highlightEmoji(null)), onMouseover: ($event) => hoverEmoji(emoji), onMouseleave: _cache[3] || (_cache[3] = ($event) => hoverEmoji(null)), onKeydown: (event) => handleKeyDownFilteredEmojis(event, index, emoji) }, [ createElementVNode("img", { class: "d-icon d-icon--size-500", alt: emoji.name, "aria-label": emoji.name, title: emoji.name, src: `${unref(CDN_URL) + emoji.unicode_character}.png` }, null, 8, _hoisted_10) ], 42, _hoisted_9); }), 128)) ]) ])) : createCommentVNode("", true) ], 512) ]); }; } }; export { _sfc_main as default }; //# sourceMappingURL=emoji_selector.vue.js.map