@wordpress/components 29.8.0 → 29.9.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 (47) hide show
  1. package/.stylelintrc.js +1 -1
  2. package/CHANGELOG.md +18 -1
  3. package/build/guide/index.js +4 -2
  4. package/build/guide/index.js.map +1 -1
  5. package/build/guide/types.js.map +1 -1
  6. package/build/item-group/styles.js +10 -10
  7. package/build/item-group/styles.js.map +1 -1
  8. package/build/popover/index.js +9 -1
  9. package/build/popover/index.js.map +1 -1
  10. package/build/toggle-group-control/toggle-group-control/component.js +1 -1
  11. package/build/toggle-group-control/toggle-group-control/component.js.map +1 -1
  12. package/build/unit-control/utils.js +9 -8
  13. package/build/unit-control/utils.js.map +1 -1
  14. package/build-module/guide/index.js +4 -2
  15. package/build-module/guide/index.js.map +1 -1
  16. package/build-module/guide/types.js.map +1 -1
  17. package/build-module/item-group/styles.js +10 -10
  18. package/build-module/item-group/styles.js.map +1 -1
  19. package/build-module/popover/index.js +9 -1
  20. package/build-module/popover/index.js.map +1 -1
  21. package/build-module/toggle-group-control/toggle-group-control/component.js +1 -1
  22. package/build-module/toggle-group-control/toggle-group-control/component.js.map +1 -1
  23. package/build-module/unit-control/utils.js +9 -8
  24. package/build-module/unit-control/utils.js.map +1 -1
  25. package/build-style/style-rtl.css +3 -0
  26. package/build-style/style.css +3 -0
  27. package/build-types/guide/index.d.ts +1 -1
  28. package/build-types/guide/index.d.ts.map +1 -1
  29. package/build-types/guide/stories/index.story.d.ts.map +1 -1
  30. package/build-types/guide/types.d.ts +12 -0
  31. package/build-types/guide/types.d.ts.map +1 -1
  32. package/build-types/popover/index.d.ts.map +1 -1
  33. package/build-types/unit-control/utils.d.ts.map +1 -1
  34. package/package.json +19 -19
  35. package/src/color-picker/test/index.tsx +103 -26
  36. package/src/duotone-picker/style.scss +4 -0
  37. package/src/guide/README.md +16 -0
  38. package/src/guide/index.tsx +4 -2
  39. package/src/guide/stories/index.story.tsx +2 -0
  40. package/src/guide/types.ts +12 -0
  41. package/src/item-group/styles.ts +1 -1
  42. package/src/item-group/test/__snapshots__/index.js.snap +1 -1
  43. package/src/popover/index.tsx +12 -1
  44. package/src/toggle-group-control/toggle-group-control/component.tsx +1 -1
  45. package/src/unit-control/test/utils.ts +36 -0
  46. package/src/unit-control/utils.ts +8 -11
  47. package/tsconfig.tsbuildinfo +1 -1
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { fireEvent, screen, render, waitFor } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
+ import { click } from '@ariakit/test';
6
7
 
7
8
  /**
8
9
  * WordPress dependencies
@@ -13,7 +14,6 @@ import { useState } from '@wordpress/element';
13
14
  * Internal dependencies
14
15
  */
15
16
  import { ColorPicker } from '..';
16
- import { click } from '@ariakit/test';
17
17
 
