diff --git a/.changeset/wise-months-taste.md b/.changeset/wise-months-taste.md new file mode 100644 index 00000000000..d60d758db09 --- /dev/null +++ b/.changeset/wise-months-taste.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Add support for custom emojis and a declarative "loading" state in `MarkdownEditor` suggestions diff --git a/src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx b/src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx index f140894d2c2..de951a2eac9 100644 --- a/src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx +++ b/src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx @@ -163,6 +163,7 @@ const emojis: Emoji[] = [ {name: 'raised_hand', character: '✋'}, {name: 'thumbsup', character: '👍'}, {name: 'thumbsdown', character: '👎'}, + {name: 'octocat', url: 'https://github.githubassets.com/images/icons/emoji/octocat.png'}, ] const references: Reference[] = [ diff --git a/src/drafts/MarkdownEditor/MarkdownEditor.test.tsx b/src/drafts/MarkdownEditor/MarkdownEditor.test.tsx index b946c373b6c..ce0b57c93c4 100644 --- a/src/drafts/MarkdownEditor/MarkdownEditor.test.tsx +++ b/src/drafts/MarkdownEditor/MarkdownEditor.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event' import {UserEvent} from '@testing-library/user-event/dist/types/setup/setup' import React, {forwardRef, useRef, useState} from 'react' import {act} from 'react-dom/test-utils' -import MarkdownEditor, {Emoji, MarkdownEditorHandle, MarkdownEditorProps, Mentionable, Reference, SavedReply} from '.' +import MarkdownEditor, {MarkdownEditorHandle, MarkdownEditorProps, Mentionable, Reference, SavedReply} from '.' import ThemeProvider from '../../ThemeProvider' declare const REACT_VERSION_LATEST: boolean @@ -866,12 +866,13 @@ describe('MarkdownEditor', () => { }) describe('suggestions', () => { - const emojis: Emoji[] = [ + const emojis = [ {name: '+1', character: '👍'}, {name: '-1', character: '👎'}, {name: 'heart', character: '❤️'}, {name: 'wave', character: '👋'}, {name: 'raised_hands', character: '🙌'}, + {name: 'octocat', url: 'https://github.githubassets.com/images/icons/emoji/octocat.png'}, ] const mentionables: Mentionable[] = [ @@ -1076,6 +1077,17 @@ describe('MarkdownEditor', () => { expect(getAllSuggestions()[0]).toHaveTextContent('+1') expect(getAllSuggestions()[1]).toHaveTextContent('-1') }) + + it('inserts shortcode for custom emojis', async () => { + const {queryForSuggestionsList, getAllSuggestions, getInput, user} = await render() + + const input = getInput() + await user.type(input, `Mona Lisa :octo`) + await user.click(getAllSuggestions()[0]) + + expect(input.value).toBe(`Mona Lisa :octocat: `) + expect(queryForSuggestionsList()).not.toBeInTheDocument() + }) }) describe('saved replies', () => { diff --git a/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx b/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx index c9e78942680..ec3108f1387 100644 --- a/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx +++ b/src/drafts/MarkdownEditor/suggestions/_useEmojiSuggestions.tsx @@ -3,24 +3,40 @@ import {suggestionsCalculator, UseSuggestionsHook} from '.' import {ActionList} from '../../../ActionList' import {Suggestion, Trigger} from '../../InlineAutocomplete' -export type Emoji = { +type BaseEmoji = { /** Name (shortcode) of the emoji. Do not include the wrapping `:` symbols. */ name: string +} + +type UnicodeEmoji = BaseEmoji & { /** Unicode representation of the emoji. */ character: string } +type CustomEmoji = BaseEmoji & { + /** URL to an image of the emoji. */ + url: string +} + +export type Emoji = UnicodeEmoji | CustomEmoji + const trigger: Trigger = { triggerChar: ':', keepTriggerCharOnCommit: false, } const emojiToSugggestion = (emoji: Emoji): Suggestion => ({ - value: emoji.character, - key: emoji.name, // emoji characters may not be unique - ie haircut and haircut_man both have the same emoji codepoint. But names are guarunteed to be unique. + value: 'character' in emoji ? emoji.character : `:${emoji.name}:`, + key: emoji.name, // emoji characters may not be unique - ie haircut and haircut_man both have the same emoji codepoint. But names are guaranteed to be unique. render: props => ( - {emoji.character} + + {'character' in emoji ? ( + emoji.character + ) : ( + {`${emoji.name} + )} + {emoji.name} ), diff --git a/src/drafts/MarkdownEditor/suggestions/index.ts b/src/drafts/MarkdownEditor/suggestions/index.ts index 664180f3438..a00939c8ffd 100644 --- a/src/drafts/MarkdownEditor/suggestions/index.ts +++ b/src/drafts/MarkdownEditor/suggestions/index.ts @@ -2,11 +2,11 @@ import {Suggestion, Trigger} from '../../InlineAutocomplete' const MAX_SUGGESTIONS = 5 -export type SuggestionOptions = T[] | (() => Promise) +export type SuggestionOptions = T[] | (() => Promise) | 'loading' export type UseSuggestionsHook = (options: SuggestionOptions) => { trigger: Trigger - calculateSuggestions: (query: string) => Promise + calculateSuggestions: (query: string) => Promise } export const suggestionsCalculator = @@ -16,6 +16,8 @@ export const suggestionsCalculator = toSuggestion: (option: T) => Suggestion, ) => async (query: string) => { + if (options === 'loading') return 'loading' + const optionsArray = Array.isArray(options) ? options : await options() // If the query is empty, scores will be -INFINITY