playbook_ui 15.7.0 → 15.8.0.pre.rc.0
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/_playbook.scss +1 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +9 -7
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/index.js +3 -8
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +1 -1
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +2 -1
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +14 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.html.erb +2 -2
- data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.jsx +3 -2
- data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +56 -6
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.html.erb +6 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.jsx +25 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_text_input/docs/example.yml +5 -0
- data/app/pb_kits/playbook/pb_text_input/docs/index.js +2 -0
- data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
- data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +6 -0
- data/app/pb_kits/playbook/pb_text_input/text_input.rb +7 -1
- data/app/pb_kits/playbook/pb_text_input/text_input.test.js +69 -0
- data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
- data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
- data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
- data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
- data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
- data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +296 -0
- data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +822 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.html.erb +2 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.jsx +16 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.html.erb +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.jsx +13 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.html.erb +4 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.jsx +29 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.html.erb +13 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.jsx +23 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.html.erb +5 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.jsx +15 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_input_options.html.erb +14 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.html.erb +2 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.jsx +15 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.html.erb +42 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.jsx +52 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +45 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.html.erb +3 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.jsx +21 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +24 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/index.js +9 -0
- data/app/pb_kits/playbook/pb_time_picker/index.ts +40 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.html.erb +1 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +80 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +114 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker_helper.ts +662 -0
- data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
- data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
- data/dist/chunks/_typeahead-CSCNg6cp.js +6 -0
- data/dist/chunks/lib-DxCgrqqG.js +29 -0
- data/dist/chunks/vendor.js +3 -3
- data/dist/menu.yml +7 -0
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/forms/builder/form_field_builder.rb +15 -2
- data/lib/playbook/forms/builder/time_picker_field.rb +24 -0
- data/lib/playbook/forms/builder.rb +1 -0
- data/lib/playbook/version.rb +2 -2
- metadata +50 -4
- data/dist/chunks/_typeahead-X3EqK1nR.js +0 -6
- data/dist/chunks/lib-BHYZzndy.js +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d90a9283e859938c8668c9bda85da84fedc435fb8c2e944540fba1a7b834ced
|
|
4
|
+
data.tar.gz: 269fdbd32bd3da95347cffbcaca0e9c130e551c2288b0eb92a6f73e9d6e205fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8718b1da13b30dc286191523f1ba14e8ec1304389bdd0263f2b71ec6543473b20234090aca5a8d06d56716e9eed8334118429038a31f2e0983dac1c7503b1b59
|
|
7
|
+
data.tar.gz: 455f5232737b607d4c6c1d6b574e3bcfd5974246c2daff08bef7196af7e50e3ed3d4512fc4752240adb309ac74f1c56065e325ec7d15202ed3992da226a580a4
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
@import 'pb_advanced_table/advanced_table';
|
|
3
2
|
@import 'pb_avatar/avatar';
|
|
4
3
|
@import 'pb_background/background';
|
|
@@ -98,6 +97,7 @@
|
|
|
98
97
|
@import 'pb_text_input/text_input';
|
|
99
98
|
@import 'pb_textarea/textarea';
|
|
100
99
|
@import 'pb_time/time';
|
|
100
|
+
@import 'pb_time_picker/time_picker';
|
|
101
101
|
@import 'pb_time_range_inline/time_range_inline';
|
|
102
102
|
@import 'pb_time_stacked/time_stacked';
|
|
103
103
|
@import 'pb_timeline/timeline';
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
},
|
|
29
|
+
}, parseInt(autoCloseDataAttr))
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
}
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<%= form.star_rating_field :example_star_rating, props: { variant: "interactive", label: true } %>
|
|
117
117
|
<%= form.time_zone_select_field :example_time_zone_select, ActiveSupport::TimeZone.us_zones, { default: "Eastern Time (US & Canada)" }, props: { label: true } %>
|
|
118
118
|
<%= form.multi_level_select :example_multi_level_select, props: { id: "multi-level-select-form-default", tree_data: treeData, margin_bottom: "sm", label: "Example Multi Level Select field" } %>
|
|
119
|
-
|
|
119
|
+
<%= form.time_picker :example_time_picker, props: { label: true } %>
|
|
120
120
|
<%= form.actions do |action| %>
|
|
121
121
|
<%= action.submit %>
|
|
122
122
|
<%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
|
|
@@ -117,7 +117,8 @@
|
|
|
117
117
|
<%= form.star_rating_field :example_star_rating_validation, props: { variant: "interactive", label: true, required: true } %>
|
|
118
118
|
<%= form.time_zone_select_field :example_time_zone_select, ActiveSupport::TimeZone.us_zones, { default: "Eastern Time (US & Canada)" }, props: { label: true, blank_selection: "Select a Time Zone...", required: true } %>
|
|
119
119
|
<%= form.multi_level_select :example_multi_level_select, props: { id: "multi-level-select-form", tree_data: treeData, margin_bottom: "sm", required: true, label: "Example Multi Level Select field" } %>
|
|
120
|
-
|
|
120
|
+
<%= form.time_picker :example_time_picker_validation, props: { label: true, required: true, validation_message: "Please select a time." } %>
|
|
121
|
+
|
|
121
122
|
<%= form.actions do |action| %>
|
|
122
123
|
<%= action.submit %>
|
|
123
124
|
<%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= pb_form_with(scope: :example, url: "", method: :get, validate: true) do |form| %>
|
|
2
|
+
<%= form.text_field :example_text_field, props: { label: true, required: true, required_indicator: true } %>
|
|
3
|
+
<%= form.text_field :example_text_field_2, props: { label: "Text Field Custom Label", required: true, required_indicator: true } %>
|
|
4
|
+
<%= form.email_field :example_email_field, props: { label: true, required: true, required_indicator: true } %>
|
|
5
|
+
<%= form.number_field :example_number_field, props: { label: true, required: true, required_indicator: true } %>
|
|
6
|
+
<%= form.search_field :example_search_field, props: { label: true, required: true, required_indicator: true } %>
|
|
7
|
+
<%= form.password_field :example_password_field, props: { label: true, required: true, required_indicator: true } %>
|
|
8
|
+
<%= form.url_field :example_url_field, props: { label: true, required: true, required_indicator: true } %>
|
|
9
|
+
|
|
10
|
+
<%= form.actions do |action| %>
|
|
11
|
+
<%= action.submit %>
|
|
12
|
+
<%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
The `required_indicator` prop adds a red asterisk (*) to the input label, visually marking the field as required. This works with both `label: true` for auto-generated labels and `label: "Custom Text"` for custom labels.
|
|
2
|
+
|
|
3
|
+
While it's typically used alongside the `required` prop for HTML5 validation, you can use `required_indicator` independently if you're handling validation differently (e.g., client-side or backend validation).
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
tooltip_id: "append-to-tooltip-2",
|
|
40
40
|
offset: true,
|
|
41
41
|
position: "top",
|
|
42
|
-
append_to: ".
|
|
42
|
+
append_to: ".pb--page--sideNav",
|
|
43
43
|
}) do %>
|
|
44
|
-
I'm a popover. I have been appended to the .
|
|
44
|
+
I'm a popover. I have been appended to the .pb--page--sideNav.
|
|
45
45
|
<% end %>
|
|
46
46
|
<% end %>
|
|
@@ -54,14 +54,15 @@ const PopoverAppendTo = (props) => {
|
|
|
54
54
|
<Body text="Click info for more details" />
|
|
55
55
|
|
|
56
56
|
<PbReactPopover
|
|
57
|
-
appendTo=".
|
|
57
|
+
appendTo=".pb--page--sideNav"
|
|
58
58
|
offset
|
|
59
59
|
placement="top"
|
|
60
60
|
reference={selectorPopoverReference}
|
|
61
61
|
show={showSelectorPopover}
|
|
62
|
+
zIndex={10}
|
|
62
63
|
{...props}
|
|
63
64
|
>
|
|
64
|
-
{'I\'m a popover. I have been appended to the .
|
|
65
|
+
{'I\'m a popover. I have been appended to the .pb--page--sideNav.'}
|
|
65
66
|
</PbReactPopover>
|
|
66
67
|
</Flex>
|
|
67
68
|
</>
|
|
@@ -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(
|
|
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={
|
|
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
|
-
|
|
212
|
-
|
|
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,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,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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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(
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
+
})
|