playbook_ui 15.6.0.pre.rc.4 β†’ 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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -1
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +3 -2
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +4 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +95 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.html.erb +43 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.md +1 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.html.erb +11 -5
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.md +7 -1
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.jsx +54 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.md +9 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.jsx +80 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.md +3 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +4 -1
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  16. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +2 -2
  17. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +57 -0
  18. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +6 -0
  19. data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
  20. data/app/pb_kits/playbook/pb_card/docs/_card_highlight.md +1 -1
  21. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.tsx +6 -0
  22. data/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +2 -2
  23. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleIcon.tsx +10 -8
  24. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_icons.jsx +0 -1
  25. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_state.jsx +0 -3
  26. data/app/pb_kits/playbook/pb_contact/_contact.tsx +51 -24
  27. data/app/pb_kits/playbook/pb_contact/contact.html.erb +53 -19
  28. data/app/pb_kits/playbook/pb_contact/contact.rb +11 -1
  29. data/app/pb_kits/playbook/pb_contact/contact.test.js +76 -0
  30. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.html.erb +33 -0
  31. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.jsx +46 -0
  32. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_rails.md +2 -0
  33. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_react.md +2 -0
  34. data/app/pb_kits/playbook/pb_contact/docs/example.yml +2 -0
  35. data/app/pb_kits/playbook/pb_contact/docs/index.js +1 -0
  36. data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +24 -0
  37. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +197 -7
  38. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.html.erb +23 -14
  39. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.md +1 -1
  40. data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +2 -1
  41. data/app/pb_kits/playbook/pb_dialog/dialog.html.erb +1 -1
  42. data/app/pb_kits/playbook/pb_dialog/dialog.rb +1 -0
  43. data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +14 -0
  44. data/app/pb_kits/playbook/pb_dialog/dialog_header.html.erb +5 -4
  45. data/app/pb_kits/playbook/pb_dialog/dialog_header.rb +2 -0
  46. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.html.erb +24 -0
  47. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.jsx +60 -0
  48. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.md +3 -0
  49. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.html.erb +71 -0
  50. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.jsx +57 -0
  51. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_rails.md +1 -0
  52. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_react.md +1 -0
  53. data/app/pb_kits/playbook/pb_dialog/docs/example.yml +4 -0
  54. data/app/pb_kits/playbook/pb_dialog/docs/index.js +3 -1
  55. data/app/pb_kits/playbook/pb_distribution_bar/docs/_distribution_bar_custom_colors.md +1 -1
  56. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +316 -15
  57. data/app/pb_kits/playbook/pb_draggable/context/types.ts +1 -1
  58. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_rails.html.erb +7 -5
  59. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_default_dates.html.erb +19 -0
  60. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.html.erb +12 -0
  61. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.md +26 -0
  62. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.html.erb +19 -0
  63. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.md +1 -0
  64. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.html.erb +30 -0
  65. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.md +3 -0
  66. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.html.erb +29 -0
  67. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.md +13 -0
  68. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_custom_display_rails.html.erb +3 -1
  69. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +5 -0
  70. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -0
  71. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +39 -5
  72. data/app/pb_kits/playbook/pb_dropdown/index.js +171 -3
  73. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +75 -0
  74. data/app/pb_kits/playbook/pb_filter/Filter/FilterBackground.tsx +3 -3
  75. data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +3 -3
  76. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +2 -1
  77. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +14 -0
  78. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.md +3 -0
  79. data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
  80. data/app/pb_kits/playbook/pb_gauge/_gauge.tsx +6 -0
  81. data/app/pb_kits/playbook/pb_line_graph/_line_graph.tsx +6 -0
  82. data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.html.erb +2 -2
  83. data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.jsx +3 -2
  84. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  85. data/app/pb_kits/playbook/pb_select/_select.tsx +8 -3
  86. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  87. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +16 -0
  88. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +30 -0
  89. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +1 -0
  90. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  91. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  92. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  93. data/app/pb_kits/playbook/pb_select/select.rb +3 -1
  94. data/app/pb_kits/playbook/pb_select/select.test.js +23 -0
  95. data/app/pb_kits/playbook/pb_table/_table.tsx +187 -33
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.jsx +134 -0
  97. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.md +34 -0
  98. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.html.erb +101 -0
  99. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +33 -0
  100. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.jsx +180 -0
  101. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.md +3 -0
  102. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.html.erb +122 -0
  103. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.md +3 -0
  104. data/app/pb_kits/playbook/pb_table/docs/example.yml +4 -0
  105. data/app/pb_kits/playbook/pb_table/docs/index.js +2 -0
  106. data/app/pb_kits/playbook/pb_table/table.html.erb +68 -12
  107. data/app/pb_kits/playbook/pb_table/table.rb +22 -3
  108. data/app/pb_kits/playbook/pb_table/table.test.js +143 -0
  109. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +56 -6
  110. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
  111. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
  112. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
  113. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  114. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.html.erb +6 -0
  115. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.jsx +25 -0
  116. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.md +3 -0
  117. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +5 -0
  118. data/app/pb_kits/playbook/pb_text_input/docs/index.js +2 -0
  119. data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
  120. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +6 -0
  121. data/app/pb_kits/playbook/pb_text_input/text_input.rb +7 -1
  122. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +69 -0
  123. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
  124. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
  125. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
  126. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
  127. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  128. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
  129. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  130. data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
  131. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
  132. data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
  133. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
  134. data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +296 -0
  135. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +822 -0
  136. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.html.erb +2 -0
  137. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.jsx +16 -0
  138. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.md +1 -0
  139. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.html.erb +1 -0
  140. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.jsx +13 -0
  141. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.md +1 -0
  142. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.html.erb +4 -0
  143. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.jsx +29 -0
  144. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.md +1 -0
  145. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.html.erb +13 -0
  146. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.jsx +23 -0
  147. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.html.erb +5 -0
  148. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.jsx +15 -0
  149. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_input_options.html.erb +14 -0
  150. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.html.erb +2 -0
  151. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.jsx +15 -0
  152. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.html.erb +42 -0
  153. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.jsx +52 -0
  154. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.md +1 -0
  155. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +45 -0
  156. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.md +1 -0
  157. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.html.erb +3 -0
  158. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.jsx +21 -0
  159. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.md +1 -0
  160. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +24 -0
  161. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +9 -0
  162. data/app/pb_kits/playbook/pb_time_picker/index.ts +40 -0
  163. data/app/pb_kits/playbook/pb_time_picker/time_picker.html.erb +1 -0
  164. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +80 -0
  165. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +114 -0
  166. data/app/pb_kits/playbook/pb_time_picker/time_picker_helper.ts +662 -0
  167. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -0
  168. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.html.erb +60 -0
  169. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.jsx +118 -0
  170. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.md +1 -0
  171. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_date.md +1 -1
  172. data/app/pb_kits/playbook/pb_timeline/docs/example.yml +2 -0
  173. data/app/pb_kits/playbook/pb_timeline/docs/index.js +1 -0
  174. data/app/pb_kits/playbook/pb_timeline/item.html.erb +1 -1
  175. data/app/pb_kits/playbook/pb_timeline/item.rb +2 -0
  176. data/app/pb_kits/playbook/pb_timeline/label.html.erb +2 -1
  177. data/app/pb_kits/playbook/pb_timeline/label.rb +2 -0
  178. data/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx +3 -0
  179. data/app/pb_kits/playbook/pb_timeline/timeline.test.js +51 -0
  180. data/app/pb_kits/playbook/tokens/_colors.scss +2 -1
  181. data/app/pb_kits/playbook/utilities/deprecated.ts +73 -0
  182. data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
  183. data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
  184. data/dist/chunks/_typeahead-CSCNg6cp.js +6 -0
  185. data/dist/chunks/lib-DxCgrqqG.js +29 -0
  186. data/dist/chunks/vendor.js +3 -3
  187. data/dist/menu.yml +16 -9
  188. data/dist/playbook-rails-react-bindings.js +1 -1
  189. data/dist/playbook-rails.js +1 -1
  190. data/dist/playbook.css +1 -1
  191. data/lib/playbook/forms/builder/collection_select_field.rb +9 -1
  192. data/lib/playbook/forms/builder/form_field_builder.rb +15 -2
  193. data/lib/playbook/forms/builder/select_field.rb +9 -1
  194. data/lib/playbook/forms/builder/time_picker_field.rb +24 -0
  195. data/lib/playbook/forms/builder/time_zone_select_field.rb +9 -1
  196. data/lib/playbook/forms/builder.rb +1 -0
  197. data/lib/playbook/pb_doc_helper.rb +3 -0
  198. data/lib/playbook/pb_kit_helper.rb +35 -0
  199. data/lib/playbook/version.rb +2 -2
  200. metadata +92 -4
  201. data/dist/chunks/_typeahead-BXM7QUuy.js +0 -6
  202. data/dist/chunks/lib-CgpqUb6l.js +0 -29
