@dialpad/dialtone
Version:
Dialpad's Dialtone design system monorepo
410 lines (409 loc) • 14.8 kB
JavaScript
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