playbook_ui 15.7.0.pre.alpha.play270013292 → 15.7.0.pre.alpha.play270013367

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +9 -7
  3. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/index.js +3 -8
  4. data/app/pb_kits/playbook/pb_pb_bar_graph/docs/_description.md +6 -1
  5. data/app/pb_kits/playbook/pb_pb_circle_chart/docs/_description.md +6 -1
  6. data/app/pb_kits/playbook/pb_pb_gauge_chart/docs/_description.md +6 -1
  7. data/app/pb_kits/playbook/pb_pb_line_graph/docs/_description.md +6 -1
  8. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +41 -3
  9. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
  10. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
  11. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
  12. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +2 -0
  13. data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
  14. data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
  15. data/app/pb_kits/playbook/pb_text_input/text_input.rb +5 -1
  16. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +53 -0
  17. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
  18. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
  19. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
  20. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
  21. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
  22. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  23. data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
  24. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
  25. data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
  26. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
  27. data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
  28. data/dist/chunks/{_pb_line_graph-BD1q16F4.js → _pb_line_graph-hxi01lk7.js} +1 -1
  29. data/dist/chunks/_typeahead-CXDxFWiJ.js +1 -0
  30. data/dist/chunks/{globalProps-DEelyJQ8.js → globalProps-DgYwLYNx.js} +1 -1
  31. data/dist/chunks/{lib-kKvUARMY.js → lib-NLxTo8OB.js} +1 -1
  32. data/dist/chunks/vendor.js +5 -5
  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 +13 -6
  38. data/dist/chunks/_typeahead-8dL5voQX.js +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d476d049a0a1338ccf092061737f48c9890290f0d072b2a8f41492c74485517f
4
- data.tar.gz: 51fe8f5c93875f9f3d23341a72e7a7a802ca6f98dbaa866f214a747f86c0ed94
3
+ metadata.gz: 73221f2b61bfb1268562ee54814653ed97086b27c106dafa2ae27249a388e8fb
4
+ data.tar.gz: 0410e1c396de358ed5109c7476781094aa21940b822aecd0af23c91f7d4ad319
5
5
  SHA512:
6
- metadata.gz: 13095cacc73ca4e0e082871b6a8b9d9b0d8ef4999f268fbe2c1b65b853a59568ed69f980d132e1e56ea68c33a06f1cc3213f804e098c7bc0daf4c225b61e2bb5
7
- data.tar.gz: 443bf90b455f43f216e1041fc384946e24a2ae9a33be62f1093bbccea715d1c8b2e930f6872b6fc28f6aa276156ab791ed5df514b620c192a0f1fbf352a7e48f
6
+ metadata.gz: e2e7bba900eaec32654a3b1db302ace38e29f6ed487f825084c9f61c000aae00b92d1ccc1ea7dfd8494f6ea5ab8011f90bfab34ec86da8e03d365c236ef2a87e
7
+ data.tar.gz: 5d512aef7687b67c7272b7be24bd7643c9a088c9e19851c52487445262e88dbb6d822d730041429dbef3654d39cad1e72b67c6e93897d504d6180a7e68277bf1
@@ -30,10 +30,6 @@ module Playbook
30
30
  closeable.present? ? " remove_toast" : ""
31
31
  end
32
32
 
33
- def auto_close_class
34
- auto_close.present? ? " auto_close_#{auto_close}" : ""
35
- end
36
-
37
33
  def position_class
38
34
  horizontal && vertical ? " positioned_toast #{vertical} #{horizontal}" : ""
39
35
  end
@@ -42,6 +38,14 @@ module Playbook
42
38
  multi_line.present? ? "multi_line" : nil
43
39
  end
44
40
 
41
+ def auto_close_attribute
42
+ auto_close.present? ? { "pb-auto-close": auto_close } : {}
43
+ end
44
+
45
+ def data
46
+ Hash(prop(:data)).merge(auto_close_attribute)
47
+ end
48
+
45
49
  def icon_value
46
50
  icon || case status
47
51
  when "success"
@@ -61,9 +65,7 @@ module Playbook
61
65
 
62
66
  def classname
63
67
  default_z_index = z_index.present? ? "" : " z_index_max"
64
- # IMPORTANT: the AutoClose class must be the last class in the string for JS to read it correctly
65
- # Changing the order will break the auto_close functionality
66
- generate_classname("pb_fixed_confirmation_toast_kit", status, multi_line_class) + close_class + position_class + icon_class + default_z_index + auto_close_class
68
+ generate_classname("pb_fixed_confirmation_toast_kit", status, multi_line_class) + close_class + position_class + icon_class + default_z_index
67
69
  end
68
70
  end
69
71
  end
@@ -21,17 +21,12 @@ export default class PbFixedConfirmationToast extends PbEnhancedElement {
21
21
  }
22
22
 
