playbook_ui 16.5.0.pre.alpha.RTEPOC15748 → 16.5.0.pre.alpha.RTEPOC15779

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8860b0cd8332d8b7d6727c3871dd06ff3b3d429e7cca826855e97657ecc82ce8
4
- data.tar.gz: fb9521d937a214c6fae9bd4ea0b3856f95b4d5e9be67e387292b279aca0f5f83
3
+ metadata.gz: a2c36115f459754d15420a726ab66d5aef94d010e98cccf7cb900c7f181a926e
4
+ data.tar.gz: 065a216cb80347d860b96b44a0d7fcc57bc588a1168f771c9cb2ab1960d49e4f
5
5
  SHA512:
6
- metadata.gz: d74bfc111083f77e948b3fda81623aa925ff89acea5370873345e3ac73d70b591f42c5d06cfbe1302867f1bdd992c3a84fe99827545a91c3eb8361eac01caed3
7
- data.tar.gz: 6371ded7a68098e54d28074edfadd3ad223d61f8361ce6684ff2081e72686b01e915efc8e5266de664b02b109648a623eee3cc390fde00f28231ac7316104e33
6
+ metadata.gz: 9948221496986eedce4addad128d3991d441e2d06e0f0fbdfd69630f748771276e74ee3ad532550972b8467a3f175a3cb04719b6811aa4847d79721dd987f0ad
7
+ data.tar.gz: 47ec682ffc0fae5d065c1d5636d21a6d288e3b54e7091fc49fed4180ee95aafd67db4282d0bc700f6af98e7de5428407222080f0305ea3599aacd88eb3573ee6
@@ -31,6 +31,7 @@
31
31
  name: object.name,
32
32
  placeholder: object.placeholder,
33
33
  required: object.required,
34
+ validation: object.validation_message.present? ? { message: object.validation_message } : {},
34
35
  }) %>
35
36
  <% end %>
36
37
  <% if object.selection_type == "quickpick" %>
@@ -0,0 +1,62 @@
1
+ import React, { useState } from "react";
2
+ import { Button, Dialog, DatePicker } from "playbook-ui";
3
+
4
+ const DatePickerDialogSubmission = () => {
5
+ const [isOpen, setIsOpen] = useState(false);
6
+ const [dateFixed, setDateFixed] = useState("");
7
+ const [pickerInstance, setPickerInstance] = useState(0);
8
+
9
+ const close = (clearDate = false) => {
10
+ if (clearDate) setDateFixed("");
11
+ setIsOpen(false);
12
+ };
13
+
14
+ const open = () => {
15
+ setPickerInstance((current) => current + 1);
16
+ setIsOpen(true);
17
+ };
18
+
19
+ const handleSubmit = () => {
20
+ if (!dateFixed.trim()) return;
21
+ close();
22
+ };
23
+
24
+ return (
25
+ <>
26
+ <Button
27
+ onClick={open}
28
+ text="Open Dialog"
29
+ />
30
+ <Dialog
31
+ onClose={() => close(true)}
32
+ opened={isOpen}
33
+ size="md"
34
+ title="Date Picker: Dialog Submission Example"
35
+ >
36
+ <Dialog.Body>
37
+ <DatePicker
38
+ defaultDate={dateFixed || undefined}
39
+ key={`fixed-${pickerInstance}`}
40
+ label="Date"
41
+ onChange={(dateStr) => setDateFixed(dateStr || "")}
42
+ pickerId={`datePickerFixed-${pickerInstance}`}
43
+ staticPosition={false}
44
+ />
45
+ </Dialog.Body>
46
+ <Dialog.Footer>
47
+ <Button
48
+ onClick={handleSubmit}
49
+ text="Submit"
50
+ />
51
+ <Button
52
+ onClick={() => close(true)}
53
+ text="Cancel"
54
+ variant="link"
55
+ />
56
+ </Dialog.Footer>
57
+ </Dialog>
58
+ </>
59
+ );
60
+ };
61
+
62
+ export default DatePickerDialogSubmission;
@@ -0,0 +1 @@
1
+ Use this pattern when a DatePicker lives inside a Dialog and needs to submit a selected value before closing. A unique `key` and `pickerId` will force the datePicker to remount each time the dialog opens.
@@ -66,3 +66,4 @@ examples:
66
66
  - date_picker_positions: Custom Positions
