@wordpress/components 28.0.3 → 28.2.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +133 -120
  2. package/build/custom-select-control-v2/custom-select.js +1 -3
  3. package/build/custom-select-control-v2/custom-select.js.map +1 -1
  4. package/build/custom-select-control-v2/legacy-component/index.js +16 -10
  5. package/build/custom-select-control-v2/legacy-component/index.js.map +1 -1
  6. package/build/custom-select-control-v2/styles.js +34 -19
  7. package/build/custom-select-control-v2/styles.js.map +1 -1
  8. package/build/custom-select-control-v2/types.js.map +1 -1
  9. package/build/drop-zone/index.js +16 -79
  10. package/build/drop-zone/index.js.map +1 -1
  11. package/build/radio-control/index.js +1 -1
  12. package/build/radio-control/index.js.map +1 -1
  13. package/build-module/custom-select-control-v2/custom-select.js +1 -3
  14. package/build-module/custom-select-control-v2/custom-select.js.map +1 -1
  15. package/build-module/custom-select-control-v2/legacy-component/index.js +16 -11
  16. package/build-module/custom-select-control-v2/legacy-component/index.js.map +1 -1
  17. package/build-module/custom-select-control-v2/styles.js +33 -18
  18. package/build-module/custom-select-control-v2/styles.js.map +1 -1
  19. package/build-module/custom-select-control-v2/types.js.map +1 -1
  20. package/build-module/drop-zone/index.js +17 -80
  21. package/build-module/drop-zone/index.js.map +1 -1
  22. package/build-module/radio-control/index.js +1 -1
  23. package/build-module/radio-control/index.js.map +1 -1
  24. package/build-style/style-rtl.css +30 -5
  25. package/build-style/style.css +30 -5
  26. package/build-types/checkbox-control/stories/index.story.d.ts.map +1 -1
  27. package/build-types/custom-select-control/stories/index.story.d.ts +15 -0
  28. package/build-types/custom-select-control/stories/index.story.d.ts.map +1 -1
  29. package/build-types/custom-select-control-v2/legacy-component/index.d.ts.map +1 -1
  30. package/build-types/custom-select-control-v2/styles.d.ts +4 -0
  31. package/build-types/custom-select-control-v2/styles.d.ts.map +1 -1
  32. package/build-types/custom-select-control-v2/types.d.ts +1 -0
  33. package/build-types/custom-select-control-v2/types.d.ts.map +1 -1
  34. package/build-types/drop-zone/index.d.ts +3 -0
  35. package/build-types/drop-zone/index.d.ts.map +1 -1
  36. package/package.json +19 -19
  37. package/src/checkbox-control/stories/index.story.tsx +3 -0
  38. package/src/checkbox-control/style.scss +4 -2
  39. package/src/custom-select-control/stories/index.story.tsx +32 -3
  40. package/src/custom-select-control/test/index.js +205 -22
  41. package/src/custom-select-control-v2/custom-select.tsx +1 -1
  42. package/src/custom-select-control-v2/legacy-component/index.tsx +18 -10
  43. package/src/custom-select-control-v2/legacy-component/test/index.tsx +220 -38
  44. package/src/custom-select-control-v2/styles.ts +22 -10
  45. package/src/custom-select-control-v2/types.ts +1 -0
  46. package/src/drop-zone/index.tsx +17 -76
  47. package/src/drop-zone/style.scss +51 -16
  48. package/src/radio-control/index.tsx +1 -1
  49. package/src/radio-control/style.scss +1 -1
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -14,7 +14,11 @@ import { useState } from '@wordpress/element';
14
14
  */
15
15
  import UncontrolledCustomSelectControl from '..';
16
16
 
17
- const customClass = 'amber-skies';
17
+ const customClassName = 'amber-skies';
18
+ const customStyles = {
19
+ backgroundColor: 'rgb(127, 255, 212)',
20
+ rotate: '13deg',
21
+ };
18
22
 
