@wordpress/components 35.1.1-next.v.202606191442.0 → 36.0.1

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 (113) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/CONTRIBUTING.md +33 -26
  3. package/build/box-control/{input-control.cjs → box-input-control.cjs} +9 -9
  4. package/build/box-control/box-input-control.cjs.map +7 -0
  5. package/build/box-control/index.cjs +9 -25
  6. package/build/box-control/index.cjs.map +3 -3
  7. package/build/disabled/index.cjs.map +2 -2
  8. package/build/divider/component.cjs +108 -3
  9. package/build/divider/component.cjs.map +4 -4
  10. package/build/higher-order/with-fallback-styles/index.cjs +18 -44
  11. package/build/higher-order/with-fallback-styles/index.cjs.map +2 -2
  12. package/build/text-control/index.cjs +3 -12
  13. package/build/text-control/index.cjs.map +3 -3
  14. package/build/validated-form-controls/components/text-control.cjs +0 -1
  15. package/build/validated-form-controls/components/text-control.cjs.map +2 -2
  16. package/build-module/box-control/{input-control.mjs → box-input-control.mjs} +6 -6
  17. package/build-module/box-control/box-input-control.mjs.map +7 -0
  18. package/build-module/box-control/index.mjs +9 -25
  19. package/build-module/box-control/index.mjs.map +2 -2
  20. package/build-module/disabled/index.mjs.map +2 -2
  21. package/build-module/divider/component.mjs +108 -3
  22. package/build-module/divider/component.mjs.map +3 -3
  23. package/build-module/higher-order/with-fallback-styles/index.mjs +20 -46
  24. package/build-module/higher-order/with-fallback-styles/index.mjs.map +2 -2
  25. package/build-module/text-control/index.mjs +3 -12
  26. package/build-module/text-control/index.mjs.map +2 -2
  27. package/build-module/validated-form-controls/components/text-control.mjs +0 -1
  28. package/build-module/validated-form-controls/components/text-control.mjs.map +2 -2
  29. package/build-style/style-rtl.css +3 -19
  30. package/build-style/style.css +3 -19
  31. package/build-types/box-control/box-input-control.d.ts +3 -0
  32. package/build-types/box-control/box-input-control.d.ts.map +1 -0
  33. package/build-types/box-control/index.d.ts +2 -6
  34. package/build-types/box-control/index.d.ts.map +1 -1
  35. package/build-types/box-control/stories/index.story.d.ts.map +1 -1
  36. package/build-types/box-control/types.d.ts +7 -6
  37. package/build-types/box-control/types.d.ts.map +1 -1
  38. package/build-types/disabled/index.d.ts +0 -1
  39. package/build-types/disabled/index.d.ts.map +1 -1
  40. package/build-types/disabled/stories/index.story.d.ts.map +1 -1
  41. package/build-types/divider/component.d.ts.map +1 -1
  42. package/build-types/higher-order/with-fallback-styles/index.d.ts +1 -35
  43. package/build-types/higher-order/with-fallback-styles/index.d.ts.map +1 -1
  44. package/build-types/higher-order/with-fallback-styles/test/index.d.ts +2 -0
  45. package/build-types/higher-order/with-fallback-styles/test/index.d.ts.map +1 -0
  46. package/build-types/icon/stories/index.story.d.ts +0 -1
  47. package/build-types/icon/stories/index.story.d.ts.map +1 -1
  48. package/build-types/placeholder/stories/index.story.d.ts.map +1 -1
  49. package/build-types/resizable-box/index.d.ts +0 -2
  50. package/build-types/resizable-box/index.d.ts.map +1 -1
  51. package/build-types/resizable-box/stories/index.story.d.ts +0 -2
  52. package/build-types/resizable-box/stories/index.story.d.ts.map +1 -1
  53. package/build-types/text-control/index.d.ts +0 -1
  54. package/build-types/text-control/index.d.ts.map +1 -1
  55. package/build-types/text-control/stories/index.story.d.ts.map +1 -1
  56. package/build-types/text-control/types.d.ts +2 -1
  57. package/build-types/text-control/types.d.ts.map +1 -1
  58. package/build-types/validated-form-controls/components/text-control.d.ts +2 -2
  59. package/build-types/validated-form-controls/components/text-control.d.ts.map +1 -1
  60. package/package.json +23 -23
  61. package/src/box-control/README.md +1 -13
  62. package/src/box-control/{input-control.tsx → box-input-control.tsx} +4 -4
  63. package/src/box-control/index.tsx +5 -23
  64. package/src/box-control/stories/index.story.tsx +1 -3
  65. package/src/box-control/test/index.tsx +15 -2
  66. package/src/box-control/types.ts +7 -6
  67. package/src/card/test/__snapshots__/index.tsx.snap +4 -6
  68. package/src/checkbox-control/test/__snapshots__/index.tsx.snap +4 -3
  69. package/src/custom-select-control/test/index.tsx +1 -1
  70. package/src/disabled/README.md +0 -1
  71. package/src/disabled/index.tsx +0 -1
  72. package/src/disabled/stories/index.story.tsx +0 -1
  73. package/src/divider/component.tsx +27 -3
  74. package/src/divider/style.module.scss +21 -0
  75. package/src/divider/test/index.tsx +21 -11
  76. package/src/higher-order/with-constrained-tabbing/README.md +0 -2
  77. package/src/higher-order/with-fallback-styles/index.tsx +31 -61
  78. package/src/higher-order/with-fallback-styles/test/index.tsx +47 -0
  79. package/src/higher-order/with-focus-return/README.md +0 -2
  80. package/src/icon/stories/index.story.tsx +0 -15
  81. package/src/menu/test/index.tsx +0 -16
  82. package/src/menu-item/test/__snapshots__/index.js.snap +2 -0
  83. package/src/modal/test/index.tsx +0 -24
  84. package/src/navigable-container/test/navigable-menu.tsx +0 -16
  85. package/src/navigable-container/test/tababble-container.tsx +0 -16
  86. package/src/navigator/test/index.tsx +0 -24
  87. package/src/notice/test/__snapshots__/index.tsx.snap +1 -0
  88. package/src/placeholder/stories/index.story.tsx +0 -1
  89. package/src/resizable-box/index.tsx +1 -2
  90. package/src/tab-panel/test/index.tsx +0 -17
  91. package/src/tabs/test/index.tsx +0 -17
  92. package/src/text-control/README.md +0 -9
  93. package/src/text-control/index.tsx +3 -14
  94. package/src/text-control/stories/index.story.tsx +0 -2
  95. package/src/text-control/style.scss +7 -11
  96. package/src/text-control/test/text-control.tsx +1 -5
  97. package/src/text-control/types.ts +2 -1
  98. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +4 -0
  99. package/src/tools-panel/tools-panel/README.md +0 -2
  100. package/src/tree-grid/test/index.tsx +0 -23
  101. package/src/validated-form-controls/components/text-control.tsx +2 -10
  102. package/build/box-control/input-control.cjs.map +0 -7
  103. package/build/divider/styles.cjs +0 -98
  104. package/build/divider/styles.cjs.map +0 -7
  105. package/build-module/box-control/input-control.mjs.map +0 -7
  106. package/build-module/divider/styles.mjs +0 -63
  107. package/build-module/divider/styles.mjs.map +0 -7
  108. package/build-types/box-control/input-control.d.ts +0 -3
  109. package/build-types/box-control/input-control.d.ts.map +0 -1
  110. package/build-types/divider/styles.d.ts +0 -10
  111. package/build-types/divider/styles.d.ts.map +0 -1
  112. package/src/divider/styles.ts +0 -81
  113. package/src/divider/test/__snapshots__/index.tsx.snap +0 -71
