diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index 022c5981d329533adee8f0c4317dd1eb201148c5..44888e59d6672c0171c76b86d739f7551c34a746 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -306,7 +306,6 @@ export default { <emoji-picker v-if="canAwardEmoji" toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary" - boundary="viewport" :right="false" data-testid="note-emoji-button" @click="handleAwardEmoji" @@ -333,6 +332,7 @@ export default { no-caret left :items="dropdownItems" + data-testid="more-actions" /> </div> </div> diff --git a/app/assets/javascripts/emoji/components/category.vue b/app/assets/javascripts/emoji/components/category.vue index 80850475b9656f2a45f024b98c4cbce9da2e76bb..eefb487b79beebb4d605a053461f36d7d2c86ffe 100644 --- a/app/assets/javascripts/emoji/components/category.vue +++ b/app/assets/javascripts/emoji/components/category.vue @@ -43,7 +43,9 @@ export default { <template> <gl-intersection-observer class="gl-px-5 gl-h-full" @appear="categoryAppeared"> - <div class="gl-top-0 gl-py-3 gl-w-full gl-z-index-1 emoji-picker-category-header"> + <div + class="gl-top-0 gl-py-3 gl-w-full gl-z-index-1 gl-font-sm gl-ml-n2 emoji-picker-category-header" + > <b>{{ categoryTitle }}</b> </div> <template v-if="emojis.length"> diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue index bb0c3b0a694c2bb157fb78c0218d85883ceb08ab..a6eb96cf947df0339faf1ff21fd922e3aee40f79 100644 --- a/app/assets/javascripts/emoji/components/emoji_group.vue +++ b/app/assets/javascripts/emoji/components/emoji_group.vue @@ -36,6 +36,7 @@ export default { data-testid="emoji-button" button-text-classes="gl-display-none!" @click="clickEmoji(emoji)" + @keydown.enter="clickEmoji(emoji)" > <template #emoji> <gl-emoji :data-name="emoji" class="gl-mr-0!" /> diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue index 8b1784ae5510eb0d8817a0c5455774633adb3c64..2c7e424dff51a2ba73a34958e4697d4fe78f40de 100644 --- a/app/assets/javascripts/emoji/components/picker.vue +++ b/app/assets/javascripts/emoji/components/picker.vue @@ -1,9 +1,18 @@ <!-- eslint-disable vue/multi-word-component-names --> <script> -import { GlButton, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { + GlButton, + GlIcon, + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, + GlSearchBoxByType, + GlTooltipDirective, +} from '@gitlab/ui'; import { findLastIndex } from 'lodash'; import VirtualList from 'vue-virtual-scroll-list'; import { getEmojiCategoryMap, state } from '~/emoji'; +import { __ } from '~/locale'; import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants'; import Category from './category.vue'; import EmojiList from './emoji_list.vue'; @@ -13,13 +22,17 @@ export default { components: { GlButton, GlIcon, - GlDropdown, - GlDropdownItem, + GlDisclosureDropdown, + GlDisclosureDropdownGroup, + GlDisclosureDropdownItem, GlSearchBoxByType, VirtualList, Category, EmojiList, }, + directives: { + GlTooltip: GlTooltipDirective, + }, inject: { newCustomEmojiPath: { default: '', @@ -41,14 +54,10 @@ export default { required: false, default: true, }, - boundary: { - type: String, - required: false, - default: '', - }, }, data() { return { + isVisible: false, currentCategory: 0, searchValue: '', }; @@ -64,6 +73,18 @@ export default { icon: CATEGORY_ICON_MAP[category], })); }, + placement() { + return this.right ? 'right' : 'left'; + }, + newCustomEmoji() { + return { + text: __('Create new emoji'), + href: this.newCustomEmojiPath, + extraAttrs: { + 'data-testid': 'create-new-emoji', + }, + }; + }, }, methods: { categoryAppeared(category) { @@ -77,15 +98,12 @@ export default { }, selectEmoji({ category, emoji }) { this.$emit('click', emoji); - this.$refs.dropdown.hide(); + this.$refs.dropdown.close(); if (category !== 'custom') { addToFrequentlyUsed(emoji); } }, - getBoundaryElement() { - return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent'; - }, onSearchInput() { this.$refs.virtualScoller.setScrollTop(0); this.$refs.virtualScoller.forceRender(); @@ -95,55 +113,72 @@ export default { this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top); }, + onShow() { + this.isVisible = true; + this.$refs.searchValue.focusInput(); + + this.$emit('shown'); + }, onHide() { + this.isVisible = false; this.currentCategory = 0; this.searchValue = ''; this.$emit('hidden'); }, }, + i18n: { + addReaction: __('Add reaction'), + }, }; </script> <template> <div class="emoji-picker"> - <gl-dropdown + <gl-disclosure-dropdown ref="dropdown" - :toggle-class="toggleClass" - :boundary="getBoundaryElement()" :class="dropdownClass" - menu-class="dropdown-extended-height" - category="secondary" - no-flip - :right="right" - lazy - @shown="$emit('shown')" + :placement="placement" + @shown="onShow" @hidden="onHide" > - <template #button-content> - <slot name="button-content"> - <gl-icon class="award-control-icon-neutral gl-button-icon gl-icon" name="slight-smile" /> - <gl-icon - class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!" - name="smiley" - /> - <gl-icon - class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!" - name="smile" - /> - </slot> - <span class="gl-sr-only">{{ __('Add reaction') }}</span> + <template #toggle> + <gl-button + v-gl-tooltip + :title="$options.i18n.addReaction" + :class="toggleClass" + class="gl-relative gl-h-full" + data-testid="add-reaction-button" + > + <slot name="button-content"> + <span class="reaction-control-icon reaction-control-icon-neutral"> + <gl-icon class="award-control-icon-neutral gl-button-icon" name="slight-smile" /> + </span> + <span class="reaction-control-icon reaction-control-icon-positive"> + <gl-icon class="award-control-icon-positive gl-button-icon" name="smiley" /> + </span> + <span class="reaction-control-icon reaction-control-icon-super-positive"> + <gl-icon class="award-control-icon-super-positive gl-button-icon" name="smile" /> + </span> + </slot> + </gl-button> + </template> + + <template #header> + <gl-search-box-by-type + ref="searchValue" + v-model="searchValue" + class="add-reaction-search gl-border-b-1 gl-border-b-solid gl-border-b-gray-200" + borderless + autofocus + debounce="500" + :aria-label="__('Search for an emoji')" + @input="onSearchInput" + /> </template> - <gl-search-box-by-type - v-model="searchValue" - class="gl-mx-5! gl-mb-2!" - autofocus - debounce="500" - :aria-label="__('Search for an emoji')" - @input="onSearchInput" - /> + <div v-show="!searchValue" - class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1" + class="award-list gl-display-flex gl-border-b-solid gl-border-gray-100 gl-border-b-1" > <gl-button v-for="(category, index) in categoryNames" @@ -154,9 +189,10 @@ export default { :icon="category.icon" :aria-label="category.name" @click="scrollToCategory(category.name)" + @keydown.enter="scrollToCategory(category.name)" /> </div> - <emoji-list :search-value="searchValue"> + <emoji-list v-if="isVisible" :search-value="searchValue"> <template #default="{ filteredCategories }"> <virtual-list ref="virtualScoller" @@ -176,11 +212,10 @@ export default { </virtual-list> </template> </emoji-list> - <template v-if="newCustomEmojiPath" #footer> - <gl-dropdown-item :href="newCustomEmojiPath"> - {{ __('Create new emoji') }} - </gl-dropdown-item> - </template> - </gl-dropdown> + + <gl-disclosure-dropdown-group v-if="newCustomEmojiPath" bordered class="gl-mt-0!"> + <gl-disclosure-dropdown-item :item="newCustomEmoji" /> + </gl-disclosure-dropdown-group> + </gl-disclosure-dropdown> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 5a1795d747915617be77adb14796774cec7ecce0..963557ff1b3d81afe7ae2f1e0fc1b2ba977affa6 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -22,7 +22,6 @@ import TimelineEventButton from './note_actions/timeline_event_button.vue'; export default { i18n: { - addReactionLabel: __('Add reaction'), editCommentLabel: __('Edit comment'), deleteCommentLabel: __('Delete comment'), moreActionsLabel: __('More actions'), @@ -317,8 +316,6 @@ export default { /> <emoji-picker v-if="canAwardEmoji" - v-gl-tooltip - :title="$options.i18n.addReactionLabel" toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary" data-testid="note-emoji-button" @click="setAwardEmoji" diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue index 19062e10758e30143c1693b922aacc5d1870c243..8786dccecc4fe117a8346d836827203be729921c 100644 --- a/app/assets/javascripts/set_status_modal/set_status_form.vue +++ b/app/assets/javascripts/set_status_modal/set_status_form.vue @@ -185,7 +185,6 @@ export default { <emoji-picker dropdown-class="gl-h-full" toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!" - boundary="viewport" :right="false" @click="handleEmojiClick" > diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 3c19df9c19689dd5e22916a37e8ab674bc0ff20e..1750551db7bbfb55ef96ec1b67f91b357ad7d647 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { groupBy } from 'lodash'; import SafeHtml from '~/vue_shared/directives/safe_html'; import EmojiPicker from '~/emoji/components/picker.vue'; @@ -13,7 +13,6 @@ const NO_USER_ID = -1; export default { components: { GlButton, - GlIcon, EmojiPicker, }, directives: { @@ -45,11 +44,6 @@ export default { required: false, default: 'selected', }, - boundary: { - type: String, - required: false, - default: '', - }, }, data() { return { @@ -197,28 +191,13 @@ export default { </gl-button> <div v-if="canAwardEmoji" class="award-menu-holder gl-my-2"> <emoji-picker - v-gl-tooltip.viewport - :title="__('Add reaction')" :toggle-class="['add-reaction-button btn-icon gl-relative!', { 'is-active': isMenuOpen }]" :right="false" - :boundary="boundary" data-testid="emoji-picker" @click="handleAward" @shown="setIsMenuOpen(true)" @hidden="setIsMenuOpen(false)" - > - <template #button-content> - <span class="reaction-control-icon reaction-control-icon-neutral"> - <gl-icon name="slight-smile" /> - </span> - <span class="reaction-control-icon reaction-control-icon-positive"> - <gl-icon name="smiley" /> - </span> - <span class="reaction-control-icon reaction-control-icon-super-positive"> - <gl-icon name="smile" /> - </span> - </template> - </emoji-picker> + /> </div> </div> </template> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue index c3b3c0e6db7f7dc89c178bbc5ebd943c16b58ecf..ab93ab3cbdd01e4e6c67865844c1a4c880ef28b5 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue @@ -29,9 +29,6 @@ export default { }, }, computed: { - awardsListBoundary() { - return this.isModal ? '.modal-body' : ''; - }, awards() { return this.note.awardEmoji.nodes.map((award) => { return { @@ -93,7 +90,6 @@ export default { :awards="awards" :can-award-emoji="hasAwardEmojiPermission" :current-user-id="currentUserId" - :boundary="awardsListBoundary" class="gl-px-2" @award="handleAward($event)" /> diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index e11fa7d880180521a07f955d5d355bcf04cc4ed2..5f68ff64759795ace893ec363747d655ee094c0f 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -269,7 +269,6 @@ // other gl-buttons despite all child elements being set to // `position:absolute` - .reaction-control-icon { .gl-icon { height: $default-icon-size; @@ -335,3 +334,25 @@ } } } + +.add-reaction-search { + $input-focus-ring-border-radius: calc(#{$gl-border-radius-large} - #{$gl-border-size-1}); + + input { + border-top-left-radius: $input-focus-ring-border-radius !important; + border-top-right-radius: $input-focus-ring-border-radius !important; + } +} + +.award-list .gl-button:focus { + @include gl-focus( + $outline: true, + $outline-offset: -2px, + $important: true + ); + box-shadow: none !important; +} + +.design-note.note-form .emoji-picker .gl-new-dropdown-panel { + left: 0 !important; +} diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 779567978175591c0e910d7d76bef068182ac2c5..39b60aa3fae125ce3391227ba2c064fd525259a3 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -50,8 +50,9 @@ gl-emoji { } } -.emoji-picker .gl-dropdown .dropdown-menu { - width: 350px; +.emoji-picker .gl-dropdown .dropdown-menu, +.emoji-picker .gl-new-dropdown .gl-new-dropdown-panel { + width: 350px !important; } .emoji-picker-category-tab { diff --git a/spec/features/merge_request/user_creates_custom_emoji_spec.rb b/spec/features/merge_request/user_creates_custom_emoji_spec.rb index 35593836dab8bdeb5fe34544e1cc4ecb79bc4616..142a2b20e3d1ad9a468d2f3fdf9554b2e1be569a 100644 --- a/spec/features/merge_request/user_creates_custom_emoji_spec.rb +++ b/spec/features/merge_request/user_creates_custom_emoji_spec.rb @@ -20,8 +20,8 @@ wait_for_requests end - it 'shows link to create custom emoji' do - first('.add-reaction-button').click + it 'shows link to create custom emoji', :js do + find_by_testid('add-reaction-button').click wait_for_requests @@ -48,8 +48,8 @@ wait_for_requests end - it 'shows link to create custom emoji' do - first('.add-reaction-button').click + it 'shows link to create custom emoji', :js do + find_by_testid('add-reaction-button').click wait_for_requests diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb index bd9d1092e17a817a975a4081268094afc8ead3ce..ab95b20ad40f8474a9d3093de59871389db4f919 100644 --- a/spec/features/projects/issues/design_management/user_views_design_spec.rb +++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb @@ -16,7 +16,7 @@ def add_diff_note_emoji(diff_note, emoji_name) page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do page.find('[data-testid="note-emoji-button"] .note-emoji-button').click - page.within('ul.dropdown-menu') do + page.within('.emoji-picker') do page.find('input[type="search"]').set(emoji_name) page.find('button[data-testid="emoji-button"]:first-child').click end diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index 7ff488f7fcb4f00062b0ef2cfc5f02e5e9125583..8c9bf8da3fb0d72c43e9afb8fd2b5db4c896c866 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -48,7 +48,7 @@ describe('Design note component', () => { const findReplyForm = () => wrapper.findComponent(DesignReplyForm); const findEditButton = () => wrapper.findByTestId('note-edit'); const findNoteContent = () => wrapper.findByTestId('note-text'); - const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findDropdown = () => wrapper.findByTestId('more-actions'); const findDropdownItems = () => findDropdown().findAllComponents(GlDisclosureDropdownItem); const findEditDropdownItem = () => findDropdownItems().at(0); const findCopyLinkDropdownItem = () => findDropdownItems().at(1); @@ -353,7 +353,6 @@ describe('Design note component', () => { expect(emojiPicker.exists()).toBe(true); expect(emojiPicker.props()).toMatchObject({ - boundary: 'viewport', right: false, }); }); diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js index 7ae2884170e1dc8e8b70a722000327001308f597..2542fc237a10f2a4f2d75f3e2c240a89a8dc69bc 100644 --- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js +++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js @@ -99,7 +99,6 @@ describe('SetStatusModalWrapper', () => { it('renders emoji picker dropdown with custom positioning', () => { expect(getEmojiPicker().props()).toMatchObject({ right: false, - boundary: 'viewport', }); }); diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js index a756bfa6889b6343fad4a1360aa27e5bf2057772..e6c2743975bf1edc92cf3177d6c2b7eaa2d26043 100644 --- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js +++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js @@ -144,7 +144,6 @@ describe('WorkItemAwardEmoji component', () => { expect(findAwardsList().exists()).toBe(true); expect(findAwardsList().props()).toEqual({ - boundary: '', canAwardEmoji: true, currentUserId: 5, defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],