@wordpress/components 28.8.5 → 28.8.6

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 (46) hide show
  1. package/CHANGELOG.md +6 -3
  2. package/build/drop-zone/index.js +1 -2
  3. package/build/drop-zone/index.js.map +1 -1
  4. package/build/palette-edit/index.js +24 -1
  5. package/build/palette-edit/index.js.map +1 -1
  6. package/build/toggle-group-control/toggle-group-control/as-radio-group.js +2 -1
  7. package/build/toggle-group-control/toggle-group-control/as-radio-group.js.map +1 -1
  8. package/build/toggle-group-control/toggle-group-control-option-base/component.js +8 -10
  9. package/build/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  10. package/build/toggle-group-control/types.js.map +1 -1
  11. package/build/tooltip/index.js +5 -1
  12. package/build/tooltip/index.js.map +1 -1
  13. package/build-module/drop-zone/index.js +1 -2
  14. package/build-module/drop-zone/index.js.map +1 -1
  15. package/build-module/palette-edit/index.js +23 -1
  16. package/build-module/palette-edit/index.js.map +1 -1
  17. package/build-module/toggle-group-control/toggle-group-control/as-radio-group.js +2 -1
  18. package/build-module/toggle-group-control/toggle-group-control/as-radio-group.js.map +1 -1
  19. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js +8 -10
  20. package/build-module/toggle-group-control/toggle-group-control-option-base/component.js.map +1 -1
  21. package/build-module/toggle-group-control/types.js.map +1 -1
  22. package/build-module/tooltip/index.js +5 -1
  23. package/build-module/tooltip/index.js.map +1 -1
  24. package/build-style/style-rtl.css +4 -4
  25. package/build-style/style.css +4 -4
  26. package/build-types/drop-zone/index.d.ts.map +1 -1
  27. package/build-types/palette-edit/index.d.ts +3 -0
  28. package/build-types/palette-edit/index.d.ts.map +1 -1
  29. package/build-types/toggle-group-control/toggle-group-control/as-radio-group.d.ts.map +1 -1
  30. package/build-types/toggle-group-control/toggle-group-control-option-base/component.d.ts.map +1 -1
  31. package/build-types/toggle-group-control/types.d.ts +1 -0
  32. package/build-types/toggle-group-control/types.d.ts.map +1 -1
  33. package/build-types/tooltip/index.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/drop-zone/index.tsx +1 -2
  36. package/src/drop-zone/style.scss +1 -1
  37. package/src/palette-edit/index.tsx +30 -5
  38. package/src/palette-edit/test/index.tsx +50 -1
  39. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +6 -6
  40. package/src/toggle-group-control/test/index.tsx +13 -0
  41. package/src/toggle-group-control/toggle-group-control/as-radio-group.tsx +3 -1
  42. package/src/toggle-group-control/toggle-group-control-option-base/component.tsx +11 -15
  43. package/src/toggle-group-control/types.ts +1 -0
  44. package/src/tooltip/index.tsx +8 -1
  45. package/src/tooltip/test/index.tsx +78 -0
  46. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/components",
3
- "version": "28.8.5",
3
+ "version": "28.8.6",
4
4
  "description": "UI components for WordPress.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -84,5 +84,5 @@
84
84
  "publishConfig": {
85
85
  "access": "public"
86
86
  },
87
- "gitHead": "21b3c2e9abda74edab42455041edc6e82fccc388"
87
+ "gitHead": "b7af02f8431034ee19cdc33dd105d21705823eed"
88
88
  }