23
23
  autoCloseToast(element) {
24
- const classListValues = element.classList.value
25
- const hasAutoCloseClass = classListValues.includes('auto_close')
26
-
27
- if (hasAutoCloseClass) {
28
- const classList = classListValues.split(' ')
29
- const autoCloseValue = classList[classList.length - 1].split('_')[2]
30
- const autoCloseIntValue = parseInt(autoCloseValue)
24
+ const autoCloseDataAttr = element.getAttribute('data-pb-auto-close')
31
25
 
26
+ if (autoCloseDataAttr) {
32
27
  setTimeout(() => {
33
28
  this.removeToast(element)
34
- }, autoCloseIntValue)
29
+ }, parseInt(autoCloseDataAttr))
35
30
  }
36
31
  }
37
32
  }
@@ -2,4 +2,9 @@
2
2
 
3
3
  This kit is a wrapper around the Highcharts library. It applies styling and default settings but does NOT ship Highcharts. Once 'highcharts' and 'highcharts-react-official are installed into your repo, any prop or functionality provided by Highcharts can be used with this kit without requiring specific props from Playbook. The doc examples below showcase a few common usecases but are not a comprehensive list of all the functionalities possible.
4
4
 
5
- See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
5
+ See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
6
+
7
+ **NOTE**: All chart kits are available through a separate entrypoint to keep Highcharts optional. Import them using:
8
+ ```javascript
9
+ import { PbBarGraph } from 'playbook-ui/charts'
10
+ ```
@@ -2,4 +2,9 @@
2
2
 
3
3
  This kit is a wrapper around the Highcharts library. It applies styling and default settings but does NOT ship Highcharts. Once 'highcharts' and 'highcharts-react-official are installed into your repo, any prop or functionality provided by Highcharts can be used with this kit without requiring specific props from Playbook. The doc examples below showcase a few common usecases but are not a comprehensive list of all the functionalities possible.
4
4
 
5
- See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
5
+ See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
6
+
7
+ **NOTE**: All chart kits are available through a separate entrypoint to keep Highcharts optional. Import them using:
8
+ ```javascript
9
+ import { PbCircleChart } from 'playbook-ui/charts'
10
+ ```
@@ -2,4 +2,9 @@
2
2
 
3
3
  This kit is a wrapper around the Highcharts library. It applies styling and default settings but does NOT ship Highcharts. Once 'highcharts' and 'highcharts-react-official are installed into your repo, any prop or functionality provided by Highcharts can be used with this kit without requiring specific props from Playbook. The doc examples below showcase a few common usecases but are not a comprehensive list of all the functionalities possible.
4
4
 
5
- See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
5
+ See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
6
+
7
+ **NOTE**: All chart kits are available through a separate entrypoint to keep Highcharts optional. Import them using:
8
+ ```javascript
9
+ import { PbGaugeChart } from 'playbook-ui/charts'
10
+ ```
@@ -2,4 +2,9 @@
2
2
 
3
3
  This kit is a wrapper around the Highcharts library. It applies styling and default settings but does NOT ship Highcharts. Once 'highcharts' and 'highcharts-react-official are installed into your repo, any prop or functionality provided by Highcharts can be used with this kit without requiring specific props from Playbook. The doc examples below showcase a few common usecases but are not a comprehensive list of all the functionalities possible.
4
4
 
5
- See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
5
+ See the [highcharts API docs](https://api.highcharts.com/highcharts/) for a comprehensive look at what is possible.
6
+
7
+ **NOTE**: All chart kits are available through a separate entrypoint to keep Highcharts optional. Import them using:
8
+ ```javascript
9
+ import { PbLineGraph } from 'playbook-ui/charts'
10
+ ```
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, ChangeEvent } from 'react'
1
+ import React, { forwardRef, ChangeEvent, ClipboardEvent } from 'react'
2
2
  import classnames from 'classnames'
3
3
 
4
4
  import { globalProps, GlobalProps, domSafeProps } from '../utilities/globalProps'
@@ -12,6 +12,7 @@ import Icon from '../pb_icon/_icon'
12
12
  import colors from '../tokens/exports/_colors.module.scss'
13
13
 
14
14
  import { INPUTMASKS } from './inputMask'
15
+ import { stripEmojisForPaste, applyEmojiMask } from '../utilities/emojiMask'
15
16
 
