playbook_ui 15.7.0.pre.alpha.PLAY2675dropdownquickpickcustomquickpickdates13330 → 15.7.0.pre.alpha.PLAY2678emojimask13284

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +1 -13
  3. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +0 -2
  4. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +1 -2
  5. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +1 -6
  6. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +0 -121
  7. data/app/pb_kits/playbook/pb_dropdown/quickpick/index.ts +9 -85
  8. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +2 -83
  9. data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +2 -2
  10. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +41 -3
  11. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
  12. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
  13. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
  14. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +2 -0
  15. data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
  16. data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
  17. data/app/pb_kits/playbook/pb_text_input/text_input.rb +5 -1
  18. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +53 -0
  19. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
  20. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
  21. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
  22. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
  23. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
  24. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  25. data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
  26. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
  27. data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
  28. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
  29. data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
  30. data/dist/chunks/{_typeahead-Ckz1ce-2.js → _typeahead-CSCNg6cp.js} +2 -2
  31. data/dist/chunks/{lib-DxDBrGZX.js → lib-DxCgrqqG.js} +1 -1
  32. data/dist/chunks/vendor.js +3 -3
  33. data/dist/playbook-rails-react-bindings.js +1 -1
  34. data/dist/playbook-rails.js +1 -1
  35. data/lib/playbook/forms/builder/form_field_builder.rb +2 -0
  36. data/lib/playbook/version.rb +1 -1
  37. metadata +11 -8
  38. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom.jsx +0 -56
  39. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom.md +0 -10
  40. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom_rails.html.erb +0 -64
  41. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom_rails.md +0 -10
@@ -1,26 +1,64 @@
1
1
  import PbEnhancedElement from "../pb_enhanced_element"
2
2
  import { INPUTMASKS } from "./inputMask"
3
+ import { stripEmojisForPaste, applyEmojiMask } from "../utilities/emojiMask"
3
4
 