19
23
  const props = {
20
24
  label: 'label!',
@@ -26,7 +30,7 @@ const props = {
26
30
  {
27
31
  key: 'flower2',
28
32
  name: 'crimson clover',
29
- className: customClass,
33
+ className: customClassName,
30
34
  },
31
35
  {
32
36
  key: 'flower3',
@@ -35,37 +39,114 @@ const props = {
35
39
  {
36
40
  key: 'color1',
37
41
  name: 'amber',
38
- className: customClass,
42
+ className: customClassName,
39
43
  },
40
44
  {
41
45
  key: 'color2',
42
46
  name: 'aquamarine',
43
- style: {
44
- backgroundColor: 'rgb(127, 255, 212)',
45
- rotate: '13deg',
46
- },
47
+ style: customStyles,
48
+ },
49
+ {
50
+ key: 'color3',
51
+ name: 'tomato (with custom props)',
52
+ className: customClassName,
53
+ style: customStyles,
54
+ // try passing a valid HTML attribute
55
+ 'aria-label': 'test label',
56
+ // try adding a custom prop
57
+ customPropFoo: 'foo',
47
58
  },
48
59
  ],
49
60
  };
50
61
 
51
- const ControlledCustomSelectControl = ( { options, ...restProps } ) => {
52
- const [ value, setValue ] = useState( options[ 0 ] );
62
+ const ControlledCustomSelectControl = ( {
63
+ options,
64
+ onChange: onChangeProp,
65
+ ...restProps
66
+ } ) => {
67
+ const [ value, setValue ] = useState( restProps.value ?? options[ 0 ] );
68
+
69
+ const onChange = ( changeObject ) => {
70
+ onChangeProp?.( changeObject );
71
+ setValue( changeObject.selectedItem );
72
+ };
73
+
53
74
  return (
54
75
  <UncontrolledCustomSelectControl
55
76
  { ...restProps }
56
77
  options={ options }
57
- onChange={ ( { selectedItem } ) => setValue( selectedItem ) }
78
+ onChange={ onChange }
58
79
  value={ options.find( ( option ) => option.key === value.key ) }
59
80
  />
60
81
  );
61
82
  };
62
83
 
84
+ it( 'Should apply external controlled updates', async () => {
85
+ const mockOnChange = jest.fn();
86
+ const { rerender } = render(
87
+ <UncontrolledCustomSelectControl
88
+ { ...props }
89
+ value={ props.options[ 0 ] }
90
+ onChange={ mockOnChange }
91
+ />
92
+ );
93
+
94
+ const currentSelectedItem = screen.getByRole( 'button', {
95
+ expanded: false,
96
+ } );
97
+
98
+ expect( currentSelectedItem ).toHaveTextContent( props.options[ 0 ].name );
99
+
100
+ rerender(
101
+ <UncontrolledCustomSelectControl
102
+ { ...props }
103
+ value={ props.options[ 1 ] }
104
+ />
105
+ );
106
+
107
+ expect( currentSelectedItem ).toHaveTextContent( props.options[ 1 ].name );
108
+
109
+ expect( mockOnChange ).not.toHaveBeenCalled();
110
+ } );
111
+
63
112
  describe.each( [
64
- [ 'uncontrolled', UncontrolledCustomSelectControl ],
65
- [ 'controlled', ControlledCustomSelectControl ],
113
+ [ 'Uncontrolled', UncontrolledCustomSelectControl ],
114
+ [ 'Controlled', ControlledCustomSelectControl ],
66
115
  ] )( 'CustomSelectControl %s', ( ...modeAndComponent ) => {
67
116
  const [ , Component ] = modeAndComponent;
68
117
 
118
+ it( 'Should select the first option when no explicit initial value is passed without firing onChange', () => {
119
+ const mockOnChange = jest.fn();
120
+ render( <Component { ...props } onChange={ mockOnChange } /> );
121
+
122
+ expect(
123
+ screen.getByRole( 'button', {
124
+ expanded: false,
125
+ } )
126
+ ).toHaveTextContent( props.options[ 0 ].name );
127
+
128
+ expect( mockOnChange ).not.toHaveBeenCalled();
129
+ } );
130
+
131
+ it( 'Should pick the initially selected option if the value prop is passed without firing onChange', async () => {
132
+ const mockOnChange = jest.fn();
133
+ render(
134
+ <Component
135
+ { ...props }
136
+ onChange={ mockOnChange }
137
+ value={ props.options[ 3 ] }
138
+ />
139
+ );
140
+
141
+ expect(
142
+ screen.getByRole( 'button', {
143
+ expanded: false,
144
+ } )
145
+ ).toHaveTextContent( props.options[ 3 ].name );
146
+
147
+ expect( mockOnChange ).not.toHaveBeenCalled();
148
+ } );
149
+
69
150
  it( 'Should replace the initial selection when a new item is selected', async () => {
70
151
  const user = userEvent.setup();
71
152
 
@@ -144,7 +225,7 @@ describe.each( [
144
225
  // assert against filtered array
145
226
  itemsWithClass.map( ( { name } ) =>
146
227
  expect( screen.getByRole( 'option', { name } ) ).toHaveClass(
147
- customClass
228
+ customClassName
148
229
  )
149
230
  );
150
231
 
@@ -156,15 +237,13 @@ describe.each( [
156
237
  // assert against filtered array
157
238
  itemsWithoutClass.map( ( { name } ) =>
158
239
  expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass(
159
- customClass
240
+ customClassName
160
241
  )
161
242
  );
162
243
  } );
163
244
 
164
245
  it( 'Should apply styles only to options that have styles defined', async () => {
165
246
  const user = userEvent.setup();
166
- const customStyles =
167
- 'background-color: rgb(127, 255, 212); rotate: 13deg;';
168
247
 
169
248
  render( <Component { ...props } /> );
170
249
 
@@ -262,6 +341,105 @@ describe.each( [
262
341
  expect( screen.getByRole( 'option', { name: /hint/i } ) ).toBeVisible();
263
342
  } );
264
343
 
344
+ it( 'Should return object onChange', async () => {
345
+ const user = userEvent.setup();
346
+ const mockOnChange = jest.fn();
347
+
348
+ render( <Component { ...props } onChange={ mockOnChange } /> );
349
+
350
+ await user.click(
351
+ screen.getByRole( 'button', {
352
+ expanded: false,
353
+ } )
354
+ );
355
+
356
+ await user.click(
357
+ screen.getByRole( 'option', {
358
+ name: 'aquamarine',
359
+ } )
360
+ );
361
+
362
+ expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
363
+ expect( mockOnChange ).toHaveBeenLastCalledWith(
364
+ expect.objectContaining( {
365
+ inputValue: '',
366
+ isOpen: false,
367
+ selectedItem: expect.objectContaining( {
368
+ name: 'aquamarine',
369
+ } ),
370
+ type: expect.any( String ),
371
+ } )
372
+ );
373
+ } );
374
+
375
+ it( 'Should return selectedItem object when specified onChange', async () => {
376
+ const user = userEvent.setup();
377
+ const mockOnChange = jest.fn();
378
+
379
+ render( <Component { ...props } onChange={ mockOnChange } /> );
380
+
381
+ await user.tab();
382
+ expect(
383
+ screen.getByRole( 'button', {
384
+ expanded: false,
385
+ } )
386
+ ).toHaveFocus();
387
+
388
+ await user.keyboard( 'p' );
389
+ await user.keyboard( '{enter}' );
390
+
391
+ expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
392
+ expect( mockOnChange ).toHaveBeenLastCalledWith(
393
+ expect.objectContaining( {
394
+ selectedItem: expect.objectContaining( {
395
+ key: 'flower3',
396
+ name: 'poppy',
397
+ } ),
398
+ } )
399
+ );
400
+ } );
401
+
402
+ it( "Should pass arbitrary props to onChange's selectedItem, but apply only style and className to DOM elements", async () => {
403
+ const user = userEvent.setup();
404
+ const onChangeMock = jest.fn();
405
+
406
+ render( <Component { ...props } onChange={ onChangeMock } /> );
407
+
408
+ const currentSelectedItem = screen.getByRole( 'button', {
409
+ expanded: false,
410
+ } );
411
+
412
+ await user.click( currentSelectedItem );
413
+
414
+ const optionWithCustomAttributes = screen.getByRole( 'option', {
415
+ name: 'tomato (with custom props)',
416
+ } );
417
+
418
+ // Assert that the option element does not have the custom attributes
419
+ expect( optionWithCustomAttributes ).not.toHaveAttribute(
420
+ 'customPropFoo'
421
+ );
422
+ expect( optionWithCustomAttributes ).not.toHaveAttribute(
423
+ 'aria-label'
424
+ );
425
+
426
+ await user.click( optionWithCustomAttributes );
427
+
428
+ expect( onChangeMock ).toHaveBeenCalledTimes( 1 );
429
+ expect( onChangeMock ).toHaveBeenCalledWith(
430
+ expect.objectContaining( {
431
+ selectedItem: expect.objectContaining( {
432
+ key: 'color3',
433
+ name: 'tomato (with custom props)',
434
+ className: customClassName,
435
+ style: customStyles,
436
+ 'aria-label': 'test label',
437
+ customPropFoo: 'foo',
438
+ } ),
439
+ } )
440
+ );
441
+ } );
442
+
265
443
  describe( 'Keyboard behavior and accessibility', () => {
266
444
  it( 'Captures the keypress event and does not let it propagate', async () => {
267
445
  const user = userEvent.setup();
@@ -311,7 +489,9 @@ describe.each( [
311
489
  await user.keyboard( '{arrowdown}' );
312
490
  await user.keyboard( '{enter}' );
313
491
 
314
- expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
492
+ expect( currentSelectedItem ).toHaveTextContent(
493
+ props.options[ 1 ].name
494
+ );
315
495
  } );
316
496
 
317
497
  it( 'Should be able to type characters to select matching options', async () => {
@@ -422,11 +602,14 @@ describe.each( [
422
602
  const onBlurMock = jest.fn();
423
603
 
424
604
  render(
425
- <Component
426
- { ...props }
427
- onFocus={ onFocusMock }
428
- onBlur={ onBlurMock }
429
- />
605
+ <>
606
+ <Component
607
+ { ...props }
608
+ onFocus={ onFocusMock }
609
+ onBlur={ onBlurMock }
610
+ />
611
+ <button>Focus stop</button>
612
+ </>
430
613
  );
431
614
 
432
615
  const currentSelectedItem = screen.getByRole( 'button', {
@@ -74,7 +74,7 @@ const CustomSelectButton = ( {
74
74
  // move selection rather than open the popover
75
75
  showOnKeyDown={ false }
76
76
  >
77
- <div>{ computedRenderSelectedValue( currentValue ) }</div>
77
+ { computedRenderSelectedValue( currentValue ) }
78
78
  </Styled.Select>
79
79
  );
80
80
  };
@@ -14,7 +14,7 @@ import * as Styled from '../styles';
14
14
 
15
15
  function CustomSelectControl( props: LegacyCustomSelectProps ) {
16
16
  const {
17
- __experimentalShowSelectedHint,
17
+ __experimentalShowSelectedHint = false,
18
18
  __next40pxDefaultSize = false,
19
19
  describedBy,
20
20
  options,
@@ -27,7 +27,11 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
27
27
  // Forward props + store from v2 implementation
28
28
  const store = Ariakit.useSelectStore( {
29
29
  async setValue( nextValue ) {
30
- if ( ! onChange ) {
30
+ const nextOption = options.find(
31
+ ( item ) => item.name === nextValue
32
+ );
33
+
34
+ if ( ! onChange || ! nextOption ) {
31
35
  return;
32
36
  }
33
37
 
@@ -42,18 +46,21 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
42
46
  ),
43
47
  inputValue: '',
44
48
  isOpen: state.open,
45
- selectedItem: {
46
- name: nextValue as string,
47
- key: nextValue as string,
48
- },
49
+ selectedItem: nextOption,
49
50
  type: '',
50
51
  };
51
52
  onChange( changeObject );
52
53
  },
54
+ value: value?.name,
55
+ // Setting the first option as a default value when no value is provided
56
+ // is already done natively by the underlying Ariakit component,
57
+ // but doing this explicitly avoids the `onChange` callback from firing
58
+ // on initial render, thus making this implementation closer to the v1.
59
+ defaultValue: options[ 0 ]?.name,
53
60
  } );
54
61
 
55
62
  const children = options.map(
56
- ( { name, key, __experimentalHint, ...rest } ) => {
63
+ ( { name, key, __experimentalHint, style, className } ) => {
57
64
  const withHint = (
58
65
  <Styled.WithHintWrapper>
59
66
  <span>{ name }</span>
@@ -68,7 +75,8 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
68
75
  key={ key }
69
76
  value={ name }
70
77
  children={ __experimentalHint ? withHint : name }
71
- { ...rest }
78
+ style={ style }
79
+ className={ className }
72
80
  />
73
81
  );
74
82
  }
@@ -82,12 +90,12 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) {
82
90
  );
83
91
 
84
92
  return (
85
- <>
93
+ <Styled.SelectedExperimentalHintWrapper>
86
94
  { currentValue }
87
95
  <Styled.SelectedExperimentalHintItem className="components-custom-select-control__hint">
88
96
  { currentHint?.__experimentalHint }
89
97
  </Styled.SelectedExperimentalHintItem>
90
- </>
98
+ </Styled.SelectedExperimentalHintWrapper>
91
99
  );
92
100
  };
93
101