67
67
  - date_picker_positions_element: Custom Position (based on element)
68
68
  - date_picker_required_indicator: Required Indicator
69
+ - date_picker_dialog_submission: Dialog Form Submission
@@ -29,3 +29,4 @@ export { default as DatePickerQuickPickDefaultDate } from './_date_picker_quick_
29
29
  export { default as DatePickerRangePattern } from './_date_picker_range_pattern'
30
30
  export { default as DatePickerAndDropdownRange } from './_date_picker_and_dropdown_range.jsx'
31
31
  export { default as DatePickerRequiredIndicator } from "./_date_picker_required_indicator.jsx";
32
+ export { default as DatePickerDialogSubmission } from "./_date_picker_dialog_submission.jsx";
@@ -97,8 +97,8 @@
97
97
  }] %>
98
98
 
99
99
  <%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
100
- <%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true, validation: { message: "Please select a user." } } %>
101
- <%= form.typeahead :example_typeahead_validation_react, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead (React Rendered)", placeholder: "Search for a user", required: true, validation: { message: "Please select a color." } } %>
100
+ <%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true } %>
101
+ <%= form.typeahead :example_typeahead_validation_react, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead (React Rendered)", placeholder: "Search for a user", required: true } %>
102
102
  <%= form.typeahead :example_typeahead_validation_react_2, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead 2 (React Rendered)", placeholder: "Search for a user", required: true } %>
103
103
  <%= form.text_field :example_text_field_validation, props: { label: true, required: true } %>
104
104
  <%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: true, required: true } %>
@@ -110,14 +110,14 @@
110
110
  <%= form.text_area :example_text_area_validation, props: { label: true, required: true } %>
111
111
  <%= form.dropdown_field :example_dropdown_validation, props: { label: true, options: example_dropdown_options, required: true } %>
112
112
  <%= form.dropdown_field :example_dropdown_validation_multi, props: { label: true, options: example_dropdown_options, multi_select: true, required: true } %>
113
- <%= form.select :example_select_validation, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true, validation_message: "Please, select an option." } %>
113
+ <%= form.select :example_select_validation, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true } %>
114
114
  <%= form.collection_select :example_collection_select_validation, example_collection, :value, :name, props: { label: true, blank_selection: "Select One...", required: true } %>
115
115
  <%= form.check_box :example_checkbox_validation, props: { text: "Example Checkbox Validation", label: true, required: true }, checked_value: "1", unchecked_value: "0" %>
116
- <%= form.date_picker :example_date_picker_2, props: { label: true, required: true, validation_message: "Please, select a date.", allow_input: true } %>
116
+ <%= form.date_picker :example_date_picker_2, props: { label: true, required: true, allow_input: true } %>
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
- <%= form.time_picker :example_time_picker_validation, props: { label: true, required: true, validation_message: "Please select a time." } %>
120
+ <%= form.time_picker :example_time_picker_validation, props: { label: true, required: true } %>
121
121
 
122
122
  <%= form.actions do |action| %>
123
123
  <%= action.submit %>