4
5
  export default class PbTextInput extends PbEnhancedElement {
5
6
  static get selector() {
6
- return '[data-pb-input-mask="true"]';
7
+ return '[data-pb-input-mask="true"], [data-pb-emoji-mask="true"]';
7
8
  }
8
9
 
9
10
  connect() {
10
11
  this.handleInput = this.handleInput.bind(this);
12
+ this.handlePaste = this.handlePaste.bind(this);
11
13
  this.element.addEventListener("input", this.handleInput);
14
+ this.element.addEventListener("paste", this.handlePaste);
12
15
  this.handleInput();
13
16
  }
14
17
 
15
18
  disconnect() {
16
19
  this.element.removeEventListener("input", this.handleInput);
20
+ this.element.removeEventListener("paste", this.handlePaste);
17
21
  }
18
22
 
19
- handleInput() {
20
- const maskType = this.element.getAttribute("mask");
23
+ hasEmojiMask() {
24
+ return this.element.dataset.pbEmojiMask === "true";
25
+ }
26
+
27
+ handlePaste(event) {
28
+ if (!this.hasEmojiMask()) return;
29
+
30
+ const pastedText = event.clipboardData.getData('text');
31
+ const filteredText = stripEmojisForPaste(pastedText);
32
+
33
+ if (pastedText !== filteredText) {
34
+ event.preventDefault();
35
+ const input = this.element;
36
+ const start = input.selectionStart || 0;
37
+ const end = input.selectionEnd || 0;
38
+ const currentValue = input.value;
39
+ const newValue = currentValue.slice(0, start) + filteredText + currentValue.slice(end);
40
+ const newCursor = start + filteredText.length;
41
+
42
+ input.value = newValue;
43
+ input.selectionStart = input.selectionEnd = newCursor;
44
+
45
+ // Continue to handleInput for mask processing, emoji filtering handled above
46
+ this.handleInput({ skipEmojiFilter: true });
47
+ }
48
+ }
49
+
50
+ handleInput({ skipEmojiFilter = false } = {}) {
21
51
  const cursorPosition = this.element.selectionStart;
22
- const rawValue = this.element.value;
23
- let formattedValue = rawValue;
52
+ let baseValue = this.element.value;
53
+
54
+ // Apply emoji mask if enabled (skip if already filtered in paste handler)
55
+ if (this.hasEmojiMask() && !skipEmojiFilter) {
56
+ const result = applyEmojiMask(this.element);
57
+ baseValue = result.value;
58
+ }
59
+
60
+ const maskType = this.element.getAttribute("mask");
61
+ let formattedValue = baseValue;
24
62
 
25
63
  const maskKey = {
26
64
  currency: 'currency',
@@ -32,13 +70,14 @@ export default class PbTextInput extends PbEnhancedElement {
32
70
  }[maskType];
33
71
 
34
72
  if (maskKey && INPUTMASKS[maskKey]) {
35
- formattedValue = INPUTMASKS[maskKey].format(rawValue);
73
+ formattedValue = INPUTMASKS[maskKey].format(baseValue);
36
74
  }
37
75
 
38
76
  const sanitizedInput = this.element
39
77
  .closest(".text_input_wrapper")
40
78
  ?.querySelector('[data="sanitized-pb-input"]');
41
79
 
80
+ // Ensure sanitized input uses the already filtered value
42
81
  if (sanitizedInput) {
43
82
  switch (maskType) {
44
83
  case "ssn":
@@ -55,8 +94,10 @@ export default class PbTextInput extends PbEnhancedElement {
55
94
  }
56
95
  }
57
96
 
58
- this.element.value = formattedValue;
59
- setCursorPosition(this.element, cursorPosition, rawValue, formattedValue);
97
+ if (maskType) {
98
+ this.element.value = formattedValue;
99
+ setCursorPosition(this.element, cursorPosition, baseValue, formattedValue);
100
+ }
60
101
  }
61
102
  }
62
103
 
@@ -18,6 +18,8 @@ module Playbook
18
18
  prop :autocomplete, default: true
19
19
  prop :disabled, type: Playbook::Props::Boolean,
20
20
  default: false
21
+ prop :emoji_mask, type: Playbook::Props::Boolean,
22
+ default: false
21
23
  prop :error
22
24
  prop :inline, type: Playbook::Props::Boolean,
23
25
  default: false
@@ -117,7 +119,9 @@ module Playbook
117
119
  def validation_data
118
120
  fields = input_options.dig(:data) || {}
119
121
  fields[:message] = validation_message unless validation_message.blank?
120
- mask ? fields.merge(pb_input_mask: true) : fields
122
+ fields[:pb_input_mask] = true if mask
123
+ fields[:pb_emoji_mask] = true if emoji_mask
124
+ fields
121
125
  end
122
126
 
123
127
  def error_class
@@ -360,3 +360,56 @@ test('renders required indicator asterisk when requiredIndicator is true', () =>
360
360
  expect(label).toBeInTheDocument()
361
361
  expect(kit).toHaveTextContent('*')
362
362
  })
363
+
364
+ const TextInputEmojiMask = (props) => {
365
+ const [value, setValue] = useState('')
366
+ const handleOnChange = ({ target }) => {
367
+ setValue(target.value)
368
+ }
369
+
370
+ return (
371
+ <TextInput
372
+ emojiMask
373
+ onChange={handleOnChange}
374
+ value={value}
375
+ {...props}
376
+ />
377
+ )
378
+ }
379
+
380
+ test('removes emoji characters when emojiMask is enabled', () => {
381
+ render(
382
+ <TextInputEmojiMask
383
+ data={{ testid: testId }}
384
+ />
385
+ )
386
+
387
+ const kit = screen.getByTestId(testId)
388
+ const input = within(kit).getByRole('textbox')
389
+
390
+ fireEvent.change(input, { target: { value: 'Hello 👋 World 🌍' } })
391
+ expect(input.value).toBe('Hello World ')
392
+
393
+ fireEvent.change(input, { target: { value: '😀😂🎉' } })
394
+ expect(input.value).toBe('')
395
+
396
+ fireEvent.change(input, { target: { value: 'Hello World' } })
397
+ expect(input.value).toBe('Hello World')
398
+ })
399
+
400
+ test('allows accented characters when emojiMask is enabled', () => {
401
+ render(
402
+ <TextInputEmojiMask
403
+ data={{ testid: testId }}
404
+ />
405
+ )
406
+
407
+ const kit = screen.getByTestId(testId)
408
+ const input = within(kit).getByRole('textbox')
409
+
410
+ fireEvent.change(input, { target: { value: 'Café résumé naïve' } })
411
+ expect(input.value).toBe('Café résumé naïve')
412
+
413
+ fireEvent.change(input, { target: { value: 'àëǒüñ' } })
414
+ expect(input.value).toBe('àëǒüñ')
415
+ })
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react-hooks/rules-of-hooks */
2
2
 
3
- import React, { forwardRef, useEffect, useRef } from 'react'
3
+ import React, { forwardRef, useEffect, useRef, ChangeEvent, ClipboardEvent } from 'react'
4
4
  import classnames from 'classnames'
5
5
 
6
6
  import PbTextarea from '.'
@@ -14,6 +14,8 @@ import Caption from '../pb_caption/_caption'
14
14
  import Flex from '../pb_flex/_flex'
15
15
  import FlexItem from '../pb_flex/_flex_item'
16
16
 
17
+ import { stripEmojisForPaste, applyEmojiMask } from '../utilities/emojiMask'
18
+
17
19
  type TextareaProps = {
18
20
  aria?: {[key: string]: string},
19
21
  characterCount?: string,
@@ -21,6 +23,7 @@ type TextareaProps = {
21
23
  children?: React.ReactChild[],
22
24
  data?: {[key: string]: string},
23
25
  disabled?: boolean,
26
+ emojiMask?: boolean,
24
27
  error?: string,
25
28
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
26
29
  id?: string,
@@ -45,6 +48,7 @@ const Textarea = ({
45
48
  children,
46
49
  data = {},
47
50
  disabled,
51
+ emojiMask = false,
48
52
  htmlOptions = {},
49
53
  inline = false,
50
54
  resize = 'none',
@@ -67,6 +71,37 @@ const Textarea = ({
67
71
  }
68
72
  })
69
73
 
74
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
75
+ // Apply emoji mask if enabled using centralized helper
76
+ if (emojiMask) {
77
+ applyEmojiMask(e.target)
78
+ }
79
+ onChange(e)
80
+ }
81
+
82
+ // Handle paste event for emoji mask - updates textarea value, cursor position, and calls onChange
83
+ const handlePaste = (e: ClipboardEvent<HTMLTextAreaElement>) => {
84
+ if (emojiMask) {
85
+ const pastedText = e.clipboardData.getData('text')
86
+ const filteredText = stripEmojisForPaste(pastedText)
87
+
88
+ if (pastedText !== filteredText) {
89
+ e.preventDefault()
90
+ const textarea = e.currentTarget
91
+ const start = textarea.selectionStart || 0
92
+ const end = textarea.selectionEnd || 0
93
+ const currentValue = textarea.value
94
+ const newValue = currentValue.slice(0, start) + filteredText + currentValue.slice(end)
95
+ const newCursorPosition = start + filteredText.length
96
+
97
+ textarea.value = newValue
98
+ textarea.selectionStart = textarea.selectionEnd = newCursorPosition
99
+
100
+ onChange({ ...e, target: textarea, currentTarget: textarea } as unknown as ChangeEvent<HTMLTextAreaElement>)
101
+ }
102
+ }
103
+ }
104
+
70
105
  const errorClass = error ? 'error' : null
71
106
  const inlineClass = inline ? 'inline' : ''
72
107
  const resizeClass = `resize_${resize}`
@@ -94,7 +129,8 @@ const Textarea = ({
94
129
  <textarea
95
130
  disabled={disabled}
96
131
  name={name}
97
- onChange={onChange}
132
+ onChange={emojiMask ? handleChange : onChange}
133
+ onPaste={emojiMask ? handlePaste : undefined}
98
134
  placeholder={placeholder}
99
135
  ref={ref}
100
136
  required={required}
@@ -0,0 +1,5 @@
1
+ <%= pb_rails("textarea", props: {
2
+ emoji_mask: true,
3
+ label: "Emoji Mask",
4
+ placeholder: "Try typing or pasting emojis...",
5
+ }) %>
@@ -0,0 +1,24 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import Textarea from '../../pb_textarea/_textarea'
4
+
5
+ const TextareaEmojiMask = (props) => {
6
+ const [basicValue, setBasicValue] = useState('')
7
+
8
+ return (
9
+ <div>
10
+ <Textarea
11
+ emojiMask
12
+ label="Emoji Mask"
13
+ onChange={({ target }) => setBasicValue(target.value)}
14
+ placeholder="Try typing or pasting emojis..."
15
+ value={basicValue}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ export default TextareaEmojiMask
23
+
24
+
@@ -0,0 +1 @@
1
+ Use the `emojiMask` / `emoji_mask` prop to prevent users from entering emoji characters (🐸 🐈 🏄‍♂️) in typed or pasted content. It allows accented characters and other non-ASCII letters (é, ü, 文).
@@ -7,6 +7,7 @@ examples:
7
7
  - textarea_error: Textarea w/ Error
8
8
  - textarea_character_counter: Character Counter
9
9
  - textarea_inline: Inline
10
+ - textarea_emoji_mask: Emoji Mask
10
11
 
11
12
  react:
12
13
  - textarea_default: Default
@@ -15,6 +16,7 @@ examples:
15
16
  - textarea_error: Textarea w/ Error
16
17
  - textarea_character_counter: Character Counter
17
18
  - textarea_inline: Inline
19
+ - textarea_emoji_mask: Emoji Mask
18
20
 
19
21
  swift:
20
22
  - textarea_default_swift: Default
@@ -4,3 +4,4 @@ export { default as TextareaCustom } from './_textarea_custom.jsx'
4
4
  export { default as TextareaError } from './_textarea_error.jsx'
5
5
  export { default as TextareaCharacterCounter } from './_textarea_character_counter.jsx'
6
6
  export { default as TextareaInline } from './_textarea_inline.jsx'
7
+ export { default as TextareaEmojiMask } from './_textarea_emoji_mask.jsx'
@@ -1,19 +1,76 @@
1
1
  import PbEnhancedElement from '../pb_enhanced_element'
2
+ import { stripEmojisForPaste, applyEmojiMask } from '../utilities/emojiMask'
2
3
 
3
4
  export default class PbTextarea extends PbEnhancedElement {
4
5
  style: {[key: string]: string}
5
6
  scrollHeight: string
7
+ private skipNextEmojiFilter = false
8
+
6
9
  static get selector(): string {
7
- return '.resize_auto textarea'
10
+ return '.resize_auto textarea, [data-pb-emoji-mask="true"]'
11
+ }
12
+
13
+ hasEmojiMask(): boolean {
14
+ return (this.element as HTMLElement).dataset.pbEmojiMask === "true"
8
15
  }
9
16
 
10
17
  onInput(): void {
11
- this.style.height = 'auto'
12
- this.style.height = (this.scrollHeight) + 'px'
18
+ if ((this.element as HTMLElement).closest('.resize_auto')) {
19
+ this.style.height = 'auto'
20
+ this.style.height = (this.scrollHeight) + 'px'
21
+ }
22
+ }
23
+
24
+ handleEmojiInput = (): void => {
25
+ if (!this.hasEmojiMask()) return
26
+
27
+ if (this.skipNextEmojiFilter) {
28
+ this.skipNextEmojiFilter = false
29
+ return
30
+ }
31
+
32
+ applyEmojiMask(this.element as HTMLTextAreaElement)
33
+ }
34
+
35
+ handleEmojiPaste = (event: ClipboardEvent): void => {
36
+ if (!this.hasEmojiMask()) return
37
+
38
+ const pastedText = event.clipboardData?.getData('text') || ''
39
+ const filteredText = stripEmojisForPaste(pastedText)
40
+
41
+ if (pastedText !== filteredText) {
42
+ event.preventDefault()
43
+ const textarea = this.element as HTMLTextAreaElement
44
+ const start = textarea.selectionStart || 0
45
+ const end = textarea.selectionEnd || 0
46
+ const currentValue = textarea.value
47
+ const newValue = currentValue.slice(0, start) + filteredText + currentValue.slice(end)
48
+ const newCursor = start + filteredText.length
49
+
50
+ textarea.value = newValue
51
+ textarea.selectionStart = textarea.selectionEnd = newCursor
52
+
53
+ this.skipNextEmojiFilter = true
54
+
55
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
56
+ }
13
57
  }
14
58
 
15
59
  connect(): void {
16
- this.element.setAttribute('style', 'height:' + (this.element.scrollHeight) + 'px;overflow-y:hidden;')
17
- this.element.addEventListener('input', this.onInput, false)
60
+ if ((this.element as HTMLElement).closest('.resize_auto')) {
61
+ this.element.setAttribute('style', 'height:' + (this.element as HTMLTextAreaElement).scrollHeight + 'px;overflow-y:hidden;')
62
+ this.element.addEventListener('input', this.onInput, false)
63
+ }
64
+
65
+ if (this.hasEmojiMask()) {
66
+ this.element.addEventListener('input', this.handleEmojiInput, false)
67
+ this.element.addEventListener('paste', this.handleEmojiPaste as EventListener, false)
68
+ }
69
+ }
70
+
71
+ disconnect(): void {
72
+ this.element.removeEventListener('input', this.onInput, false)
73
+ this.element.removeEventListener('input', this.handleEmojiInput, false)
74
+ this.element.removeEventListener('paste', this.handleEmojiPaste as EventListener, false)
18
75
  }
19
76
  }
@@ -11,6 +11,7 @@
11
11
  <%= text_area(
12
12
  :object,
13
13
  :method,
14
+ :data => object.textarea_options[:data],
14
15
  :max_characters => object.max_characters,
15
16
  :name => object.name,
16
17
  :onkeyup => object.onkeyup,
@@ -3,6 +3,8 @@
3
3
  module Playbook
4
4
  module PbTextarea
5
5
  class Textarea < Playbook::KitBase
6
+ prop :emoji_mask, type: Playbook::Props::Boolean,
7
+ default: false
6
8
  prop :error
7
9
  prop :inline, type: Playbook::Props::Boolean,
8
10
  default: false
@@ -28,6 +30,12 @@ module Playbook
28
30
  max_characters && character_count ? "#{character_count} / #{max_characters}" : character_count
29
31
  end
30
32
 
33
+ def textarea_options
34
+ {
35
+ data: emoji_mask ? { pb_emoji_mask: true } : {},
36
+ }
37
+ end
38
+
31
39
  private
32
40
 
33
41
  def error_class
@@ -1,5 +1,5 @@
1
- import React from "react"
2
- import { render, screen } from "../utilities/test-utils"
1
+ import React, { useState } from "react"
2
+ import { render, screen, fireEvent } from "../utilities/test-utils"
3
3
 
4
4
  import Textarea from "./_textarea"
5
5
 
@@ -211,3 +211,58 @@ describe("TextArea Kit Props", () => {
211
211
  expect(textarea.required).toBeTruthy()
212
212
  })
213
213
  })
214
+
215
+ describe("Textarea Emoji Mask", () => {
216
+ const TextareaEmojiMask = (props) => {
217
+ const [value, setValue] = useState('')
218
+ const handleOnChange = ({ target }) => {
219
+ setValue(target.value)
220
+ }
221
+
222
+ return (
223
+ <Textarea
224
+ emojiMask
225
+ onChange={handleOnChange}
226
+ value={value}
227
+ {...props}
228
+ />
229
+ )
230
+ }
231
+
232
+ test("removes emoji characters when emojiMask is enabled", () => {
233
+ render(
234
+ <TextareaEmojiMask
235
+ data={{ testid: testId }}
236
+ />
237
+ )
238
+
239
+ const kit = screen.getByTestId(testId)
240
+ const textarea = kit.querySelector("textarea")
241
+
242
+ fireEvent.change(textarea, { target: { value: 'Hello 👋 World 🌍' } })
243
+ expect(textarea.value).toBe('Hello World ')
244
+
245
+ fireEvent.change(textarea, { target: { value: '😀😂🎉' } })
246
+ expect(textarea.value).toBe('')
247
+
248
+ fireEvent.change(textarea, { target: { value: 'Hello World' } })
249
+ expect(textarea.value).toBe('Hello World')
250
+ })
251
+
252
+ test("allows accented characters when emojiMask is enabled", () => {
253
+ render(
254
+ <TextareaEmojiMask
255
+ data={{ testid: testId }}
256
+ />
257
+ )
258
+
259
+ const kit = screen.getByTestId(testId)
260
+ const textarea = kit.querySelector("textarea")
261
+
262
+ fireEvent.change(textarea, { target: { value: 'Café résumé naïve' } })
263
+ expect(textarea.value).toBe('Café résumé naïve')
264
+
265
+ fireEvent.change(textarea, { target: { value: 'àëǒüñ' } })
266
+ expect(textarea.value).toBe('àëǒüñ')
267
+ })
268
+ })
@@ -0,0 +1,42 @@
1
+ // Regex to match emoji/pictographic characters
2
+ // With modifiers: Zero Width Joiner, Variation Selectors, Skin Tone Modifiers
3
+ export const EMOJI_REGEX = /\p{Extended_Pictographic}|\u200D|\uFE0F|[\u{1F3FB}-\u{1F3FF}]/gu
4
+
5
+ // Utility function to strip emojis from text when typing emojis
6
+ export const stripEmojisForTyping = (text: string): string => {
7
+ return text.replace(EMOJI_REGEX, '')
8
+ }
9
+
10
+ // Utility function to strip emojis and clean up whitespace when pasting emojis
11
+ export const stripEmojisForPaste = (text: string): string => {
12
+ return stripEmojisForTyping(text)
13
+ .replace(/\s+/g, ' ')
14
+ .trim()
15
+ }
16
+
17
+ type EmojiMaskResult = {
18
+ value: string
19
+ cursor: number | null
20
+ }
21
+
22
+ // Union type for elements that support emoji masking
23
+ type TextInputElement = HTMLInputElement | HTMLTextAreaElement
24
+
25
+ export const applyEmojiMask = (
26
+ element: TextInputElement
27
+ ): EmojiMaskResult => {
28
+ const cursor = element.selectionStart
29
+ const original = element.value
30
+ const filtered = stripEmojisForTyping(original)
31
+
32
+ if (original !== filtered) {
33
+ const beforeCursor = original.slice(0, cursor || 0)
34
+ const newCursor = stripEmojisForTyping(beforeCursor).length
35
+ element.value = filtered
36
+ element.selectionStart = element.selectionEnd = newCursor
37
+ return { value: filtered, cursor: newCursor }
38
+ }
39
+ return { value: original, cursor }
40
+ }
41
+
42
+