16
17
  type TextInputProps = {
17
18
  aria?: { [key: string]: string },
@@ -19,6 +20,7 @@ type TextInputProps = {
19
20
  data?: { [key: string]: string },
20
21
  dark?: boolean,
21
22
  disabled?: boolean,
23
+ emojiMask?: boolean,
22
24
  error?: string,
23
25
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
24
26
  id?: string,
@@ -49,6 +51,7 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
49
51
  dark = false,
50
52
  data = {},
51
53
  disabled,
54
+ emojiMask = false,
52
55
  error,
53
56
  htmlOptions = {},
54
57
  id,
@@ -102,6 +105,11 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
102
105
  const isMaskedInput = mask && mask in INPUTMASKS
103
106
 
104
107
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
108
+ // Apply emoji mask if enabled using centralized helper
109
+ if (emojiMask) {
110
+ applyEmojiMask(e.target)
111
+ }
112
+
105
113
  if (isMaskedInput) {
106
114
  const inputValue = e.target.value
107
115
 
@@ -134,6 +142,29 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
134
142
  }
135
143
  }
136
144
 
145
+ // Handle paste event for emoji mask - updates input value, cursor position, and calls onChange
146
+ const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
147
+ if (emojiMask) {
148
+ const pastedText = e.clipboardData.getData('text')
149
+ const filteredText = stripEmojisForPaste(pastedText)
150
+
151
+ if (pastedText !== filteredText) {
152
+ e.preventDefault()
153
+ const input = e.currentTarget
154
+ const start = input.selectionStart || 0
155
+ const end = input.selectionEnd || 0
156
+ const currentValue = input.value
157
+ const newValue = currentValue.slice(0, start) + filteredText + currentValue.slice(end)
158
+ const newCursorPosition = start + filteredText.length
159
+
160
+ input.value = newValue
161
+ input.selectionStart = input.selectionEnd = newCursorPosition
162
+
163
+ onChange({ ...e, target: input, currentTarget: input } as unknown as ChangeEvent<HTMLInputElement>)
164
+ }
165
+ }
166
+ }
167
+
137
168
  const childInput = children ? children.type === "input" : undefined
138
169
 
139
170
  let formattedValue;
@@ -145,10 +176,16 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
145
176
 
146
177
  const errorId = error ? `${id}-error` : undefined
147
178
 
179
+ // Set custom handler between emoji mask and input mask
180
+ const shouldUseCustomHandler = isMaskedInput || emojiMask
181
+
182
+ // Filter out emojiMask from props passed to DOM element
183
+ const { emojiMask: _emojiMask, ...domProps } = props
184
+
148
185
  const textInput = (
149
186
  childInput ? React.cloneElement(children, { className: "text_input" }) :
150
187
  (<input
151
- {...domSafeProps(props)}
188
+ {...domSafeProps(domProps)}
152
189
  aria-describedby={errorId}
153
190
  aria-invalid={!!error}
154
191
  autoComplete={typeof autoComplete === "string" ? autoComplete : ( autoComplete ? undefined : "off" )}
@@ -157,7 +194,8 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
157
194
  id={id}
158
195
  key={id}
159
196
  name={name}
160
- onChange={isMaskedInput ? handleChange : onChange}
197
+ onChange={shouldUseCustomHandler ? handleChange : onChange}
198
+ onPaste={emojiMask ? handlePaste : undefined}
161
199
  pattern={isMaskedInput ? INPUTMASKS[mask]?.pattern : undefined}
162
200
  placeholder={placeholder || (isMaskedInput ? INPUTMASKS[mask]?.placeholder : undefined)}
163
201
  ref={ref}
@@ -0,0 +1,7 @@
1
+ <%= pb_rails("text_input", props: {
2
+ emoji_mask: true,
3
+ label: "Emoji Mask",
4
+ placeholder: "Try typing or pasting emojis...",
5
+ }) %>
6
+
7
+
@@ -0,0 +1,24 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import TextInput from '../../pb_text_input/_text_input'
4
+
5
+ const TextInputEmojiMask = (props) => {
6
+ const [basicValue, setBasicValue] = useState('')
7
+
8
+ return (
9
+ <div>
10
+ <TextInput
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 TextInputEmojiMask
23
+
24
+
@@ -0,0 +1,2 @@
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 (é, ü, 文).
2
+
@@ -11,6 +11,7 @@ examples:
11
11
  - text_input_mask: Mask
12
12
  - text_input_autocomplete: Autocomplete
13
13
  - text_input_required_indicator: Required Indicator
14
+ - text_input_emoji_mask: Emoji Mask
14
15
 
15
16
 
16
17
  react:
@@ -25,6 +26,7 @@ examples:
25
26
  - text_input_sanitize: Sanitized Masked Input
26
27
  - text_input_autocomplete: Autocomplete
27
28
  - text_input_required_indicator: Required Indicator
29
+ - text_input_emoji_mask: Emoji Mask
28
30
 
29
31
 
30
32
  swift:
@@ -9,3 +9,4 @@ export { default as TextInputMask } from './_text_input_mask.jsx'
9
9
  export { default as TextInputSanitize } from './_text_input_sanitize.jsx'
10
10
  export { default as TextInputAutocomplete } from './_text_input_autocomplete.jsx'
11
11
  export { default as TextInputRequiredIndicator } from './_text_input_required_indicator.jsx'
12
+ export { default as TextInputEmojiMask } from './_text_input_emoji_mask.jsx'
@@ -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'