@@ -10,7 +10,7 @@ import warning from '@wordpress/warning';
10
10
  * Internal dependencies
11
11
  */
12
12
  import { BaseControl } from '../base-control';
13
- import InputControl from './input-control';
13
+ import BoxInputControl from './box-input-control';
14
14
  import LinkedButton from './linked-button';
15
15
  import { Grid } from '../grid';
16
16
  import {
@@ -27,7 +27,6 @@ import {
27
27
  } from './utils';
28
28
  import { useControlledState } from '../utils/hooks';
29
29
  import type { BoxControlProps, BoxControlValue } from './types';
30
- import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size';
31
30
 
32
31
  const defaultInputProps = {
33
32
  min: 0,
@@ -58,17 +57,12 @@ function useUniqueId( idProp?: string ) {
58
57
  * } );
59
58
  *
60
59
  * return (
61
- * <BoxControl
62
- * __next40pxDefaultSize
63
- * values={ values }
64
- * onChange={ setValues }
65
- * />
60
+ * <BoxControl values={ values } onChange={ setValues } />
66
61
  * );
67
62
  * };
68
63
  * ```
69
64
  */
70
65
  function BoxControl( {
71
- __next40pxDefaultSize = false,
72
66
  id: idProp,
73
67
  inputProps = defaultInputProps,
74
68
  onChange = noop,
@@ -137,16 +131,10 @@ function BoxControl( {
137
131
  setSelectedUnits,
138
132
  sides,
139
133
  values: inputValues,
140
- __next40pxDefaultSize,
141
134
  presets,
142
135
  presetKey,
143
136
  };
144
137
 
145
- maybeWarnDeprecated36pxSize( {
146
- componentName: 'BoxControl',
147
- __next40pxDefaultSize,
148
- size: undefined,
149
- } );
150
138
  const sidesToRender = getAllowedSides( sides );
151
139
 
152
140
  if ( ( presets && ! presetKey ) || ( ! presets && presetKey ) ) {
@@ -170,9 +158,7 @@ function BoxControl( {
170
158
  </BaseControl.VisualLabel>
171
159
  { isLinked && (
172
160
  <InputWrapper>
173
- { /* Disable reason: the parent component is handling the __next40pxDefaultSize prop */ }
174
- { /* eslint-disable-next-line @wordpress/components-no-missing-40px-size-prop */ }
175
- <InputControl side="all" { ...inputControlProps } />
161
+ <BoxInputControl side="all" { ...inputControlProps } />
176
162
  </InputWrapper>
177
163
  ) }
178
164
  { ! hasOneSide && (
@@ -187,9 +173,7 @@ function BoxControl( {
187
173
  { ! isLinked &&
188
174
  splitOnAxis &&
189
175
  [ 'vertical', 'horizontal' ].map( ( axis ) => (
190
- // Disable reason: the parent component is handling the __next40pxDefaultSize prop
191
- // eslint-disable-next-line @wordpress/components-no-missing-40px-size-prop
192
- <InputControl
176
+ <BoxInputControl
193
177
  key={ axis }
194
178
  side={ axis as 'horizontal' | 'vertical' }
195
179
  { ...inputControlProps }
@@ -198,9 +182,7 @@ function BoxControl( {
198
182
  { ! isLinked &&
199
183
  ! splitOnAxis &&
200
184
  Array.from( sidesToRender ).map( ( axis ) => (
201
- // Disable reason: the parent component is handling the __next40pxDefaultSize prop
202
- // eslint-disable-next-line @wordpress/components-no-missing-40px-size-prop
203
- <InputControl
185
+ <BoxInputControl
204
186
  key={ axis }
205
187
  side={ axis }
206
188
  { ...inputControlProps }
@@ -36,7 +36,7 @@ const meta: Meta< typeof BoxControl > = {
36
36
  export default meta;
37
37
 
38
38
  const TemplateUncontrolled: StoryFn< typeof BoxControl > = ( props ) => {
39
- return <BoxControl __next40pxDefaultSize { ...props } />;
39
+ return <BoxControl { ...props } />;
40
40
  };
41
41
 
42
42
  const TemplateControlled: StoryFn< typeof BoxControl > = ( props ) => {
@@ -44,7 +44,6 @@ const TemplateControlled: StoryFn< typeof BoxControl > = ( props ) => {
44
44
 
45
45
  return (
46
46
  <BoxControl
47
- __next40pxDefaultSize
48
47
  values={ values }
49
48
  { ...props }
50
49
  onChange={ ( nextValue ) => {
@@ -58,7 +57,6 @@ const TemplateControlled: StoryFn< typeof BoxControl > = ( props ) => {
58
57
  export const Default = TemplateUncontrolled.bind( {} );
59
58
  Default.args = {
60
59
  label: 'Label',
61
- __next40pxDefaultSize: true,
62
60
  };
63
61
 
64
62
  export const Controlled = TemplateControlled.bind( {} );
@@ -29,7 +29,6 @@ const ControlledBoxControl = (
29
29
 
30
30
  return (
31
31
  <BoxControl
32
- __next40pxDefaultSize
33
32
  values={ state }
34
33
  onChange={ ( next ) => setState( next ) }
35
34
  { ...extraProps }
@@ -42,7 +41,7 @@ const UncontrolledBoxControl = ( {
42
41
  ...props
43
42
  }: DistributiveOmit< BoxControlProps, 'onChange' > & {
44
43
  onChange?: BoxControlProps[ 'onChange' ];
45
- } ) => <BoxControl __next40pxDefaultSize onChange={ onChange } { ...props } />;
44
+ } ) => <BoxControl onChange={ onChange } { ...props } />;
46
45
 
47
46
  describe( 'BoxControl', () => {
48
47
  describe( 'Basic rendering', () => {
@@ -474,4 +473,18 @@ describe( 'BoxControl', () => {
474
473
  } );
475
474
  } );
476
475
  } );
476
+
477
+ describe( 'Placeholder', () => {
478
+ it( 'applies a consumer-supplied placeholder passed via inputProps', () => {
479
+ render(
480
+ <UncontrolledBoxControl
481
+ inputProps={ { placeholder: 'Inherited' } }
482
+ />
483
+ );
484
+
485
+ expect(
486
+ screen.getByRole( 'textbox', { name: 'All sides' } )
487
+ ).toHaveAttribute( 'placeholder', 'Inherited' );
488
+ } );
489
+ } );
477
490
  } );
@@ -27,6 +27,13 @@ type UnitControlPassthroughProps = Omit<
27
27
  >;
28
28
 
29
29
  type DeprecatedBoxControlProps = {
30
+ /**
31
+ * Start opting into the larger default height that will become the default size in a future version.
32
+ *
33
+ * @deprecated Default behavior since WordPress 7.1. Prop can be safely removed.
34
+ * @ignore
35
+ */
36
+ __next40pxDefaultSize?: boolean;
30
37
  /**
31
38
  * @deprecated Pass to the `inputProps` prop instead.
32
39
  * @ignore
@@ -94,12 +101,6 @@ export type BoxControlProps = Pick< UnitControlProps, 'units' > &
94
101
  * The current values of the control, expressed as an object of `top`, `right`, `bottom`, and `left` values.
95
102
  */
96
103
  values?: BoxControlValue;
97
- /**
98
- * Start opting into the larger default height that will become the default size in a future version.
99
- *
100
- * @default false
101
- */
102
- __next40pxDefaultSize?: boolean;
103
104
  } & (
104
105
  | {
105
106
  /**
@@ -281,14 +281,12 @@ exports[`Card Card component should render correctly 1`] = `
281
281
  }
