@wordpress/components 23.3.0 → 23.3.2

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 (220) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/build/color-palette/index.js +7 -4
  3. package/build/color-palette/index.js.map +1 -1
  4. package/build/color-palette/utils.js +12 -4
  5. package/build/color-palette/utils.js.map +1 -1
  6. package/build/custom-select-control/index.js +7 -0
  7. package/build/custom-select-control/index.js.map +1 -1
  8. package/build/index.js +16 -10
  9. package/build/index.js.map +1 -1
  10. package/build/navigator/context.js +5 -1
  11. package/build/navigator/context.js.map +1 -1
  12. package/build/navigator/index.js +8 -0
  13. package/build/navigator/index.js.map +1 -1
  14. package/build/navigator/navigator-back-button/hook.js +11 -3
  15. package/build/navigator/navigator-back-button/hook.js.map +1 -1
  16. package/build/navigator/navigator-provider/component.js +119 -11
  17. package/build/navigator/navigator-provider/component.js.map +1 -1
  18. package/build/navigator/navigator-screen/component.js +18 -7
  19. package/build/navigator/navigator-screen/component.js.map +1 -1
  20. package/build/navigator/navigator-to-parent-button/component.js +75 -0
  21. package/build/navigator/navigator-to-parent-button/component.js.map +1 -0
  22. package/build/navigator/navigator-to-parent-button/index.js +16 -0
  23. package/build/navigator/navigator-to-parent-button/index.js.map +1 -0
  24. package/build/navigator/use-navigator.js +6 -2
  25. package/build/navigator/use-navigator.js.map +1 -1
  26. package/build/navigator/utils/router.js +57 -0
  27. package/build/navigator/utils/router.js.map +1 -0
  28. package/build/private-apis.js +35 -0
  29. package/build/private-apis.js.map +1 -0
  30. package/build/select-control/index.js +1 -1
  31. package/build/select-control/index.js.map +1 -1
  32. package/build/select-control/styles/select-control-styles.js +38 -25
  33. package/build/select-control/styles/select-control-styles.js.map +1 -1
  34. package/build/tools-panel/tools-panel/hook.js +4 -4
  35. package/build/tools-panel/tools-panel/hook.js.map +1 -1
  36. package/build/tools-panel/tools-panel-item/hook.js +20 -11
  37. package/build/tools-panel/tools-panel-item/hook.js.map +1 -1
  38. package/build-module/color-palette/index.js +8 -5
  39. package/build-module/color-palette/index.js.map +1 -1
  40. package/build-module/color-palette/utils.js +12 -4
  41. package/build-module/color-palette/utils.js.map +1 -1
  42. package/build-module/custom-select-control/index.js +5 -0
  43. package/build-module/custom-select-control/index.js.map +1 -1
  44. package/build-module/index.js +5 -4
  45. package/build-module/index.js.map +1 -1
  46. package/build-module/navigator/context.js +5 -1
  47. package/build-module/navigator/context.js.map +1 -1
  48. package/build-module/navigator/index.js +1 -0
  49. package/build-module/navigator/index.js.map +1 -1
  50. package/build-module/navigator/navigator-back-button/hook.js +11 -3
  51. package/build-module/navigator/navigator-back-button/hook.js.map +1 -1
  52. package/build-module/navigator/navigator-provider/component.js +117 -12
  53. package/build-module/navigator/navigator-provider/component.js.map +1 -1
  54. package/build-module/navigator/navigator-screen/component.js +20 -9
  55. package/build-module/navigator/navigator-screen/component.js.map +1 -1
  56. package/build-module/navigator/navigator-to-parent-button/component.js +61 -0
  57. package/build-module/navigator/navigator-to-parent-button/component.js.map +1 -0
  58. package/build-module/navigator/navigator-to-parent-button/index.js +2 -0
  59. package/build-module/navigator/navigator-to-parent-button/index.js.map +1 -0
  60. package/build-module/navigator/use-navigator.js +6 -2
  61. package/build-module/navigator/use-navigator.js.map +1 -1
  62. package/build-module/navigator/utils/router.js +51 -0
  63. package/build-module/navigator/utils/router.js.map +1 -0
  64. package/build-module/private-apis.js +20 -0
  65. package/build-module/private-apis.js.map +1 -0
  66. package/build-module/select-control/index.js +1 -1
  67. package/build-module/select-control/index.js.map +1 -1
  68. package/build-module/select-control/styles/select-control-styles.js +38 -25
  69. package/build-module/select-control/styles/select-control-styles.js.map +1 -1
  70. package/build-module/tools-panel/tools-panel/hook.js +4 -4
  71. package/build-module/tools-panel/tools-panel/hook.js.map +1 -1
  72. package/build-module/tools-panel/tools-panel-item/hook.js +20 -11
  73. package/build-module/tools-panel/tools-panel-item/hook.js.map +1 -1
  74. package/build-style/style-rtl.css +0 -11
  75. package/build-style/style.css +0 -11
  76. package/build-types/base-control/hooks.d.ts +1 -1
  77. package/build-types/base-field/hook.d.ts +2 -2
  78. package/build-types/border-box-control/border-box-control/hook.d.ts +2 -2
  79. package/build-types/border-box-control/border-box-control-linked-button/hook.d.ts +2 -2
  80. package/build-types/border-box-control/border-box-control-split-controls/hook.d.ts +2 -2
  81. package/build-types/border-box-control/border-box-control-visualizer/hook.d.ts +2 -2
  82. package/build-types/border-control/border-control/hook.d.ts +2 -2
  83. package/build-types/border-control/border-control-dropdown/hook.d.ts +2 -2
  84. package/build-types/border-control/border-control-style-picker/hook.d.ts +2 -2
  85. package/build-types/button/deprecated.d.ts +2 -2
  86. package/build-types/button/types.d.ts +3 -1
  87. package/build-types/button/types.d.ts.map +1 -1
  88. package/build-types/card/card/hook.d.ts +2 -2
  89. package/build-types/card/card-body/hook.d.ts +2 -2
  90. package/build-types/card/card-divider/hook.d.ts +2 -2
  91. package/build-types/card/card-footer/hook.d.ts +2 -2
  92. package/build-types/card/card-header/hook.d.ts +2 -2
  93. package/build-types/card/card-media/hook.d.ts +2 -2
  94. package/build-types/color-palette/index.d.ts.map +1 -1
  95. package/build-types/color-palette/styles.d.ts +1 -1
  96. package/build-types/color-palette/utils.d.ts +8 -5
  97. package/build-types/color-palette/utils.d.ts.map +1 -1
  98. package/build-types/color-picker/styles.d.ts +7 -7
  99. package/build-types/custom-select-control/index.d.ts +1 -0
  100. package/build-types/custom-select-control/index.d.ts.map +1 -1
  101. package/build-types/date-time/date/styles.d.ts +3 -3
  102. package/build-types/date-time/date-time/styles.d.ts +3 -3
  103. package/build-types/date-time/time/styles.d.ts +8 -8
  104. package/build-types/elevation/hook.d.ts +2 -2
  105. package/build-types/external-link/styles/external-link-styles.d.ts +1 -1
  106. package/build-types/flex/flex/hook.d.ts +2 -2
  107. package/build-types/flex/flex-block/hook.d.ts +2 -2
  108. package/build-types/flex/flex-item/hook.d.ts +2 -2
  109. package/build-types/focal-point-picker/styles/focal-point-picker-style.d.ts +2 -2
  110. package/build-types/form-token-field/styles.d.ts +1 -1
  111. package/build-types/grid/hook.d.ts +2 -2
  112. package/build-types/h-stack/component.d.ts +1 -1
  113. package/build-types/h-stack/hook.d.ts +2 -2
  114. package/build-types/heading/hook.d.ts +2 -2
  115. package/build-types/input-control/styles/input-control-styles.d.ts +2 -2
  116. package/build-types/item-group/item/hook.d.ts +2 -2
  117. package/build-types/item-group/item-group/hook.d.ts +2 -2
  118. package/build-types/navigator/context.d.ts.map +1 -1
  119. package/build-types/navigator/index.d.ts +1 -0
  120. package/build-types/navigator/index.d.ts.map +1 -1
  121. package/build-types/navigator/navigator-back-button/component.d.ts +22 -3
  122. package/build-types/navigator/navigator-back-button/component.d.ts.map +1 -1
  123. package/build-types/navigator/navigator-back-button/hook.d.ts +24 -6
  124. package/build-types/navigator/navigator-back-button/hook.d.ts.map +1 -1
  125. package/build-types/navigator/navigator-button/component.d.ts +22 -3
  126. package/build-types/navigator/navigator-button/component.d.ts.map +1 -1
  127. package/build-types/navigator/navigator-button/hook.d.ts +22 -4
  128. package/build-types/navigator/navigator-button/hook.d.ts.map +1 -1
  129. package/build-types/navigator/navigator-provider/component.d.ts.map +1 -1
  130. package/build-types/navigator/navigator-screen/component.d.ts.map +1 -1
  131. package/build-types/navigator/navigator-to-parent-button/component.d.ts +27 -0
  132. package/build-types/navigator/navigator-to-parent-button/component.d.ts.map +1 -0
  133. package/build-types/navigator/navigator-to-parent-button/index.d.ts +2 -0
  134. package/build-types/navigator/navigator-to-parent-button/index.d.ts.map +1 -0
  135. package/build-types/navigator/stories/index.d.ts +1 -0
  136. package/build-types/navigator/stories/index.d.ts.map +1 -1
  137. package/build-types/navigator/test/router.d.ts +2 -0
  138. package/build-types/navigator/test/router.d.ts.map +1 -0
  139. package/build-types/navigator/types.d.ts +25 -9
  140. package/build-types/navigator/types.d.ts.map +1 -1
  141. package/build-types/navigator/use-navigator.d.ts.map +1 -1
  142. package/build-types/navigator/utils/router.d.ts +10 -0
  143. package/build-types/navigator/utils/router.d.ts.map +1 -0
  144. package/build-types/number-control/index.d.ts +2 -2
  145. package/build-types/number-control/stories/index.d.ts +2 -2
  146. package/build-types/popover/index.d.ts +1 -1
  147. package/build-types/popover/stories/e2e/index.d.ts +1 -1
  148. package/build-types/private-apis.d.ts +4 -0
  149. package/build-types/private-apis.d.ts.map +1 -0
  150. package/build-types/range-control/index.d.ts +2 -2
  151. package/build-types/range-control/styles/range-control-styles.d.ts +2 -2
  152. package/build-types/resizable-box/index.d.ts +1 -1
  153. package/build-types/resizable-box/resize-tooltip/index.d.ts +1 -1
  154. package/build-types/resizable-box/stories/index.d.ts +2 -2
  155. package/build-types/scrollable/hook.d.ts +2 -2
  156. package/build-types/search-control/index.d.ts +1 -1
  157. package/build-types/search-control/stories/index.d.ts +2 -2
  158. package/build-types/select-control/index.d.ts.map +1 -1
  159. package/build-types/select-control/styles/select-control-styles.d.ts +1 -1
  160. package/build-types/select-control/styles/select-control-styles.d.ts.map +1 -1
  161. package/build-types/select-control/types.d.ts +3 -1
  162. package/build-types/select-control/types.d.ts.map +1 -1
  163. package/build-types/spacer/hook.d.ts +2 -2
  164. package/build-types/spinner/index.d.ts +1 -1
  165. package/build-types/surface/hook.d.ts +2 -2
  166. package/build-types/text/hook.d.ts +2 -2
  167. package/build-types/text-control/index.d.ts +3 -3
  168. package/build-types/tools-panel/tools-panel/hook.d.ts +2 -2
  169. package/build-types/tools-panel/tools-panel/hook.d.ts.map +1 -1
  170. package/build-types/tools-panel/tools-panel-header/hook.d.ts +2 -2
  171. package/build-types/tools-panel/tools-panel-item/hook.d.ts +2 -2
  172. package/build-types/tools-panel/tools-panel-item/hook.d.ts.map +1 -1
  173. package/build-types/truncate/hook.d.ts +2 -2
  174. package/build-types/ui/control-group/hook.d.ts +2 -2
  175. package/build-types/ui/control-label/hook.d.ts +2 -2
  176. package/build-types/ui/form-group/form-group.d.ts +2 -2
  177. package/build-types/ui/form-group/use-form-group.d.ts +2 -2
  178. package/build-types/unit-control/index.d.ts +1 -1
  179. package/build-types/unit-control/styles/unit-control-styles.d.ts +2 -2
  180. package/build-types/v-stack/component.d.ts +2 -2
  181. package/build-types/v-stack/hook.d.ts +2 -2
  182. package/build-types/v-stack/stories/index.d.ts +2 -2
  183. package/package.json +20 -18
  184. package/src/button/types.ts +5 -2
  185. package/src/color-palette/index.tsx +13 -5
  186. package/src/color-palette/test/utils.ts +17 -1
  187. package/src/color-palette/utils.ts +12 -7
  188. package/src/custom-select-control/index.js +9 -0
  189. package/src/custom-select-control/stories/index.js +1 -1
  190. package/src/custom-select-control/test/index.js +2 -2
  191. package/src/dimension-control/test/__snapshots__/index.test.js.snap +4 -4
  192. package/src/index.js +5 -2
  193. package/src/navigator/context.ts +4 -0
  194. package/src/navigator/index.ts +1 -0
  195. package/src/navigator/navigator-back-button/README.md +1 -17
  196. package/src/navigator/navigator-back-button/hook.ts +10 -5
  197. package/src/navigator/navigator-button/README.md +1 -1
  198. package/src/navigator/navigator-provider/README.md +25 -4
  199. package/src/navigator/navigator-provider/component.tsx +170 -14
  200. package/src/navigator/navigator-screen/component.tsx +22 -11
  201. package/src/navigator/navigator-to-parent-button/README.md +15 -0
  202. package/src/navigator/navigator-to-parent-button/component.tsx +65 -0
  203. package/src/navigator/navigator-to-parent-button/index.ts +1 -0
  204. package/src/navigator/stories/index.tsx +93 -3
  205. package/src/navigator/test/index.tsx +267 -23
  206. package/src/navigator/test/router.ts +122 -0
  207. package/src/navigator/types.ts +31 -12
  208. package/src/navigator/use-navigator.ts +4 -1
  209. package/src/navigator/utils/router.ts +49 -0
  210. package/src/private-apis.js +22 -0
  211. package/src/select-control/README.md +3 -1
  212. package/src/select-control/index.tsx +3 -1
  213. package/src/select-control/style.scss +0 -10
  214. package/src/select-control/styles/select-control-styles.ts +36 -22
  215. package/src/select-control/types.ts +3 -1
  216. package/src/tools-panel/test/index.js +65 -0
  217. package/src/tools-panel/tools-panel/hook.ts +4 -5
  218. package/src/tools-panel/tools-panel-item/hook.ts +24 -14
  219. package/tsconfig.json +5 -1
  220. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { patternMatch, findParent } from '../utils/router';