@@ -99,6 +99,7 @@ export function DropZoneComponent( {
99
99
  setType( _type );
100
100
  },
101
101
  onDragEnd() {
102
+ setIsDraggingOverElement( false );
102
103
  setIsDraggingOverDocument( false );
103
104
  setType( undefined );
104
105
  },
@@ -116,8 +117,6 @@ export function DropZoneComponent( {
116
117
  ( ( type === 'file' && onFilesDrop ) ||
117
118
  ( type === 'html' && onHTMLDrop ) ||
118
119
  ( type === 'default' && onDrop ) ),
119
- 'has-dragged-out': ! isDraggingOverElement,
120
- // Keeping the following classnames for legacy purposes
121
120
  'is-dragging-over-document': isDraggingOverDocument,
122
121
  'is-dragging-over-element': isDraggingOverElement,
123
122
  [ `is-dragging-${ type }` ]: !! type,
@@ -42,7 +42,7 @@
42
42
  transform: scale(0.9);
43
43
  }
44
44
 
45
- &.is-active:not(.has-dragged-out) {
45
+ &.is-active.is-dragging-over-element {
46
46
  .components-drop-zone__content {
47
47
  opacity: 1;
48
48
 
@@ -49,7 +49,6 @@ import { kebabCase } from '../utils/strings';
49
49
  import type {
50
50
  Color,
51
51
  ColorPickerPopoverProps,
52
- Gradient,
53
52
  NameInputProps,
54
53
  OptionProps,
55
54
  PaletteEditListViewProps,
@@ -70,6 +69,28 @@ function NameInput( { value, onChange, label }: NameInputProps ) {
70
69
  );
71
70
  }
72
71
 
72
+ /*
73
+ * Deduplicates the slugs of the provided elements.
74
+ */
75
+ export function deduplicateElementSlugs< T extends PaletteElement >(
76
+ elements: T[]
77
+ ) {
78
+ const slugCounts: { [ slug: string ]: number } = {};
79
+
80
+ return elements.map( ( element ) => {
81
+ let newSlug: string | undefined;
82
+
83
+ const { slug } = element;
84
+ slugCounts[ slug ] = ( slugCounts[ slug ] || 0 ) + 1;
85
+
86
+ if ( slugCounts[ slug ] > 1 ) {
87
+ newSlug = `${ slug }-${ slugCounts[ slug ] - 1 }`;
88
+ }
89
+
90
+ return { ...element, slug: newSlug ?? slug };
91
+ } );
92
+ }
93
+
73
94
  /**
74
95
  * Returns a name and slug for a palette item. The name takes the format "Color + id".
75
96
  * To ensure there are no duplicate ids, this function checks all slugs.
@@ -109,7 +130,7 @@ export function getNameAndSlugForPosition(
109
130
  };
110
131
  }
111
132
 
112
- function ColorPickerPopover< T extends Color | Gradient >( {
133
+ function ColorPickerPopover< T extends PaletteElement >( {
113
134
  isGradient,
114
135
  element,
115
136
  onChange,
@@ -167,7 +188,7 @@ function ColorPickerPopover< T extends Color | Gradient >( {
167
188
  );
168
189
  }
169
190
 
170
- function Option< T extends Color | Gradient >( {
191
+ function Option< T extends PaletteElement >( {
171
192
  canOnlyChangeValues,
172
193
  element,
173
194
  onChange,
@@ -265,7 +286,7 @@ function Option< T extends Color | Gradient >( {
265
286
  );
266
287
  }
267
288
 
268
- function PaletteEditListView< T extends Color | Gradient >( {
289
+ function PaletteEditListView< T extends PaletteElement >( {
269
290
  elements,
270
291
  onChange,
271
292
  canOnlyChangeValues,
@@ -280,7 +301,11 @@ function PaletteEditListView< T extends Color | Gradient >( {
280
301
  elementsReferenceRef.current = elements;
281
302
  }, [ elements ] );
282
303
 
283
- const debounceOnChange = useDebounce( onChange, 100 );
304
+ const debounceOnChange = useDebounce(
305
+ ( updatedElements: T[] ) =>
306
+ onChange( deduplicateElementSlugs( updatedElements ) ),
307
+ 100
308
+ );
284
309
 
285
310
  return (
286
311
  <VStack spacing={ 3 }>
@@ -7,7 +7,10 @@ import { click, type, press } from '@ariakit/test';
7
7
  /**
8
8
  * Internal dependencies
9
9
  */
10
- import PaletteEdit, { getNameAndSlugForPosition } from '..';
10
+ import PaletteEdit, {
11
+ getNameAndSlugForPosition,
12
+ deduplicateElementSlugs,
13
+ } from '..';
11
14
  import type { PaletteElement } from '../types';
12
15
 
13
16
  const noop = () => {};
@@ -97,6 +100,52 @@ describe( 'getNameAndSlugForPosition', () => {
97
100
  } );
98
101
  } );
99
102
 
103
+ describe( 'deduplicateElementSlugs', () => {
104
+ it( 'should not change the slugs if they are unique', () => {
105
+ const elements: PaletteElement[] = [
106
+ {
107
+ slug: 'test-color-1',
108
+ color: '#ffffff',
109
+ name: 'Test Color 1',
110
+ },
111
+ {
112
+ slug: 'test-color-2',
113
+ color: '#1a4548',
114
+ name: 'Test Color 2',
115
+ },
116
+ ];
117
+
118
+ expect( deduplicateElementSlugs( elements ) ).toEqual( elements );
119
+ } );
120
+ it( 'should change the slugs if they are not unique', () => {
121
+ const elements: PaletteElement[] = [
122
+ {
123
+ slug: 'test-color-1',
124
+ color: '#ffffff',
125
+ name: 'Test Color 1',
126
+ },
127
+ {
128
+ slug: 'test-color-1',
129
+ color: '#1a4548',
130
+ name: 'Test Color 2',
131
+ },
132
+ ];
133
+
134
+ expect( deduplicateElementSlugs( elements ) ).toEqual( [
135
+ {
136
+ slug: 'test-color-1',
137
+ color: '#ffffff',
138
+ name: 'Test Color 1',
139
+ },
140
+ {
141
+ slug: 'test-color-1-1',
142
+ color: '#1a4548',
143
+ name: 'Test Color 2',
144
+ },
145
+ ] );
146
+ } );
147
+ } );
148
+
100
149
  describe( 'PaletteEdit', () => {
101
150
  const defaultProps = {
102
151
  paletteLabel: 'Test label',
@@ -244,7 +244,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
244
244
  class="components-toggle-group-control emotion-8 emotion-9"
245
245
  data-wp-c16t="true"
246
246
  data-wp-component="ToggleGroupControl"
247
- id="toggle-group-control-as-radio-group-11"
247
+ id="toggle-group-control-as-radio-group-12"
248
248
  role="radiogroup"
249
249
  >
250
250
  <div
@@ -258,7 +258,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
258
258
  data-value="uppercase"
259
259
  data-wp-c16t="true"
260
260
  data-wp-component="ToggleGroupControlOptionBase"
261
- id="toggle-group-control-as-radio-group-11-30"
261
+ id="toggle-group-control-as-radio-group-12-32"
262
262
  role="radio"
263
263
  type="button"
264
264
  value="uppercase"
@@ -297,7 +297,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
297
297
  data-value="lowercase"
298
298
  data-wp-c16t="true"
299
299
  data-wp-component="ToggleGroupControlOptionBase"
300
- id="toggle-group-control-as-radio-group-11-31"
300
+ id="toggle-group-control-as-radio-group-12-33"
301
301
  role="radio"
302
302
  type="button"
303
303
  value="lowercase"
@@ -493,7 +493,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options
493
493
  class="components-toggle-group-control emotion-8 emotion-9"
494
494
  data-wp-c16t="true"
495
495
  data-wp-component="ToggleGroupControl"
496
- id="toggle-group-control-as-radio-group-10"
496
+ id="toggle-group-control-as-radio-group-11"
497
497
  role="radiogroup"
498
498
  >
499
499
  <div
@@ -506,7 +506,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options
506
506
  data-value="rigas"
507
507
  data-wp-c16t="true"
508
508
  data-wp-component="ToggleGroupControlOptionBase"
509
- id="toggle-group-control-as-radio-group-10-28"
509
+ id="toggle-group-control-as-radio-group-11-30"
510
510
  role="radio"
511
511
  type="button"
512
512
  value="rigas"
@@ -528,7 +528,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options
528
528
  data-value="jack"
529
529
  data-wp-c16t="true"
530
530
  data-wp-component="ToggleGroupControlOptionBase"
531
- id="toggle-group-control-as-radio-group-10-29"
531
+ id="toggle-group-control-as-radio-group-11-31"
532
532
  role="radio"
533
533
  type="button"
534
534
  value="jack"
@@ -162,6 +162,19 @@ describe.each( [
162
162
  expect( mockOnChange ).toHaveBeenCalledWith( 'rigas' );
163
163
  } );
164
164
 
165
+ it( 'should not set a value on focus', async () => {
166
+ render(
167
+ <Component label="Test Toggle Group Control">{ options }</Component>
168
+ );
169
+
170
+ const radio = screen.getByRole( 'radio', { name: 'R' } );
171
+ expect( radio ).not.toBeChecked();
172
+
173
+ await press.Tab();
174
+ expect( radio ).toHaveFocus();
175
+ expect( radio ).not.toBeChecked();
176
+ } );
177
+
165
178
  it( 'should render tooltip where `showTooltip` === `true`', async () => {
166
179
  render(
167
180
  <Component label="Test Toggle Group Control">
@@ -75,13 +75,15 @@ function UnforwardedToggleGroupControlAsRadioGroup(
75
75
  const groupContextValue = useMemo(
76
76
  () =>
77
77
  ( {
78
+ activeItemIsNotFirstItem: () =>
79
+ radio.getState().activeId !== radio.first(),
78
80
  baseId,
79
81
  isBlock: ! isAdaptiveWidth,
80
82
  size,
81
83
  value: selectedValue,
82
84
  setValue,
83
85
  } ) as ToggleGroupControlContextProps,
84
- [ baseId, isAdaptiveWidth, size, selectedValue, setValue ]
86
+ [ baseId, isAdaptiveWidth, radio, size, selectedValue, setValue ]
85
87
  );
86
88
 
87
89
  return (
@@ -83,7 +83,6 @@ function ToggleGroupControlOptionBase(
83
83
  value,
84
84
  children,
85
85
  showTooltip = false,
86
- onFocus: onFocusProp,
87
86
  disabled,
88
87
  ...otherButtonProps
89
88
  } = buttonProps;
@@ -134,7 +133,6 @@ function ToggleGroupControlOptionBase(
134
133
  <button
135
134
  { ...commonProps }
136
135
  disabled={ disabled }
137
- onFocus={ onFocusProp }
138
136
  aria-pressed={ isPressed }
139
137
  type="button"
140
138
  onClick={ buttonOnClick }
@@ -144,19 +142,17 @@ function ToggleGroupControlOptionBase(
144
142
  ) : (
145
143
  <Ariakit.Radio
146
144
  disabled={ disabled }
147
- render={
148
- <button
149
- type="button"
150
- { ...commonProps }
151
- onFocus={ ( event ) => {
152
- onFocusProp?.( event );
153
- if ( event.defaultPrevented ) {
154
- return;
155
- }
156
- toggleGroupControlContext.setValue( value );
157
- } }
158
- />
159
- }
145
+ onFocusVisible={ () => {
146
+ // Conditions ensure that the first visible focus to a radio group
147
+ // without a selected option will not automatically select the option.
148
+ if (
149
+ toggleGroupControlContext.value !== null ||
150
+ toggleGroupControlContext.activeItemIsNotFirstItem?.()
151
+ ) {
152
+ toggleGroupControlContext.setValue( value );
153
+ }
154
+ } }
155
+ render={ <button type="button" { ...commonProps } /> }
160
156
  value={ value }
161
157
  >
162
158
  <ButtonContentView>{ children }</ButtonContentView>
@@ -131,6 +131,7 @@ export type ToggleGroupControlProps = Pick<
131
131
  };
132
132
 
133
133
  export type ToggleGroupControlContextProps = {
134
+ activeItemIsNotFirstItem?: () => boolean;
134
135
  isDeselectable?: boolean;
135
136
  baseId: string;
136
137
  isBlock: ToggleGroupControlProps[ 'isBlock' ];
@@ -107,9 +107,16 @@ function UnforwardedTooltip(
107
107
  // TODO: this is a temporary workaround to minimize the effects of the
108
108
  // Ariakit upgrade. Ariakit doesn't pass the `aria-describedby` prop to
109
109
  // the tooltip anchor anymore since 0.4.0, so we need to add it manually.
110
+ // The `aria-describedby` attribute is added only if the anchor doesn't have
111
+ // one already, and if the tooltip text is not the same as the anchor's
112
+ // `aria-label`
110
113
  // See: https://github.com/WordPress/gutenberg/pull/64066
114
+ // See: https://github.com/WordPress/gutenberg/pull/65989
111
115
  function addDescribedById( element: React.ReactElement ) {
112
- return describedById && mounted
116
+ return describedById &&
117
+ mounted &&
118
+ element.props[ 'aria-describedby' ] === undefined &&
119
+ element.props[ 'aria-label' ] !== text
113
120
  ? cloneElement( element, { 'aria-describedby': describedById } )
114
121
  : element;
115
122
  }
@@ -516,4 +516,82 @@ describe( 'Tooltip', () => {
516
516
  ).not.toHaveClass( 'components-tooltip' );
517
517
  } );
518
518
  } );
519
+
520
+ describe( 'aria-describedby', () => {
521
+ it( "should not override the anchor's aria-describedby attribute if specified", async () => {
522
+ render(
523
+ <>
524
+ <Tooltip { ...props }>
525
+ <button aria-describedby="tooltip-test-description">
526
+ Tooltip anchor
527
+ </button>
528
+ </Tooltip>
529
+ { /* eslint-disable-next-line no-restricted-syntax */ }
530
+ <p id="tooltip-test-description">Tooltip description</p>
531
+ <button>focus trap outside</button>
532
+ </>
533
+ );
534
+
535
+ expect(
536
+ screen.getByRole( 'button', { name: 'Tooltip anchor' } )
537
+ ).toHaveAccessibleDescription( 'Tooltip description' );
538
+
539
+ // Focus the anchor, tooltip should show
540
+ await press.Tab();
541
+ expect(
542
+ screen.getByRole( 'button', { name: 'Tooltip anchor' } )
543
+ ).toHaveFocus();
544
+ await waitExpectTooltipToShow();
545
+
546
+ // The anchors should retain its previous accessible description,
547
+ // since the tooltip shouldn't override it.
548
+ expect(
549
+ screen.getByRole( 'button', { name: 'Tooltip anchor' } )
550
+ ).toHaveAccessibleDescription( 'Tooltip description' );
551
+
552
+ // Focus the other button, tooltip should hide
553
+ await press.Tab();
554
+ expect(
555
+ screen.getByRole( 'button', { name: 'focus trap outside' } )
556
+ ).toHaveFocus();
557
+ await waitExpectTooltipToHide();
558
+ } );
559
+
560
+ it( "should not add the aria-describedby attribute to the anchor if the tooltip text matches the anchor's aria-label", async () => {
561
+ render(
562
+ <>
563
+ <Tooltip { ...props }>
564
+ <button aria-label={ props.text }>
565
+ Tooltip anchor
566
+ </button>
567
+ </Tooltip>
568
+ <button>focus trap outside</button>
569
+ </>
570
+ );
571
+
572
+ expect(
573
+ screen.getByRole( 'button', { name: props.text } )
574
+ ).not.toHaveAccessibleDescription();
575
+
576
+ // Focus the anchor, tooltip should show
577
+ await press.Tab();
578
+ expect(
579
+ screen.getByRole( 'button', { name: props.text } )
580
+ ).toHaveFocus();
581
+ await waitExpectTooltipToShow();
582
+
583
+ // The anchor shouldn't have an aria-describedby prop
584
+ // because its aria-label matches the tooltip text.
585
+ expect(
586
+ screen.getByRole( 'button', { name: props.text } )
587
+ ).not.toHaveAccessibleDescription();
588
+
589
+ // Focus the other button, tooltip should hide
590
+ await press.Tab();
591
+ expect(
592
+ screen.getByRole( 'button', { name: 'focus trap outside' } )
593
+ ).toHaveFocus();
594
+ await waitExpectTooltipToHide();
595
+ } );
596
+ } );
519
597
  } );