@@ -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'
@@ -9,8 +9,10 @@ import Card from '../pb_card/_card'
9
9
  import Caption from '../pb_caption/_caption'
10
10
  import Body from '../pb_body/_body'
11
11
  import Icon from '../pb_icon/_icon'
12
+ import colors from '../tokens/exports/_colors.module.scss'
12
13
 
13
14
  import { INPUTMASKS } from './inputMask'
15
+ import { stripEmojisForPaste, applyEmojiMask } from '../utilities/emojiMask'
14
16
 
15
17
  type TextInputProps = {
16
18
  aria?: { [key: string]: string },
@@ -18,6 +20,7 @@ type TextInputProps = {
18
20
  data?: { [key: string]: string },
19
21
  dark?: boolean,
20
22
  disabled?: boolean,
23
+ emojiMask?: boolean,
21
24
  error?: string,
22
25
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
23
26
  id?: string,
@@ -28,6 +31,7 @@ type TextInputProps = {
28
31
  onChange: (e: React.FormEvent<HTMLInputElement>, sanitizedValue?: string) => void,
29
32
  placeholder: string,
30
33
  required?: boolean,
34
+ requiredIndicator?: boolean,
31
35
  type: string,
32
36
  value: string | number,
33
37
  children: React.ReactElement,
@@ -47,6 +51,7 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
47
51
  dark = false,
48
52
  data = {},
49
53
  disabled,
54
+ emojiMask = false,
50
55
  error,
51
56
  htmlOptions = {},
52
57
  id,
@@ -60,6 +65,7 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
60
65
  type = 'text',
61
66
  value = '',
62
67
  children = null,
68
+ requiredIndicator = false,
63
69
  autoComplete = true,
64
70
  } = props
65
71
 
@@ -99,6 +105,11 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
99
105
  const isMaskedInput = mask && mask in INPUTMASKS
100
106
 
101
107
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
108
+ // Apply emoji mask if enabled using centralized helper
109
+ if (emojiMask) {
110
+ applyEmojiMask(e.target)
111
+ }
112
+
102
113
  if (isMaskedInput) {
103
114
  const inputValue = e.target.value
104
115
 
@@ -131,6 +142,29 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
131
142
  }
132
143
  }
133
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
+
134
168
  const childInput = children ? children.type === "input" : undefined
135
169
 
136
170
  let formattedValue;
@@ -142,10 +176,16 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
142
176
 
143
177
  const errorId = error ? `${id}-error` : undefined
144
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
+
145
185
  const textInput = (
146
186
  childInput ? React.cloneElement(children, { className: "text_input" }) :
147
187
  (<input
148
- {...domSafeProps(props)}
188
+ {...domSafeProps(domProps)}
149
189
  aria-describedby={errorId}
150
190
  aria-invalid={!!error}
151
191
  autoComplete={typeof autoComplete === "string" ? autoComplete : ( autoComplete ? undefined : "off" )}
@@ -154,7 +194,8 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
154
194
  id={id}
155
195
  key={id}
156
196
  name={name}
157
- onChange={isMaskedInput ? handleChange : onChange}
197
+ onChange={shouldUseCustomHandler ? handleChange : onChange}
198
+ onPaste={emojiMask ? handlePaste : undefined}
158
199
  pattern={isMaskedInput ? INPUTMASKS[mask]?.pattern : undefined}
159
200
  placeholder={placeholder || (isMaskedInput ? INPUTMASKS[mask]?.placeholder : undefined)}
160
201
  ref={ref}
@@ -208,9 +249,18 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
208
249
  >
209
250
  {label && (
210
251
  <label htmlFor={id}>
211
- <Caption className="pb_text_input_kit_label"
212
- text={label}
213
- />
252
+ {
253
+ requiredIndicator ? (
254
+ <Caption className="pb_text_input_kit_label">
255
+ {label} <span style={{ color: `${colors.error}` }}>*</span>
256
+ </Caption>
257
+ ) : (
258
+ <Caption className="pb_text_input_kit_label"
259
+ text={label}
260
+ />
261
+ )
262
+ }
263
+
214
264
  </label>
215
265
  )}
216
266
  <div className={`${addOnCss} text_input_wrapper`}>
@@ -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
+
@@ -1 +1 @@
1
- Text Input w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Text Input w/ Error shows that the input must be filled out (i.e. when used in a form it signals a user to fix an error).
@@ -0,0 +1,6 @@
1
+ <%= pb_rails("text_input", props: {
2
+ label: "First Name",
3
+ placeholder: "Enter first name",
4
+ id: "text_input_required_indicator",
5
+ required_indicator: true,
6
+ }) %>
@@ -0,0 +1,25 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import TextInput from '../../pb_text_input/_text_input'
4
+
5
+ const TextInputDefault = (props) => {
6
+ const [firstName, setFirstName] = useState('')
7
+ const handleOnChangeFirstName = ({ target }) => {
8
+ setFirstName(target.value)
9
+ }
10
+
11
+ return (
12
+ <TextInput
13
+ id="text_input_required_indicator"
14
+ label="First Name"
15
+ name="firstName"
16
+ onChange={handleOnChangeFirstName}
17
+ placeholder="Enter first name"
18
+ requiredIndicator
19
+ value={firstName}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ export default TextInputDefault
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -10,6 +10,9 @@ examples:
10
10
  - text_input_options: Input Options
11
11
  - text_input_mask: Mask
12
12
  - text_input_autocomplete: Autocomplete
13
+ - text_input_required_indicator: Required Indicator
14
+ - text_input_emoji_mask: Emoji Mask
15
+
13
16
 
14
17
  react:
15
18
  - text_input_default: Default
@@ -22,6 +25,8 @@ examples:
22
25
  - text_input_mask: Mask
23
26
  - text_input_sanitize: Sanitized Masked Input
24
27
  - text_input_autocomplete: Autocomplete
28
+ - text_input_required_indicator: Required Indicator
29
+ - text_input_emoji_mask: Emoji Mask
25
30
 
26
31
 
27
32
  swift:
@@ -8,3 +8,5 @@ export { default as TextInputNoLabel } from './_text_input_no_label.jsx'
8
8
  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
+ 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
 
@@ -1,7 +1,13 @@
1
1
  <%= pb_content_tag(:div, id: nil ) do %>
2
2
  <% if object.label.present? %>
3
3
  <label for="<%= object.input_options[:id] || object.id %>" >
4
+ <% if object.required_indicator %>
5
+ <%= pb_rails("caption", props: { dark: object.dark, classname: "pb_text_input_kit_label" }) do %>
6
+ <%= object.label %><span style="color: #DA0014;"> *</span>
7
+ <% end %>
8
+ <% else %>
4
9
  <%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
10
+ <% end %>
5
11
  </label>
6
12
  <% end %>
7
13
  <%= content_tag(:div, class: "#{add_on_class} text_input_wrapper") do %>
@@ -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
@@ -38,6 +40,8 @@ module Playbook
38
40
  prop :mask, type: Playbook::Props::Enum,
39
41
  values: ["currency", "zip_code", "postal_code", "ssn", "credit_card", "cvv", nil],
40
42
  default: nil
43
+ prop :required_indicator, type: Playbook::Props::Boolean,
44
+ default: false
41
45
 
42
46
  def classname
43
47
  default_margin_bottom = margin_bottom.present? ? "" : " mb_sm"
@@ -115,7 +119,9 @@ module Playbook
115
119
  def validation_data
116
120
  fields = input_options.dig(:data) || {}
117
121
  fields[:message] = validation_message unless validation_message.blank?
118
- 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
119
125
  end
120
126
 
121
127
  def error_class
@@ -344,3 +344,72 @@ test('does not add autocomplete attribute otherwise', () => {
344
344
  const input = within(kit).getByRole('textbox')
345
345
  expect(input).not.toHaveAttribute("autocomplete")
346
346
  })
347
+
348
+ test('renders required indicator asterisk when requiredIndicator is true', () => {
349
+ render(
350
+ <TextInput
351
+ data={{ testid: testId }}
352
+ label="Email Address"
353
+ requiredIndicator
354
+ />
355
+ )
356
+
357
+ const kit = screen.getByTestId(testId)
358
+ const label = within(kit).getByText(/Email Address/)
359
+
360
+ expect(label).toBeInTheDocument()
361
+ expect(kit).toHaveTextContent('*')
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 (Γ©, ΓΌ, ζ–‡).
@@ -1 +1 @@
1
- Textarea w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Textarea w/ Error shows that the input must be filled out (i.e. when used in a form it signals a user to fix an error).
@@ -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'