5
+
6
+ describe( 'patternMatch', () => {
7
+ it( 'should return undefined if not pattern is matched', () => {
8
+ const result = patternMatch( '/test', [ { id: 'route', path: '/' } ] );
9
+ expect( result ).toBeUndefined();
10
+ } );
11
+
12
+ it( 'should match a pattern with no params', () => {
13
+ const result = patternMatch( '/test', [
14
+ { id: 'route', path: '/test' },
15
+ ] );
16
+ expect( result ).toEqual( { id: 'route', params: {} } );
17
+ } );
18
+
19
+ it( 'should match a pattern with params', () => {
20
+ const result = patternMatch( '/test/123', [
21
+ { id: 'route', path: '/test/:id' },
22
+ ] );
23
+ expect( result ).toEqual( { id: 'route', params: { id: '123' } } );
24
+ } );
25
+
26
+ it( 'should match the first pattern in case of ambiguity', () => {
27
+ const result = patternMatch( '/test/123', [
28
+ { id: 'route1', path: '/test/:id' },
29
+ { id: 'route2', path: '/test/123' },
30
+ ] );
31
+ expect( result ).toEqual( { id: 'route1', params: { id: '123' } } );
32
+ } );
33
+
34
+ it( 'should match a pattern with optional params', () => {
35
+ const result = patternMatch( '/test', [
36
+ { id: 'route', path: '/test/:id?' },
37
+ ] );
38
+ expect( result ).toEqual( { id: 'route', params: {} } );
39
+ } );
40
+
41
+ it( 'should return an array of matches for the same param', () => {
42
+ const result = patternMatch( '/some/basic/route', [
43
+ { id: 'route', path: '/:test+' },
44
+ ] );
45
+ expect( result ).toEqual( {
46
+ id: 'route',
47
+ params: { test: [ 'some', 'basic', 'route' ] },
48
+ } );
49
+ } );
50
+ } );
51
+
52
+ describe( 'findParent', () => {
53
+ it( 'should return undefined if no parent is found', () => {
54
+ const result = findParent( '/test', [
55
+ { id: 'route', path: '/test' },
56
+ ] );
57
+ expect( result ).toBeUndefined();
58
+ } );
59
+
60
+ it( 'should return the parent path', () => {
61
+ const result = findParent( '/test', [
62
+ { id: 'route1', path: '/test' },
63
+ { id: 'route2', path: '/' },
64
+ ] );
65
+ expect( result ).toEqual( '/' );
66
+ } );
67
+
68
+ it( 'should return to another parent path', () => {
69
+ const result = findParent( '/test/123', [
70
+ { id: 'route1', path: '/test/:id' },
71
+ { id: 'route2', path: '/test' },
72
+ ] );
73
+ expect( result ).toEqual( '/test' );
74
+ } );
75
+
76
+ it( 'should return the parent path with params', () => {
77
+ const result = findParent( '/test/123/456', [
78
+ { id: 'route1', path: '/test/:id/:subId' },
79
+ { id: 'route2', path: '/test/:id' },
80
+ ] );
81
+ expect( result ).toEqual( '/test/123' );
82
+ } );
83
+
84
+ it( 'should return the parent path with optional params', () => {
85
+ const result = findParent( '/test/123', [
86
+ { id: 'route', path: '/test/:id?' },
87
+ ] );
88
+ expect( result ).toEqual( '/test' );
89
+ } );
90
+
91
+ it( 'should return the grand parent if no parent found', () => {
92
+ const result = findParent( '/test/123/456', [
93
+ { id: 'route1', path: '/test/:id/:subId' },
94
+ { id: 'route2', path: '/test' },
95
+ ] );
96
+ expect( result ).toEqual( '/test' );
97
+ } );
98
+
99
+ it( 'should return the root when no grand parent found', () => {
100
+ const result = findParent( '/test/nested/path', [
101
+ { id: 'route1', path: '/other-path' },
102
+ { id: 'route2', path: '/yet-another-path' },
103
+ { id: 'root', path: '/' },
104
+ ] );
105
+ expect( result ).toEqual( '/' );
106
+ } );
107
+
108
+ it( 'should return undefined when no potential parent found', () => {
109
+ const result = findParent( '/test/nested/path', [
110
+ { id: 'route1', path: '/other-path' },
111
+ { id: 'route2', path: '/yet-another-path' },
112
+ ] );
113
+ expect( result ).toBeUndefined();
114
+ } );
115
+
116
+ it( 'should return undefined for non supported paths', () => {
117
+ const result = findParent( 'this-is-a-path', [
118
+ { id: 'route', path: '/' },
119
+ ] );
120
+ expect( result ).toBeUndefined();
121
+ } );
122
+ } );
@@ -3,25 +3,38 @@
3
3
  */