@@ -0,0 +1,90 @@
1
+ <%
2
+ example_collection = [
3
+ OpenStruct.new(name: "Alabama", value: 1),
4
+ OpenStruct.new(name: "Alaska", value: 2),
5
+ OpenStruct.new(name: "Arizona", value: 3),
6
+ OpenStruct.new(name: "Arkansas", value: 4),
7
+ OpenStruct.new(name: "California", value: 5),
8
+ OpenStruct.new(name: "Colorado", value: 6),
9
+ OpenStruct.new(name: "Connecticut", value: 7),
10
+ OpenStruct.new(name: "Delaware", value: 8),
11
+ OpenStruct.new(name: "Florida", value: 9),
12
+ OpenStruct.new(name: "Georgia", value: 10),
13
+ ]
14
+ %>
15
+
16
+
17
+ <%
18
+ example_typeahead_options = [
19
+ { label: 'Orange', value: '#FFA500' },
20
+ { label: 'Red', value: '#FF0000' },
21
+ { label: 'Green', value: '#00FF00' },
22
+ { label: 'Blue', value: '#0000FF' },
23
+ ]
24
+ %>
25
+
26
+
27
+ <%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
28
+ <%= form.text_field :example_text_field_validation_msg, props: { label: "Text Field With Validation Message", required: true, validation: { message: "I am a custom validation message for text field." } } %>
29
+ <%= form.typeahead :example_typeahead_validation_msg, props: { data: { typeahead_example_msg: true, user: {} }, label: "Typeahead With Validation Message", placeholder: "Search for a user", required: true, validation: { message: "I am a custom validation message for typeahead." } } %>
30
+ <%= form.typeahead :example_typeahead_validation_react_msg, props: { options: example_typeahead_options, pills: true, label: "Typeahead With Validation Message (React Rendered)", placeholder: "Search for a color", required: true, validation: { message: "I am a custom validation message for React typeahead." } } %>
31
+ <%= form.select :example_select_validation_msg, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true, validation_message: "I am a custom validation message for select." } %>
32
+ <%= form.collection_select :example_collection_select_validation_msg, example_collection, :value, :name, props: { label: "Collection Select With Validation Message", blank_selection: "Select a State...", required: true, validation_message: "I am a custom validation message for collection select." } %>
33
+ <%= form.date_picker :example_date_picker_validation_msg, props: { label: "Date Picker With Validation Message", required: true, validation_message: "I am a custom validation message for date picker.", allow_input: true } %>
34
+ <%= form.time_picker :example_time_picker_validation_msg, props: { label: "Time Picker With Validation Message", required: true, validation_message: "I am a custom validation message for time picker." } %>
35
+
36
+ <%= form.actions do |action| %>
37
+ <%= action.submit %>
38
+ <%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
39
+ <% end %>
40
+ <% end %>
41
+
42
+ <!-- form.typeahead user results example template -->
43
+ <template data-typeahead-example-result-option>
44
+ <%= pb_rails("user", props: {
45
+ name: tag(:slot, name: "name"),
46
+ orientation: "horizontal",
47
+ align: "left",
48
+ avatar_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApocMXEAAAAASUVORK5CYII=",
49
+ avatar: true
50
+ }) %>
51
+ </template>
52
+
53
+ <!-- form.typeahead JS example implementation -->
54
+ <%= javascript_tag defer: "defer" do %>
55
+ document.addEventListener("pb-typeahead-kit-search", function(event) {
56
+ if (!event.target.dataset || !event.target.dataset.typeaheadExampleMsg) return
57
+
58
+ fetch(`https://api.github.com/search/users?q=${encodeURIComponent(event.detail.searchingFor)}`)
59
+ .then(response => response.json())
60
+ .then((result) => {
61
+ const resultOptionTemplate = document.querySelector("[data-typeahead-example-result-option]")
62
+
63
+ event.detail.setResults((result.items || []).map((user) => {
64
+ const wrapper = resultOptionTemplate.content.cloneNode(true)
65
+ wrapper.children[0].dataset.user = JSON.stringify(user)
66
+ wrapper.querySelector('slot[name="name"]').replaceWith(user.login)
67
+ wrapper.querySelector('img').dataset.src = user.avatar_url
68
+ return wrapper
69
+ }))
70
+ })
71
+ })
72
+
73
+
74
+ document.addEventListener("pb-typeahead-kit-result-option-selected", function(event) {
75
+ if (!event.target.dataset.typeaheadExampleMsg) return
76
+
77
+ const selectedUserJSON = event.detail.selected.firstElementChild.dataset.user
78
+ const selectedUserData = JSON.parse(selectedUserJSON)
79
+
80
+ // set the input field's value
81
+ event.target.querySelector('input[name=example_typeahead_validation_msg]').value = selectedUserData.login
82
+
83
+ // log the selected option's dataset
84
+ console.log('The selected user data:')
85
+ console.dir(selectedUserData)
86
+
87
+ // do even more with the data later - TBD
88
+ event.target.dataset.user = selectedUserJSON
89
+ })
90
+ <% end %>
@@ -0,0 +1,13 @@
1
+ Custom validation messages allow you to override the browser's default validation text with your own messaging. This provides a better user experience by giving specific, actionable feedback.
2
+
3
+ **Text-based inputs** (TextInput, Typeahead) use the `validation` prop with a `message` key:
4
+ ```ruby
5
+ validation: { message: "Please enter a valid email address." }
6
+ ```
7
+
8
+ **Selection-based inputs** (Select, DatePicker, TimePicker) use the `validation_message` prop:
9
+ ```ruby
10
+ validation_message: "Please select an option."
11
+ ```
12
+
13
+ When a required field is left empty or fails validation, your custom message will display instead of the generic browser default.
@@ -3,5 +3,6 @@ examples:
3
3
  rails:
4
4
  - form_form_with: Default
5
5
  - form_form_with_validate: Default + Validation
6
+ - form_form_with_validation_msg: Validation + Custom Validation Message
6
7
  - form_form_with_loading: Default + Loading
7
8
  - form_with_required_indicator: With Optional Required Indicator
@@ -121,4 +121,6 @@ class PbFormValidation extends PbEnhancedElement {
121
121
  }
122
122
  }
123
123
 
124
- window.PbFormValidation = PbFormValidation
124
+ window.PbFormValidation = PbFormValidation
125
+
126
+ export default PbFormValidation
@@ -134,61 +134,15 @@
134
134
  height: $space_xl;
135
135
  }
136
136
 
137
- // Match React ToolbarDropdown: secondary trigger flattened to text-style control.
137
+ // React ToolbarDropdown match master (fixed width trigger; prod playbook.cloud).
138
138
  .editor-dropdown-button {
139
139
  background: transparent;
140
140
  border: none;
141
141
  color: $text_lt_light;
142
142
  cursor: pointer;
143
143
  font-weight: $light;
144
- letter-spacing: normal;
145
- line-height: 1;
146
- min-height: unset;
147
- max-width: 100%;
148
- // Plain length only — SassC cannot compile CSS min(%, px). Narrow modals rely on kit/root min-width: 0 above.
149
- min-width: $space_xl * 5;
150
- padding: ($space_xs - 1) $space_xs;
151
- width: auto;
152
-
153
- // Undo Playbook .pb_button_kit defaults that throw off icon vs label (line-height 1.5, min-height 40px).
154
- .pb_button_content {
155
- align-items: center;
156
- display: inline-flex;
157
- line-height: 1;
158
- }
159
-
160
- // React: single Flex row inside the button.
161
- .pb_button_content > .pb_flex_kit {
162
- align-items: center;
163
- }
164
-
165
- // Rails: block-style trigger row (spans + icons).
166
- .rte-block-style-trigger-inner {
167
- align-items: center;
168
- }
169
-
170
- .rte-block-style-trigger-icon,
171
- .rte-block-style-chevron {
172
- display: inline-flex;
173
- flex-shrink: 0;
174
- line-height: 0;
175
-
176
- .pb_icon_kit {
177
- align-items: center;
178
- display: flex;
179
- line-height: 0;
180
- }
181
-
182
- svg {
183
- display: block;
184
- }
185
- }
186
-
187
- .rte-block-style-trigger-label {
188
- align-items: center;
189
- display: inline-flex;
190
- line-height: 1.2;
191
- }
144
+ padding: ($space_xs - 1) 0px;
145
+ width: $space_xl * 3;
192
146
 
193
147
  &:focus-visible {
194
148
  box-shadow: unset;
@@ -243,23 +197,71 @@
243
197
  align-items: center;
244
198
  display: inline-flex;
245
199
  }
200
+
201
+ // Override master’s fixed-width React trigger: Rails block-style label + icons needs flexible width.
202
+ .editor-dropdown-button {
203
+ justify-content: flex-start;
204
+ letter-spacing: normal;
205
+ line-height: 1;
206
+ max-width: 100%;
207
+ min-height: unset;
208
+ padding: ($space_xs - 1) $space_xs;
209
+ text-align: left;
210
+ width: auto;
211
+
212
+ .pb_button_content {
213
+ align-items: center;
214
+ display: inline-flex;
215
+ line-height: 1;
216
+ }
217
+
218
+ .pb_button_content > .pb_flex_kit {
219
+ align-items: center;
220
+ }
221
+
222
+ .rte-block-style-trigger-inner {
223
+ align-items: center;
224
+ }
225
+
226
+ .rte-block-style-trigger-icon,
227
+ .rte-block-style-chevron {
228
+ display: inline-flex;
229
+ flex-shrink: 0;
230
+ line-height: 0;
231
+
232
+ .pb_icon_kit {
233
+ align-items: center;
234
+ display: flex;
235
+ line-height: 0;
236
+ }
237
+
238
+ svg {
239
+ display: block;
240
+ }
241
+ }
242
+
243
+ .rte-block-style-trigger-label {
244
+ align-items: center;
245
+ display: inline-flex;
246
+ line-height: 1.2;
247
+ }
248
+
249
+ &:focus-visible {
250
+ box-shadow: unset;
251
+ }
252
+ }
246
253
  }
