playbook_ui 16.1.0.pre.rc.2 → 16.1.0.pre.rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.jsx +30 -0
  3. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.md +1 -0
  4. data/app/pb_kits/playbook/pb_background/docs/example.yml +1 -0
  5. data/app/pb_kits/playbook/pb_background/docs/index.js +1 -0
  6. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +3 -1
  7. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.html.erb +74 -0
  8. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.jsx +87 -0
  9. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.md +3 -0
  10. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +35 -33
  11. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  12. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +33 -6
  13. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +35 -0
  14. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.md +3 -0
  15. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.html.erb +10 -0
  16. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +21 -0
  17. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.md +3 -0
  18. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +3 -0
  19. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
  20. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +5 -0
  21. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +33 -18
  22. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +29 -11
  23. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.html.erb +5 -0
  24. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.jsx +25 -0
  25. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.md +3 -0
  26. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +3 -1
  27. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  28. data/app/pb_kits/playbook/pb_textarea/index.ts +12 -5
  29. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +6 -0
  30. data/app/pb_kits/playbook/pb_textarea/textarea.rb +2 -0
  31. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +18 -1
  32. data/app/pb_kits/playbook/utilities/test/globalProps/globalProps.integration.test.js +936 -0
  33. data/dist/chunks/{_pb_line_graph-hxi01lk7.js → _pb_line_graph-BgKF_zz1.js} +1 -1
  34. data/dist/chunks/{_typeahead-BgLnlhzP.js → _typeahead-B9a6ZsEP.js} +1 -1
  35. data/dist/chunks/{globalProps-DgYwLYNx.js → globalProps-BhVYCqRf.js} +1 -1
  36. data/dist/chunks/{lib-NLxTo8OB.js → lib-DD34ZrWL.js} +1 -1
  37. data/dist/chunks/vendor.js +2 -2
  38. data/dist/playbook-rails-react-bindings.js +1 -1
  39. data/dist/playbook-rails.js +1 -1
  40. data/lib/playbook/version.rb +1 -1
  41. metadata +20 -6
