fix: finish custom categories
This commit is contained in:
parent
07cd30c8e0
commit
fa16515fd0
|
@ -78,19 +78,19 @@
|
|||
</div>
|
||||
<div class="nav"
|
||||
role="tablist"
|
||||
style="grid-template-columns: repeat({categories.length}, 1fr);"
|
||||
style="grid-template-columns: repeat({groups.length}, 1fr);"
|
||||
aria-label={i18n.categoriesLabel}
|
||||
on:keydown={onNavKeydown}>
|
||||
{#each categories as category (category.group)}
|
||||
{#each groups as group (group.id)}
|
||||
<button role="tab"
|
||||
class="nav-button"
|
||||
aria-controls="tab-{category.group}"
|
||||
aria-label={i18n.categories[category.name]}
|
||||
aria-selected={currentCategory.group === category.group}
|
||||
title={i18n.categories[category.name]}
|
||||
on:click={() => onCategoryClick(category)}>
|
||||
aria-controls="tab-{group.id}"
|
||||
aria-label={i18n.categories[group.name]}
|
||||
aria-selected={group.id === group.id}
|
||||
title={i18n.categories[group.name]}
|
||||
on:click={() => onNavClick(group)}>
|
||||
<div class="emoji">
|
||||
{category.emoji}
|
||||
{group.emoji}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
@ -111,41 +111,41 @@
|
|||
|
||||
<div class="tabpanel {message ? 'gone': ''}"
|
||||
role={searchMode ? 'region' : 'tabpanel'}
|
||||
aria-label={searchMode ? i18n.searchResultsLabel : i18n.categories[currentCategory.name]}
|
||||
id={searchMode ? '' : `tab-${currentCategory.group}`}
|
||||
aria-label={searchMode ? i18n.searchResultsLabel : i18n.categories[currentGroup.name]}
|
||||
id={searchMode ? '' : `tab-${currentGroup.id}`}
|
||||
tabindex="0"
|
||||
on:click={onEmojiClick}
|
||||
>
|
||||
{#each currentEmojisWithCategories as emojiWithCategory (emojiWithCategory.category)}
|
||||
<div class="emoji-menu"
|
||||
role={searchMode ? 'listbox' : 'menu'}
|
||||
aria-label={searchMode ? i18n.searchResultsLabel : emojiWithCategory.category ? emojiWithCategory.category : i18n.categories[currentCategory.name]}
|
||||
{...(searchMode ? { 'id': 'search-results' } : null)}
|
||||
use:calculateEmojiGridWith
|
||||
>
|
||||
{#if currentEmojisWithCategories.length}
|
||||
<span class="category" aria-hidden="true">
|
||||
{emojiWithCategory.category ? emojiWithCategory.category : i18n.categories[currentCategory.name]}
|
||||
</span>
|
||||
{#each currentEmojisWithCategories as emojiWithCategory (emojiWithCategory.category)}
|
||||
{#if currentEmojisWithCategories.length > 1}
|
||||
<div class="category" aria-hidden="true">
|
||||
{emojiWithCategory.category || i18n.categories[currentGroup.name]}
|
||||
</div>
|
||||
{/if}
|
||||
{#each emojiWithCategory.emojis as emoji, i (emoji.id)}
|
||||
<button role={searchMode ? 'option' : 'menuitem'}
|
||||
{...(searchMode ? { 'aria-selected': i == activeSearchItem } : null)}
|
||||
aria-label={emoji.label}
|
||||
title={emoji.title}
|
||||
class="emoji {searchMode && i === activeSearchItem ? 'active' : ''}"
|
||||
data-emoji={emoji.id}>
|
||||
{#if emoji.unicode}
|
||||
{unicodeWithSkin(emoji, currentSkinTone)}
|
||||
{:else}
|
||||
<img class="custom-emoji"
|
||||
src={emoji.url}
|
||||
loading="lazy"
|
||||
alt=""
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="emoji-menu"
|
||||
role={searchMode ? 'listbox' : 'menu'}
|
||||
aria-label={searchMode ? i18n.searchResultsLabel : (emojiWithCategory.category || i18n.categories[currentGroup.name])}
|
||||
{...(searchMode ? { 'id': 'search-results' } : null)}
|
||||
use:calculateEmojiGridWith
|
||||
>
|
||||
{#each emojiWithCategory.emojis as emoji, i (emoji.id)}
|
||||
<button role={searchMode ? 'option' : 'menuitem'}
|
||||
{...(searchMode ? { 'aria-selected': i == activeSearchItem } : null)}
|
||||
aria-label={emoji.label}
|
||||
title={emoji.title}
|
||||
class="emoji {searchMode && i === activeSearchItem ? 'active' : ''}"
|
||||
data-emoji={emoji.id}>
|
||||
{#if emoji.unicode}
|
||||
{unicodeWithSkin(emoji, currentSkinTone)}
|
||||
{:else}
|
||||
<img class="custom-emoji"
|
||||
src={emoji.url}
|
||||
loading="lazy"
|
||||
alt=""
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Database from '../../ImportedDatabase'
|
||||
import enI18n from '../../i18n/en'
|
||||
import { categories as defaultCategories, customCategory } from '../../categories'
|
||||
import { groups as defaultGroups, customGroup } from '../../groups'
|
||||
import { DEFAULT_LOCALE, DEFAULT_DATA_SOURCE } from '../../../database/constants'
|
||||
import { MIN_SEARCH_TEXT_LENGTH, NUM_SKIN_TONES } from '../../../shared/constants'
|
||||
import { requestIdleCallback } from '../../utils/requestIdleCallback'
|
||||
|
@ -36,7 +36,7 @@ let customEmoji = null
|
|||
// private
|
||||
let initialLoad = true
|
||||
let currentEmojis = []
|
||||
let currentEmojisWithCategories = []
|
||||
let currentEmojisWithCategories = [] // eslint-disable-line no-unused-vars
|
||||
let rawSearchText = ''
|
||||
let searchText = ''
|
||||
let rootElement
|
||||
|
@ -60,9 +60,9 @@ let currentFavorites = [] // eslint-disable-line no-unused-vars
|
|||
let defaultFavoriteEmojis
|
||||
let numColumns = DEFAULT_NUM_COLUMNS
|
||||
let scrollbarWidth = 0 // eslint-disable-line no-unused-vars
|
||||
let currentCategoryIndex = 0
|
||||
let categories = defaultCategories
|
||||
let currentCategory
|
||||
let currentGroupIndex = 0
|
||||
let groups = defaultGroups
|
||||
let currentGroup
|
||||
|
||||
//
|
||||
// Utils/helpers
|
||||
|
@ -142,7 +142,7 @@ Promise.resolve().then(() => {
|
|||
|
||||
$: style = `
|
||||
--font-family: ${FONT_FAMILY};
|
||||
--num-categories: ${categories.length};
|
||||
--num-groups: ${groups.length};
|
||||
--indicator-opacity: ${searchMode ? 0 : 1};
|
||||
--num-skintones: ${NUM_SKIN_TONES};`
|
||||
|
||||
|
@ -168,9 +168,9 @@ $: {
|
|||
|
||||
$: {
|
||||
if (customEmoji && customEmoji.length) {
|
||||
categories = [customCategory, ...defaultCategories]
|
||||
} else if (categories !== defaultCategories) {
|
||||
categories = defaultCategories
|
||||
groups = [customGroup, ...defaultGroups]
|
||||
} else if (groups !== defaultGroups) {
|
||||
groups = defaultGroups
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,10 +248,10 @@ function calculateEmojiGridWith (node) {
|
|||
}
|
||||
|
||||
//
|
||||
// Update the current category based on the currentCategoryIndex
|
||||
// Update the current group based on the currentGroupIndex
|
||||
//
|
||||
|
||||
$: currentCategory = categories[currentCategoryIndex]
|
||||
$: currentGroup = groups[currentGroupIndex]
|
||||
|
||||
//
|
||||
// Animate the indicator
|
||||
|
@ -271,9 +271,9 @@ function calculateIndicatorWidth (node) {
|
|||
$: {
|
||||
/* istanbul ignore if */
|
||||
if (resizeObserverSupported) {
|
||||
indicatorStyle = `transform: translateX(${currentCategoryIndex * computedIndicatorWidth}px);` // exact pixels
|
||||
indicatorStyle = `transform: translateX(${currentGroupIndex * computedIndicatorWidth}px);` // exact pixels
|
||||
} else {
|
||||
indicatorStyle = `transform: translateX(${currentCategoryIndex * 100}%);`// fallback to percent-based
|
||||
indicatorStyle = `transform: translateX(${currentGroupIndex * 100}%);`// fallback to percent-based
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,9 +290,9 @@ $: {
|
|||
} else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH) {
|
||||
searchMode = true
|
||||
currentEmojis = await getEmojisBySearchQuery(searchText)
|
||||
} else if (currentCategory) {
|
||||
} else if (currentGroup) {
|
||||
searchMode = false
|
||||
currentEmojis = await getEmojisByGroup(currentCategory.group)
|
||||
currentEmojis = await getEmojisByGroup(currentGroup.id)
|
||||
}
|
||||
}
|
||||
/* no await */ updateEmojis()
|
||||
|
@ -368,20 +368,20 @@ async function getEmojisBySearchQuery (query) {
|
|||
//
|
||||
|
||||
$: {
|
||||
function calculateCurrentEmojisWithCategories() {
|
||||
const categoriesToEmoji = {}
|
||||
for (const currentEmoji of currentEmojis) {
|
||||
const category = currentEmoji.category || ''
|
||||
if (currentEmoji.category) {
|
||||
categoriesToEmoji[category] = categoriesToEmoji[category] || []
|
||||
categoriesToEmoji[category].push(currentEmoji)
|
||||
function calculateCurrentEmojisWithCategories () {
|
||||
const categoriesToEmoji = new Map()
|
||||
for (const emoji of currentEmojis) {
|
||||
const category = emoji.category || ''
|
||||
let emojis = categoriesToEmoji.get(category)
|
||||
if (!emojis) {
|
||||
emojis = []
|
||||
categoriesToEmoji.set(category, emojis)
|
||||
}
|
||||
emojis.push(emoji)
|
||||
}
|
||||
const res = []
|
||||
for (const key of Object.keys(categoriesToEmoji).sort()) {
|
||||
res.push({ category: key, emojis: categoriesToEmoji[key] })
|
||||
}
|
||||
return res
|
||||
return [...categoriesToEmoji.entries()]
|
||||
.map(([category, emojis]) => ({ category, emojis }))
|
||||
.sort((a, b) => a.category < b.category ? -1 : 1)
|
||||
}
|
||||
|
||||
currentEmojisWithCategories = calculateCurrentEmojisWithCategories()
|
||||
|
@ -427,11 +427,11 @@ function onSearchKeydown (event) {
|
|||
//
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function onCategoryClick (category) {
|
||||
function onNavClick (group) {
|
||||
rawSearchText = ''
|
||||
searchText = ''
|
||||
activeSearchItem = -1
|
||||
currentCategoryIndex = categories.findIndex(_ => _.group === category.group)
|
||||
currentGroupIndex = groups.findIndex(_ => _.id === group.id)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
@ -68,6 +68,11 @@ $skintoneZIndex3: 3;
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.category {
|
||||
padding: var(--emoji-padding);
|
||||
font-size: var(--category-font-size);
|
||||
}
|
||||
|
||||
//
|
||||
// emoji
|
||||
//
|
||||
|
@ -137,7 +142,7 @@ button.emoji,
|
|||
.indicator {
|
||||
$opacityAnim: 0.1s;
|
||||
$transformAnim: 0.25s;
|
||||
width: calc(100% / var(--num-categories));
|
||||
width: calc(100% / var(--num-groups));
|
||||
height: var(--indicator-height);
|
||||
opacity: var(--indicator-opacity);
|
||||
background-color: var(--indicator-color);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// via https://unpkg.com/browse/emojibase-data@5.0.1/meta/groups.json
|
||||
const allCategories = [
|
||||
const allGroups = [
|
||||
[-1, '✨', 'custom'],
|
||||
[0, '😀', 'smileys-emotion'],
|
||||
[1, '👋', 'people-body'],
|
||||
|
@ -10,7 +10,7 @@ const allCategories = [
|
|||
[7, '📝', 'objects'],
|
||||
[8, '⛔️', 'symbols'],
|
||||
[9, '🏁', 'flags']
|
||||
].map(([group, emoji, name]) => ({ group, emoji, name }))
|
||||
].map(([id, emoji, name]) => ({ id, emoji, name }))
|
||||
|
||||
export const categories = allCategories.slice(1)
|
||||
export const customCategory = allCategories[0]
|
||||
export const groups = allGroups.slice(1)
|
||||
export const customGroup = allGroups[0]
|
|
@ -11,6 +11,7 @@
|
|||
--outline-size: 2px; /* Focus outline width */
|
||||
--border-size: 1px; /* Width of border used in most of the picker */
|
||||
--skintone-border-radius: 1rem; /* border radius of the skintone dropdown */
|
||||
--category-font-size: 1rem; /* Font size of custom emoji category headings */
|
||||
}
|
||||
|
||||
@mixin colors($dark) {
|
||||
|
|
|
@ -16,13 +16,14 @@ export function summarizeEmojisForUI (emojis, emojiSupportLevel) {
|
|||
return res
|
||||
}
|
||||
|
||||
return emojis.map(({ unicode, skins, shortcodes, url, name }) => ({
|
||||
return emojis.map(({ unicode, skins, shortcodes, url, name, category }) => ({
|
||||
unicode,
|
||||
name,
|
||||
skins: skins && toSimpleSkinsMap(skins),
|
||||
label: uniq([(unicode || name), ...shortcodes]).join(', '),
|
||||
title: shortcodes.join(', '),
|
||||
url,
|
||||
id: unicode || name
|
||||
id: unicode || name,
|
||||
category
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { basicBeforeEach, basicAfterEach, ALL_EMOJI, truncatedEmoji, tick } from
|
|||
import * as testingLibrary from '@testing-library/dom'
|
||||
import Picker from '../../../src/picker/PickerElement.js'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { categories } from '../../../src/picker/categories'
|
||||
import { groups } from '../../../src/picker/groups'
|
||||
|
||||
const { waitFor, fireEvent } = testingLibrary
|
||||
const { type } = userEvent
|
||||
|
@ -44,7 +44,7 @@ describe('Picker tests', () => {
|
|||
|
||||
test('basic picker test', async () => {
|
||||
await waitFor(() => expect(getByRole('button', { name: 'Choose a skin tone (currently Default)' })).toBeVisible())
|
||||
expect(getAllByRole('tab')).toHaveLength(categories.length)
|
||||
expect(getAllByRole('tab')).toHaveLength(groups.length)
|
||||
|
||||
expect(getByRole('tab', { name: 'Smileys and emoticons', selected: true })).toBeVisible()
|
||||
await waitFor(() => expect(
|
||||
|
@ -263,4 +263,40 @@ describe('Picker tests', () => {
|
|||
fireEvent.focusOut(getByRole('listbox', { name: 'Skin tones' }))
|
||||
await waitFor(() => expect(queryAllByRole('listbox', { name: 'Skin tones' })).toHaveLength(0))
|
||||
})
|
||||
|
||||
test('Custom emoji with categories', async () => {
|
||||
picker.customEmoji = [
|
||||
{
|
||||
name: 'monkey',
|
||||
shortcodes: ['monkey'],
|
||||
url: 'monkey.png',
|
||||
category: 'Primates'
|
||||
},
|
||||
{
|
||||
name: 'donkey',
|
||||
shortcodes: ['donkey'],
|
||||
url: 'donkey.png',
|
||||
category: 'Ungulates'
|
||||
},
|
||||
{
|
||||
name: 'horse',
|
||||
shortcodes: ['horse'],
|
||||
url: 'horse.png',
|
||||
category: 'Ungulates'
|
||||
},
|
||||
{
|
||||
name: 'human',
|
||||
shortcodes: ['human'],
|
||||
url: 'human.png'
|
||||
}
|
||||
]
|
||||
await waitFor(() => expect(getAllByRole('tab')).toHaveLength(groups.length + 1))
|
||||
await waitFor(() => expect(getAllByRole('menu')).toHaveLength(4)) // favorites + three custom categories
|
||||
await waitFor(() => expect(getByRole('menuitem', { name: 'human' })).toBeVisible())
|
||||
await waitFor(() => expect(getByRole('menuitem', { name: 'donkey' })).toBeVisible())
|
||||
await waitFor(() => expect(getByRole('menuitem', { name: 'monkey' })).toBeVisible())
|
||||
await waitFor(() => expect(getByRole('menuitem', { name: 'horse' })).toBeVisible())
|
||||
// TODO: can't actually test the category names because they're only exposed as menus, and
|
||||
// testing-library doesn't seem to understand that menus can have aria-labels
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,7 +7,7 @@ import Picker from '../../../src/picker/PickerElement'
|
|||
import allData from 'emojibase-data/en/data.json'
|
||||
import { MOST_COMMONLY_USED_EMOJI } from '../../../src/picker/constants'
|
||||
import { uniqBy } from '../../../src/shared/uniqBy'
|
||||
import { categories } from '../../../src/picker/categories'
|
||||
import { groups } from '../../../src/picker/groups'
|
||||
|
||||
describe('Favorites UI', () => {
|
||||
let picker
|
||||
|
@ -73,13 +73,13 @@ describe('Favorites UI', () => {
|
|||
}
|
||||
]
|
||||
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(categories.length))
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(groups.length))
|
||||
|
||||
// when setting custom emoji, they can appear in the favorites
|
||||
|
||||
picker.customEmoji = customEmoji
|
||||
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(categories.length + 1))
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(groups.length + 1))
|
||||
|
||||
expect(getByRole(container, 'tab', { name: 'Custom', selected: true })).toBeVisible()
|
||||
await waitFor(() => expect(getByRole(container, 'menuitem', { name: /transparent/i })).toBeVisible())
|
||||
|
@ -98,7 +98,7 @@ describe('Favorites UI', () => {
|
|||
// when setting custom emoji back to [], the favorites bar removes the custom emoji
|
||||
picker.customEmoji = []
|
||||
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(categories.length))
|
||||
await waitFor(() => expect(getAllByRole(container, 'tab')).toHaveLength(groups.length))
|
||||
|
||||
await waitFor(
|
||||
() => expect(queryAllByRole(getByTestId(container, 'favorites'), 'menuitem', { name: /transparent/i })).toHaveLength(0)
|
||||
|
|
Loading…
Reference in New Issue