4
4
  import type { ReactNode } from 'react';
5
5
 
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { ButtonAsButtonProps } from '../button/types';
10
+
11
+ export type MatchParams = Record< string, string | string[] >;
12
+
6
13
  type NavigateOptions = {
7
14
  focusTargetSelector?: string;
15
+ isBack?: boolean;
8
16
  };
9
17
 
10
18
  export type NavigatorLocation = NavigateOptions & {
11
19
  isInitial?: boolean;
12
- isBack?: boolean;
13
20
  path?: string;
14
21
  hasRestoredFocus?: boolean;
15
22
  };
16
23
 
17
- export type NavigatorContext = {
24
+ // Returned by the `useNavigator` hook.
25
+ export type Navigator = {
18
26
  location: NavigatorLocation;
27
+ params: MatchParams;
19
28
  goTo: ( path: string, options?: NavigateOptions ) => void;
20
29
  goBack: () => void;
30
+ goToParent: () => void;
21
31
  };
22
32
 
23
- // Returned by the `useNavigator` hook.
24
- export type Navigator = NavigatorContext;
33
+ export type NavigatorContext = Navigator & {
34
+ addScreen: ( screen: Screen ) => void;
35
+ removeScreen: ( screen: Screen ) => void;
36
+ match?: string;
37
+ };
25
38
 
26
39
  export type NavigatorProviderProps = {
27
40
  /**
@@ -45,18 +58,19 @@ export type NavigatorScreenProps = {
45
58
  children: ReactNode;
46
59
  };
47
60
 
48
- type ButtonProps = {
49
- // TODO: should also extend `Button` prop types once the `Button` component
50
- // is refactored to TypeScript.
51
- variant?: 'primary' | 'secondary' | 'tertiary' | 'link';
52
- };
53
- export type NavigatorBackButtonProps = Omit< ButtonProps, 'href' > & {
61
+ export type NavigatorBackButtonProps = ButtonAsButtonProps;
62
+
63
+ export type NavigatorBackButtonHookProps = NavigatorBackButtonProps & {
54
64
  /**
55
- * The children elements.
65
+ * Whether we should navigate to the parent screen.
66
+ *
67
+ * @default 'false'
56
68
  */
57
- children: ReactNode;
69
+ goToParent?: boolean;
58
70
  };
59
71
 
72
+ export type NavigatorToParentButtonProps = NavigatorBackButtonProps;
73
+
60
74
  export type NavigatorButtonProps = NavigatorBackButtonProps & {
61
75
  /**
62
76
  * The path of the screen to navigate to. The value of this prop needs to be
@@ -71,3 +85,8 @@ export type NavigatorButtonProps = NavigatorBackButtonProps & {
71
85
  */
72
86
  attributeName?: string;
73
87
  };
88
+
89
+ export type Screen = {
90
+ id: string;
91
+ path: string;
92
+ };
@@ -13,12 +13,15 @@ import type { Navigator } from './types';
13
13
  * Retrieves a `navigator` instance.
14
14
  */
15
15
  function useNavigator(): Navigator {
16
- const { location, goTo, goBack } = useContext( NavigatorContext );
16
+ const { location, params, goTo, goBack, goToParent } =
17
+ useContext( NavigatorContext );
17
18
 
18
19
  return {
19
20
  location,
20
21
  goTo,
21
22
  goBack,
23
+ goToParent,
24
+ params,
22
25
  };
23
26
  }
24
27
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { match } from 'path-to-regexp';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { Screen, MatchParams } from '../types';
10
+
11
+ function matchPath( path: string, pattern: string ) {
12
+ const matchingFunction = match< MatchParams >( pattern, {
13
+ decode: decodeURIComponent,
14
+ } );
15
+ return matchingFunction( path );
16
+ }
17
+
18
+ export function patternMatch( path: string, screens: Screen[] ) {
19
+ for ( const screen of screens ) {
20
+ const matched = matchPath( path, screen.path );
21
+ if ( matched ) {
22
+ return { params: matched.params, id: screen.id };
23
+ }
24
+ }
25
+
26
+ return undefined;
27
+ }
28
+
29
+ export function findParent( path: string, screens: Screen[] ) {
30
+ if ( ! path.startsWith( '/' ) ) {
31
+ return undefined;
32
+ }
33
+ const pathParts = path.split( '/' );
34
+ let parentPath;
35
+ while ( pathParts.length > 1 && parentPath === undefined ) {
36
+ pathParts.pop();
37
+ const potentialParentPath =
38
+ pathParts.join( '/' ) === '' ? '/' : pathParts.join( '/' );
39
+ if (
40
+ screens.find( ( screen ) => {
41
+ return matchPath( potentialParentPath, screen.path ) !== false;
42
+ } )
43
+ ) {
44
+ parentPath = potentialParentPath;
45
+ }
46
+ }
47
+
48
+ return parentPath;
49
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { default as CustomSelectControl } from './custom-select-control';
10
+ import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils';
11
+
12
+ export const { lock, unlock } =
13
+ __dangerousOptInToUnstableAPIsOnlyForCoreModules(
14
+ 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
15
+ '@wordpress/components'
16
+ );
17
+
18
+ export const privateApis = {};
19
+ lock( privateApis, {
20
+ CustomSelectControl,
21
+ __experimentalPopoverLegacyPositionToPlacement,
22
+ } );
@@ -187,7 +187,9 @@ If this property is added, a help text will be generated using help property as
187
187
 
188
188
  #### multiple
189
189
 
190
- If this property is added, multiple values can be selected. The value passed should be an array.
190
+ If this property is added, multiple values can be selected. The `value` passed should be an array.
191
+
192
+ In most cases, it is preferable to use the `FormTokenField` or `CheckboxControl` components instead.
191
193
 
192
194
  - Type: `Boolean`
193
195
  - Required: No
@@ -101,7 +101,9 @@ function UnforwardedSelectControl(
101
101
  isFocused={ isFocused }
102
102
  label={ label }
103
103
  size={ size }
104
- suffix={ suffix || <SelectControlChevronDown /> }
104
+ suffix={
105
+ suffix || ( ! multiple && <SelectControlChevronDown /> )
106
+ }
105
107
  prefix={ prefix }
106
108
  labelPosition={ labelPosition }
107
109
  __next36pxDefaultSize={ __next36pxDefaultSize }
@@ -1,16 +1,6 @@
1
1
  .components-select-control__input {
2
- background: $white;
3
- height: 36px;
4
- line-height: 36px;
5
- margin: 1px;
6
2
  outline: 0;
7
- width: 100%;
8
3
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
9
-
10
- @include break-medium() {
11
- height: 28px;
12
- line-height: 28px;
13
- }
14
4
  }
15
5
 
16
6
  @media (max-width: #{ ($break-medium) }) {
@@ -13,7 +13,10 @@ import type { SelectControlProps } from '../types';
13
13
  import InputControlSuffixWrapper from '../../input-control/input-suffix-wrapper';
14
14
 
15
15
  interface SelectProps
16
- extends Pick< SelectControlProps, '__next36pxDefaultSize' | 'disabled' > {
16
+ extends Pick<
17
+ SelectControlProps,
18
+ '__next36pxDefaultSize' | 'disabled' | 'multiple'
19
+ > {
17
20
  // Using `selectSize` instead of `size` to avoid a type conflict with the
18
21
  // `size` HTML attribute of the `select` element.
19
22
  selectSize?: SelectControlProps[ 'size' ];
@@ -50,8 +53,15 @@ const fontSizeStyles = ( { selectSize = 'default' }: SelectProps ) => {
50
53
 
51
54
  const sizeStyles = ( {
52
55
  __next36pxDefaultSize,
56
+ multiple,
53
57
  selectSize = 'default',
54
58
  }: SelectProps ) => {
59
+ if ( multiple ) {
60
+ // When `multiple`, just use the native browser styles
61
+ // without setting explicit height.
62
+ return;
63
+ }
64
+
55
65
  const sizes = {
56
66
  default: {
57
67
  height: 36,
@@ -91,33 +101,37 @@ export const chevronIconSize = 18;
91
101
 
92
102
  const sizePaddings = ( {
93
103
  __next36pxDefaultSize,
104
+ multiple,
94
105
  selectSize = 'default',
95
106
  }: SelectProps ) => {
96
- const iconWidth = chevronIconSize;
97
-
98
- const sizes = {
99
- default: {
100
- paddingLeft: 16,
101
- paddingRight: 16 + iconWidth,
102
- },
103
- small: {
104
- paddingLeft: 8,
105
- paddingRight: 8 + iconWidth,
106
- },
107
- '__unstable-large': {
108
- paddingLeft: 16,
109
- paddingRight: 16 + iconWidth,
110
- },
107
+ const padding = {
108
+ default: 16,
109
+ small: 8,
110
+ '__unstable-large': 16,
111
111
  };
112
112
 
113
113
  if ( ! __next36pxDefaultSize ) {
114
- sizes.default = {
115
- paddingLeft: 8,
116
- paddingRight: 8 + iconWidth,
117
- };
114
+ padding.default = 8;
118
115
  }
119
116
 
120
- return rtl( sizes[ selectSize ] || sizes.default );
117
+ const selectedPadding = padding[ selectSize ] || padding.default;
118
+
119
+ return rtl( {
120
+ paddingLeft: selectedPadding,
121
+ paddingRight: selectedPadding + chevronIconSize,
122
+ ...( multiple
123
+ ? {
124
+ paddingTop: selectedPadding,
125
+ paddingBottom: selectedPadding,
126
+ }
127
+ : {} ),
128
+ } );
129
+ };
130
+
131
+ const overflowStyles = ( { multiple }: SelectProps ) => {
132
+ return {
133
+ overflow: multiple ? 'auto' : 'hidden',
134
+ };
121
135
  };
122
136
 
123
137
  // TODO: Resolve need to use &&& to increase specificity
@@ -137,7 +151,6 @@ export const Select = styled.select< SelectProps >`
137
151
  width: 100%;
138
152
  max-width: none;
139
153
  cursor: pointer;
140
- overflow: hidden;
141
154
  white-space: nowrap;
142
155
  text-overflow: ellipsis;
143
156
 
@@ -145,6 +158,7 @@ export const Select = styled.select< SelectProps >`
145
158
  ${ fontSizeStyles };
146
159
  ${ sizeStyles };
147
160
  ${ sizePaddings };
161
+ ${ overflowStyles }
148
162
  }
149
163
  `;
150
164
 
@@ -23,7 +23,9 @@ export interface SelectControlProps
23
23
  >,
24
24
  Pick< BaseControlProps, 'help' | '__nextHasNoMarginBottom' > {
25
25
  /**
26
- * If this property is added, multiple values can be selected. The value passed should be an array.
26
+ * If this property is added, multiple values can be selected. The `value` passed should be an array.
27
+ *
28
+ * In most cases, it is preferable to use the `FormTokenField` or `CheckboxControl` components instead.
27
29
  *
28
30
  * @default false
29
31
  */
@@ -316,6 +316,71 @@ describe( 'ToolsPanel', () => {
316
316
  expect( announcement ).toHaveAttribute( 'aria-live', 'assertive' );
317
317
  } );
318
318
 
319
+ it( 'should render optional panel item when value is updated externally and panel has an ID', async () => {
320
+ const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
321
+ const itemProps = {
322
+ attributes: { value: toolsPanelItemValue },
323
+ hasValue: () => !! toolsPanelItemValue,
324
+ label: 'Alt',
325
+ onDeselect: jest.fn(),
326
+ onSelect: jest.fn(),
327
+ };
328
+
329
+ return (
330
+ <ToolsPanel { ...defaultProps }>
331
+ <ToolsPanelItem { ...itemProps }>
332
+ <div>Optional control</div>
333
+ </ToolsPanelItem>
334
+ </ToolsPanel>
335
+ );
336
+ };
337
+ const { rerender } = render( <ToolsPanelOptional /> );
338
+
339
+ const control = screen.queryByText( 'Optional control' );
340
+
341
+ expect( control ).not.toBeInTheDocument();
342
+
343
+ rerender( <ToolsPanelOptional toolsPanelItemValue={ 100 } /> );
344
+
345
+ const controlRerendered = screen.getByText( 'Optional control' );
346
+
347
+ expect( controlRerendered ).toBeInTheDocument();
348
+ } );
349
+
350
+ it( 'should render optional item when value is updated externally and panelId is null', async () => {
351
+ // This test partially covers: https://github.com/WordPress/gutenberg/issues/47368
352
+ const ToolsPanelOptional = ( { toolsPanelItemValue } ) => {
353
+ const itemProps = {
354
+ attributes: { value: toolsPanelItemValue },
355
+ hasValue: () => !! toolsPanelItemValue,
356
+ label: 'Alt',
357
+ onDeselect: jest.fn(),
358
+ onSelect: jest.fn(),
359
+ };
360
+
361
+ // The null panelId below simulates the panel prop when there
362
+ // are multiple blocks selected.
363
+ return (
364
+ <ToolsPanel { ...defaultProps } panelId={ null }>
365
+ <ToolsPanelItem { ...itemProps }>
366
+ <div>Optional control</div>
367
+ </ToolsPanelItem>
368
+ </ToolsPanel>
369
+ );
370
+ };
371
+
372
+ const { rerender } = render( <ToolsPanelOptional /> );
373
+ const control = screen.queryByText( 'Optional control' );
374
+
375
+ expect( control ).not.toBeInTheDocument();
376
+
377
+ rerender( <ToolsPanelOptional toolsPanelItemValue={ 99 } /> );
378
+
379
+ const controlRerendered = screen.getByText( 'Optional control' );
380
+
381
+ expect( controlRerendered ).toBeInTheDocument();
382
+ } );
383
+
319
384
  it( 'should continue to render shown by default item after it is toggled off via menu item', async () => {
320
385
  render(
321
386
  <ToolsPanel { ...defaultProps }>
@@ -35,12 +35,11 @@ const generateMenuItems = ( {
35
35
  panelItems.forEach( ( { hasValue, isShownByDefault, label } ) => {
36
36
  const group = isShownByDefault ? 'default' : 'optional';
37
37
 
38
- // If a menu item for this label already exists, do not overwrite its value.
39
- // This can cause default controls that have been flagged as customized to
40
- // lose their value.
38
+ // If a menu item for this label has already been flagged as customized
39
+ // (for default controls), or toggled on (for optional controls), do not
40
+ // overwrite its value as those controls would lose that state.
41
41
  const existingItemValue = currentMenuItems?.[ group ]?.[ label ];
42
- const value =
43
- existingItemValue !== undefined ? existingItemValue : hasValue();
42
+ const value = existingItemValue ? existingItemValue : hasValue();
44
43
 
45
44
  menuItems[ group ][ label ] = shouldReset ? false : value;
46
45
  } );
@@ -86,30 +86,40 @@ export function useToolsPanelItem(
86
86
  deregisterPanelItem,
87
87
  ] );
88
88
 
89
+ // Note: `label` is used as a key when building menu item state in
90
+ // `ToolsPanel`.
91
+ const menuGroup = isShownByDefault ? 'default' : 'optional';
92
+ const isMenuItemChecked = menuItems?.[ menuGroup ]?.[ label ];
93
+ const wasMenuItemChecked = usePrevious( isMenuItemChecked );
94
+ const isRegistered = menuItems?.[ menuGroup ]?.[ label ] !== undefined;
95
+
89
96
  const isValueSet = hasValue();
90
97
  const wasValueSet = usePrevious( isValueSet );
91
-
92
- // If this item represents a default control it will need to notify the
93
- // panel when a custom value has been set.
98
+ const newValueSet = isValueSet && ! wasValueSet;
99
+
100
+ // Notify the panel when an item's value has been set.
101
+ //
102
+ // 1. For default controls, this is so "reset" appears beside its menu item.
103
+ // 2. For optional controls, when the panel ID is `null`, it allows the
104
+ // panel to ensure the item is toggled on for display in the menu, given the
105
+ // value has been set external to the control.
94
106
  useEffect( () => {
95
- if ( isShownByDefault && isValueSet && ! wasValueSet ) {
96
- flagItemCustomization( label );
107
+ if ( ! newValueSet ) {
108
+ return;
109
+ }
110
+
111
+ if ( isShownByDefault || currentPanelId === null ) {
112
+ flagItemCustomization( label, menuGroup );
97
113
  }
98
114
  }, [
99
- isValueSet,
100
- wasValueSet,
115
+ currentPanelId,
116
+ newValueSet,
101
117
  isShownByDefault,
118
+ menuGroup,
102
119
  label,
103
120
  flagItemCustomization,
104
121
  ] );
105
122
 
106
- // Note: `label` is used as a key when building menu item state in
107
- // `ToolsPanel`.
108
- const menuGroup = isShownByDefault ? 'default' : 'optional';
109
- const isMenuItemChecked = menuItems?.[ menuGroup ]?.[ label ];
110
- const wasMenuItemChecked = usePrevious( isMenuItemChecked );
111
- const isRegistered = menuItems?.[ menuGroup ]?.[ label ] !== undefined;
112
-
113
123
  // Determine if the panel item's corresponding menu is being toggled and
114
124
  // trigger appropriate callback if it is.
115
125
  useEffect( () => {
package/tsconfig.json CHANGED
@@ -24,11 +24,15 @@
24
24
  { "path": "../deprecated" },
25
25
  { "path": "../dom" },
26
26
  { "path": "../element" },
27
- { "path": "../html-entities" },
27
+ { "path": "../escape-html" },
28
28
  { "path": "../hooks" },
29
+ { "path": "../html-entities" },
30
+ { "path": "../i18n" },
29
31
  { "path": "../icons" },
30
32
  { "path": "../is-shallow-equal" },
33
+ { "path": "../keycodes" },
31
34
  { "path": "../primitives" },
35
+ { "path": "../private-apis" },
32
36
  { "path": "../react-i18n" },
33
37
  { "path": "../warning" }
34
38
  ],