@portabletext/plugin-emoji-picker 3.0.22 → 3.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,264 +0,0 @@
1
- Feature: Emoji Picker
2
-
3
- Scenario Outline: Picking a direct hit
4
- When the editor is focused
5
- And <initial text> is inserted
6
- And <inserted text> is inserted
7
- Then the text is <final text>
8
-
9
- Examples:
10
- | initial text | inserted text | final text |
11
- | "" | ":joy:" | "😂" |
12
- | ":jo" | "y:" | "😂" |
13
- | ":joy" | ":" | "😂" |
14
-
15
- Scenario Outline: Is only triggered when an initial colon is typed
16
- Given the text <text>
17
- When the caret is put <position>
18
- And <inserted text> is inserted
19
- Then the text is <final text>
20
- And the keyword is <keyword>
21
-
22
- Examples:
23
- | text | position | inserted text | final text | keyword |
24
- | "" | after "" | ":j" | ":j" | "j" |
25
- | ":" | after ":" | "j" | ":j" | "" |
26
- | ":j" | after ":j" | "o" | ":jo" | "" |
27
- | ":jo" | after ":jo" | ":" | ":jo:" | "" |
28
- | ":joy" | after ":joy" | ":" | ":joy:" | "" |
29
-
30
- Scenario: Undo after direct hit
31
- When ":joy:" is typed
32
- Then the text is "😂"
33
- When undo is performed
34
- Then the text is ":joy:"
35
-
36
- Scenario: Picking direct hit after undo
37
- When ":joy:" is typed
38
- And undo is performed
39
- And "{Backspace}" is pressed
40
- And ":" is typed
41
- Then the text is ":joy:"
42
- And the keyword is ""
43
- And the matches are ""
44
-
45
- Scenario: Picking wrong direct hit
46
- When ":jo:" is typed
47
- Then the text is ":jo:"
48
- And the keyword is "jo"
49
- And the matches are "😂,😹,đŸ•šī¸"
50
-
51
- Scenario: Colon after wrong direct hit
52
- When ":jo:" is typed
53
- And ":" is typed
54
- Then the text is ":jo::"
55
- And the keyword is "jo:"
56
- And the matches are ""
57
-
58
- Scenario: Picking wrong direct hit after undoing direct hit
59
- When ":joy:" is typed
60
- And undo is performed
61
- And "{Backspace}{Backspace}" is pressed
62
- And ":" is typed
63
- Then the text is ":jo:"
64
- And the keyword is ""
65
- And the matches are ""
66
-
67
- Scenario: Two consecutive direct hits
68
- When ":joy:" is typed
69
- And ":joy_cat:" is typed
70
- Then the text is "😂😹"
71
-
72
- Scenario: Picking the closest hit with Enter
73
- When ":joy" is typed
74
- And "{Enter}" is pressed
75
- Then the text is "😂"
76
-
77
- Scenario: Picking the closest hit with Tab
78
- When ":joy" is typed
79
- And "{Tab}" is pressed
80
- Then the text is "😂"
81
-
82
- Scenario: Navigating down the list
83
- When ":joy" is typed
84
- And "{ArrowDown}" is pressed
85
- And "{Enter}" is pressed
86
- Then the text is "😹"
87
-
88
- Scenario: Aborting on Escape
89
- When ":joy" is typed
90
- And "{Escape}" is pressed
91
- And "{Enter}" is pressed
92
- Then the text is ":joy|"
93
-
94
- Scenario: Aborting and forwarding Enter if there is no keyword
95
- When ":" is typed
96
- And "{Enter}" is pressed
97
- Then the text is ":|"
98
-
99
- Scenario: Aborting Enter if there are no matches
100
- When ":asdf" is typed
101
- And "{Enter}" is pressed
102
- Then the text is ":asdf"
103
- And the keyword is ""
104
-
105
- Scenario: Backspacing to narrow search
106
- When ":joy" is typed
107
- And "{Backspace}" is pressed
108
- And "{Enter}" is pressed
109
- Then the text is "😂"
110
-
111
- Scenario Outline: Inserting longer trigger text
112
- Given the text <text>
113
- When <inserted text> is inserted
114
- And <new text> is typed
115
- Then the text is <final text>
116
-
117
- Examples:
118
- | text | inserted text | new text | final text |
119
- | "" | ":" | "joy:" | "😂" |
120
- | "" | ":j" | "oy:" | "😂" |
121
- | "" | ":joy" | ":" | "😂" |
122
- | "" | ":joy:" | ":" | "😂:" |
123
-
124
- Scenario Outline: Matching inside decorator
125
- Given the text <text>
126
- And "strong" around <decorated>
127
- When the caret is put <position>
128
- And <keyword> is typed
129
- Then the text is <final text>
130
-
131
- Examples:
132
- | text | decorated | position | keyword | final text |
133
- | "foo bar baz" | "bar" | before "ar baz" | ":joy:" | "foo ,b😂ar, baz" |
134
- | "foo bar baz" | "bar" | before "r baz" | ":joy:" | "foo ,ba😂r, baz" |
135
-
136
- Scenario Outline: Triggering at the edge of decorator
137
- Given the text "foo bar baz"
138
- And "strong" around "bar"
139
- When the caret is put <position>
140
- And ":j" is typed
141
- Then the keyword is "j"
142
-
143
- Examples:
144
- | position |
145
- | after "foo " |
146
- | before "bar" |
147
- | after "bar" |
148
- | before " baz" |
149
-
150
- Scenario Outline: Matching at the edge of decorator
151
- Given the text "foo bar baz"
152
- And "strong" around "bar"
153
- When the caret is put <position>
154
- And ":joy:" is typed
155
- Then the text is <final text>
156
-
157
- Examples:
158
- | position | final text |
159
- | after "foo " | "foo 😂,bar, baz" |
160
- | before "bar" | "foo 😂,bar, baz" |
161
- | after "bar" | "foo ,bar😂, baz" |
162
- | before " baz" | "foo ,bar😂, baz" |
163
-
164
- Scenario Outline: Matching inside annotation
165
- Given the text <text>
166
- And a "link" "l1" around <annotated>
167
- When the caret is put <position>
168
- And <keyword> is typed
169
- Then the text is <final text>
170
-
171
- Examples:
172
- | text | annotated | position | keyword | final text |
173
- | "foo bar baz" | "bar" | before "ar baz" | ":joy:" | "foo ,b😂ar, baz" |
174
- | "foo bar baz" | "bar" | before "r baz" | ":joy:" | "foo ,ba😂r, baz" |
175
-
176
- Scenario Outline: Triggering at the edge of an annotation
177
- Given the text "foo bar baz"
178
- And a "link" "l1" around "bar"
179
- When the caret is put <position>
180
- And ":j" is typed
181
- Then the keyword is "j"
182
-
183
- Examples:
184
- | position |
185
- | after "foo " |
186
- | before "bar" |
187
- | after "bar" |
188
- | before " baz" |
189
-
190
- Scenario Outline: Matching at the edge of an annotation
191
- Given the text "foo bar baz"
192
- And a "link" "l1" around "bar"
193
- When the caret is put <position>
194
- And ":joy:" is typed
195
- Then the text is <final text>
196
-
197
- Examples:
198
- | position | final text |
199
- | after "foo " | "foo 😂,bar, baz" |
200
- | before "bar" | "foo 😂,bar, baz" |
201
- | after "bar" | "foo ,bar,😂 baz" |
202
- | before " baz" | "foo ,bar,😂 baz" |
203
-
204
- Scenario Outline: Typing before the colon dismisses the emoji picker
205
- Given the text <text>
206
- When <inserted text> is typed
207
- And <button> is pressed
208
- And <new text> is typed
209
- Then the keyword is ""
210
-
211
- Examples:
212
- | text | inserted text | button | new text |
213
- | "" | ":j" | "{ArrowLeft}{ArrowLeft}" | "f" |
214
- | "fo" | ":j" | "{ArrowLeft}{ArrowLeft}" | "o" |
215
- | "" | ":j" | "{ArrowLeft}{ArrowLeft}" | ":" |
216
-
217
- Scenario Outline: Navigating away from the keyword
218
- Given the text <text>
219
- When the caret is put <position>
220
- And <keyword> is typed
221
- And <button> is pressed
222
- And "{Enter}" is pressed
223
- Then the text is <final text>
224
-
225
- Examples:
226
- | text | position | keyword | button | final text |
227
- | "foo bar baz" | after "foo" | ":j" | "{ArrowRight}" | "foo:j \|bar baz" |
228
- | "foo bar baz" | after "foo" | ":j" | "{ArrowLeft}{ArrowLeft}{ArrowLeft}" | "fo\|o:j bar baz" |
229
-
230
- Scenario: Dismissing by pressing Space
231
- Given the text ""
232
- When ":joy" is typed
233
- Then the keyword is "joy"
234
- When " " is typed
235
- Then the keyword is ""
236
-
237
- Scenario Outline: Allow special characters
238
- Given the text <text>
239
- When <inserted text> is inserted
240
- Then the keyword is <keyword>
241
- And the matches are <matches>
242
-
243
- Examples:
244
- | text | inserted text | keyword | matches |
245
- | "" | ":joy!" | "joy!" | "đŸ•šī¸" |
246
- | "" | ":*" | "*" | "😘" |
247
- | "" | ":!" | "!" | "đŸ•šī¸,â—ī¸,â‰ī¸,â€ŧī¸" |
248
- | "" | ":!!" | "!!" | "â€ŧī¸" |
249
- | "" | "::)" | ":)" | "😊" |
250
- | "" | "::" | ":" | "😊" |
251
-
252
- Scenario: Narrowing keyword by deletion
253
- Given the text "foo"
254
- When the caret is put after "fo"
255
- And <inserted text> is inserted
256
- And "{ArrowLeft}" is pressed
257
- And "{Backspace}" is pressed
258
- Then the text is <final text>
259
- And the keyword is <keyword>
260
-
261
- Examples:
262
- | inserted text | final text | keyword |
263
- | ":joy" | "fo:jyo" | "jy" |
264
- | ":jđŸ‘ģy" | "fo:jyo" | "jy" |
@@ -1,88 +0,0 @@
1
- import {parameterTypes} from '@portabletext/editor/test'
2
- import {
3
- createTestEditor,
4
- stepDefinitions,
5
- type Context,
6
- } from '@portabletext/editor/test/vitest'
7
- import {defineSchema} from '@portabletext/schema'
8
- import {Before, Then} from 'racejar'
9
- import {Feature} from 'racejar/vitest'
10
- import {expect, vi} from 'vitest'
11
- import {page, type Locator} from 'vitest/browser'
12
- import {createMatchEmojis} from './create-match-emojis'
13
- import emojiPickerFeature from './emoji-picker.feature?raw'
14
- import {useEmojiPicker} from './use-emoji-picker'
15
-
16
- type EmojiPickerTestContext = Context & {
17
- keywordLocator: Locator
18
- matchesLocator: Locator
19
- }
20
-
21
- Feature({
22
- hooks: [
23
- Before(async (context: EmojiPickerTestContext) => {
24
- const {editor, locator} = await createTestEditor({
25
- children: <EmojiPickerPlugin />,
26
- schemaDefinition: defineSchema({
27
- decorators: [{name: 'strong'}],
28
- annotations: [{name: 'link'}],
29
- }),
30
- })
31
-
32
- context.locator = locator
33
- context.editor = editor
34
- context.keywordLocator = page.getByTestId('keyword')
35
- context.matchesLocator = page.getByTestId('matches')
36
-
37
- await vi.waitFor(() =>
38
- expect.element(context.keywordLocator).toBeInTheDocument(),
39
- )
40
- await vi.waitFor(() =>
41
- expect.element(context.matchesLocator).toBeInTheDocument(),
42
- )
43
- }),
44
- ],
45
- featureText: emojiPickerFeature,
46
- stepDefinitions: [
47
- ...stepDefinitions,
48
- Then(
49
- 'the keyword is {string}',
50
- (context: EmojiPickerTestContext, keyword: string) => {
51
- expect(context.keywordLocator.element().textContent).toEqual(keyword)
52
- },
53
- ),
54
- Then(
55
- 'the matches are {string}',
56
- (context: EmojiPickerTestContext, matches: string) => {
57
- expect(context.matchesLocator.element().textContent).toEqual(matches)
58
- },
59
- ),
60
- ],
61
- parameterTypes,
62
- })
63
-
64
- const emojis: Record<string, Array<string>> = {
65
- '😂': ['joy'],
66
- '😹': ['joy_cat'],
67
- 'đŸ•šī¸': ['joy!stick'],
68
- '😘': ['*'],
69
- 'â—ī¸': ['!'],
70
- 'â‰ī¸': ['!?'],
71
- 'â€ŧī¸': ['!!'],
72
- '😊': [':)'],
73
- }
74
-
75
- const matchEmojis = createMatchEmojis({emojis})
76
-
77
- function EmojiPickerPlugin() {
78
- const {keyword, matches} = useEmojiPicker({matchEmojis})
79
-
80
- return (
81
- <>
82
- <div data-testid="keyword">{keyword}</div>
83
- <div data-testid="matches">
84
- {matches.map((match) => match.emoji).join(',')}
85
- </div>
86
- </>
87
- )
88
- }
package/src/global.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare module '*.feature?raw' {
2
- const content: string
3
- export default content
4
- }
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './create-match-emojis'
2
- export * from './match-emojis'
3
- export * from './use-emoji-picker'
@@ -1,22 +0,0 @@
1
- /**
2
- * The base type representing an emoji match.
3
- *
4
- * @beta
5
- */
6
- export type BaseEmojiMatch =
7
- | {
8
- type: 'exact'
9
- emoji: string
10
- }
11
- | {
12
- type: 'partial'
13
- emoji: string
14
- }
15
-
16
- /**
17
- * A function that returns an array of emoji matches for a given keyword.
18
- *
19
- * @beta
20
- */
21
- export type MatchEmojis<TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch> =
22
- (query: {keyword: string}) => ReadonlyArray<TEmojiMatch>
@@ -1,187 +0,0 @@
1
- import {useEditor} from '@portabletext/editor'
2
- import {useActorRef, useSelector} from '@xstate/react'
3
- import {useCallback} from 'react'
4
- import {emojiPickerMachine} from './emoji-picker-machine'
5
- import type {BaseEmojiMatch, MatchEmojis} from './match-emojis'
6
-
7
- /**
8
- * @beta
9
- */
10
- export type EmojiPicker<TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch> = {
11
- /**
12
- * The matched keyword.
13
- *
14
- * Can be used to display the keyword in the UI or conditionally render the
15
- * list of matches.
16
- *
17
- * @example
18
- * ```tsx
19
- * if (keyword.length < 1) {
20
- * return null
21
- * }
22
- * ```
23
- */
24
- keyword: string
25
-
26
- /**
27
- * Emoji matches found for the current keyword.
28
- *
29
- * Can be used to display the matches in a list.
30
- */
31
- matches: ReadonlyArray<TEmojiMatch>
32
-
33
- /**
34
- * The index of the selected match.
35
- *
36
- * Can be used to highlight the selected match in the list.
37
- *
38
- * @example
39
- * ```tsx
40
- * <EmojiListItem
41
- * key={match.key}
42
- * match={match}
43
- * selected={selectedIndex === index}
44
- * />
45
- * ```
46
- */
47
- selectedIndex: number
48
-
49
- /**
50
- * Navigate to a specific match by index.
51
- *
52
- * Can be used to control the `selectedIndex`. For example, using
53
- * `onMouseEnter`.
54
- *
55
- * @example
56
- * ```tsx
57
- * <EmojiListItem
58
- * key={match.key}
59
- * match={match}
60
- * selected={selectedIndex === index}
61
- * onMouseEnter={() => {onNavigateTo(index)}}
62
- * />
63
- * ```
64
- */
65
- onNavigateTo: (index: number) => void
66
-
67
- /**
68
- * Select the current match.
69
- *
70
- * Can be used to insert the currently selected match.
71
- *
72
- *
73
- * @example
74
- * ```tsx
75
- * <EmojiListItem
76
- * key={match.key}
77
- * match={match}
78
- * selected={selectedIndex === index}
79
- * onMouseEnter={() => {onNavigateTo(index)}}
80
- * onSelect={() => {onSelect()}}
81
- * />
82
- * ```
83
- *
84
- * Note: The currently selected match is automatically inserted on Enter or
85
- * Tab.
86
- */
87
- onSelect: () => void
88
-
89
- /**
90
- * Dismiss the emoji picker. Can be used to let the user dismiss the picker
91
- * by clicking a button.
92
- *
93
- * @example
94
- * ```tsx
95
- * {matches.length === 0 ? (
96
- * <Button onPress={onDismiss}>Dismiss</Button>
97
- * ) : <EmojiListBox {...props} />}
98
- * ```
99
- *
100
- * Note: The emoji picker is automatically dismissed on Escape.
101
- */
102
- onDismiss: () => void
103
- }
104
-
105
- /**
106
- * @beta
107
- */
108
- export type EmojiPickerProps<
109
- TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch,
110
- > = {
111
- matchEmojis: MatchEmojis<TEmojiMatch>
112
- }
113
-
114
- /**
115
- * Handles the state and logic needed to create an emoji picker.
116
- *
117
- * The `matchEmojis` function is generic and can return any shape of emoji
118
- * match required for the emoji picker.
119
- *
120
- * However, the default implementation of `matchEmojis` returns an array of
121
- * `EmojiMatch` objects and can be created using the `createMatchEmojis`
122
- * function.
123
- *
124
- * @example
125
- *
126
- * ```tsx
127
- * const matchEmojis = createMatchEmojis({emojis: {
128
- * '😂': ['joy'],
129
- * '😹': ['joy_cat'],
130
- * }})
131
- *
132
- * const {keyword, matches, selectedIndex, onDismiss, onNavigateTo, onSelect} =
133
- * useEmojiPicker({matchEmojis})
134
- * ```
135
- *
136
- * Note: This hook is not concerned with the UI, how the emoji picker is
137
- * rendered or positioned in the document.
138
- *
139
- * @beta
140
- */
141
- export function useEmojiPicker<
142
- TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch,
143
- >(props: EmojiPickerProps<TEmojiMatch>): EmojiPicker<TEmojiMatch> {
144
- const editor = useEditor()
145
- const emojiPickerActor = useActorRef(emojiPickerMachine, {
146
- input: {editor, matchEmojis: props.matchEmojis},
147
- })
148
- const keyword = useSelector(emojiPickerActor, (snapshot) => {
149
- const rawKeyword = snapshot.context.keyword.startsWith(':')
150
- ? snapshot.context.keyword.slice(1)
151
- : snapshot.context.keyword
152
- return rawKeyword.length > 1 && rawKeyword.endsWith(':')
153
- ? rawKeyword.slice(0, -1)
154
- : rawKeyword
155
- })
156
- const matches = useSelector(
157
- emojiPickerActor,
158
- (snapshot) => snapshot.context.matches as ReadonlyArray<TEmojiMatch>,
159
- )
160
- const selectedIndex = useSelector(
161
- emojiPickerActor,
162
- (snapshot) => snapshot.context.selectedIndex,
163
- )
164
-
165
- const onDismiss = useCallback(() => {
166
- emojiPickerActor.send({type: 'dismiss'})
167
- }, [emojiPickerActor])
168
- const onNavigateTo = useCallback(
169
- (index: number) => {
170
- emojiPickerActor.send({type: 'navigate to', index})
171
- },
172
- [emojiPickerActor],
173
- )
174
- const onSelect = useCallback(() => {
175
- emojiPickerActor.send({type: 'insert selected match'})
176
- editor.send({type: 'focus'})
177
- }, [emojiPickerActor, editor])
178
-
179
- return {
180
- keyword,
181
- matches,
182
- selectedIndex,
183
- onDismiss,
184
- onNavigateTo,
185
- onSelect,
186
- }
187
- }