@wordpress/components 21.1.2-next.4d3b314fd5.0 → 21.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 (152) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/build/border-box-control/utils.js +42 -2
  3. package/build/border-box-control/utils.js.map +1 -1
  4. package/build/combobox-control/index.js +0 -1
  5. package/build/combobox-control/index.js.map +1 -1
  6. package/build/custom-select-control/index.js +4 -2
  7. package/build/custom-select-control/index.js.map +1 -1
  8. package/build/font-size-picker/index.js +46 -55
  9. package/build/font-size-picker/index.js.map +1 -1
  10. package/build/font-size-picker/styles.js +73 -0
  11. package/build/font-size-picker/styles.js.map +1 -0
  12. package/build/font-size-picker/types.js +6 -0
  13. package/build/font-size-picker/types.js.map +1 -0
  14. package/build/font-size-picker/utils.js +17 -15
  15. package/build/font-size-picker/utils.js.map +1 -1
  16. package/build/form-token-field/token-input.js +20 -1
  17. package/build/form-token-field/token-input.js.map +1 -1
  18. package/build/index.js +6 -0
  19. package/build/index.js.map +1 -1
  20. package/build/navigator/navigator-screen/component.js +8 -1
  21. package/build/navigator/navigator-screen/component.js.map +1 -1
  22. package/build/resizable-box/resize-tooltip/utils.js +12 -14
  23. package/build/resizable-box/resize-tooltip/utils.js.map +1 -1
  24. package/build/sandbox/index.js +13 -8
  25. package/build/sandbox/index.js.map +1 -1
  26. package/build/sandbox/index.native.js +3 -1
  27. package/build/sandbox/index.native.js.map +1 -1
  28. package/build/search-control/index.native.js +6 -2
  29. package/build/search-control/index.native.js.map +1 -1
  30. package/build/slot-fill/bubbles-virtually/slot-fill-context.js +8 -2
  31. package/build/slot-fill/bubbles-virtually/slot-fill-context.js.map +1 -1
  32. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js +31 -41
  33. package/build/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  34. package/build/slot-fill/bubbles-virtually/use-slot-fills.js +39 -0
  35. package/build/slot-fill/bubbles-virtually/use-slot-fills.js.map +1 -0
  36. package/build/slot-fill/bubbles-virtually/use-slot.js +13 -4
  37. package/build/slot-fill/bubbles-virtually/use-slot.js.map +1 -1
  38. package/build/slot-fill/index.js +8 -0
  39. package/build/slot-fill/index.js.map +1 -1
  40. package/build/toggle-group-control/toggle-group-control-option-base/styles.js +8 -8
  41. package/build/toggle-group-control/toggle-group-control-option-base/styles.js.map +1 -1
  42. package/build-module/border-box-control/utils.js +36 -1
  43. package/build-module/border-box-control/utils.js.map +1 -1
  44. package/build-module/combobox-control/index.js +0 -1
  45. package/build-module/combobox-control/index.js.map +1 -1
  46. package/build-module/custom-select-control/index.js +2 -1
  47. package/build-module/custom-select-control/index.js.map +1 -1
  48. package/build-module/font-size-picker/index.js +45 -53
  49. package/build-module/font-size-picker/index.js.map +1 -1
  50. package/build-module/font-size-picker/styles.js +62 -0
  51. package/build-module/font-size-picker/styles.js.map +1 -0
  52. package/build-module/font-size-picker/types.js +2 -0
  53. package/build-module/font-size-picker/types.js.map +1 -0
  54. package/build-module/font-size-picker/utils.js +21 -15
  55. package/build-module/font-size-picker/utils.js.map +1 -1
  56. package/build-module/form-token-field/token-input.js +21 -2
  57. package/build-module/form-token-field/token-input.js.map +1 -1
  58. package/build-module/index.js +1 -1
  59. package/build-module/index.js.map +1 -1
  60. package/build-module/navigator/navigator-screen/component.js +8 -1
  61. package/build-module/navigator/navigator-screen/component.js.map +1 -1
  62. package/build-module/resizable-box/resize-tooltip/utils.js +13 -15
  63. package/build-module/resizable-box/resize-tooltip/utils.js.map +1 -1
  64. package/build-module/sandbox/index.js +13 -8
  65. package/build-module/sandbox/index.js.map +1 -1
  66. package/build-module/sandbox/index.native.js +3 -1
  67. package/build-module/sandbox/index.native.js.map +1 -1
  68. package/build-module/search-control/index.native.js +6 -2
  69. package/build-module/search-control/index.native.js.map +1 -1
  70. package/build-module/slot-fill/bubbles-virtually/slot-fill-context.js +7 -2
  71. package/build-module/slot-fill/bubbles-virtually/slot-fill-context.js.map +1 -1
  72. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js +30 -42
  73. package/build-module/slot-fill/bubbles-virtually/slot-fill-provider.js.map +1 -1
  74. package/build-module/slot-fill/bubbles-virtually/use-slot-fills.js +27 -0
  75. package/build-module/slot-fill/bubbles-virtually/use-slot-fills.js.map +1 -0
  76. package/build-module/slot-fill/bubbles-virtually/use-slot.js +13 -5
  77. package/build-module/slot-fill/bubbles-virtually/use-slot.js.map +1 -1
  78. package/build-module/slot-fill/index.js +1 -0
  79. package/build-module/slot-fill/index.js.map +1 -1
  80. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.js +7 -7
  81. package/build-module/toggle-group-control/toggle-group-control-option-base/styles.js.map +1 -1
  82. package/build-style/style-rtl.css +1 -98
  83. package/build-style/style.css +1 -98
  84. package/build-types/border-box-control/utils.d.ts +1 -3
  85. package/build-types/border-box-control/utils.d.ts.map +1 -1
  86. package/build-types/custom-select-control/index.d.ts +13 -0
  87. package/build-types/custom-select-control/index.d.ts.map +1 -0
  88. package/build-types/custom-select-control/styles.d.ts +9 -0
  89. package/build-types/custom-select-control/styles.d.ts.map +1 -0
  90. package/build-types/font-size-picker/index.d.ts +5 -0
  91. package/build-types/font-size-picker/index.d.ts.map +1 -0
  92. package/build-types/font-size-picker/stories/e2e/index.d.ts +16 -0
  93. package/build-types/font-size-picker/stories/e2e/index.d.ts.map +1 -0
  94. package/build-types/font-size-picker/stories/index.d.ts +31 -0
  95. package/build-types/font-size-picker/stories/index.d.ts.map +1 -0
  96. package/build-types/font-size-picker/styles.d.ts +27 -0
  97. package/build-types/font-size-picker/styles.d.ts.map +1 -0
  98. package/build-types/font-size-picker/test/index.d.ts +2 -0
  99. package/build-types/font-size-picker/test/index.d.ts.map +1 -0
  100. package/build-types/font-size-picker/test/utils.d.ts +2 -0
  101. package/build-types/font-size-picker/test/utils.d.ts.map +1 -0
  102. package/build-types/font-size-picker/types.d.ts +93 -0
  103. package/build-types/font-size-picker/types.d.ts.map +1 -0
  104. package/build-types/font-size-picker/utils.d.ts +41 -0
  105. package/build-types/font-size-picker/utils.d.ts.map +1 -0
  106. package/build-types/form-token-field/token-input.d.ts.map +1 -1
  107. package/build-types/navigator/navigator-screen/component.d.ts.map +1 -1
  108. package/build-types/slot-fill/bubbles-virtually/slot-fill-context.d.ts +2 -2
  109. package/build-types/slot-fill/bubbles-virtually/slot-fill-context.d.ts.map +1 -1
  110. package/build-types/slot-fill/bubbles-virtually/slot-fill-provider.d.ts.map +1 -1
  111. package/build-types/slot-fill/bubbles-virtually/use-slot-fills.d.ts +2 -0
  112. package/build-types/slot-fill/bubbles-virtually/use-slot-fills.d.ts.map +1 -0
  113. package/build-types/slot-fill/bubbles-virtually/use-slot.d.ts.map +1 -1
  114. package/build-types/slot-fill/index.d.ts +1 -0
  115. package/build-types/slot-fill/index.d.ts.map +1 -1
  116. package/build-types/toggle-group-control/toggle-group-control-option-base/styles.d.ts +1 -1
  117. package/build-types/toggle-group-control/toggle-group-control-option-base/styles.d.ts.map +1 -1
  118. package/package.json +19 -18
  119. package/src/alignment-matrix-control/test/index.js +17 -62
  120. package/src/border-box-control/test/utils.js +48 -0
  121. package/src/border-box-control/utils.ts +44 -1
  122. package/src/combobox-control/index.js +0 -5
  123. package/src/custom-select-control/index.js +2 -1
  124. package/src/font-size-picker/{index.js → index.tsx} +113 -79
  125. package/src/font-size-picker/stories/e2e/{index.js → index.tsx} +13 -4
  126. package/src/font-size-picker/stories/{index.js → index.tsx} +42 -36
  127. package/src/font-size-picker/styles.ts +44 -0
  128. package/src/font-size-picker/test/{index.js → index.tsx} +1 -1
  129. package/src/font-size-picker/test/{utils.js → utils.ts} +6 -2
  130. package/src/font-size-picker/types.ts +98 -0
  131. package/src/font-size-picker/{utils.js → utils.ts} +51 -27
  132. package/src/form-token-field/test/index.tsx +22 -1
  133. package/src/form-token-field/token-input.tsx +25 -3
  134. package/src/index.js +1 -0
  135. package/src/navigator/navigator-screen/component.tsx +8 -1
  136. package/src/navigator/test/index.js +119 -54
  137. package/src/placeholder/style.scss +2 -2
  138. package/src/resizable-box/resize-tooltip/utils.ts +13 -13
  139. package/src/sandbox/index.js +13 -7
  140. package/src/sandbox/index.native.js +3 -0
  141. package/src/search-control/index.native.js +6 -0
  142. package/src/slot-fill/bubbles-virtually/slot-fill-context.js +6 -2
  143. package/src/slot-fill/bubbles-virtually/slot-fill-provider.js +51 -58
  144. package/src/slot-fill/bubbles-virtually/use-slot-fills.js +24 -0
  145. package/src/slot-fill/bubbles-virtually/use-slot.js +11 -6
  146. package/src/slot-fill/index.js +1 -0
  147. package/src/style.scss +0 -1
  148. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +1 -0
  149. package/src/toggle-group-control/toggle-group-control-option-base/styles.ts +9 -7
  150. package/tsconfig.json +0 -2
  151. package/tsconfig.tsbuildinfo +1 -1
  152. package/src/font-size-picker/style.scss +0 -78