282
282
 
283
283
  .emotion-10 {
284
- border: 0;
285
- margin: 0;
286
- border-bottom: 1px solid currentColor;
287
- height: 0;
288
- width: auto;
289
284
  box-sizing: border-box;
290
285
  display: block;
291
286
  width: 100%;
287
+ }
288
+
289
+ .emotion-11 {
292
290
  border-color: rgba(0, 0, 0, 0.1);
293
291
  }
294
292
 
@@ -412,7 +410,7 @@ exports[`Card Card component should render correctly 1`] = `
412
410
  </div>
413
411
  <hr
414
412
  aria-orientation="horizontal"
415
- class="components-divider components-card__divider components-card-divider emotion-10 emotion-11"
413
+ class="components-divider emotion-10 emotion-11 components-card__divider components-card-divider"
416
414
  data-wp-c16t="true"
417
415
  data-wp-component="CardDivider"
418
416
  role="separator"
@@ -5,7 +5,7 @@ Snapshot Diff:
5
5
  - First value
6
6
  + Second value
7
7
 
8
- @@ -13,14 +13,28 @@
8
+ @@ -13,14 +13,29 @@
9
9
  <span
10
10
  class="components-checkbox-control__input-container"
11
11
  >
@@ -15,10 +15,11 @@ Snapshot Diff:
15
15
  + id="inspector-checkbox-control-7"
16
16
  type="checkbox"
17
17
  value="1"
18
- + />
18
+ />
19
19
  + <svg
20
20
  + aria-hidden="true"
21
21
  + class="components-checkbox-control__indeterminate"
22
+ + fill="currentColor"
22
23
  + focusable="false"
23
24
  + height="24"
24
25
  + role="presentation"
@@ -28,7 +29,7 @@ Snapshot Diff:
28
29
  + >
29
30
  + <path
30
31
  + d="M7 11.5h10V13H7z"
31
- />
32
+ + />
32
33
  + </svg>
33
34
  </span>
34
35
  </div>
@@ -483,7 +483,7 @@ describe.each( [
483
483
  const customSelect = screen.getByRole( 'listbox', {
484
484
  name: props.label,
485
485
  } );
486
- expect( customSelect ).toHaveFocus();
486
+ await waitFor( () => expect( customSelect ).toHaveFocus() );
487
487
  await press.Enter();
488
488
 
489
489
  expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
@@ -15,7 +15,6 @@ const MyDisabled = () => {
15
15
 
16
16
  let input = (
17
17
  <TextControl
18
- __next40pxDefaultSize
19
18
  label="Input"
20
19
  onChange={ () => {} }
21
20
  />
@@ -25,7 +25,6 @@ const { Consumer, Provider } = Context;
25
25
  *
26
26
  * let input = (
27
27
  * <TextControl
28
- * __next40pxDefaultSize
29
28
  * label="Input"
30
29
  * onChange={ () => {} }
31
30
  * />
@@ -46,7 +46,6 @@ const Form = () => {
46
46
  return (
47
47
  <VStack>
48
48
  <TextControl
49
- __next40pxDefaultSize
50
49
  label="Text Control"
51
50
  value={ textControlValue }
52
51
  onChange={ setTextControlValue }
@@ -2,6 +2,7 @@
2
2
  * External dependencies
3
3
  */
4
4
  import * as Ariakit from '@ariakit/react';
5
+ import clsx from 'clsx';
5
6
  import type { ForwardedRef } from 'react';
6
7
 
7
8
  /**
@@ -9,18 +10,41 @@ import type { ForwardedRef } from 'react';
9
10
  */
10
11
  import type { WordPressComponentProps } from '../context';
11
12
  import { contextConnect, useContextSystem } from '../context';
12
- import { DividerView } from './styles';
13
+ import { space } from '../utils/space';
13
14
  import type { DividerProps } from './types';
15
+ import styles from './style.module.scss';
14
16
 
15
17
  function UnconnectedDivider(
16
18
  props: WordPressComponentProps< DividerProps, 'hr', false >,
17
19
  forwardedRef: ForwardedRef< any >
18
20
  ) {
19
- const contextProps = useContextSystem( props, 'Divider' );
21
+ const {
22
+ className,
23
+ margin,
24
+ marginEnd,
25
+ marginStart,
26
+ style,
27
+ ...contextProps
28
+ } = useContextSystem( props, 'Divider' );
29
+
30
+ const dividerStyle = { ...style };
31
+ const resolvedMarginStart = space( marginStart ?? margin );
32
+ const resolvedMarginEnd = space( marginEnd ?? margin );
33
+
34
+ if ( resolvedMarginStart ) {
35
+ dividerStyle[ '--wp-components-divider-margin-start' ] =
36
+ resolvedMarginStart;
37
+ }
38
+
39
+ if ( resolvedMarginEnd ) {
40
+ dividerStyle[ '--wp-components-divider-margin-end' ] =
41
+ resolvedMarginEnd;
42
+ }
20
43
 
21
44
  return (
22
45
  <Ariakit.Separator
23
- render={ <DividerView /> }
46
+ className={ clsx( styles.divider, className ) }
47
+ style={ dividerStyle }
24
48
  { ...contextProps }
25
49
  ref={ forwardedRef }
26
50
  />
@@ -0,0 +1,21 @@
1
+ .divider {
2
+ border: 0;
3
+ margin: 0;
4
+
5
+ &[aria-orientation="horizontal"] {
6
+ border-block-end: 1px solid currentColor;
7
+ block-size: 0;
8
+ inline-size: auto;
9
+ margin-block-start: var(--wp-components-divider-margin-start, 0);
10
+ margin-block-end: var(--wp-components-divider-margin-end, 0);
11
+ }
12
+
13
+ &[aria-orientation="vertical"] {
14
+ display: inline;
15
+ border-inline-end: 1px solid currentColor;
16
+ block-size: auto;
17
+ inline-size: 0;
18
+ margin-inline-start: var(--wp-components-divider-margin-start, 0);
19
+ margin-inline-end: var(--wp-components-divider-margin-end, 0);
20
+ }
21
+ }
@@ -9,32 +9,42 @@ import { render, screen } from '@testing-library/react';
9
9
  import { Divider } from '..';
10
10
 
11
11
  describe( 'props', () => {
12
- test( 'should render correctly', () => {
12
+ test( 'should render a horizontal separator by default', () => {
13
13
  render( <Divider /> );
14
- expect( screen.getByRole( 'separator' ) ).toMatchSnapshot();
14
+
15
+ const divider = screen.getByRole( 'separator' );
16
+ expect( divider ).toHaveAttribute( 'aria-orientation', 'horizontal' );
15
17
  } );
16
18
 
17
19
  test( 'should render marginStart', () => {
18
- render( <Divider /> );
19
20
  render( <Divider marginStart={ 5 } /> );
20
21
 
21
- const dividers = screen.getAllByRole( 'separator' );
22
- expect( dividers[ 0 ] ).toMatchStyleDiffSnapshot( dividers[ 1 ] );
22
+ expect( screen.getByRole( 'separator' ) ).toHaveStyle( {
23
+ '--wp-components-divider-margin-start': 'calc(4px * 5)',
24
+ } );
23
25
  } );
24
26
 
25
27
  test( 'should render marginEnd', () => {
26
- render( <Divider /> );
27
28
  render( <Divider marginEnd={ 5 } /> );
28
29
 
29
- const dividers = screen.getAllByRole( 'separator' );
30
- expect( dividers[ 0 ] ).toMatchStyleDiffSnapshot( dividers[ 1 ] );
30
+ expect( screen.getByRole( 'separator' ) ).toHaveStyle( {
31
+ '--wp-components-divider-margin-end': 'calc(4px * 5)',
32
+ } );
31
33
  } );
32
34
 
33
35
  test( 'should render margin', () => {
34
- render( <Divider /> );
35
36
  render( <Divider margin={ 7 } /> );
36
37
 
37
- const dividers = screen.getAllByRole( 'separator' );
38
- expect( dividers[ 0 ] ).toMatchStyleDiffSnapshot( dividers[ 1 ] );
38
+ expect( screen.getByRole( 'separator' ) ).toHaveStyle( {
39
+ '--wp-components-divider-margin-start': 'calc(4px * 7)',
40
+ '--wp-components-divider-margin-end': 'calc(4px * 7)',
41
+ } );
42
+ } );
43
+
44
+ test( 'should render vertical orientation', () => {
45
+ render( <Divider orientation="vertical" /> );
46
+
47
+ const divider = screen.getByRole( 'separator' );
48
+ expect( divider ).toHaveAttribute( 'aria-orientation', 'vertical' );
39
49
  } );
40
50
  } );
@@ -23,12 +23,10 @@ const MyComponentWithConstrainedTabbing = () => {
23
23
  let form = (
24
24
  <form>
25
25
  <TextControl
26
- __next40pxDefaultSize
27
26
  label="Input 1"
28
27
  onChange={ () => {} }
29
28
  />
30
29
  <TextControl
31
- __next40pxDefaultSize
32
30
  label="Input 2"
33
31
  onChange={ () => {} }
34
32
  />
@@ -6,19 +6,17 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { Component } from '@wordpress/element';
10
- import { createHigherOrderComponent } from '@wordpress/compose';
9
+ import { useState, useRef } from '@wordpress/element';
10
+ import {
11
+ createHigherOrderComponent,
12
+ useIsomorphicLayoutEffect,
13
+ } from '@wordpress/compose';
11
14
 
12
15
  type Props = {
13
16
  node?: HTMLElement;
14
17
  [ key: string ]: any;
15
18
  };
16
19
 
17
- type State = {
18
- fallbackStyles?: { [ key: string ]: any };
19
- grabStylesCompleted: boolean;
20
- };
21
-
22
20
  export default (
23
21
  mapNodeToProps: (
24
22
  node: HTMLElement,
@@ -26,69 +24,41 @@ export default (
26
24
  ) => { [ key: string ]: any }
27
25
  ) =>
28
26
  createHigherOrderComponent( ( WrappedComponent ) => {
29
- return class WithFallbackStyles extends Component< Props, State > {
30
- nodeRef?: HTMLElement;
27
+ return function WithFallbackStyles( props: Props ) {
28
+ const [ fallbackStyles, setFallbackStyles ] = useState<
29
+ { [ key: string ]: any } | undefined
30
+ >( undefined );
31
31
 
32
- constructor( props: Props ) {
33
- super( props );
34
- this.nodeRef = this.props.node;
35
- this.state = {
36
- fallbackStyles: undefined,
37
- grabStylesCompleted: false,
38
- };
32
+ const nodeRef = useRef< HTMLDivElement >( null );
39
33
 
40
- this.bindRef = this.bindRef.bind( this );
41
- }
42
-
43
- bindRef( node: HTMLDivElement ) {
44
- if ( ! node ) {
45
- return;
46
- }
47
- this.nodeRef = node;
48
- }
34
+ // Runs before paint, so the fallback styles apply without a flash.
35
+ useIsomorphicLayoutEffect( () => {
36
+ const node = props.node ?? nodeRef.current;
37
+ // Derived from fallbackStyles: the grab is complete once every
38
+ // mapped value has resolved to a truthy style.
39
+ const grabStylesCompleted =
40
+ !! fallbackStyles &&
41
+ Object.values( fallbackStyles ).every( Boolean );
49
42
 
50
- componentDidMount() {
51
- this.grabFallbackStyles();
52
- }
53
-
54
- componentDidUpdate() {
55
- this.grabFallbackStyles();
56
- }
57
-
58
- grabFallbackStyles() {
59
- const { grabStylesCompleted, fallbackStyles } = this.state;
60
- if ( this.nodeRef && ! grabStylesCompleted ) {
61
- const newFallbackStyles = mapNodeToProps(
62
- this.nodeRef,
63
- this.props
64
- );
43
+ if ( node && ! grabStylesCompleted ) {
44
+ const newFallbackStyles = mapNodeToProps( node, props );
65
45
 
66
46
  if (
67
47
  ! fastDeepEqual( newFallbackStyles, fallbackStyles )
68
48
  ) {
69
- this.setState( {
70
- fallbackStyles: newFallbackStyles,
71
- grabStylesCompleted:
72
- Object.values( newFallbackStyles ).every(
73
- Boolean
74
- ),
75
- } );
49
+ setFallbackStyles( newFallbackStyles );
76
50
  }
77
51
  }
78
- }
52
+ } );
53
+
54
+ const wrappedComponent = (
55
+ <WrappedComponent { ...props } { ...fallbackStyles } />
56
+ );
79
57
 
80
- render() {
81
- const wrappedComponent = (
82
- <WrappedComponent
83
- { ...this.props }
84
- { ...this.state.fallbackStyles }
85
- />
86
- );
87
- return this.props.node ? (
88
- wrappedComponent
89
- ) : (
90
- <div ref={ this.bindRef }> { wrappedComponent } </div>
91
- );
92
- }
58
+ return props.node ? (
59
+ wrappedComponent
60
+ ) : (
61
+ <div ref={ nodeRef }> { wrappedComponent } </div>
62
+ );
93
63
  };
94
64
  }, 'withFallbackStyles' );
@@ -0,0 +1,47 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import withFallbackStyles from '..';
10
+
11
+ describe( 'withFallbackStyles', () => {
12
+ const Wrapped = ( props: { fallbackColor?: string } ) => (
13
+ <div>{ props.fallbackColor ?? 'no-fallback' }</div>
14
+ );
15
+
16
+ it( 'derives the fallback styles from the provided node prop', () => {
17
+ const node = document.createElement( 'span' );
18
+ node.dataset.color = 'rgb(1, 2, 3)';
19
+ const mapNodeToProps = jest.fn( ( el: HTMLElement ) => ( {
20
+ fallbackColor: el.dataset.color,
21
+ } ) );
22
+ const Component = withFallbackStyles( mapNodeToProps )( Wrapped );
23
+
24
+ render( <Component node={ node } /> );
25
+
26
+ // The rendered value proves the actual node was read by mapNodeToProps.
27
+ expect( mapNodeToProps ).toHaveBeenCalledWith(
28
+ node,
29
+ expect.objectContaining( { node } )
30
+ );
31
+ expect( screen.getByText( 'rgb(1, 2, 3)' ) ).toBeInTheDocument();
32
+ } );
33
+
34
+ it( 'derives the fallback styles from the internal wrapper node when no node prop is given', () => {
35
+ const mapNodeToProps = jest.fn( ( el: HTMLElement ) => ( {
36
+ fallbackColor: el.tagName.toLowerCase(),
37
+ } ) );
38
+ const Component = withFallbackStyles( mapNodeToProps )( Wrapped );
39
+
40
+ render( <Component /> );
41
+
42
+ // The HOC wraps in a <div>, so reading the node's tagName yields 'div'.
43
+ expect( mapNodeToProps ).toHaveBeenCalled();
44
+ expect( mapNodeToProps.mock.calls[ 0 ][ 0 ].tagName ).toBe( 'DIV' );
45
+ expect( screen.getByText( 'div' ) ).toBeInTheDocument();
46
+ } );
47
+ } );
@@ -14,7 +14,6 @@ const EnhancedComponent = withFocusReturn( () => (
14
14
  <div>
15
15
  Focus will return to the previous input when this component is unmounted
16
16
  <TextControl
17
- __next40pxDefaultSize
18
17
  autoFocus={ true }
19
18
  onChange={ () => {} }
20
19
  />
@@ -31,7 +30,6 @@ const MyComponentWithFocusReturn = () => {
31
30
  return (
32
31
  <div>
33
32
  <TextControl
34
- __next40pxDefaultSize
35
33
  placeholder="Type something"
36
34
  value={ text }
37
35
  onChange={ ( value ) => setText( value ) }
@@ -26,21 +26,6 @@ Default.args = {
26
26
  icon: wordpress,
27
27
  };
28
28
 
29
- export const FillColor: StoryFn< typeof Icon > = ( args ) => {
30
- return (
31
- <div
32
- style={ {
33
- fill: 'blue',
34
- } }
35
- >
36
- <Icon { ...args } />
37
- </div>
38
- );
39
- };
40
- FillColor.args = {
41
- ...Default.args,
42
- };
43
-
44
29
  /**
45
30
  * When `icon` is a function, it will be passed the `size` prop and any other additional props.
46
31
  */
@@ -31,22 +31,6 @@ const resetTypeahead = () => {
31
31
 
32
32
  describe( 'Menu', () => {
33
33
  let user: ReturnType< typeof userEvent.setup >;
34
- let originalGetClientRects: () => DOMRectList;
35
-
36
- beforeAll( () => {
37
- // Code that considers focusability considers rects in whether the
38
- // element is visible, but since jsdom does not simulate layout, we
39
- // need to fake it. This should ideally be a global test mock to
40
- // faithfully emulate a true browser environment.
41
- originalGetClientRects = window.Element.prototype.getClientRects;
42
- window.Element.prototype.getClientRects = jest.fn(
43
- () => [ { width: 1, height: 1 } ] as unknown as DOMRectList
44
- );
45
- } );
46
-
47
- afterAll( () => {
48
- window.Element.prototype.getClientRects = originalGetClientRects;
49
- } );
50
34
 
51
35
  beforeEach( () => {
52
36
  user = userEvent.setup();
@@ -20,6 +20,7 @@ exports[`MenuItem should match snapshot when all props provided 1`] = `
20
20
  <svg
21
21
  aria-hidden="true"
22
22
  class="components-menu-items__item-icon has-icon-right"
23
+ fill="currentColor"
23
24
  focusable="false"
24
25
  height="24"
25
26
  viewBox="0 0 24 24"
@@ -79,6 +80,7 @@ exports[`MenuItem should match snapshot when isSelected and role are optionally
79
80
  <svg
80
81
  aria-hidden="true"
81
82
  class="components-menu-items__item-icon has-icon-right"
83
+ fill="currentColor"
82
84
  focusable="false"
83
85
  height="24"
84
86
  viewBox="0 0 24 24"