diff --git a/css/emoji-mart.css b/css/emoji-mart.css index 04cde68e..381e0401 100644 --- a/css/emoji-mart.css +++ b/css/emoji-mart.css @@ -5,7 +5,7 @@ } .emoji-mart { - font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; font-size: 16px; /* display: inline-block; */ display: flex; @@ -22,6 +22,9 @@ position: relative; display: inline-block; font-size: 0; + border: none; + background: none; + box-shadow: none; } .emoji-mart-emoji span { @@ -35,7 +38,9 @@ } .emoji-type-native { - font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji"; + font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', + 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji', 'EmojiOne Color', + 'Android Emoji'; word-break: keep-all; } @@ -98,7 +103,10 @@ text-align: center; padding: 12px 4px; overflow: hidden; - transition: color .1s ease-out; + transition: color 0.1s ease-out; + border: none; + background: none; + box-shadow: none; } .emoji-mart-anchor:hover, .emoji-mart-anchor-selected { @@ -111,8 +119,10 @@ .emoji-mart-anchor-bar { position: absolute; - bottom: -3px; left: 0; - width: 100%; height: 3px; + bottom: -3px; + left: 0; + width: 100%; + height: 3px; background-color: #464646; } @@ -145,7 +155,7 @@ font-size: 16px; display: block; width: 100%; - padding: .2em .6em; + padding: 0.2em 0.6em; border-radius: 25px; border: 1px solid #d9d9d9; outline: 0; @@ -166,18 +176,21 @@ cursor: default; } -.emoji-mart-category .emoji-mart-emoji:hover:before { +.emoji-mart-category .emoji-mart-emoji:hover:before, +.emoji-mart-emoji-selected:before { z-index: 0; - content: ""; + content: ''; position: absolute; - top: 0; left: 0; - width: 100%; height: 100%; + top: 0; + left: 0; + width: 100%; + height: 100%; background-color: #f4f4f4; border-radius: 100%; opacity: 0; } - -.emoji-mart-category .emoji-mart-emoji:hover:before { +.emoji-mart-category .emoji-mart-emoji:hover:before, +.emoji-mart-emoji-selected:before { opacity: 1; } @@ -192,13 +205,14 @@ /* position: -webkit-sticky; */ } -.emoji-mart-category-label span { +.emoji-mart-category-label h3 { display: block; + font-size: 16px; width: 100%; font-weight: 500; padding: 5px 6px; background-color: #fff; - background-color: rgba(255, 255, 255, .95); + background-color: rgba(255, 255, 255, 0.95); } .emoji-mart-emoji { @@ -217,7 +231,7 @@ display: none; } .emoji-mart-no-results .emoji-mart-no-results-label { - margin-top: .2em; + margin-top: 0.2em; } .emoji-mart-no-results .emoji-mart-emoji:hover:before { content: none; @@ -241,7 +255,8 @@ } .emoji-mart-preview-data { - left: 68px; right: 12px; + left: 68px; + right: 12px; word-break: break-all; } @@ -261,7 +276,7 @@ .emoji-mart-preview-shortname + .emoji-mart-preview-shortname, .emoji-mart-preview-shortname + .emoji-mart-preview-emoticon, .emoji-mart-preview-emoticon + .emoji-mart-preview-emoticon { - margin-left: .5em; + margin-left: 0.5em; } .emoji-mart-preview-emoticon { @@ -279,7 +294,7 @@ } .emoji-mart-title-label { - color: #999A9C; + color: #999a9c; font-size: 21px; font-weight: 300; } @@ -298,7 +313,7 @@ } .emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch-selected:after { - opacity: .75; + opacity: 0.75; } .emoji-mart-skin-swatch { @@ -306,16 +321,28 @@ width: 0; vertical-align: middle; transition-property: width, padding; - transition-duration: .125s; + transition-duration: 0.125s; transition-timing-function: ease-out; } -.emoji-mart-skin-swatch:nth-child(1) { transition-delay: 0s } -.emoji-mart-skin-swatch:nth-child(2) { transition-delay: .03s } -.emoji-mart-skin-swatch:nth-child(3) { transition-delay: .06s } -.emoji-mart-skin-swatch:nth-child(4) { transition-delay: .09s } -.emoji-mart-skin-swatch:nth-child(5) { transition-delay: .12s } -.emoji-mart-skin-swatch:nth-child(6) { transition-delay: .15s } +.emoji-mart-skin-swatch:nth-child(1) { + transition-delay: 0s; +} +.emoji-mart-skin-swatch:nth-child(2) { + transition-delay: 0.03s; +} +.emoji-mart-skin-swatch:nth-child(3) { + transition-delay: 0.06s; +} +.emoji-mart-skin-swatch:nth-child(4) { + transition-delay: 0.09s; +} +.emoji-mart-skin-swatch:nth-child(5) { + transition-delay: 0.12s; +} +.emoji-mart-skin-swatch:nth-child(6) { + transition-delay: 0.15s; +} .emoji-mart-skin-swatch-selected { position: relative; @@ -323,32 +350,46 @@ padding: 0 2px; } .emoji-mart-skin-swatch-selected:after { - content: ""; + content: ''; position: absolute; - top: 50%; left: 50%; - width: 4px; height: 4px; + top: 50%; + left: 50%; + width: 4px; + height: 4px; margin: -2px 0 0 -2px; background-color: #fff; border-radius: 100%; pointer-events: none; opacity: 0; - transition: opacity .2s ease-out; + transition: opacity 0.2s ease-out; } .emoji-mart-skin { display: inline-block; - width: 100%; padding-top: 100%; + width: 100%; + padding-top: 100%; max-width: 12px; border-radius: 100%; } -.emoji-mart-skin-tone-1 { background-color: #ffc93a } -.emoji-mart-skin-tone-2 { background-color: #fadcbc } -.emoji-mart-skin-tone-3 { background-color: #e0bb95 } -.emoji-mart-skin-tone-4 { background-color: #bf8f68 } -.emoji-mart-skin-tone-5 { background-color: #9b643d } -.emoji-mart-skin-tone-6 { background-color: #594539 } - +.emoji-mart-skin-tone-1 { + background-color: #ffc93a; +} +.emoji-mart-skin-tone-2 { + background-color: #fadcbc; +} +.emoji-mart-skin-tone-3 { + background-color: #e0bb95; +} +.emoji-mart-skin-tone-4 { + background-color: #bf8f68; +} +.emoji-mart-skin-tone-5 { + background-color: #9b643d; +} +.emoji-mart-skin-tone-6 { + background-color: #594539; +} /* vue-virtual-scroller/dist/vue-virtual-scroller.css */ .emoji-mart .vue-recycle-scroller { @@ -378,17 +419,23 @@ left: 0; will-change: transform; } -.emoji-mart .vue-recycle-scroller.direction-vertical .vue-recycle-scroller__item-wrapper { +.emoji-mart + .vue-recycle-scroller.direction-vertical + .vue-recycle-scroller__item-wrapper { width: 100%; } -.emoji-mart .vue-recycle-scroller.direction-horizontal .vue-recycle-scroller__item-wrapper { +.emoji-mart + .vue-recycle-scroller.direction-horizontal + .vue-recycle-scroller__item-wrapper { height: 100%; } -.emoji-mart .vue-recycle-scroller.ready.direction-vertical +.emoji-mart + .vue-recycle-scroller.ready.direction-vertical .vue-recycle-scroller__item-view { width: 100%; } -.emoji-mart .vue-recycle-scroller.ready.direction-horizontal +.emoji-mart + .vue-recycle-scroller.ready.direction-horizontal .vue-recycle-scroller__item-view { height: 100%; } @@ -417,3 +464,7 @@ pointer-events: none; z-index: -1; } +.hidden { + display: none; + visibility: hidden; +} diff --git a/spec/__snapshots__/emoji-spec.js.snap b/spec/__snapshots__/emoji-spec.js.snap index f02587f0..bd3d2a1b 100644 --- a/spec/__snapshots__/emoji-spec.js.snap +++ b/spec/__snapshots__/emoji-spec.js.snap @@ -2,6 +2,7 @@ exports[`Emoji native renders correctly native emoji 1`] = ` @@ -11,13 +11,19 @@ exports[`Picker renders correctly 1`] = `
- -
+
@@ -263,20145 +315,31095 @@ exports[`Picker renders correctly 1`] = ` class="emoji-mart-search" > + +
- -
-
- - Frequently Used - -
- - - - - - - - - - - - - + Frequently Used + +
+ + + + + + + + + + + + + + +
-
-
- - Smileys & Emotion - -
- - - + + + + + + + + +
- - - - - - + Smileys & Emotion + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- - - - - - + People & Body + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Animals & Nature + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Food & Drink + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Activity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Travel & Places + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Objects + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Symbols + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - + Flags + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -20415,6 +31417,7 @@ exports[`Picker renders correctly 1`] = ` class="emoji-mart-preview-emoji" > - + `; diff --git a/spec/__snapshots__/picker-virtual-scroll-spec.js.snap b/spec/__snapshots__/picker-virtual-scroll-spec.js.snap index 9c7965b0..bfb267dd 100644 --- a/spec/__snapshots__/picker-virtual-scroll-spec.js.snap +++ b/spec/__snapshots__/picker-virtual-scroll-spec.js.snap @@ -11,13 +11,18 @@ exports[`Picker renders correctly 1`] = `
- -
+
@@ -264,9 +316,22 @@ exports[`Picker renders correctly 1`] = ` class="emoji-mart-search" > + +
@@ -287,20 +352,29 @@ exports[`Picker renders correctly 1`] = `
-
- +

Frequently Used - +

- - - + -
+
-
- +

Smileys & Emotion - +

- - - + -
+
-
- +

People & Body - +

- - - + -
+
-
- +

Animals & Nature - +

- - - + -
+
-
- +

Food & Drink - +

- - - + -
+
-
- +

Activity - +

- - - + -
+
-
- +

Travel & Places - +

- - - + -
+
@@ -12371,6 +18949,7 @@ exports[`Picker renders correctly 1`] = ` class="emoji-mart-preview-emoji" > { it('Has default emojis initially', () => { let categories = picker.findAll(Category) - expect(categories.at(1).vm.name).toBe('Recent') - expect(categories.at(1).vm.emojis.length).toBe(16) + expect(categories.at(0).vm.name).toBe('Recent') + expect(categories.at(0).vm.emojis.length).toBe(16) const DEFAULTS = [ '+1', @@ -36,8 +36,8 @@ describe('Picker frequnt category', () => { 'hankey', ] - for (let idx in categories.at(1).vm.emojis) { - let emoji = categories.at(1).vm.emojis[idx] + for (let idx in categories.at(0).vm.emojis) { + let emoji = categories.at(0).vm.emojis[idx] expect(emoji.id).toBe(DEFAULTS[idx]) } }) @@ -93,7 +93,7 @@ describe('Picker frequnt category', () => { // Wait for picker to be rendered. picker.vm.$nextTick(() => { let categories = newPicker.findAll(Category) - let recent = categories.at(1).vm + let recent = categories.at(0).vm expect(recent.emojis[0].id).toBe('nerd_face') expect(recent.emojis[1].id).toBe('space_invader') diff --git a/spec/keyboard-spec.js b/spec/keyboard-spec.js new file mode 100644 index 00000000..57f70ecf --- /dev/null +++ b/spec/keyboard-spec.js @@ -0,0 +1,130 @@ +import { mount } from '@vue/test-utils' + +import { Picker, Search } from '../src/components' +import data from '../data/all.json' +import { EmojiIndex } from '../src/utils/emoji-data' + +describe('Picker keyboard control', () => { + let index = new EmojiIndex(data) + let picker = null + + beforeEach(() => { + picker = mount(Picker, { + propsData: { + data: index, + }, + }) + }) + + it('Arrow down selects emoji below', () => { + expect(picker.vm.view.previewEmoji).toEqual(null) + + const search = picker.find(Search) + const input = search.find('input') + input.trigger('click') + + input.trigger('keydown.down') + expect(picker.vm.view.previewEmoji.native).toEqual('👍') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.down') + expect(picker.vm.view.previewEmoji.native).toEqual('😞') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.down') + expect(picker.vm.view.previewEmoji.native).toEqual('😀') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('smileys') + }) + + it('Arrow down selects emoji above', () => { + expect(picker.vm.view.previewEmoji).toEqual(null) + + const search = picker.find(Search) + const input = search.find('input') + + input.trigger('click') + input.trigger('keydown.down') + input.trigger('keydown.down') + input.trigger('keydown.down') + + expect(picker.vm.view.previewEmoji.native).toEqual('😀') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('smileys') + + input.trigger('keydown.up') + expect(picker.vm.view.previewEmoji.native).toEqual('😞') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.up') + expect(picker.vm.view.previewEmoji.native).toEqual('👍') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + }) + + it('Arrow right selects emoji on the right', () => { + expect(picker.vm.view.previewEmoji).toEqual(null) + + const search = picker.find(Search) + const input = search.find('input') + input.trigger('click') + + input.trigger('keydown.right') + expect(picker.vm.view.previewEmoji.native).toEqual('👍') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.right') + expect(picker.vm.view.previewEmoji.native).toEqual('😀') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.right') + expect(picker.vm.view.previewEmoji.native).toEqual('😘') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + }) + + it('Arrow left selects emoji on the left', () => { + expect(picker.vm.view.previewEmoji).toEqual(null) + + const search = picker.find(Search) + const input = search.find('input') + + input.trigger('click') + input.trigger('keydown.right') + input.trigger('keydown.right') + input.trigger('keydown.right') + + expect(picker.vm.view.previewEmoji.native).toEqual('😘') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.left') + expect(picker.vm.view.previewEmoji.native).toEqual('😀') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + + input.trigger('keydown.left') + expect(picker.vm.view.previewEmoji.native).toEqual('👍') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('recent') + }) + + it('Enter selects the emoji', () => { + expect(picker.vm.view.previewEmoji).toEqual(null) + + const search = picker.find(Search) + const input = search.find('input') + input.trigger('click') + + input.trigger('keydown.down') + input.trigger('keydown.down') + input.trigger('keydown.down') + + expect(picker.vm.view.previewEmoji.native).toEqual('😀') + expect(picker.vm.view.previewEmojiCategory.id).toEqual('smileys') + + input.trigger('keydown.enter') + + let events = picker.emitted().select + expect(events.length).toBe(1) + let emojiData = events[0][0] + expect(emojiData).toBe(index.emoji('grinning')) + expect(emojiData.id).toBe('grinning') + expect(emojiData.name).toBe('Grinning Face') + expect(emojiData.colons).toBe(':grinning:') + expect(emojiData.native).toBe('😀') + }) +}) diff --git a/spec/picker-spec.js b/spec/picker-spec.js index 1ed4fc73..76304fa8 100644 --- a/spec/picker-spec.js +++ b/spec/picker-spec.js @@ -43,19 +43,19 @@ describe('Picker', () => { it('renders 10 categories', () => { let categories = picker.findAll(Category) - expect(categories.length).toBe(11) - // Hidden category with search results - expect(categories.at(0).vm.name).toBe('Search') - expect(categories.at(1).vm.name).toBe('Recent') - expect(categories.at(2).vm.name).toBe('Smileys & Emotion') - expect(categories.at(3).vm.name).toBe('People & Body') - expect(categories.at(4).vm.name).toBe('Animals & Nature') - expect(categories.at(5).vm.name).toBe('Food & Drink') - expect(categories.at(6).vm.name).toBe('Activities') - expect(categories.at(7).vm.name).toBe('Travel & Places') - expect(categories.at(8).vm.name).toBe('Objects') - expect(categories.at(9).vm.name).toBe('Symbols') - expect(categories.at(10).vm.name).toBe('Flags') + expect(categories.length).toBe(10) + // // Hidden category with search results + // expect(categories.at(0).vm.name).toBe('Search') + expect(categories.at(0).vm.name).toBe('Recent') + expect(categories.at(1).vm.name).toBe('Smileys & Emotion') + expect(categories.at(2).vm.name).toBe('People & Body') + expect(categories.at(3).vm.name).toBe('Animals & Nature') + expect(categories.at(4).vm.name).toBe('Food & Drink') + expect(categories.at(5).vm.name).toBe('Activities') + expect(categories.at(6).vm.name).toBe('Travel & Places') + expect(categories.at(7).vm.name).toBe('Objects') + expect(categories.at(8).vm.name).toBe('Symbols') + expect(categories.at(9).vm.name).toBe('Flags') }) it('no error when clicking anchor when search is active', (done) => { @@ -70,7 +70,7 @@ describe('Picker', () => { expect(searchCategory.vm.id).toBe('search') let anchors = picker.find(Anchors) - let anchorsCategories = anchors.findAll('span.emoji-mart-anchor') + let anchorsCategories = anchors.findAll('.emoji-mart-anchor') let symbols = anchorsCategories.at(8) expect(symbols.element.attributes['data-title'].value).toBe('Symbols') symbols.trigger('click') @@ -105,18 +105,15 @@ describe('categories', () => { it('will not show some based upon our filter', () => { let categories = picker.findAll(Category) - expect(categories.length).toBe(3) - // Hidden category with search results - expect(categories.at(0).vm.name).toBe('Search') - expect(categories.at(0).vm.id).toBe('search') + expect(categories.length).toBe(2) // Visible cateogires - Flags and Activity - expect(categories.at(1).vm.name).toBe('Activities') - expect(categories.at(1).vm.id).toBe('activity') + expect(categories.at(0).vm.name).toBe('Activities') + expect(categories.at(0).vm.id).toBe('activity') // only 1 emoji from Activities matches - expect(categories.at(1).vm.emojis.length).toBe(1) - expect(categories.at(2).vm.name).toBe('Flags') - expect(categories.at(2).vm.id).toBe('flags') - expect(categories.at(2).vm.emojis.length).toBe(250) + expect(categories.at(0).vm.emojis.length).toBe(1) + expect(categories.at(1).vm.name).toBe('Flags') + expect(categories.at(1).vm.id).toBe('flags') + expect(categories.at(1).vm.emojis.length).toBe(250) }) }) @@ -155,11 +152,10 @@ describe('categories include allows to select and order categories', () => { // Note: the error is printed into console. it('will not throw an error if default emoji is not available', () => { let categories = picker.findAll(Category) - expect(categories.length).toBe(4) - expect(categories.at(0).vm.name).toBe('Search') - expect(categories.at(1).vm.name).toBe('Recent') - expect(categories.at(2).vm.name).toBe('Animals & Nature') - expect(categories.at(3).vm.name).toBe('Smileys & Emotion') + expect(categories.length).toBe(3) + expect(categories.at(0).vm.name).toBe('Recent') + expect(categories.at(1).vm.name).toBe('Animals & Nature') + expect(categories.at(2).vm.name).toBe('Smileys & Emotion') }) }) @@ -183,7 +179,7 @@ describe('anchors', () => { it('contains all categories', () => { let anchors = picker.find(Anchors) - let categories = anchors.findAll('span.emoji-mart-anchor') + let categories = anchors.findAll('.emoji-mart-anchor') let names = [] for (let idx = 0; idx < categories.length; idx++) { names.push(categories.at(idx).element.attributes['data-title'].value) @@ -206,12 +202,12 @@ describe('anchors', () => { it('can be clicked to scroll to the category', async () => { let anchors = picker.find(Anchors) - let anchorsCategories = anchors.findAll('span.emoji-mart-anchor') + let anchorsCategories = anchors.findAll('.emoji-mart-anchor') let symbols = anchorsCategories.at(8) expect(symbols.element.attributes['data-title'].value).toBe('Symbols') // The `recent` category is selected initially. - expect(picker.vm.activeCategory.id).toBe('recent') + expect(picker.vm.view.activeCategory.id).toBe('recent') expect(anchors.vm.activeCategory.id).toBe('recent') symbols.trigger('click') @@ -227,7 +223,7 @@ describe('anchors', () => { // Picker change - the check below fails (although works in demo app) // scrollTop if 0 for all categories and activeCategory is changed in the // onScroll handler, need to find a way to thes this. - // expect(picker.vm.activeCategory.id).toBe('symbols') + // expect(picker.vm.view.activeCategory.id).toBe('symbols') // expect(anchors.vm.activeCategory.id).toBe('symbols') }) }) @@ -356,9 +352,9 @@ describe('emjoiSize', () => { let emoji = picker.find('[data-title="+1"]') // The inner span with applied inline style. let emojiSpan = emoji.element.childNodes[0] - // Font-size is 80% of width/height value. + // Font-size is 95% of width/height value. expect(emojiSpan.style.cssText).toBe( - 'background-position: 22.81% 49.12%; font-size: 19.2px;', + 'background-position: 22.81% 49.12%; font-size: 22.8px;', ) }) @@ -367,9 +363,9 @@ describe('emjoiSize', () => { let emoji = picker.find('[data-title="+1"]') // The inner span with applied inline style. let emojiSpan = emoji.element.childNodes[0] - // Font-size is 80% of width/height value. + // Font-size is 95% of width/height value. expect(emojiSpan.style.cssText).toBe( - 'background-position: 22.81% 49.12%; font-size: 16px;', + 'background-position: 22.81% 49.12%; font-size: 19px;', ) }) }) diff --git a/spec/picker-virtual-scroll-spec.js b/spec/picker-virtual-scroll-spec.js index 15a090a7..c277b0e6 100644 --- a/spec/picker-virtual-scroll-spec.js +++ b/spec/picker-virtual-scroll-spec.js @@ -145,7 +145,7 @@ describe('anchors', () => { it('contains all categories', () => { let anchors = picker.find(Anchors) - let categories = anchors.findAll('span.emoji-mart-anchor') + let categories = anchors.findAll('.emoji-mart-anchor') let names = [] for (let idx = 0; idx < categories.length; idx++) { names.push(categories.at(idx).element.attributes['data-title'].value) @@ -168,7 +168,7 @@ describe('anchors', () => { it('can be clicked to scroll to the category', () => { let anchors = picker.find(Anchors) - let anchorsCategories = anchors.findAll('span.emoji-mart-anchor') + let anchorsCategories = anchors.findAll('.emoji-mart-anchor') let symbols = anchorsCategories.at(8) expect(symbols.element.attributes['data-title'].value).toBe('Symbols') @@ -307,9 +307,9 @@ describe('emjoiSize', () => { let emoji = picker.find('[data-title="+1"]') // The inner span with applied inline style. let emojiSpan = emoji.element.childNodes[0] - // Font-size is 80% of width/height value. + // Font-size is 95% of width/height value. expect(emojiSpan.style.cssText).toBe( - 'background-position: 22.81% 49.12%; font-size: 19.2px;', + 'background-position: 22.81% 49.12%; font-size: 22.8px;', ) }) @@ -318,9 +318,9 @@ describe('emjoiSize', () => { let emoji = picker.find('[data-title="+1"]') // The inner span with applied inline style. let emojiSpan = emoji.element.childNodes[0] - // Font-size is 80% of width/height value. + // Font-size is 95% of width/height value. expect(emojiSpan.style.cssText).toBe( - 'background-position: 22.81% 49.12%; font-size: 16px;', + 'background-position: 22.81% 49.12%; font-size: 19px;', ) }) }) diff --git a/spec/search-spec.js b/spec/search-spec.js index 5b5f3a29..b69a4404 100644 --- a/spec/search-spec.js +++ b/spec/search-spec.js @@ -60,7 +60,7 @@ describe('search', () => { expect(searchCategory.vm.id).toBe('search') let anchors = picker.find(Anchors) - let anchorsCategories = anchors.findAll('span.emoji-mart-anchor') + let anchorsCategories = anchors.findAll('.emoji-mart-anchor') let symbols = anchorsCategories.at(8) expect(symbols.element.attributes['data-title'].value).toBe('Symbols') symbols.trigger('click') diff --git a/src/components/Emoji.vue b/src/components/Emoji.vue index fe26475e..7a2ce92f 100644 --- a/src/components/Emoji.vue +++ b/src/components/Emoji.vue @@ -1,7 +1,9 @@ diff --git a/src/components/category.vue b/src/components/category.vue index 82a0c982..777d7c31 100644 --- a/src/components/category.vue +++ b/src/components/category.vue @@ -1,68 +1,99 @@ diff --git a/src/components/search.vue b/src/components/search.vue index 11eeefcb..f3a867a7 100644 --- a/src/components/search.vue +++ b/src/components/search.vue @@ -1,49 +1,86 @@ diff --git a/src/utils/emoji-data.js b/src/utils/emoji-data.js index 6aee47b0..d6484670 100644 --- a/src/utils/emoji-data.js +++ b/src/utils/emoji-data.js @@ -500,6 +500,13 @@ export class EmojiData { y = Math.round(multiply * this._data.sheet_y * 100) / 100 return `${x}% ${y}%` } + + ariaLabel() { + return [this.native] + .concat(this.short_names) + .filter(Boolean) + .join(', ') + } } export class EmojiView { @@ -523,6 +530,7 @@ export class EmojiView { this.cssStyle = this._cssStyle(emojiSize) this.content = this._content() this.title = emojiTooltip === true ? emoji.short_name : null + this.ariaLabel = emoji.ariaLabel() Object.freeze(this) } @@ -560,9 +568,9 @@ export class EmojiView { // Set font-size for native emoji. cssStyle = Object.assign(cssStyle, { // font-size is used for native emoji which we need - // to scale with 0.8 factor to have them look approximately - // the same size as image-based emojl. - fontSize: Math.round(emojiSize * 0.8 * 10) / 10 + 'px', + // to scale with 0.95 factor to have them look approximately + // the same size as image-based emoji. + fontSize: Math.round(emojiSize * 0.95 * 10) / 10 + 'px', }) } else { // Set width/height for image emoji. diff --git a/src/utils/picker.js b/src/utils/picker.js new file mode 100644 index 00000000..75f9dc04 --- /dev/null +++ b/src/utils/picker.js @@ -0,0 +1,280 @@ +export class PickerView { + constructor(pickerComponent) { + this._vm = pickerComponent + this._data = pickerComponent.data + this._perLine = pickerComponent.perLine + + this._categories = [] + this._categories.push(...this._data.categories()) + this._categories = this._categories.filter((category) => { + return category.emojis.length > 0 + }) + + this._categories[0].first = true + Object.freeze(this._categories) + + this.activeCategory = this._categories[0] + this.searchEmojis = null + + // Preview emoji, shown on mouse over or when we move + // with arrow keys. + this.previewEmoji = null + // Indexes are used to keep the position when moving + // with arrows: current category and current emoji + // inside the category. + this.previewEmojiCategoryIdx = 0 + this.previewEmojiIdx = -1 + } + + onScroll() { + const scrollElement = this._vm.$refs.scroll + const scrollTop = scrollElement.scrollTop + + let activeCategory = this.filteredCategories[0] + for (let i = 0, l = this.filteredCategories.length; i < l; i++) { + let category = this.filteredCategories[i] + let component = this._vm.getCategoryComponent(i) + // The `-50` offset switches active category (selected in the + // anchors bar) a bit eariler, before it actually reaches the top. + if (component && component.$el.offsetTop - 50 > scrollTop) { + break + } + activeCategory = category + } + this.activeCategory = activeCategory + } + + get allCategories() { + return this._categories + } + + get filteredCategories() { + if (this.searchEmojis) { + return [ + { + id: 'search', + name: 'Search', + emojis: this.searchEmojis, + }, + ] + } + return this._categories.filter((category) => { + let hasEmojis = category.emojis.length > 0 + return hasEmojis + }) + } + + get previewEmojiCategory() { + if (this.previewEmojiCategoryIdx >= 0) { + return this.filteredCategories[this.previewEmojiCategoryIdx] + } + return null + } + + onAnchorClick(category) { + if (this.searchEmojis) { + // No categories are shown when search is active. + return + } + let i = this.filteredCategories.indexOf(category) + let component = this._vm.getCategoryComponent(i) + let scrollToComponent = () => { + if (component) { + let top = component.$el.offsetTop + if (category.first) { + top = 0 + } + this._vm.$refs.scroll.scrollTop = top + } + } + if (this._vm.infiniteScroll) { + scrollToComponent() + } else { + this.activeCategory = this.filteredCategories[i] + } + } + + onSearch(value) { + let emojis = this._data.search(value, this.maxSearchResults) + this.searchEmojis = emojis + + this.previewEmojiCategoryIdx = 0 + this.previewEmojiIdx = 0 + this.updatePreviewEmoji() + } + + onEmojiEnter(emoji) { + this.previewEmoji = emoji + this.previewEmojiIdx = -1 + this.previewEmojiCategoryIdx = -1 + } + + onEmojiLeave(emoji) { + this.previewEmoji = null + } + + onArrowLeft() { + // Moving left, decrease emoji index. + if (this.previewEmojiIdx > 0) { + this.previewEmojiIdx -= 1 + } else { + // If emoji index is zero, go to the previous category. + this.previewEmojiCategoryIdx -= 1 + if (this.previewEmojiCategoryIdx < 0) { + // If we reached first category, keep it. + this.previewEmojiCategoryIdx = 0 + } else { + // Update emoji index - we moved to the previous category, + // get the last emoji in it. + this.previewEmojiIdx = + this.filteredCategories[this.previewEmojiCategoryIdx].emojis.length - + 1 + } + } + this.updatePreviewEmoji() + } + + onArrowRight() { + if ( + this.previewEmojiIdx < + this.emojisLength(this.previewEmojiCategoryIdx) - 1 + ) { + // Moving right within category, increase emoji index. + this.previewEmojiIdx += 1 + } else { + // Go to the next category. + this.previewEmojiCategoryIdx += 1 + if (this.previewEmojiCategoryIdx >= this.filteredCategories.length) { + // If we reached the last category - keep it. + this.previewEmojiCategoryIdx = this.filteredCategories.length - 1 + } else { + // If we moved to the next category, update emoji index to the + // first emoji in the new category. + this.previewEmojiIdx = 0 + } + } + this.updatePreviewEmoji() + } + + onArrowDown() { + // If we are out of the emoji control (index is -1), select the first + // emoji in the first category by calling `onArrowRight`. + if (this.previewEmojiIdx == -1) { + return this.onArrowRight() + } + + const categoryLength = this.filteredCategories[this.previewEmojiCategoryIdx] + .emojis.length + + // When moving down, we can move `_perLine` icons right to + // jump to the same position in the next row. + let diff = this._perLine + + // TODO: previewCategory should match activeCategory + // (so it would be both highlighted in UI and used + // when we start moving with arrows after clicking + // the category). + + // Note: probably we can alwasy take current row length + // as a `diff` - it will fit both case of any row and + // special case of the last row. + // Note: it can be also easier to update indexes + // directly here instead of calling onArrowRight. + // Same is true for `onArrowUp`. + + // Unless if we are on the last row of the category and + // there are less then `_perLine` emojis in it. + // In this case we use the last row length as diff + // so we go straight down, for example: + // + // 1 2 3 4 5 6 + // 7 8 9 + // A B C D E F + // + // If we go down from `8`, we need to move 3 emojis right + // to lend at `B` (and 3 is the length of the last row of + // this category). + // And if we used 6 instead (row length, `_perLine`), we would + // lend up at `E`. + if (this.previewEmojiIdx + diff > categoryLength) { + // Calculate the last row length. + diff = categoryLength % this._perLine + } + for (let i = 0; i < diff; i++) { + this.onArrowRight() + } + this.updatePreviewEmoji() + } + + onArrowUp() { + // Similar to `onArrowDown`, to move up we can move left + // by `_perLine` number of emojis. + let diff = this._perLine + + if (this.previewEmojiIdx - diff < 0) { + if (this.previewEmojiCategoryIdx > 0) { + // Unless if we are on the first line of the category and + // the last line in the previous category is shorter than + // `_perLine`. + // In this case we use the last row length as diff, so + // we go straight up, for example: + // + // 1 2 3 4 5 + // 6 7 8 + // 9 A B C D + // + // If we go up from `A`, we need to move 3 emojis left to get + // to `7` (and 3 is the length of the last row of the previous + // category). + const prevCategoryLastRowLength = + this.filteredCategories[this.previewEmojiCategoryIdx - 1].emojis + .length % this._perLine + // diff = this.previewEmojiIdx + prevCategoryLastRowLength + diff = prevCategoryLastRowLength + } else { + diff = 0 + } + } + for (let i = 0; i < diff; i++) { + this.onArrowLeft() + } + this.updatePreviewEmoji() + } + + updatePreviewEmoji() { + this.previewEmoji = this.filteredCategories[ + this.previewEmojiCategoryIdx + ].emojis[this.previewEmojiIdx] + + this._vm.$nextTick(() => { + // Scroll the view if the `previewEmoji` goes out of the visible area. + const scrollEl = this._vm.$refs.scroll + + // Note: it would be more Vue-ish to mark all emojis with `ref`s + // and then do something similar here to what we do in the + // `getCategories` instead of using `querySelector` directly, + // but I am not sure if having many refs would affect the performance + // (it might, so I use `querySelector` for now). + const emojiEl = scrollEl.querySelector('.emoji-mart-emoji-selected') + + const scrollHeight = scrollEl.offsetTop - scrollEl.offsetHeight + if ( + emojiEl && + emojiEl.offsetTop + emojiEl.offsetHeight > + scrollHeight + scrollEl.scrollTop + ) { + scrollEl.scrollTop += emojiEl.offsetHeight + } + if (emojiEl && emojiEl.offsetTop < scrollEl.scrollTop) { + scrollEl.scrollTop -= emojiEl.offsetHeight + } + }) + } + + emojisLength(categoryIdx) { + if (categoryIdx == -1) { + return 0 + } + return this.filteredCategories[categoryIdx].emojis.length + } +} diff --git a/src/utils/shared-props.js b/src/utils/shared-props.js index 8fb7d5d9..b57b5336 100644 --- a/src/utils/shared-props.js +++ b/src/utils/shared-props.js @@ -26,6 +26,10 @@ const EmojiProps = { type: Number, default: null, }, + tag: { + type: String, + default: 'span', + }, } const PickerProps = {