18
18
  const hslaMatcher = expect.objectContaining( {
19
19
  h: expect.any( Number ),
@@ -41,6 +41,34 @@ const legacyColorMatcher = {
41
41
  source: 'hex',
42
42
  };
43
43
 
44
+ // Without the controlled component, slider values don't update after changes.
45
+ // Controlled component with state helps synchronize input box and slider during testing.
46
+ const ControlledColorPicker = ( {
47
+ onChange: onChangeProp,
48
+ initialColor = '#000000',
49
+ ...restProps
50
+ }: React.ComponentProps< typeof ColorPicker > & { initialColor?: string } ) => {
51
+ const [ colorState, setColorState ] = useState( initialColor );
52
+
53
+ const internalOnChange: typeof onChangeProp = ( newColor ) => {
54
+ onChangeProp?.( newColor );
55
+ setColorState( newColor );
56
+ };
57
+
58
+ return (
59
+ <>
60
+ <ColorPicker
61
+ { ...restProps }
62
+ onChange={ internalOnChange }
63
+ color={ colorState }
64
+ />
65
+ <button onClick={ () => setColorState( '#4d87ba' ) }>
66
+ Set color to #4d87ba
67
+ </button>
68
+ </>
69
+ );
70
+ };
71
+
44
72
  describe( 'ColorPicker', () => {
45
73
  describe( 'legacy props', () => {
46
74
  it( 'should fire onChangeComplete with the legacy color format', async () => {
@@ -144,31 +172,6 @@ describe( 'ColorPicker', () => {
144
172
  const user = userEvent.setup();
145
173
  const onChange = jest.fn();
146
174
 
147
- const ControlledColorPicker = ( {
148
- onChange: onChangeProp,
149
- ...restProps
150
- }: React.ComponentProps< typeof ColorPicker > ) => {
151
- const [ colorState, setColorState ] = useState( '#000000' );
152
-
153
- const internalOnChange: typeof onChangeProp = ( newColor ) => {
154
- onChangeProp?.( newColor );
155
- setColorState( newColor );
156
- };
157
-
158
- return (
159
- <>
160
- <ColorPicker
161
- { ...restProps }
162
- onChange={ internalOnChange }
163
- color={ colorState }
164
- />
165
- <button onClick={ () => setColorState( '#4d87ba' ) }>
166
- Set color to #4d87ba
167
- </button>
168
- </>
169
- );
170
- };
171
-
172
175
  render(
173
176
  <ControlledColorPicker
174
177
  onChange={ onChange }
@@ -342,4 +345,78 @@ describe( 'ColorPicker', () => {
342
345
  } );
343
346
  } );
344
347
  } );
348
+
349
+ describe.each( [
350
+ [ 'hsl', 'HSL' ],
351
+ [ 'rgb', 'RGB' ],
352
+ ] )( 'Alpha-enabled %s format', ( format, formatLabel ) => {
353
+ it( `should update alpha correctly when ${ formatLabel } format is selected`, async () => {
354
+ const user = userEvent.setup();
355
+ const onChange = jest.fn();
356
+
357
+ render(
358
+ <ControlledColorPicker
359
+ onChange={ onChange }
360
+ enableAlpha
361
+ initialColor="#ffffff80"
362
+ />
363
+ );
364
+
365
+ const formatSelector = screen.getByRole( 'combobox' );
366
+ expect( formatSelector ).toBeVisible();
367
+ await user.selectOptions( formatSelector, format );
368
+
369
+ const alphaInput = screen.getByRole( 'spinbutton', {
370
+ name: 'Alpha',
371
+ } );
372
+ expect( alphaInput ).toBeVisible();
373
+
374
+ const alphaSliders = screen.getAllByRole( 'slider', {
375
+ name: 'Alpha',
376
+ } );
377
+
378
+ expect( alphaSliders ).toHaveLength( 2 );
379
+
380
+ // Choose the second slider which is the actual slider of type: input[type="range"]
381
+ const alphaSlider = alphaSliders.at( -1 )!;
382
+
383
+ expect( alphaSlider ).toHaveValue( '50' );
384
+ expect( alphaInput ).toHaveValue( 50 );
385
+
386
+ expect( onChange ).not.toHaveBeenCalled();
387
+
388
+ // Test pattern 1: Update the slider
389
+ fireEvent.change( alphaSlider, {
390
+ target: { value: 75 },
391
+ } );
392
+
393
+ await waitFor( () => {
394
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
395
+ } );
396
+
397
+ expect( onChange ).toHaveBeenLastCalledWith( '#ffffffbf' );
398
+ expect( alphaInput ).toHaveValue( 75 );
399
+ expect( alphaSlider ).toHaveValue( '75' );
400
+
401
+ onChange.mockClear();
402
+
403
+ // Test pattern 2: Update the alphaInput
404
+ await user.clear( alphaInput );
405
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
406
+
407
+ // Initially type 7 in the alpha input, we expect it to be called with #ffffff12
408
+ await user.keyboard( '7' );
409
+
410
+ // Now with 75% opacity we expect it to be called with #ffffffbf
411
+ await user.keyboard( '5' );
412
+
413
+ // Called twice, once per key stroke (`7` and `5`)
414
+ expect( onChange ).toHaveBeenCalledTimes( 3 );
415
+ expect( onChange ).toHaveBeenNthCalledWith( 2, '#ffffff12' );
416
+ expect( onChange ).toHaveBeenNthCalledWith( 3, '#ffffffbf' );
417
+
418
+ expect( alphaSlider ).toHaveValue( '75' );
419
+ expect( alphaInput ).toHaveValue( 75 );
420
+ } );
421
+ } );
345
422
  } );
@@ -13,6 +13,10 @@
13
13
  color: transparent;
14
14
  }
15
15
 
16
+ &:hover:not(:disabled):not([aria-disabled="true"]) {
17
+ color: transparent;
18
+ }
19
+
16
20
  &:not([aria-disabled="true"]):active {
17
21
  color: transparent;
18
22
  }
@@ -59,6 +59,22 @@ Use this to customize the label of the _Finish_ button shown at the end of the g
59
59
  - Required: No
60
60
  - Default: `'Finish'`
61
61
 
62
+ ### nextButtonText
63
+
64
+ Use this to customize the label of the _Next_ button shown on each page of the guide.
65
+
66
+ - Type: `string`
67
+ - Required: No
68
+ - Default: `'Next'`
69
+
70
+ ### previousButtonText
71
+
72
+ Use this to customize the label of the _Previous_ button shown on each page of the guide except the first.
73
+
74
+ - Type: `string`
75
+ - Required: No
76
+ - Default: `'Previous'`
77
+
62
78
  ### onFinish
63
79
 
64
80
  A function which is called when the guide is finished. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide.
@@ -55,6 +55,8 @@ function Guide( {
55
55
  className,
56
56
  contentLabel,
57
57
  finishButtonText = __( 'Finish' ),
58
+ nextButtonText = __( 'Next' ),
59
+ previousButtonText = __( 'Previous' ),
58
60
  onFinish,
59
61
  pages = [],
60
62
  }: GuideProps ) {
@@ -146,7 +148,7 @@ function Guide( {
146
148
  onClick={ goBack }
147
149
  __next40pxDefaultSize
148
150
  >
149
- { __( 'Previous' ) }
151
+ { previousButtonText }
150
152
  </Button>
151
153
  ) }
152
154
  { canGoForward && (
@@ -156,7 +158,7 @@ function Guide( {
156
158
  onClick={ goForward }
157
159
  __next40pxDefaultSize
158
160
  >
159
- { __( 'Next' ) }
161
+ { nextButtonText }
160
162
  </Button>
161
163
  ) }
162
164
  { ! canGoForward && (
@@ -20,6 +20,8 @@ const meta: Meta< typeof Guide > = {
20
20
  argTypes: {
21
21
  contentLabel: { control: 'text' },
22
22
  finishButtonText: { control: 'text' },
23
+ nextButtonText: { control: 'text' },
24
+ previousButtonText: { control: 'text' },
23
25
  onFinish: { action: 'onFinish' },
24
26
  },
25
27
  };
@@ -40,6 +40,18 @@ export type GuideProps = {
40
40
  * @default 'Finish'
41
41
  */
42
42
  finishButtonText?: string;
43
+ /**
44
+ * Use this to customize the label of the _Next_ button shown on each page of the guide.
45
+ *
46
+ * @default 'Next'
47
+ */
48
+ nextButtonText?: string;
49
+ /**
50
+ * Use this to customize the label of the _Previous_ button shown on each page of the guide except the first.
51
+ *
52
+ * @default 'Previous'
53
+ */
54
+ previousButtonText?: string;
43
55
  /**
44
56
  * A function which is called when the guide is finished.
45
57
  */
@@ -65,7 +65,7 @@ export const separated = css`
65
65
  border-bottom: 1px solid ${ CONFIG.surfaceBorderColor };
66
66
  }
67
67
 
68
- > *:last-of-type > *:not( :focus ) {
68
+ > *:last-of-type > * {
69
69
  border-bottom-color: transparent;
70
70
  }
71
71
  `;
@@ -115,7 +115,7 @@ Snapshot Diff:
115
115
  <div>
116
116
  <div
117
117
  - class="components-item-group css-1iattls-PolymorphicDiv-rounded e19lxcc00"
118
- + class="components-item-group css-zgfros-PolymorphicDiv-separated-rounded e19lxcc00"
118
+ + class="components-item-group css-1hvp0tq-PolymorphicDiv-separated-rounded e19lxcc00"
119
119
  data-wp-c16t="true"
120
120
  data-wp-component="ItemGroup"
121
121
  role="list"
@@ -71,6 +71,13 @@ import { StyleProvider } from '../style-provider';
71
71
  */
72
72
  export const SLOT_NAME = 'Popover';
73
73
 
74
+ /**
75
+ * Virtual padding to account for overflow boundaries.
76
+ *
77
+ * @type {number}
78
+ */
79
+ const OVERFLOW_PADDING = 8;
80
+
74
81
  // An SVG displaying a triangle facing down, filled with a solid
75
82
  // color and bordered in such a way to create an arrow-like effect.
76
83
  // Keeping the SVG's viewbox squared simplify the arrow positioning
@@ -224,6 +231,7 @@ const UnforwardedPopover = (
224
231
  computedFlipProp && flipMiddleware(),
225
232
  computedResizeProp &&
226
233
  size( {
234
+ padding: OVERFLOW_PADDING,
227
235
  apply( sizeProps ) {
228
236
  const { firstElementChild } = refs.floating.current ?? {};
229
237
 
@@ -234,7 +242,10 @@ const UnforwardedPopover = (
234
242
 
235
243
  // Reduce the height of the popover to the available space.
236
244
  Object.assign( firstElementChild.style, {
237
- maxHeight: `${ sizeProps.availableHeight }px`,
245
+ maxHeight: `${ Math.max(
246
+ 0,
247
+ sizeProps.availableHeight
248
+ ) }px`,
238
249
  overflow: 'auto',
239
250
  } );
240
251
  },
@@ -54,7 +54,7 @@ function UnconnectedToggleGroupControl(
54
54
  const [ controlElement, setControlElement ] = useState< HTMLElement >();
55
55
  const refs = useMergeRefs( [ setControlElement, forwardedRef ] );
56
56
  const selectedRect = useTrackElementOffsetRect(
57
- value || value === 0 ? selectedElement : undefined
57
+ value !== null && value !== undefined ? selectedElement : undefined
58
58
  );
59
59
  useAnimatedOffsetRect( controlElement, selectedRect, {
60
60
  prefix: 'selected',
@@ -42,6 +42,42 @@ describe( 'UnitControl utils', () => {
42
42
  ] );
43
43
  } );
44
44
 
45
+ it( 'should not mutate default units argument definiton', () => {
46
+ const unitsA = useCustomUnits( {
47
+ availableUnits: [ 'px', 'em', 'rem' ],
48
+ defaultValues: { px: 8, em: 0.5, rem: 0.5 },
49
+ } );
50
+
51
+ const unitsB = useCustomUnits( {
52
+ availableUnits: [ 'px', 'em', 'rem' ],
53
+ defaultValues: { px: 16, em: 1, rem: 1 },
54
+ } );
55
+
56
+ expect( unitsA ).not.toEqual( unitsB );
57
+ } );
58
+
59
+ it( 'should not mutate custon units argument definitons', () => {
60
+ const units = [
61
+ { value: 'px', label: 'pixel' },
62
+ { value: 'em', label: 'em' },
63
+ { value: 'rem', label: 'rem' },
64
+ ];
65
+
66
+ const unitsA = useCustomUnits( {
67
+ availableUnits: [ 'px', 'em', 'rem' ],
68
+ defaultValues: { px: 8, em: 0.5, rem: 0.5 },
69
+ units,
70
+ } );
71
+
72
+ const unitsB = useCustomUnits( {
73
+ availableUnits: [ 'px', 'em', 'rem' ],
74
+ defaultValues: { px: 16, em: 1, rem: 1 },
75
+ units,
76
+ } );
77
+
78
+ expect( unitsA ).not.toEqual( unitsB );
79
+ } );
80
+
45
81
  it( 'should add default values to available units even if the default values are strings', () => {
46
82
  // Although the public APIs of the component expect a `number` as the type of the
47
83
  // default values, it's still good to test for strings (as it can happen in un-typed
@@ -428,19 +428,16 @@ export const useCustomUnits = ( {
428
428
  units
429
429
  );
430
430
 
431
- if ( defaultValues ) {
432
- customUnitsToReturn.forEach( ( unit, i ) => {
433
- if ( defaultValues[ unit.value ] ) {
434
- const [ parsedDefaultValue ] = parseQuantityAndUnitFromRawValue(
435
- defaultValues[ unit.value ]
436
- );
437
-
438
- customUnitsToReturn[ i ].default = parsedDefaultValue;
439
- }
440
- } );
431
+ if ( ! defaultValues ) {
432
+ return customUnitsToReturn;
441
433
  }
442
434
 
443
- return customUnitsToReturn;
435
+ return customUnitsToReturn.map( ( unit ) => {
436
+ const [ defaultValue ] = defaultValues[ unit.value ]
437
+ ? parseQuantityAndUnitFromRawValue( defaultValues[ unit.value ] )
438
+ : [];
439
+ return { ...unit, default: defaultValue };
440
+ } );
444
441
  };
445
442
 
446
443
  /**