@@ -0,0 +1,98 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { ReactNode } from 'react';
5
+
6
+ export type FontSizePickerProps = {
7
+ /**
8
+ * If `true`, it will not be possible to choose a custom fontSize. The user
9
+ * will be forced to pick one of the pre-defined sizes passed in fontSizes.
10
+ *
11
+ * @default false
12
+ */
13
+ disableCustomFontSizes?: boolean;
14
+ /**
15
+ * If no value exists, this prop defines the starting position for the font
16
+ * size picker slider. Only relevant if `withSlider` is `true`.
17
+ */
18
+ fallbackFontSize?: number;
19
+ /**
20
+ * An array of font size objects. The object should contain properties size,
21
+ * name, and slug.
22
+ */
23
+ fontSizes?: FontSize[];
24
+ /**
25
+ * A function that receives the new font size value.
26
+ * If onChange is called without any parameter, it should reset the value,
27
+ * attending to what reset means in that context, e.g., set the font size to
28
+ * undefined or set the font size a starting value.
29
+ */
30
+ onChange?: ( value: number | string | undefined ) => void;
31
+ /**
32
+ * The current font size value.
33
+ */
34
+ value?: number | string;
35
+ /**
36
+ * If `true`, the UI will contain a slider, instead of a numeric text input
37
+ * field. If `false`, no slider will be present.
38
+ *
39
+ * @default false
40
+ */
41
+ withSlider?: boolean;
42
+ /**
43
+ * If `true`, a reset button will be displayed alongside the input field
44
+ * when a custom font size is active. Has no effect when
45
+ * `disableCustomFontSizes` or `withSlider` is `true`.
46
+ *
47
+ * @default true
48
+ */
49
+ withReset?: boolean;
50
+ /**
51
+ * Start opting into the new margin-free styles that will become the default
52
+ * in a future version, currently scheduled to be WordPress 6.4. (The prop
53
+ * can be safely removed once this happens.)
54
+ *
55
+ * @default false
56
+ */
57
+ __nextHasNoMarginBottom?: boolean;
58
+ /**
59
+ * Size of the control.
60
+ *
61
+ * @default default
62
+ */
63
+ size?: 'default' | '__unstable-large';
64
+ };
65
+
66
+ export type FontSize = {
67
+ /**
68
+ * The property `size` contains a number with the font size value, in `px` or
69
+ * a string specifying the font size CSS property that should be used eg:
70
+ * "13px", "1em", or "clamp(12px, 5vw, 100px)".
71
+ */
72
+ size: number | string;
73
+ /**
74
+ * The `name` property includes a label for that font size e.g.: `Small`.
75
+ */
76
+ name?: string;
77
+ /**
78
+ * The `slug` property is a string with a unique identifier for the font
79
+ * size. Used for the class generation process.
80
+ */
81
+ slug: string;
82
+ };
83
+
84
+ export type FontSizeOption = Omit< FontSize, 'size' > &
85
+ Partial< Pick< FontSize, 'size' > >;
86
+
87
+ export type FontSizeSelectOption = Pick< FontSizeOption, 'size' > & {
88
+ key: string;
89
+ name?: string;
90
+ __experimentalHint: ReactNode;
91
+ };
92
+
93
+ export type FontSizeToggleGroupOption = {
94
+ key: string;
95
+ value: number | string;
96
+ label: string;
97
+ name: string;
98
+ };
@@ -3,6 +3,17 @@
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type {
10
+ FontSize,
11
+ FontSizeOption,
12
+ FontSizeSelectOption,
13
+ FontSizeToggleGroupOption,
14
+ FontSizePickerProps,
15
+ } from './types';
16
+
6
17
  const DEFAULT_FONT_SIZE = 'default';