@@ -0,0 +1,936 @@
1
+ import React from 'react'
2
+ import { render, screen } from '../../test-utils'
3
+ import Body from '../../../pb_body/_body'
4
+ import Button from '../../../pb_button/_button'
5
+ import Card from '../../../pb_card/_card'
6
+ import Flex from '../../../pb_flex/_flex'
7
+ import TextInput from '../../../pb_text_input/_text_input'
8
+ import Title from '../../../pb_title/_title'
9
+ import Link from '../../../pb_link/_link'
10
+ import Badge from '../../../pb_badge/_badge'
11
+
12
+ describe('Global Props Integration Tests', () => {
13
+ describe('All global props at once (comprehensive showcase)', () => {
14
+ test('All global props generate correct classes when used together', () => {
15
+ render(
16
+ <Body
17
+ alignContent="center"
18
+ alignItems="center"
19
+ alignSelf="center"
20
+ borderRadius="md"
21
+ cursor="pointer"
22
+ data={{ testid: 'all-props' }}
23
+ display="flex"
24
+ flex={1}
25
+ flexDirection="column"
26
+ flexGrow={1}
27
+ flexShrink={0}
28
+ flexWrap="wrap"
29
+ gap="md"
30
+ height="auto"
31
+ justifyContent="spaceBetween"
32
+ lineHeight="loose"
33
+ margin="lg"
34
+ maxHeight="xl"
35
+ maxWidth="lg"
36
+ minHeight="xs"
37
+ minWidth="sm"
38
+ order={1}
39
+ overflow="hidden"
40
+ padding="md"
41
+ position="relative"
42
+ shadow="deep"
43
+ text="Test"
44
+ textAlign="center"
45
+ truncate={1}
46
+ verticalAlign="middle"
47
+ width="100%"
48
+ zIndex={5}
49
+ />
50
+ )
51
+ const body = screen.getByTestId('all-props')
52
+
53
+ // Verify all global prop classes are present
54
+ expect(body).toHaveClass('align_content_center')
55
+ expect(body).toHaveClass('align_items_center')
56
+ expect(body).toHaveClass('align_self_center')
57
+ expect(body).toHaveClass('border_radius_md')
58
+ expect(body).toHaveClass('cursor_pointer')
59
+ expect(body).toHaveClass('display_flex')
60
+ expect(body).toHaveClass('flex_1')
61
+ expect(body).toHaveClass('flex_direction_column')
62
+ expect(body).toHaveClass('flex_grow_1')
63
+ expect(body).toHaveClass('flex_shrink_0')
64
+ expect(body).toHaveClass('flex_wrap_wrap')
65
+ expect(body).toHaveClass('gap_md')
66
+ expect(body).toHaveClass('height_auto')
67
+ expect(body).toHaveClass('justify_content_space_between')
68
+ expect(body).toHaveClass('line_height_loose')
69
+ expect(body).toHaveClass('m_lg')
70
+ expect(body).toHaveClass('max_height_xl')
71
+ expect(body).toHaveClass('max_width_lg')
72
+ expect(body).toHaveClass('min_height_xs')
73
+ expect(body).toHaveClass('min_width_sm')
74
+ expect(body).toHaveClass('flex_order_1')
75
+ expect(body).toHaveClass('overflow_hidden')
76
+ expect(body).toHaveClass('p_md')
77
+ expect(body).toHaveClass('position_relative')
78
+ expect(body).toHaveClass('shadow_deep')
79
+ expect(body).toHaveClass('text_align_center')
80
+ expect(body).toHaveClass('truncate_1')
81
+ expect(body).toHaveClass('vertical_align_middle')
82
+ expect(body).toHaveClass('width_100_percent')
83
+ expect(body).toHaveClass('z_index_5')
84
+ })
85
+ })
86
+
87
+ describe('Global props + other props on the same kit', () => {
88
+ test('Button: global props work with variant, size, and fullWidth', () => {
89
+ render(
90
+ <Button
91
+ data={{ testid: 'button-integration-1' }}
92
+ fullWidth
93
+ margin="md"
94
+ padding="lg"
95
+ size="lg"
96
+ textAlign="center"
97
+ variant="secondary"
98
+ />
99
+ )
100
+ const button = screen.getByTestId('button-integration-1')
101
+
102
+ // Kit-specific classes
103
+ expect(button).toHaveClass('pb_button_kit')
104
+ expect(button).toHaveClass('pb_button_secondary')
105
+ expect(button).toHaveClass('pb_button_block')
106
+ expect(button).toHaveClass('pb_button_size_lg')
107
+
108
+ // Global prop classes
109
+ expect(button).toHaveClass('m_md')
110
+ expect(button).toHaveClass('p_lg')
111
+ expect(button).toHaveClass('text_align_center')
112
+ })
113
+
114
+ test('Title: global props work with size prop', () => {
115
+ render(
116
+ <Title
117
+ data={{ testid: 'title-integration-1' }}
118
+ margin="sm"
119
+ padding="xs"
120
+ size={3}
121
+ text="Test Title"
122
+ textAlign="right"
123
+ />
124
+ )
125
+ const title = screen.getByTestId('title-integration-1')
126
+
127
+ // Kit-specific classes
128
+ expect(title).toHaveClass('pb_title_kit')
129
+ expect(title).toHaveClass('pb_title_3')
130
+
131
+ // Global prop classes
132
+ expect(title).toHaveClass('m_sm')
133
+ expect(title).toHaveClass('p_xs')
134
+ expect(title).toHaveClass('text_align_right')
135
+ })
136
+
137
+ test('Card: global props work with borderRadius and padding props', () => {
138
+ render(
139
+ <Card
140
+ borderRadius="lg"
141
+ data={{ testid: 'card-integration-1' }}
142
+ margin="xl"
143
+ padding="md"
144
+ textAlign="center"
145
+ >
146
+ {"Card content"}
147
+ </Card>
148
+ )
149
+ const card = screen.getByTestId('card-integration-1')
150
+
151
+ // Kit-specific classes
152
+ expect(card).toHaveClass('pb_card_kit')
153
+ expect(card).toHaveClass('border_radius_lg')
154
+
155
+ // Global prop classes
156
+ expect(card).toHaveClass('m_xl')
157
+ expect(card).toHaveClass('p_md')
158
+ expect(card).toHaveClass('text_align_center')
159
+ })
160
+
161
+ test('Flex: global props work with orientation, justify, and align props', () => {
162
+ render(
163
+ <Flex
164
+ align="center"
165
+ data={{ testid: 'flex-integration-1' }}
166
+ justify="spaceBetween"
167
+ margin="lg"
168
+ orientation="column"
169
+ padding="sm"
170
+ >
171
+ {"Flex content"}
172
+ </Flex>
173
+ )
174
+ const flex = screen.getByTestId('flex-integration-1')
175
+
176
+ // Kit-specific classes
177
+ expect(flex).toHaveClass('pb_flex_kit')
178
+ expect(flex).toHaveClass('pb_flex_kit_orientation_column')
179
+ expect(flex).toHaveClass('pb_flex_kit_justify_content_spaceBetween')
180
+ expect(flex).toHaveClass('pb_flex_kit_align_items_center')
181
+
182
+ // Global prop classes
183
+ expect(flex).toHaveClass('m_lg')
184
+ expect(flex).toHaveClass('p_sm')
185
+ })
186
+
187
+ test('Link: global props work with link prop', () => {
188
+ render(
189
+ <Link
190
+ data={{ testid: 'link-integration-1' }}
191
+ link="https://example.com"
192
+ margin="xs"
193
+ text="Test Link"
194
+ textAlign="center"
195
+ />
196
+ )
197
+ const link = screen.getByTestId('link-integration-1')
198
+
199
+ // Kit-specific classes
200
+ expect(link).toHaveClass('pb_link_kit')
201
+
202
+ // Global prop classes
203
+ expect(link).toHaveClass('m_xs')
204
+ expect(link).toHaveClass('text_align_center')
205
+ })
206
+
207
+ test('Badge: global props work with variant prop', () => {
208
+ render(
209
+ <Badge
210
+ data={{ testid: 'badge-integration-1' }}
211
+ margin="md"
212
+ text="Badge"
213
+ variant="success"
214
+ />
215
+ )
216
+ const badge = screen.getByTestId('badge-integration-1')
217
+
218
+ // Kit-specific classes (Badge includes size in classname)
219
+ expect(badge.className).toMatch(/pb_badge_kit_success/)
220
+
221
+ // Global prop classes
222
+ expect(badge).toHaveClass('m_md')
223
+ })
224
+ })
225
+
226
+ describe('Global props + other global props on the same kit', () => {
227
+ test('Multiple spacing global props work together', () => {
228
+ render(
229
+ <Body
230
+ data={{ testid: 'body-multiple-spacing' }}
231
+ margin="md"
232
+ marginBottom="lg"
233
+ marginTop="sm"
234
+ padding="lg"
235
+ paddingLeft="xs"
236
+ text="Test"
237
+ />
238
+ )
239
+ const body = screen.getByTestId('body-multiple-spacing')
240
+
241
+ expect(body).toHaveClass('m_md')
242
+ expect(body).toHaveClass('mb_lg')
243
+ expect(body).toHaveClass('mt_sm')
244
+ expect(body).toHaveClass('p_lg')
245
+ expect(body).toHaveClass('pl_xs')
246
+ })
247
+
248
+ test('Spacing + layout + typography global props work together', () => {
249
+ render(
250
+ <Body
251
+ data={{ testid: 'body-layout-typography' }}
252
+ display="flex"
253
+ margin="md"
254
+ padding="sm"
255
+ text="Test"
256
+ textAlign="center"
257
+ />
258
+ )
259
+ const body = screen.getByTestId('body-layout-typography')
260
+
261
+ expect(body).toHaveClass('m_md')
262
+ expect(body).toHaveClass('p_sm')
263
+ expect(body).toHaveClass('display_flex')
264
+ expect(body).toHaveClass('text_align_center')
265
+ })
266
+
267
+ test('Position + spacing + z-index global props work together', () => {
268
+ render(
269
+ <Body
270
+ data={{ testid: 'body-position-zindex' }}
271
+ margin="lg"
272
+ position="absolute"
273
+ text="Test"
274
+ top="md"
275
+ zIndex={5}
276
+ />
277
+ )
278
+ const body = screen.getByTestId('body-position-zindex')
279
+
280
+ expect(body).toHaveClass('m_lg')
281
+ expect(body).toHaveClass('position_absolute')
282
+ expect(body).toHaveClass('top_md')
283
+ expect(body).toHaveClass('z_index_5')
284
+ })
285
+
286
+ test('Flexbox + spacing + typography global props work together', () => {
287
+ render(
288
+ <Body
289
+ alignItems="center"
290
+ data={{ testid: 'body-flexbox-combo' }}
291
+ flexDirection="column"
292
+ gap="md"
293
+ justifyContent="spaceBetween"
294
+ margin="sm"
295
+ padding="lg"
296
+ text="Test"
297
+ textAlign="right"
298
+ />
299
+ )
300
+ const body = screen.getByTestId('body-flexbox-combo')
301
+
302
+ expect(body).toHaveClass('m_sm')
303
+ expect(body).toHaveClass('p_lg')
304
+ expect(body).toHaveClass('flex_direction_column')
305
+ expect(body).toHaveClass('justify_content_space_between')
306
+ expect(body).toHaveClass('align_items_center')
307
+ expect(body).toHaveClass('gap_md')
308
+ expect(body).toHaveClass('text_align_right')
309
+ })
310
+
311
+ test('All major global prop categories work together', () => {
312
+ render(
313
+ <Body
314
+ borderRadius="md"
315
+ cursor="pointer"
316
+ data={{ testid: 'body-all-categories' }}
317
+ display="flex"
318
+ margin="lg"
319
+ padding="md"
320
+ position="relative"
321
+ shadow="lg"
322
+ text="Test"
323
+ textAlign="center"
324
+ zIndex={3}
325
+ />
326
+ )
327
+ const body = screen.getByTestId('body-all-categories')
328
+
329
+ // Spacing
330
+ expect(body).toHaveClass('m_lg')
331
+ expect(body).toHaveClass('p_md')
332
+
333
+ // Layout
334
+ expect(body).toHaveClass('display_flex')
335
+ expect(body).toHaveClass('position_relative')
336
+
337
+ // Visual
338
+ expect(body).toHaveClass('border_radius_md')
339
+ expect(body).toHaveClass('shadow_lg')
340
+ expect(body).toHaveClass('cursor_pointer')
341
+
342
+ // Typography
343
+ expect(body).toHaveClass('text_align_center')
344
+
345
+ // Z-index
346
+ expect(body).toHaveClass('z_index_3')
347
+ })
348
+ })
349
+
350
+ describe('Global props in nested layouts / clickable wrappers', () => {
351
+ test('Button inside Card: both have global props', () => {
352
+ render(
353
+ <Card
354
+ data={{ testid: 'card-nested' }}
355
+ margin="lg"
356
+ padding="md"
357
+ >
358
+ <Button
359
+ data={{ testid: 'button-nested' }}
360
+ margin="sm"
361
+ text="Click me"
362
+ variant="primary"
363
+ />
364
+ </Card>
365
+ )
366
+
367
+ const card = screen.getByTestId('card-nested')
368
+ const button = screen.getByTestId('button-nested')
369
+
370
+ // Card global props
371
+ expect(card).toHaveClass('m_lg')
372
+ expect(card).toHaveClass('p_md')
373
+
374
+ // Button global props
375
+ expect(button).toHaveClass('m_sm')
376
+
377
+ // Button kit-specific props
378
+ expect(button).toHaveClass('pb_button_kit')
379
+ expect(button).toHaveClass('pb_button_primary')
380
+ })
381
+
382
+ test('Multiple Body elements in Flex: each has different global props', () => {
383
+ render(
384
+ <Flex
385
+ data={{ testid: 'flex-nested' }}
386
+ gap="md"
387
+ margin="lg"
388
+ >
389
+ <Body
390
+ data={{ testid: 'body-nested-1' }}
391
+ margin="xs"
392
+ text="First"
393
+ textAlign="left"
394
+ />
395
+ <Body
396
+ data={{ testid: 'body-nested-2' }}
397
+ margin="xs"
398
+ text="Second"
399
+ textAlign="center"
400
+ />
401
+ <Body
402
+ data={{ testid: 'body-nested-3' }}
403
+ margin="xs"
404
+ text="Third"
405
+ textAlign="right"
406
+ />
407
+ </Flex>
408
+ )
409
+
410
+ const flex = screen.getByTestId('flex-nested')
411
+ const body1 = screen.getByTestId('body-nested-1')
412
+ const body2 = screen.getByTestId('body-nested-2')
413
+ const body3 = screen.getByTestId('body-nested-3')
414
+
415
+ // Flex global props
416
+ expect(flex).toHaveClass('m_lg')
417
+ expect(flex).toHaveClass('gap_md')
418
+
419
+ // Each Body has its own global props
420
+ expect(body1).toHaveClass('m_xs')
421
+ expect(body1).toHaveClass('text_align_left')
422
+
423
+ expect(body2).toHaveClass('m_xs')
424
+ expect(body2).toHaveClass('text_align_center')
425
+
426
+ expect(body3).toHaveClass('m_xs')
427
+ expect(body3).toHaveClass('text_align_right')
428
+ })
429
+
430
+ test('Deep nesting: Card > Flex > Button with global props', () => {
431
+ render(
432
+ <Card
433
+ data={{ testid: 'card-deep' }}
434
+ margin="xl"
435
+ padding="lg"
436
+ >
437
+ <Flex
438
+ data={{ testid: 'flex-deep' }}
439
+ gap="sm"
440
+ justify="center"
441
+ >
442
+ <Button
443
+ data={{ testid: 'button-deep' }}
444
+ margin="xs"
445
+ text="Nested Button"
446
+ variant="secondary"
447
+ />
448
+ </Flex>
449
+ </Card>
450
+ )
451
+
452
+ const card = screen.getByTestId('card-deep')
453
+ const flex = screen.getByTestId('flex-deep')
454
+ const button = screen.getByTestId('button-deep')
455
+
456
+ // Card global props
457
+ expect(card).toHaveClass('m_xl')
458
+ expect(card).toHaveClass('p_lg')
459
+
460
+ // Flex global props
461
+ expect(flex).toHaveClass('gap_sm')
462
+ expect(flex).toHaveClass('pb_flex_kit_justify_content_center')
463
+
464
+ // Button global props
465
+ expect(button).toHaveClass('m_xs')
466
+ })
467
+
468
+ test('Link wrapper with global props containing Body with global props', () => {
469
+ render(
470
+ <Link
471
+ data={{ testid: 'link-wrapper' }}
472
+ link="https://example.com"
473
+ margin="md"
474
+ textAlign="center"
475
+ >
476
+ <Body
477
+ data={{ testid: 'body-in-link' }}
478
+ margin="xs"
479
+ text="Link content"
480
+ textAlign="left"
481
+ />
482
+ </Link>
483
+ )
484
+
485
+ const link = screen.getByTestId('link-wrapper')
486
+ const body = screen.getByTestId('body-in-link')
487
+
488
+ // Link global props
489
+ expect(link).toHaveClass('m_md')
490
+ expect(link).toHaveClass('text_align_center')
491
+
492
+ // Body global props (should still work even though inside Link)
493
+ expect(body).toHaveClass('m_xs')
494
+ expect(body).toHaveClass('text_align_left')
495
+ })
496
+
497
+ test('Button with onClick and global props in Card with global props', () => {
498
+ const handleClick = jest.fn()
499
+
500
+ render(
501
+ <Card
502
+ data={{ testid: 'card-clickable' }}
503
+ margin="lg"
504
+ onClick={handleClick}
505
+ padding="md"
506
+ >
507
+ <Button
508
+ data={{ testid: 'button-clickable' }}
509
+ margin="sm"
510
+ onClick={handleClick}
511
+ text="Click me"
512
+ variant="primary"
513
+ />
514
+ </Card>
515
+ )
516
+
517
+ const card = screen.getByTestId('card-clickable')
518
+ const button = screen.getByTestId('button-clickable')
519
+
520
+ // Both should have their global props
521
+ expect(card).toHaveClass('m_lg')
522
+ expect(card).toHaveClass('p_md')
523
+ expect(button).toHaveClass('m_sm')
524
+
525
+ // Both should be clickable
526
+ button.click()
527
+ expect(handleClick).toHaveBeenCalledTimes(1)
528
+ })
529
+
530
+ test('Responsive global props in nested layout', () => {
531
+ render(
532
+ <Card
533
+ data={{ testid: 'card-responsive' }}
534
+ margin={{ default: 'md', sm: 'lg', md: 'xl' }}
535
+ padding="md"
536
+ >
537
+ <Flex
538
+ data={{ testid: 'flex-responsive' }}
539
+ gap={{ default: 'sm', md: 'lg' }}
540
+ >
541
+ <Body
542
+ data={{ testid: 'body-responsive' }}
543
+ margin="xs"
544
+ text="Content"
545
+ textAlign={{ default: 'left', sm: 'center', md: 'right' }}
546
+ />
547
+ </Flex>
548
+ </Card>
549
+ )
550
+
551
+ const card = screen.getByTestId('card-responsive')
552
+ const flex = screen.getByTestId('flex-responsive')
553
+ const body = screen.getByTestId('body-responsive')
554
+
555
+ // Card responsive margin (uses break_on format)
556
+ expect(card).toHaveClass('m_md')
557
+ expect(card.className).toMatch(/break_on_sm:m_lg/)
558
+ expect(card.className).toMatch(/break_on_md:m_xl/)
559
+
560
+ // Flex responsive gap
561
+ expect(flex).toHaveClass('gap_sm')
562
+ expect(flex).toHaveClass('gap_md_lg')
563
+
564
+ // Body responsive textAlign
565
+ expect(body).toHaveClass('text_align_left')
566
+ expect(body).toHaveClass('text_align_sm_center')
567
+ expect(body).toHaveClass('text_align_md_right')
568
+ })
569
+ })
570
+
571
+ describe('Prop precedence and conflicts', () => {
572
+ test('className prop overrides global prop classes', () => {
573
+ render(
574
+ <Body
575
+ className="custom-class override-margin"
576
+ data={{ testid: 'precedence-1' }}
577
+ margin="lg"
578
+ text="Test"
579
+ />
580
+ )
581
+ const body = screen.getByTestId('precedence-1')
582
+
583
+ // Should have global prop class
584
+ expect(body).toHaveClass('m_lg')
585
+ // Should have custom className
586
+ expect(body).toHaveClass('custom-class')
587
+ expect(body).toHaveClass('override-margin')
588
+ })
589
+
590
+ test('htmlOptions overrides aria and data props', () => {
591
+ render(
592
+ <Body
593
+ aria={{ label: 'Aria label' }}
594
+ data={{ testid: 'precedence-2' }}
595
+ htmlOptions={{
596
+ 'aria-label': 'HTML options label',
597
+ 'data-testid': 'html-testid'
598
+ }}
599
+ text="Test"
600
+ />
601
+ )
602
+ const body = screen.getByTestId('html-testid')
603
+
604
+ // htmlOptions should override aria prop
605
+ expect(body).toHaveAttribute('aria-label', 'HTML options label')
606
+ // htmlOptions should override data prop
607
+ expect(body).toHaveAttribute('data-testid', 'html-testid')
608
+ })
609
+
610
+ test('Multiple conflicting spacing props work together', () => {
611
+ render(
612
+ <Body
613
+ data={{ testid: 'precedence-3' }}
614
+ margin="md"
615
+ marginBottom="lg"
616
+ marginTop="sm"
617
+ text="Test"
618
+ />
619
+ )
620
+ const body = screen.getByTestId('precedence-3')
621
+
622
+ // All should be present
623
+ expect(body).toHaveClass('m_md')
624
+ expect(body).toHaveClass('mb_lg')
625
+ expect(body).toHaveClass('mt_sm')
626
+ })
627
+
628
+ test('Global props do not interfere with kit-specific props', () => {
629
+ render(
630
+ <Button
631
+ data={{ testid: 'precedence-4' }}
632
+ margin="md"
633
+ size="lg"
634
+ text="Click me"
635
+ variant="secondary"
636
+ />
637
+ )
638
+ const button = screen.getByTestId('precedence-4')
639
+
640
+ // Kit-specific classes should still be present
641
+ expect(button).toHaveClass('pb_button_kit')
642
+ expect(button).toHaveClass('pb_button_secondary')
643
+ expect(button).toHaveClass('pb_button_size_lg')
644
+ // Global prop classes should also be present
645
+ expect(button).toHaveClass('m_md')
646
+ })
647
+
648
+ test('id prop takes precedence over htmlOptions.id', () => {
649
+ render(
650
+ <Body
651
+ data={{ testid: 'precedence-5' }}
652
+ htmlOptions={{ id: 'html-id' }}
653
+ id="prop-id"
654
+ text="Test"
655
+ />
656
+ )
657
+ const body = screen.getByTestId('precedence-5')
658
+
659
+ // id prop should win
660
+ expect(body).toHaveAttribute('id', 'prop-id')
661
+ expect(body).not.toHaveAttribute('id', 'html-id')
662
+ })
663
+ })
664
+
665
+ describe('Accessibility integration', () => {
666
+ test('Global props work with aria props', () => {
667
+ render(
668
+ <Body
669
+ aria={{
670
+ label: 'Test label',
671
+ describedby: 'desc-id',
672
+ hidden: 'false'
673
+ }}
674
+ data={{ testid: 'a11y-1' }}
675
+ margin="md"
676
+ text="Test"
677
+ textAlign="center"
678
+ />
679
+ )
680
+ const body = screen.getByTestId('a11y-1')
681
+
682
+ // Aria attributes should be present
683
+ expect(body).toHaveAttribute('aria-label', 'Test label')
684
+ expect(body).toHaveAttribute('aria-describedby', 'desc-id')
685
+ expect(body).toHaveAttribute('aria-hidden', 'false')
686
+ // Global props should still work
687
+ expect(body).toHaveClass('m_md')
688
+ expect(body).toHaveClass('text_align_center')
689
+ })
690
+
691
+ test('Global props work with data props', () => {
692
+ render(
693
+ <Body
694
+ data={{
695
+ testid: 'a11y-2',
696
+ 'custom-attr': 'custom-value',
697
+ 'analytics-id': 'analytics-123'
698
+ }}
699
+ margin="sm"
700
+ padding="md"
701
+ text="Test"
702
+ />
703
+ )
704
+ const body = screen.getByTestId('a11y-2')
705
+
706
+ // Data attributes should be present
707
+ expect(body).toHaveAttribute('data-custom-attr', 'custom-value')
708
+ expect(body).toHaveAttribute('data-analytics-id', 'analytics-123')
709
+ // Global props should still work
710
+ expect(body).toHaveClass('m_sm')
711
+ expect(body).toHaveClass('p_md')
712
+ })
713
+
714
+ test('htmlOptions aria attributes override aria prop', () => {
715
+ render(
716
+ <Body
717
+ aria={{ label: 'Aria prop label' }}
718
+ data={{ testid: 'a11y-3' }}
719
+ htmlOptions={{
720
+ 'aria-label': 'HTML options label',
721
+ 'aria-live': 'polite'
722
+ }}
723
+ text="Test"
724
+ />
725
+ )
726
+ const body = screen.getByTestId('a11y-3')
727
+
728
+ // htmlOptions should override aria prop
729
+ expect(body).toHaveAttribute('aria-label', 'HTML options label')
730
+ expect(body).toHaveAttribute('aria-live', 'polite')
731
+ })
732
+
733
+ test('Global props with role and tabIndex', () => {
734
+ render(
735
+ <Body
736
+ data={{ testid: 'a11y-4' }}
737
+ htmlOptions={{
738
+ role: 'button',
739
+ tabIndex: 0
740
+ }}
741
+ margin="lg"
742
+ text="Test"
743
+ />
744
+ )
745
+ const body = screen.getByTestId('a11y-4')
746
+
747
+ // Accessibility attributes should be present
748
+ expect(body).toHaveAttribute('role', 'button')
749
+ expect(body).toHaveAttribute('tabIndex', '0')
750
+ // Global props should still work
751
+ expect(body).toHaveClass('m_lg')
752
+ })
753
+
754
+ test('Screen reader compatibility with global props', () => {
755
+ render(
756
+ <Button
757
+ aria={{
758
+ label: 'Submit form',
759
+ describedby: 'submit-help'
760
+ }}
761
+ data={{ testid: 'a11y-5' }}
762
+ margin="md"
763
+ text="Submit"
764
+ />
765
+ )
766
+ const button = screen.getByTestId('a11y-5')
767
+
768
+ // Should be accessible
769
+ expect(button).toHaveAttribute('aria-label', 'Submit form')
770
+ expect(button).toHaveAttribute('aria-describedby', 'submit-help')
771
+ expect(button).toHaveAttribute('role', 'button')
772
+ // Global props should not interfere
773
+ expect(button).toHaveClass('m_md')
774
+ })
775
+ })
776
+
777
+ describe('Form elements', () => {
778
+ test('Global props work with TextInput', () => {
779
+ const handleChange = jest.fn()
780
+
781
+ render(
782
+ <TextInput
783
+ data={{ testid: 'form-1' }}
784
+ label="Test Input"
785
+ margin="md"
786
+ name="test-input"
787
+ onChange={handleChange}
788
+ padding="sm"
789
+ placeholder="Enter text"
790
+ textAlign="right"
791
+ type="text"
792
+ value=""
793
+ >
794
+ <input />
795
+ </TextInput>
796
+ )
797
+
798
+ const wrapper = screen.getByTestId('form-1')
799
+
800
+ // Global props should work on TextInput wrapper
801
+ expect(wrapper).toHaveClass('m_md')
802
+ expect(wrapper).toHaveClass('p_sm')
803
+ expect(wrapper).toHaveClass('text_align_right')
804
+ })
805
+
806
+ test('Global props with TextInput error state', () => {
807
+ const handleChange = jest.fn()
808
+
809
+ render(
810
+ <TextInput
811
+ data={{ testid: 'form-2' }}
812
+ error="This field is required"
813
+ label="Test Input"
814
+ margin="sm"
815
+ name="test-input"
816
+ onChange={handleChange}
817
+ placeholder="Enter text"
818
+ type="text"
819
+ value=""
820
+ >
821
+ <input />
822
+ </TextInput>
823
+ )
824
+
825
+ const wrapper = screen.getByTestId('form-2')
826
+
827
+ // Error state should work
828
+ expect(wrapper).toHaveClass('error')
829
+ // Global props should still work
830
+ expect(wrapper).toHaveClass('m_sm')
831
+ })
832
+
833
+ test('Global props with disabled TextInput', () => {
834
+ const handleChange = jest.fn()
835
+
836
+ render(
837
+ <TextInput
838
+ data={{ testid: 'form-3' }}
839
+ disabled
840
+ label="Test Input"
841
+ margin="lg"
842
+ name="test-input"
843
+ onChange={handleChange}
844
+ placeholder="Enter text"
845
+ type="text"
846
+ value=""
847
+ >
848
+ <input disabled />
849
+ </TextInput>
850
+ )
851
+
852
+ const wrapper = screen.getByTestId('form-3')
853
+ const input = wrapper.querySelector('input')
854
+
855
+ // Input should be disabled (check attribute directly)
856
+ expect(input).toHaveAttribute('disabled')
857
+ // Global props should still work
858
+ expect(wrapper).toHaveClass('m_lg')
859
+ })
860
+
861
+ test('Global props with required TextInput', () => {
862
+ const handleChange = jest.fn()
863
+
864
+ render(
865
+ <TextInput
866
+ data={{ testid: 'form-4' }}
867
+ label="Test Input"
868
+ margin="md"
869
+ name="test-input"
870
+ onChange={handleChange}
871
+ placeholder="Enter text"
872
+ required
873
+ requiredIndicator
874
+ type="text"
875
+ value=""
876
+ >
877
+ <input />
878
+ </TextInput>
879
+ )
880
+
881
+ const wrapper = screen.getByTestId('form-4')
882
+
883
+ // Global props should work
884
+ expect(wrapper).toHaveClass('m_md')
885
+ // Required indicator should be present if requiredIndicator is true
886
+ const requiredIndicator = screen.queryByText('*')
887
+ if (requiredIndicator) {
888
+ expect(requiredIndicator).toBeInTheDocument()
889
+ }
890
+ })
891
+ })
892
+
893
+ describe('Edge cases and boundary conditions', () => {
894
+ test('Empty responsive object', () => {
895
+ render(
896
+ <Body
897
+ data={{ testid: 'edge-2' }}
898
+ margin={{}}
899
+ text="Test"
900
+ />
901
+ )
902
+ const body = screen.getByTestId('edge-2')
903
+
904
+ // Should not crash with empty object
905
+ expect(body).toBeInTheDocument()
906
+ })
907
+
908
+ test('Only default value in responsive object', () => {
909
+ render(
910
+ <Body
911
+ data={{ testid: 'edge-3' }}
912
+ margin={{ default: 'md' }}
913
+ text="Test"
914
+ />
915
+ )
916
+ const body = screen.getByTestId('edge-3')
917
+
918
+ // Should have default class
919
+ expect(body).toHaveClass('m_md')
920
+ })
921
+
922
+ test('Only breakpoints, no default in responsive object', () => {
923
+ render(
924
+ <Body
925
+ data={{ testid: 'edge-4' }}
926
+ margin={{ sm: 'md', md: 'lg' }}
927
+ text="Test"
928
+ />
929
+ )
930
+ const body = screen.getByTestId('edge-4')
931
+
932
+ // Should handle breakpoints without default
933
+ expect(body.className).toBeTruthy()
934
+ })
935
+ })
936
+ })