247
254
  }
248
255
 
256
+ // TipTap content — match master (prod playbook React tab).
249
257
  .ProseMirror {
250
258
  background: $white;
251
259
  border: 1px solid $input_border_default;
252
260
  border-radius: $border_rad_heaviest;
253
- box-sizing: border-box;
254
261
  height: 100%;
255
262
  line-height: $lh_loose;
256
- overflow-wrap: anywhere;
257
- padding: 1.25rem 1.5rem 1.5rem 1.5rem;
258
- word-break: break-word;
263
+ padding: 1rem 1.5rem 1.5rem 1.5rem;
259
264
  @include transition_default;
260
- :first-child {
261
- margin-top: 0;
262
- }
263
265
 
264
266
  h4,
265
267
  h5,
@@ -311,6 +313,11 @@
311
313
  ul {
312
314
  @include preview_tiptap_ul;
313
315
  }
316
+
317
+ // After heading mixins: first block should not pick up extra top margin (wins over `p { margin-top: 1rem }`).
318
+ > :first-child {
319
+ margin-top: 0;
320
+ }
314
321
  }
315
322
 
316
323
  // Toolbar + editor stack: toolbar keeps its border; editor has no top stroke (classic layout).
@@ -325,6 +332,13 @@
325
332
  }
326
333
  }
327
334
 
335
+ // Rails-only: outer kit sets `data-pb-rte-tiptap` — roomier padding + wrapping for vanilla TipTap in forms/dialogs.
336
+ [data-pb-rte-tiptap="true"] .pb_rich_text_editor_kit .ProseMirror {
337
+ overflow-wrap: anywhere;
338
+ padding: 1.25rem 1.5rem 1.5rem 1.5rem;
339
+ word-break: break-word;
340
+ }
341
+
328
342
  .pb_tiptap_toolbar_dropdown_list_item {
329
343
  &.is-active,
330
344
  &:active {
@@ -43,9 +43,16 @@ module Playbook
43
43
  multiple: multiple,
44
44
  onchange: onchange,
45
45
  include_blank: include_blank,
46
+ data: validation_data,
46
47
  }.merge(attributes).merge(input_options)
47
48
  end
48
49
 
50
+ def validation_data
51
+ fields = input_options[:data] || {}
52
+ fields[:message] = validation_message unless validation_message.blank?
53
+ fields
54
+ end
55
+
49
56
  # Same resolved id as the native +<select>+ (+all_attributes[:id]+) for label +for+.
50
57
  def select_input_id
51
58
  all_attributes[:id].presence