7
18
  const DEFAULT_FONT_SIZE_OPTION = {
8
19
  slug: DEFAULT_FONT_SIZE,
@@ -36,13 +47,18 @@ const FONT_SIZES_ALIASES = [
36
47
  * Helper util to split a font size to its numeric value
37
48
  * and its `unit`, if exists.
38
49
  *
39
- * @param {string|number} size Font size.
40
- * @return {[number, string]} An array with the numeric value and the unit if exists.
50
+ * @param size Font size.
51
+ * @return An array with the numeric value and the unit if exists.
41
52
  */
42
- export function splitValueAndUnitFromSize( size ) {
43
- const [ numericValue, unit ] = `${ size }`.match( /[\d\.]+|\D+/g );
53
+ export function splitValueAndUnitFromSize(
54
+ size: NonNullable< FontSizePickerProps[ 'value' ] >
55
+ ) {
56
+ const [ numericValue, unit ] = `${ size }`.match( /[\d\.]+|\D+/g ) ?? [];
44
57
 
45
- if ( ! isNaN( parseFloat( numericValue ) ) && isFinite( numericValue ) ) {
58
+ if (
59
+ ! isNaN( parseFloat( numericValue ) ) &&
60
+ isFinite( Number( numericValue ) )
61
+ ) {
46
62
  return [ numericValue, unit ];
47
63
  }
48
64
 
@@ -53,28 +69,30 @@ export function splitValueAndUnitFromSize( size ) {
53
69
  * Some themes use css vars for their font sizes, so until we
54
70
  * have the way of calculating them don't display them.
55
71
  *
56
- * @param {string|number} value The value that is checked.
57
- * @return {boolean} Whether the value is a simple css value.
72
+ * @param value The value that is checked.
73
+ * @return Whether the value is a simple css value.
58
74
  */
59
- export function isSimpleCssValue( value ) {
75
+ export function isSimpleCssValue(
76
+ value: NonNullable< FontSizePickerProps[ 'value' ] >
77
+ ) {
60
78
  const sizeRegex = /^[\d\.]+(px|em|rem|vw|vh|%)?$/i;
61
- return sizeRegex.test( value );
79
+ return sizeRegex.test( String( value ) );
62
80
  }
63
81
 
64
82
  /**
65
83
  * Return font size options in the proper format depending
66
84
  * on the currently used control (select, toggle group).
67
85
  *
68
- * @param {boolean} useSelectControl Whether to use a select control.
69
- * @param {Object[]} optionsArray Array of available font sizes objects.
70
- * @param {boolean} disableCustomFontSizes Flag that indicates if custom font sizes are disabled.
71
- * @return {Object[]|null} Array of font sizes in proper format for the used control.
86
+ * @param useSelectControl Whether to use a select control.
87
+ * @param optionsArray Array of available font sizes objects.
88
+ * @param disableCustomFontSizes Flag that indicates if custom font sizes are disabled.
89
+ * @return Array of font sizes in proper format for the used control.
72
90
  */
73
91
  export function getFontSizeOptions(
74
- useSelectControl,
75
- optionsArray,
76
- disableCustomFontSizes
77
- ) {
92
+ useSelectControl: boolean,
93
+ optionsArray: FontSize[],
94
+ disableCustomFontSizes: boolean
95
+ ): FontSizeSelectOption[] | FontSizeToggleGroupOption[] | null {
78
96
  if ( disableCustomFontSizes && ! optionsArray.length ) {
79
97
  return null;
80
98
  }
@@ -83,8 +101,11 @@ export function getFontSizeOptions(
83
101
  : getToggleGroupOptions( optionsArray );
84
102
  }
85
103
 
86
- function getSelectOptions( optionsArray, disableCustomFontSizes ) {
87
- const options = [
104
+ function getSelectOptions(
105
+ optionsArray: FontSize[],
106
+ disableCustomFontSizes: boolean
107
+ ): FontSizeSelectOption[] {
108
+ const options: FontSizeOption[] = [
88
109
  DEFAULT_FONT_SIZE_OPTION,
89
110
  ...optionsArray,
90
111
  ...( disableCustomFontSizes ? [] : [ CUSTOM_FONT_SIZE_OPTION ] ),
@@ -94,21 +115,21 @@ function getSelectOptions( optionsArray, disableCustomFontSizes ) {
94
115
  name,
95
116
  size,
96
117
  __experimentalHint:
97
- size && isSimpleCssValue( size ) && parseFloat( size ),
118
+ size && isSimpleCssValue( size ) && parseFloat( String( size ) ),
98
119
  } ) );
99
120
  }
100
121
 
101
122
  /**
102
123
  * Build options for the toggle group options.
103
124
  *
104
- * @param {Array} optionsArray An array of font size options.
105
- * @param {string[]} labelAliases An array of alternative labels.
106
- * @return {Array} Remapped optionsArray.
125
+ * @param optionsArray An array of font size options.
126
+ * @param labelAliases An array of alternative labels.
127
+ * @return Remapped optionsArray.
107
128
  */
108
129
  export function getToggleGroupOptions(
109
- optionsArray,
110
- labelAliases = FONT_SIZES_ALIASES
111
- ) {
130
+ optionsArray: FontSize[],
131
+ labelAliases: string[] = FONT_SIZES_ALIASES
132
+ ): FontSizeToggleGroupOption[] {
112
133
  return optionsArray.map( ( { slug, size, name }, index ) => {
113
134
  return {
114
135
  key: slug,
@@ -119,7 +140,10 @@ export function getToggleGroupOptions(
119
140
  } );
120
141
  }
121
142
 
122
- export function getSelectedOption( fontSizes, value ) {
143
+ export function getSelectedOption(
144
+ fontSizes: FontSize[],
145
+ value: FontSizePickerProps[ 'value' ]
146
+ ): FontSizeOption {
123
147
  if ( ! value ) {
124
148
  return DEFAULT_FONT_SIZE_OPTION;
125
149
  }
@@ -2057,7 +2057,12 @@ describe( 'FormTokenField', () => {
2057
2057
 
2058
2058
  const suggestions = [ 'Pine', 'Pistachio', 'Sage' ];
2059
2059
 
2060
- render( <FormTokenFieldWithState suggestions={ suggestions } /> );
2060
+ render(
2061
+ <>
2062
+ <FormTokenFieldWithState suggestions={ suggestions } />
2063
+ <button>Click me</button>
2064
+ </>
2065
+ );
2061
2066
 
2062
2067
  // No suggestions visible
2063
2068
  const input = screen.getByRole( 'combobox' );
@@ -2093,6 +2098,22 @@ describe( 'FormTokenField', () => {
2093
2098
  pineSuggestion.id
2094
2099
  );
2095
2100
 
2101
+ // Blur the input and make sure that the `aria-activedescendant`
2102
+ // is removed
2103
+ const button = screen.getByRole( 'button', { name: 'Click me' } );
2104
+
2105
+ await user.click( button );
2106
+
2107
+ expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
2108
+
2109
+ // Focus the input again, `aria-activedescendant` should be added back.
2110
+ await user.click( input );
2111
+
2112
+ expect( input ).toHaveAttribute(
2113
+ 'aria-activedescendant',
2114
+ pineSuggestion.id
2115
+ );
2116
+
2096
2117
  // Add the suggestion, which hides the list
2097
2118
  await user.keyboard( '[Enter]' );
2098
2119
 
@@ -2,12 +2,12 @@
2
2
  * External dependencies
3
3
  */
4
4
  import classnames from 'classnames';
5
- import type { ChangeEvent, ForwardedRef } from 'react';
5
+ import type { ChangeEvent, ForwardedRef, FocusEventHandler } from 'react';
6
6
 
7
7
  /**
8
8
  * WordPress dependencies
9
9
  */
10
- import { forwardRef } from '@wordpress/element';
10
+ import { forwardRef, useState } from '@wordpress/element';
11
11
 
12
12
  /**
13
13
  * Internal dependencies
@@ -26,9 +26,13 @@ export function UnForwardedTokenInput(
26
26
  selectedSuggestionIndex,
27
27
  className,
28
28
  onChange,
29
+ onFocus,
30
+ onBlur,
29
31
  ...restProps
30
32
  } = props;
31
33
 
34
+ const [ hasFocus, setHasFocus ] = useState( false );
35
+
32
36
  const size = value ? value.length + 1 : 0;
33
37
 
34
38
  const onChangeHandler = ( event: ChangeEvent< HTMLInputElement > ) => {
@@ -39,6 +43,18 @@ export function UnForwardedTokenInput(
39
43
  }
40
44
  };
41
45
 
46
+ const onFocusHandler: FocusEventHandler< HTMLInputElement > = ( e ) => {
47
+ setHasFocus( true );
48
+ onFocus?.( e );
49
+ };
50
+
51
+ const onBlurHandler: React.FocusEventHandler< HTMLInputElement > = (
52
+ e
53
+ ) => {
54
+ setHasFocus( false );
55
+ onBlur?.( e );
56
+ };
57
+
42
58
  return (
43
59
  <input
44
60
  ref={ ref }
@@ -47,6 +63,8 @@ export function UnForwardedTokenInput(
47
63
  { ...restProps }
48
64
  value={ value || '' }
49
65
  onChange={ onChangeHandler }
66
+ onFocus={ onFocusHandler }
67
+ onBlur={ onBlurHandler }
50
68
  size={ size }
51
69
  className={ classnames(
52
70
  className,
@@ -62,7 +80,11 @@ export function UnForwardedTokenInput(
62
80
  : undefined
63
81
  }
64
82
  aria-activedescendant={
65
- selectedSuggestionIndex !== -1
83
+ // Only add the `aria-activedescendant` attribute when:
84
+ // - the user is actively interacting with the input (`hasFocus`)
85
+ // - there is a selected suggestion (`selectedSuggestionIndex !== -1`)
86
+ // - the list of suggestions are rendered in the DOM (`isExpanded`)
87
+ hasFocus && selectedSuggestionIndex !== -1 && isExpanded
66
88
  ? `components-form-token-suggestions-${ instanceId }-${ selectedSuggestionIndex }`
67
89
  : undefined
68
90
  }
package/src/index.js CHANGED
@@ -188,6 +188,7 @@ export {
188
188
  Fill,
189
189
  Provider as SlotFillProvider,
190
190
  useSlot as __experimentalUseSlot,
191
+ useSlotFills as __experimentalUseSlotFills,
191
192
  } from './slot-fill';
192
193
  export { default as __experimentalStyleProvider } from './style-provider';
193
194
  export { ZStack as __experimentalZStack } from './z-stack';
@@ -83,6 +83,14 @@ function NavigatorScreen( props: Props, forwardedRef: ForwardedRef< any > ) {
83
83
  return;
84
84
  }
85
85
 
86
+ const activeElement = wrapperRef.current.ownerDocument.activeElement;
87
+
88
+ // If an element is already focused within the wrapper do not focus the
89
+ // element. This prevents inputs or buttons from losing focus unecessarily.
90
+ if ( wrapperRef.current.contains( activeElement ) ) {
91
+ return;
92
+ }
93
+
86
94
  let elementToFocus: HTMLElement | null = null;
87
95
 
88
96
  // When navigating back, if a selector is provided, use it to look for the
@@ -99,7 +107,6 @@ function NavigatorScreen( props: Props, forwardedRef: ForwardedRef< any > ) {
99
107
  const firstTabbable = (
100
108
  focus.tabbable.find( wrapperRef.current ) as HTMLElement[]
101
109
  )[ 0 ];
102
-
103
110
  elementToFocus = firstTabbable ?? wrapperRef.current;
104
111
  }
105
112
 
@@ -2,6 +2,12 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { render, screen, fireEvent } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { useState } from '@wordpress/element';
5
11
 
6
12
  /**
7
13
  * Internal dependencies
@@ -86,60 +92,74 @@ function CustomNavigatorBackButton( { onClick, ...props } ) {
86
92
  const MyNavigation = ( {
87
93
  initialPath = PATHS.HOME,
88
94
  onNavigatorButtonClick,
89
- } ) => (
90
- <NavigatorProvider initialPath={ initialPath }>
91
- <NavigatorScreen path={ PATHS.HOME }>
92
- <p>This is the home screen.</p>
93
- <CustomNavigatorButton
94
- path={ PATHS.NOT_FOUND }
95
- onClick={ onNavigatorButtonClick }
96
- >
97
- Navigate to non-existing screen.
98
- </CustomNavigatorButton>
99
- <CustomNavigatorButton
100
- path={ PATHS.CHILD }
101
- onClick={ onNavigatorButtonClick }
102
- >
103
- Navigate to child screen.
104
- </CustomNavigatorButton>
105
- <CustomNavigatorButton
106
- path={ PATHS.INVALID_HTML_ATTRIBUTE }
107
- onClick={ onNavigatorButtonClick }
108
- >
109
- Navigate to screen with an invalid HTML value as a path.
110
- </CustomNavigatorButton>
111
- </NavigatorScreen>
112
-
113
- <NavigatorScreen path={ PATHS.CHILD }>
114
- <p>This is the child screen.</p>
115
- <CustomNavigatorButtonWithFocusRestoration
116
- path={ PATHS.NESTED }
117
- onClick={ onNavigatorButtonClick }
118
- >
119
- Navigate to nested screen.
120
- </CustomNavigatorButtonWithFocusRestoration>
121
- <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
122
- Go back
123
- </CustomNavigatorBackButton>
124
- </NavigatorScreen>
125
-
126
- <NavigatorScreen path={ PATHS.NESTED }>
127
- <p>This is the nested screen.</p>
128
- <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
129
- Go back
130
- </CustomNavigatorBackButton>
131
- </NavigatorScreen>
132
-
133
- <NavigatorScreen path={ PATHS.INVALID_HTML_ATTRIBUTE }>
134
- <p>This is the screen with an invalid HTML value as a path.</p>
135
- <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
136
- Go back
137
- </CustomNavigatorBackButton>
138
- </NavigatorScreen>
139
-
140
- { /* A `NavigatorScreen` with `path={ PATHS.NOT_FOUND }` is purposefully not included. */ }
141
- </NavigatorProvider>
142
- );
95
+ } ) => {
96
+ const [ inputValue, setInputValue ] = useState( '' );
97
+ return (
98
+ <NavigatorProvider initialPath={ initialPath }>
99
+ <NavigatorScreen path={ PATHS.HOME }>
100
+ <p>This is the home screen.</p>
101
+ <CustomNavigatorButton
102
+ path={ PATHS.NOT_FOUND }
103
+ onClick={ onNavigatorButtonClick }
104
+ >
105
+ Navigate to non-existing screen.
106
+ </CustomNavigatorButton>
107
+ <CustomNavigatorButton
108
+ path={ PATHS.CHILD }
109
+ onClick={ onNavigatorButtonClick }
110
+ >
111
+ Navigate to child screen.
112
+ </CustomNavigatorButton>
113
+ <CustomNavigatorButton
114
+ path={ PATHS.INVALID_HTML_ATTRIBUTE }
115
+ onClick={ onNavigatorButtonClick }
116
+ >
117
+ Navigate to screen with an invalid HTML value as a path.
118
+ </CustomNavigatorButton>
119
+ </NavigatorScreen>
120
+
121
+ <NavigatorScreen path={ PATHS.CHILD }>
122
+ <p>This is the child screen.</p>
123
+ <CustomNavigatorButtonWithFocusRestoration
124
+ path={ PATHS.NESTED }
125
+ onClick={ onNavigatorButtonClick }
126
+ >
127
+ Navigate to nested screen.
128
+ </CustomNavigatorButtonWithFocusRestoration>
129
+ <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
130
+ Go back
131
+ </CustomNavigatorBackButton>
132
+
133
+ <label htmlFor="test-input">This is a test input</label>
134
+ <input
135
+ name="test-input"
136
+ // eslint-disable-next-line no-restricted-syntax
137
+ id="test-input"
138
+ onChange={ ( e ) => {
139
+ setInputValue( e.target.value );
140
+ } }
141
+ value={ inputValue }
142
+ />
143
+ </NavigatorScreen>
144
+
145
+ <NavigatorScreen path={ PATHS.NESTED }>
146
+ <p>This is the nested screen.</p>
147
+ <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
148
+ Go back
149
+ </CustomNavigatorBackButton>
150
+ </NavigatorScreen>
151
+
152
+ <NavigatorScreen path={ PATHS.INVALID_HTML_ATTRIBUTE }>
153
+ <p>This is the screen with an invalid HTML value as a path.</p>
154
+ <CustomNavigatorBackButton onClick={ onNavigatorButtonClick }>
155
+ Go back
156
+ </CustomNavigatorBackButton>
157
+ </NavigatorScreen>
158
+
159
+ { /* A `NavigatorScreen` with `path={ PATHS.NOT_FOUND }` is purposefully not included. */ }
160
+ </NavigatorProvider>
161
+ );
162
+ };
143
163
 
144
164
  const getNavigationScreenByText = ( text, { throwIfNotFound = true } = {} ) => {
145
165
  const fnName = throwIfNotFound ? 'getByText' : 'queryByText';
@@ -194,6 +214,28 @@ const getBackButton = ( { throwIfNotFound } = {} ) =>
194
214
  } );
195
215
 
196
216
  describe( 'Navigator', () => {
217
+ const originalGetClientRects = window.Element.prototype.getClientRects;
218
+
219
+ // `getClientRects` needs to be mocked so that `isVisible` from the `@wordpress/dom`
220
+ // `focusable` module can pass, in a JSDOM env where the DOM elements have no width/height.
221
+ const mockedGetClientRects = jest.fn( () => [
222
+ {
223
+ x: 0,
224
+ y: 0,
225
+ width: 100,
226
+ height: 100,
227
+ },
228
+ ] );
229
+
230
+ beforeAll( () => {
231
+ window.Element.prototype.getClientRects =
232
+ jest.fn( mockedGetClientRects );
233
+ } );
234
+
235
+ afterAll( () => {
236
+ window.Element.prototype.getClientRects = originalGetClientRects;
237
+ } );
238
+
197
239
  it( 'should render', () => {
198
240
  render( <MyNavigation /> );
199
241
 
@@ -404,4 +446,27 @@ describe( 'Navigator', () => {
404
446
  expect( getHomeScreen() ).toBeInTheDocument();
405
447
  expect( getToInvalidHTMLPathScreenButton() ).toHaveFocus();
406
448
  } );
449
+
450
+ it( 'should keep focus on the element that is being interacted with, while re-rendering', async () => {
451
+ const user = userEvent.setup( {
452
+ advanceTimers: jest.advanceTimersByTime,
453
+ } );
454
+
455
+ render( <MyNavigation /> );
456
+
457
+ expect( getHomeScreen() ).toBeInTheDocument();
458
+ expect( getToChildScreenButton() ).toBeInTheDocument();
459
+
460
+ // Navigate to child screen.
461
+ await user.click( getToChildScreenButton() );
462
+
463
+ expect( getChildScreen() ).toBeInTheDocument();
464
+ expect( getBackButton() ).toBeInTheDocument();
465
+ expect( getToNestedScreenButton() ).toHaveFocus();
466
+
467
+ // Interact with the input, the focus should stay on the input element.
468
+ const input = screen.getByLabelText( 'This is a test input' );
469
+ await user.type( input, 'd' );
470
+ expect( input ).toHaveFocus();
471
+ } );
407
472
  } );
@@ -178,9 +178,9 @@
178
178
  min-width: 100px;
179
179
 
180
180
  // Blur the background so layered dashed placeholders are still visually separate.
181
- // We also provide a semitransparent background so as to allow duotones to sheen through.
181
+ // Make the background transparent to not interfere with the background overlay in placeholder-style() pseudo element
182
182
  backdrop-filter: blur(100px);
183
- background-color: rgba($white, 0.1);
183
+ background-color: transparent;
184
184
 
185
185
  // Invert the colors in themes deemed dark.
186
186
  .is-dark-theme & {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { useEffect, useRef, useState } from '@wordpress/element';
4
+ import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
5
5
  import { useResizeObserver } from '@wordpress/compose';
6
6
 
7
7
  const noop = () => {};
@@ -84,23 +84,23 @@ export function useResizeLabel( {
84
84
  */
85
85
  const moveTimeoutRef = useRef< number >();
86
86
 
87
- const unsetMoveXY = () => {
88
- /*
89
- * If axis is controlled, we will avoid resetting the moveX and moveY values.
90
- * This will allow for the preferred axis values to persist in the label.
91
- */
92
- if ( isAxisControlled ) return;
93
- setMoveX( false );
94
- setMoveY( false );
95
- };
87
+ const debounceUnsetMoveXY = useCallback( () => {
88
+ const unsetMoveXY = () => {
89
+ /*
90
+ * If axis is controlled, we will avoid resetting the moveX and moveY values.
91
+ * This will allow for the preferred axis values to persist in the label.
92
+ */
93
+ if ( isAxisControlled ) return;
94
+ setMoveX( false );
95
+ setMoveY( false );
96
+ };
96
97
 
97
- const debounceUnsetMoveXY = () => {
98
98
  if ( moveTimeoutRef.current ) {
99
99
  window.clearTimeout( moveTimeoutRef.current );
100
100
  }
101
101
 
102
102
  moveTimeoutRef.current = window.setTimeout( unsetMoveXY, fadeTimeout );
103
- };
103
+ }, [ fadeTimeout, isAxisControlled ] );
104
104
 
105
105
  useEffect( () => {
106
106
  /*
@@ -143,7 +143,7 @@ export function useResizeLabel( {
143
143
 
144
144
  onResize( { width, height } );
145
145
  debounceUnsetMoveXY();
146
- }, [ width, height ] );
146
+ }, [ width, height, onResize, debounceUnsetMoveXY ] );
147
147
 
148
148
  const label = getSizeLabel( {
149
149
  axis,