@wordpress/block-editor 16.0.0 → 16.1.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 (368) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +8 -0
  3. package/build/components/alignment-control/ui.cjs +2 -4
  4. package/build/components/alignment-control/ui.cjs.map +2 -2
  5. package/build/components/background-image-control/index.cjs +40 -10
  6. package/build/components/background-image-control/index.cjs.map +2 -2
  7. package/build/components/block-alignment-control/ui.cjs +5 -3
  8. package/build/components/block-alignment-control/ui.cjs.map +2 -2
  9. package/build/components/block-inspector/index.cjs +73 -46
  10. package/build/components/block-inspector/index.cjs.map +2 -2
  11. package/build/components/block-list/block-html.cjs +4 -1
  12. package/build/components/block-list/block-html.cjs.map +2 -2
  13. package/build/components/block-list/use-block-props/index.cjs +5 -2
  14. package/build/components/block-list/use-block-props/index.cjs.map +2 -2
  15. package/build/components/block-list/use-block-props/use-focus-handler.cjs +2 -3
  16. package/build/components/block-list/use-block-props/use-focus-handler.cjs.map +2 -2
  17. package/build/components/block-list/use-block-props/use-register-block-event-handlers.cjs +48 -0
  18. package/build/components/block-list/use-block-props/use-register-block-event-handlers.cjs.map +7 -0
  19. package/build/components/block-parent-selector/index.cjs +2 -2
  20. package/build/components/block-parent-selector/index.cjs.map +2 -2
  21. package/build/components/block-toolbar/switch-section-style.cjs +4 -2
  22. package/build/components/block-toolbar/switch-section-style.cjs.map +3 -3
  23. package/build/components/block-tools/empty-block-inserter.cjs +2 -7
  24. package/build/components/block-tools/empty-block-inserter.cjs.map +2 -2
  25. package/build/components/block-vertical-alignment-control/ui.cjs +3 -5
  26. package/build/components/block-vertical-alignment-control/ui.cjs.map +2 -2
  27. package/build/components/border-radius-control/utils.cjs +8 -58
  28. package/build/components/border-radius-control/utils.cjs.map +2 -2
  29. package/build/components/colors-gradients/control.cjs +5 -3
  30. package/build/components/colors-gradients/control.cjs.map +2 -2
  31. package/build/components/dimension-control/index.cjs +30 -20
  32. package/build/components/dimension-control/index.cjs.map +3 -3
  33. package/build/components/dimensions-tool/aspect-ratio-tool.cjs +9 -2
  34. package/build/components/dimensions-tool/aspect-ratio-tool.cjs.map +3 -3
  35. package/build/components/editor-styles/index.cjs +9 -1
  36. package/build/components/editor-styles/index.cjs.map +2 -2
  37. package/build/components/global-styles/background-panel.cjs +66 -25
  38. package/build/components/global-styles/background-panel.cjs.map +3 -3
  39. package/build/components/global-styles/border-panel.cjs +153 -55
  40. package/build/components/global-styles/border-panel.cjs.map +3 -3
  41. package/build/components/global-styles/color-gradient-dropdown-item.cjs +46 -14
  42. package/build/components/global-styles/color-gradient-dropdown-item.cjs.map +3 -3
  43. package/build/components/global-styles/color-panel.cjs +63 -17
  44. package/build/components/global-styles/color-panel.cjs.map +2 -2
  45. package/build/components/global-styles/dimensions-panel.cjs +242 -64
  46. package/build/components/global-styles/dimensions-panel.cjs.map +3 -3
  47. package/build/components/global-styles/filters-panel.cjs +59 -20
  48. package/build/components/global-styles/filters-panel.cjs.map +3 -3
  49. package/build/components/global-styles/hooks.cjs +4 -1
  50. package/build/components/global-styles/hooks.cjs.map +2 -2
  51. package/build/components/global-styles/inheritance/index.cjs +142 -0
  52. package/build/components/global-styles/inheritance/index.cjs.map +7 -0
  53. package/build/components/global-styles/inherited-value-context.cjs +173 -0
  54. package/build/components/global-styles/inherited-value-context.cjs.map +7 -0
  55. package/build/components/global-styles/shadow-panel-components.cjs +38 -20
  56. package/build/components/global-styles/shadow-panel-components.cjs.map +2 -2
  57. package/build/components/global-styles/state-control.cjs +1 -1
  58. package/build/components/global-styles/state-control.cjs.map +2 -2
  59. package/build/components/global-styles/typography-panel.cjs +271 -78
  60. package/build/components/global-styles/typography-panel.cjs.map +3 -3
  61. package/build/components/inspector-controls-tabs/styles-tab.cjs +10 -1
  62. package/build/components/inspector-controls-tabs/styles-tab.cjs.map +2 -2
  63. package/build/components/line-height-control/index.cjs +1 -1
  64. package/build/components/line-height-control/index.cjs.map +2 -2
  65. package/build/components/link-control/index.cjs +3 -1
  66. package/build/components/link-control/index.cjs.map +2 -2
  67. package/build/components/preset-input-control/index.cjs +2 -1
  68. package/build/components/preset-input-control/index.cjs.map +2 -2
  69. package/build/components/provider/block-refs-provider.cjs +8 -2
  70. package/build/components/provider/block-refs-provider.cjs.map +2 -2
  71. package/build/components/provider/index.cjs +1 -1
  72. package/build/components/provider/index.cjs.map +2 -2
  73. package/build/components/provider/use-block-sync.cjs +18 -6
  74. package/build/components/provider/use-block-sync.cjs.map +2 -2
  75. package/build/components/provider/use-media-upload-settings.cjs +3 -1
  76. package/build/components/provider/use-media-upload-settings.cjs.map +2 -2
  77. package/build/components/rich-text/index.cjs +23 -13
  78. package/build/components/rich-text/index.cjs.map +2 -2
  79. package/build/components/spacing-sizes-control/utils.cjs +6 -19
  80. package/build/components/spacing-sizes-control/utils.cjs.map +2 -2
  81. package/build/components/text-indent-control/index.cjs +8 -2
  82. package/build/components/text-indent-control/index.cjs.map +2 -2
  83. package/build/components/url-input/index.cjs +303 -344
  84. package/build/components/url-input/index.cjs.map +3 -3
  85. package/build/components/writing-flow/index.cjs +2 -0
  86. package/build/components/writing-flow/index.cjs.map +3 -3
  87. package/build/components/writing-flow/use-arrow-nav.cjs +24 -1
  88. package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
  89. package/build/components/writing-flow/use-click-selection.cjs +28 -2
  90. package/build/components/writing-flow/use-click-selection.cjs.map +2 -2
  91. package/build/components/writing-flow/use-editable-root-event-handlers.cjs +179 -0
  92. package/build/components/writing-flow/use-editable-root-event-handlers.cjs.map +7 -0
  93. package/build/components/writing-flow/use-editable-root.cjs +5 -42
  94. package/build/components/writing-flow/use-editable-root.cjs.map +2 -2
  95. package/build/components/writing-flow/use-home-end.cjs +1 -1
  96. package/build/components/writing-flow/use-home-end.cjs.map +2 -2
  97. package/build/components/writing-flow/use-selection-observer.cjs +17 -13
  98. package/build/components/writing-flow/use-selection-observer.cjs.map +2 -2
  99. package/build/components/writing-flow/utils.cjs +3 -14
  100. package/build/components/writing-flow/utils.cjs.map +2 -2
  101. package/build/hooks/background.cjs +13 -10
  102. package/build/hooks/background.cjs.map +2 -2
  103. package/build/hooks/block-style-variation.cjs +4 -45
  104. package/build/hooks/block-style-variation.cjs.map +3 -3
  105. package/build/hooks/border.cjs +19 -4
  106. package/build/hooks/border.cjs.map +2 -2
  107. package/build/hooks/custom-css.cjs +6 -2
  108. package/build/hooks/custom-css.cjs.map +2 -2
  109. package/build/hooks/dimensions.cjs +15 -4
  110. package/build/hooks/dimensions.cjs.map +2 -2
  111. package/build/hooks/duotone.cjs +26 -14
  112. package/build/hooks/duotone.cjs.map +3 -3
  113. package/build/hooks/elements.cjs +14 -4
  114. package/build/hooks/elements.cjs.map +2 -2
  115. package/build/hooks/line-height.cjs +1 -2
  116. package/build/hooks/line-height.cjs.map +2 -2
  117. package/build/hooks/typography.cjs +13 -4
  118. package/build/hooks/typography.cjs.map +2 -2
  119. package/build/private-apis.cjs +9 -5
  120. package/build/private-apis.cjs.map +3 -3
  121. package/build/store/actions.cjs +9 -4
  122. package/build/store/actions.cjs.map +2 -2
  123. package/build/store/private-selectors.cjs +28 -1
  124. package/build/store/private-selectors.cjs.map +2 -2
  125. package/build/store/reducer.cjs +6 -6
  126. package/build/store/reducer.cjs.map +2 -2
  127. package/build/store/selectors.cjs +23 -0
  128. package/build/store/selectors.cjs.map +2 -2
  129. package/build/utils/native-undo.cjs +55 -0
  130. package/build/utils/native-undo.cjs.map +7 -0
  131. package/build-module/components/alignment-control/ui.mjs +2 -4
  132. package/build-module/components/alignment-control/ui.mjs.map +2 -2
  133. package/build-module/components/background-image-control/index.mjs +43 -10
  134. package/build-module/components/background-image-control/index.mjs.map +2 -2
  135. package/build-module/components/block-alignment-control/ui.mjs +5 -3
  136. package/build-module/components/block-alignment-control/ui.mjs.map +2 -2
  137. package/build-module/components/block-inspector/index.mjs +74 -46
  138. package/build-module/components/block-inspector/index.mjs.map +2 -2
  139. package/build-module/components/block-list/block-html.mjs +4 -1
  140. package/build-module/components/block-list/block-html.mjs.map +2 -2
  141. package/build-module/components/block-list/use-block-props/index.mjs +5 -2
  142. package/build-module/components/block-list/use-block-props/index.mjs.map +2 -2
  143. package/build-module/components/block-list/use-block-props/use-focus-handler.mjs +2 -3
  144. package/build-module/components/block-list/use-block-props/use-focus-handler.mjs.map +2 -2
  145. package/build-module/components/block-list/use-block-props/use-register-block-event-handlers.mjs +28 -0
  146. package/build-module/components/block-list/use-block-props/use-register-block-event-handlers.mjs.map +7 -0
  147. package/build-module/components/block-parent-selector/index.mjs +2 -2
  148. package/build-module/components/block-parent-selector/index.mjs.map +2 -2
  149. package/build-module/components/block-toolbar/switch-section-style.mjs +4 -2
  150. package/build-module/components/block-toolbar/switch-section-style.mjs.map +2 -2
  151. package/build-module/components/block-tools/empty-block-inserter.mjs +2 -7
  152. package/build-module/components/block-tools/empty-block-inserter.mjs.map +2 -2
  153. package/build-module/components/block-vertical-alignment-control/ui.mjs +4 -6
  154. package/build-module/components/block-vertical-alignment-control/ui.mjs.map +2 -2
  155. package/build-module/components/border-radius-control/utils.mjs +11 -53
  156. package/build-module/components/border-radius-control/utils.mjs.map +2 -2
  157. package/build-module/components/colors-gradients/control.mjs +5 -3
  158. package/build-module/components/colors-gradients/control.mjs.map +2 -2
  159. package/build-module/components/dimension-control/index.mjs +30 -20
  160. package/build-module/components/dimension-control/index.mjs.map +2 -2
  161. package/build-module/components/dimensions-tool/aspect-ratio-tool.mjs +10 -6
  162. package/build-module/components/dimensions-tool/aspect-ratio-tool.mjs.map +2 -2
  163. package/build-module/components/editor-styles/index.mjs +9 -1
  164. package/build-module/components/editor-styles/index.mjs.map +2 -2
  165. package/build-module/components/global-styles/background-panel.mjs +71 -29
  166. package/build-module/components/global-styles/background-panel.mjs.map +2 -2
  167. package/build-module/components/global-styles/border-panel.mjs +157 -56
  168. package/build-module/components/global-styles/border-panel.mjs.map +2 -2
  169. package/build-module/components/global-styles/color-gradient-dropdown-item.mjs +51 -15
  170. package/build-module/components/global-styles/color-gradient-dropdown-item.mjs.map +2 -2
  171. package/build-module/components/global-styles/color-panel.mjs +63 -17
  172. package/build-module/components/global-styles/color-panel.mjs.map +2 -2
  173. package/build-module/components/global-styles/dimensions-panel.mjs +248 -66
  174. package/build-module/components/global-styles/dimensions-panel.mjs.map +2 -2
  175. package/build-module/components/global-styles/filters-panel.mjs +64 -21
  176. package/build-module/components/global-styles/filters-panel.mjs.map +2 -2
  177. package/build-module/components/global-styles/hooks.mjs +4 -1
  178. package/build-module/components/global-styles/hooks.mjs.map +2 -2
  179. package/build-module/components/global-styles/inheritance/index.mjs +108 -0
  180. package/build-module/components/global-styles/inheritance/index.mjs.map +7 -0
  181. package/build-module/components/global-styles/inherited-value-context.mjs +141 -0
  182. package/build-module/components/global-styles/inherited-value-context.mjs.map +7 -0
  183. package/build-module/components/global-styles/shadow-panel-components.mjs +38 -20
  184. package/build-module/components/global-styles/shadow-panel-components.mjs.map +2 -2
  185. package/build-module/components/global-styles/state-control.mjs +1 -1
  186. package/build-module/components/global-styles/state-control.mjs.map +2 -2
  187. package/build-module/components/global-styles/typography-panel.mjs +276 -79
  188. package/build-module/components/global-styles/typography-panel.mjs.map +2 -2
  189. package/build-module/components/inspector-controls-tabs/styles-tab.mjs +6 -1
  190. package/build-module/components/inspector-controls-tabs/styles-tab.mjs.map +2 -2
  191. package/build-module/components/line-height-control/index.mjs +1 -1
  192. package/build-module/components/line-height-control/index.mjs.map +2 -2
  193. package/build-module/components/link-control/index.mjs +4 -2
  194. package/build-module/components/link-control/index.mjs.map +2 -2
  195. package/build-module/components/preset-input-control/index.mjs +2 -1
  196. package/build-module/components/preset-input-control/index.mjs.map +2 -2
  197. package/build-module/components/provider/block-refs-provider.mjs +8 -2
  198. package/build-module/components/provider/block-refs-provider.mjs.map +2 -2
  199. package/build-module/components/provider/index.mjs +1 -1
  200. package/build-module/components/provider/index.mjs.map +2 -2
  201. package/build-module/components/provider/use-block-sync.mjs +18 -6
  202. package/build-module/components/provider/use-block-sync.mjs.map +2 -2
  203. package/build-module/components/provider/use-media-upload-settings.mjs +3 -1
  204. package/build-module/components/provider/use-media-upload-settings.mjs.map +2 -2
  205. package/build-module/components/rich-text/index.mjs +28 -14
  206. package/build-module/components/rich-text/index.mjs.map +2 -2
  207. package/build-module/components/spacing-sizes-control/utils.mjs +6 -18
  208. package/build-module/components/spacing-sizes-control/utils.mjs.map +2 -2
  209. package/build-module/components/text-indent-control/index.mjs +8 -2
  210. package/build-module/components/text-indent-control/index.mjs.map +2 -2
  211. package/build-module/components/url-input/index.mjs +306 -353
  212. package/build-module/components/url-input/index.mjs.map +2 -2
  213. package/build-module/components/writing-flow/index.mjs +2 -0
  214. package/build-module/components/writing-flow/index.mjs.map +2 -2
  215. package/build-module/components/writing-flow/use-arrow-nav.mjs +24 -1
  216. package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
  217. package/build-module/components/writing-flow/use-click-selection.mjs +28 -2
  218. package/build-module/components/writing-flow/use-click-selection.mjs.map +2 -2
  219. package/build-module/components/writing-flow/use-editable-root-event-handlers.mjs +154 -0
  220. package/build-module/components/writing-flow/use-editable-root-event-handlers.mjs.map +7 -0
  221. package/build-module/components/writing-flow/use-editable-root.mjs +5 -37
  222. package/build-module/components/writing-flow/use-editable-root.mjs.map +2 -2
  223. package/build-module/components/writing-flow/use-home-end.mjs +1 -1
  224. package/build-module/components/writing-flow/use-home-end.mjs.map +2 -2
  225. package/build-module/components/writing-flow/use-selection-observer.mjs +18 -14
  226. package/build-module/components/writing-flow/use-selection-observer.mjs.map +2 -2
  227. package/build-module/components/writing-flow/utils.mjs +2 -11
  228. package/build-module/components/writing-flow/utils.mjs.map +2 -2
  229. package/build-module/hooks/background.mjs +13 -10
  230. package/build-module/hooks/background.mjs.map +2 -2
  231. package/build-module/hooks/block-style-variation.mjs +8 -44
  232. package/build-module/hooks/block-style-variation.mjs.map +2 -2
  233. package/build-module/hooks/border.mjs +19 -4
  234. package/build-module/hooks/border.mjs.map +2 -2
  235. package/build-module/hooks/custom-css.mjs +7 -3
  236. package/build-module/hooks/custom-css.mjs.map +2 -2
  237. package/build-module/hooks/dimensions.mjs +15 -4
  238. package/build-module/hooks/dimensions.mjs.map +2 -2
  239. package/build-module/hooks/duotone.mjs +20 -9
  240. package/build-module/hooks/duotone.mjs.map +2 -2
  241. package/build-module/hooks/elements.mjs +14 -4
  242. package/build-module/hooks/elements.mjs.map +2 -2
  243. package/build-module/hooks/line-height.mjs +1 -2
  244. package/build-module/hooks/line-height.mjs.map +2 -2
  245. package/build-module/hooks/typography.mjs +13 -4
  246. package/build-module/hooks/typography.mjs.map +2 -2
  247. package/build-module/private-apis.mjs +7 -3
  248. package/build-module/private-apis.mjs.map +2 -2
  249. package/build-module/store/actions.mjs +9 -4
  250. package/build-module/store/actions.mjs.map +2 -2
  251. package/build-module/store/private-selectors.mjs +29 -1
  252. package/build-module/store/private-selectors.mjs.map +2 -2
  253. package/build-module/store/reducer.mjs +6 -6
  254. package/build-module/store/reducer.mjs.map +2 -2
  255. package/build-module/store/selectors.mjs +23 -0
  256. package/build-module/store/selectors.mjs.map +2 -2
  257. package/build-module/utils/native-undo.mjs +29 -0
  258. package/build-module/utils/native-undo.mjs.map +7 -0
  259. package/build-style/content-rtl.css +10 -18
  260. package/build-style/content.css +10 -18
  261. package/build-style/style-rtl.css +150 -7
  262. package/build-style/style.css +150 -7
  263. package/package.json +40 -41
  264. package/src/components/alignment-control/test/__snapshots__/index.js.snap +1 -1
  265. package/src/components/alignment-control/ui.js +2 -4
  266. package/src/components/background-image-control/index.js +74 -37
  267. package/src/components/background-image-control/style.scss +13 -4
  268. package/src/components/block-alignment-control/test/__snapshots__/index.js.snap +3 -3
  269. package/src/components/block-alignment-control/test/index.js +1 -1
  270. package/src/components/block-alignment-control/ui.js +4 -2
  271. package/src/components/block-breadcrumb/test/__snapshots__/index.js.snap +1 -1
  272. package/src/components/block-compare/test/__snapshots__/block-view.js.snap +1 -1
  273. package/src/components/block-icon/test/__snapshots__/index.js.snap +1 -1
  274. package/src/components/block-inspector/index.js +60 -31
  275. package/src/components/block-list/block-html.js +5 -0
  276. package/src/components/block-list/content.scss +1 -11
  277. package/src/components/block-list/use-block-props/index.js +5 -1
  278. package/src/components/block-list/use-block-props/use-focus-handler.js +9 -8
  279. package/src/components/block-list/use-block-props/use-register-block-event-handlers.js +45 -0
  280. package/src/components/block-parent-selector/index.js +5 -2
  281. package/src/components/block-toolbar/switch-section-style.js +5 -2
  282. package/src/components/block-tools/empty-block-inserter.js +3 -7
  283. package/src/components/block-vertical-alignment-control/test/__snapshots__/index.js.snap +3 -3
  284. package/src/components/block-vertical-alignment-control/test/index.js +18 -2
  285. package/src/components/block-vertical-alignment-control/ui.js +3 -5
  286. package/src/components/border-radius-control/test/utils.js +0 -41
  287. package/src/components/border-radius-control/utils.js +16 -107
  288. package/src/components/button-block-appender/content.scss +6 -2
  289. package/src/components/color-palette/test/__snapshots__/control.js.snap +1 -1
  290. package/src/components/colors-gradients/control.js +6 -3
  291. package/src/components/default-block-appender/test/__snapshots__/index.js.snap +1 -1
  292. package/src/components/dimension-control/index.js +13 -1
  293. package/src/components/dimensions-tool/aspect-ratio-tool.js +13 -6
  294. package/src/components/editor-styles/index.js +16 -1
  295. package/src/components/global-styles/background-panel.js +107 -34
  296. package/src/components/global-styles/border-panel.js +219 -48
  297. package/src/components/global-styles/color-gradient-dropdown-item.js +93 -28
  298. package/src/components/global-styles/color-panel.js +92 -11
  299. package/src/components/global-styles/dimensions-panel.js +422 -71
  300. package/src/components/global-styles/filters-panel.js +88 -28
  301. package/src/components/global-styles/hooks.js +8 -1
  302. package/src/components/global-styles/inheritance/index.js +204 -0
  303. package/src/components/global-styles/inheritance/style.scss +184 -0
  304. package/src/components/global-styles/inheritance/test/index.js +281 -0
  305. package/src/components/global-styles/inherited-value-context.js +271 -0
  306. package/src/components/global-styles/shadow-panel-components.js +48 -26
  307. package/src/components/global-styles/state-control.js +2 -2
  308. package/src/components/global-styles/test/background-panel.js +338 -1
  309. package/src/components/global-styles/test/border-panel.js +441 -0
  310. package/src/components/global-styles/test/color-panel.js +182 -2
  311. package/src/components/global-styles/test/dimensions-panel.js +753 -0
  312. package/src/components/global-styles/test/filters-panel.js +194 -0
  313. package/src/components/global-styles/test/inherited-value-context-core.js +98 -0
  314. package/src/components/global-styles/test/inherited-value-context.js +454 -0
  315. package/src/components/global-styles/test/typography-panel-core.js +185 -0
  316. package/src/components/global-styles/test/typography-panel.js +849 -12
  317. package/src/components/global-styles/typography-panel.js +449 -100
  318. package/src/components/image-editor/style.scss +33 -0
  319. package/src/components/inner-blocks/test/__snapshots__/index.js.snap +1 -1
  320. package/src/components/inspector-controls-tabs/styles-tab.js +5 -1
  321. package/src/components/line-height-control/index.js +1 -1
  322. package/src/components/link-control/index.js +7 -2
  323. package/src/components/preset-input-control/index.js +4 -1
  324. package/src/components/provider/block-refs-provider.js +12 -2
  325. package/src/components/provider/index.js +2 -2
  326. package/src/components/provider/use-block-sync.js +18 -6
  327. package/src/components/provider/use-media-upload-settings.js +2 -0
  328. package/src/components/rich-text/index.js +37 -16
  329. package/src/components/spacing-sizes-control/test/utils.js +16 -5
  330. package/src/components/spacing-sizes-control/utils.js +11 -28
  331. package/src/components/text-indent-control/index.js +8 -0
  332. package/src/components/url-input/index.js +363 -431
  333. package/src/components/url-input/test/index.js +592 -0
  334. package/src/components/warning/test/__snapshots__/index.js.snap +1 -1
  335. package/src/components/writing-flow/index.js +2 -0
  336. package/src/components/writing-flow/use-arrow-nav.js +51 -1
  337. package/src/components/writing-flow/use-click-selection.js +54 -3
  338. package/src/components/writing-flow/use-editable-root-event-handlers.js +266 -0
  339. package/src/components/writing-flow/use-editable-root.js +7 -77
  340. package/src/components/writing-flow/use-home-end.js +17 -9
  341. package/src/components/writing-flow/use-selection-observer.js +32 -15
  342. package/src/components/writing-flow/utils.js +3 -24
  343. package/src/hooks/background.js +26 -21
  344. package/src/hooks/block-fields/rich-text/styles.scss +1 -1
  345. package/src/hooks/block-style-variation.js +8 -74
  346. package/src/hooks/border.js +19 -4
  347. package/src/hooks/custom-css.js +15 -6
  348. package/src/hooks/dimensions.js +17 -4
  349. package/src/hooks/duotone.js +26 -8
  350. package/src/hooks/elements.js +14 -3
  351. package/src/hooks/line-height.js +0 -1
  352. package/src/hooks/test/inherited-value-wiring.js +411 -0
  353. package/src/hooks/test/preset-round-trip.js +456 -0
  354. package/src/hooks/typography.js +35 -23
  355. package/src/private-apis.js +11 -2
  356. package/src/store/actions.js +10 -4
  357. package/src/store/private-selectors.js +57 -1
  358. package/src/store/reducer.js +9 -9
  359. package/src/store/selectors.js +56 -8
  360. package/src/store/test/private-selectors.js +160 -1
  361. package/src/style.scss +2 -0
  362. package/src/utils/native-undo.js +56 -0
  363. package/build/components/duotone/utils.cjs +0 -87
  364. package/build/components/duotone/utils.cjs.map +0 -7
  365. package/build-module/components/duotone/utils.mjs +0 -59
  366. package/build-module/components/duotone/utils.mjs.map +0 -7
  367. package/src/components/duotone/utils.js +0 -90
  368. package/src/hooks/test/get-variation-styles-with-ref-values.js +0 -91
@@ -6,25 +6,20 @@ import clsx from 'clsx';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
+ import { speak } from '@wordpress/a11y';
9
10
  import { __, sprintf, _n } from '@wordpress/i18n';
10
- import { Component, createRef } from '@wordpress/element';
11
+ import { useEffect, useRef, useState } from '@wordpress/element';
11
12
  import { UP, DOWN, ENTER, TAB } from '@wordpress/keycodes';
12
13
  import {
13
14
  BaseControl,
14
15
  Button,
15
16
  __experimentalInputControl as InputControl,
16
17
  Spinner,
17
- withSpokenMessages,
18
18
  Popover,
19
19
  privateApis as componentsPrivateApis,
20
20
  } from '@wordpress/components';
21
- import {
22
- compose,
23
- debounce,
24
- withInstanceId,
25
- withSafeTimeout,
26
- } from '@wordpress/compose';
27
- import { withSelect } from '@wordpress/data';
21
+ import { useDebounce, useEvent, useInstanceId } from '@wordpress/compose';
22
+ import { useSelect } from '@wordpress/data';
28
23
  import { isURL } from '@wordpress/url';
29
24
 
30
25
  /**
@@ -35,6 +30,8 @@ import { unlock } from '../../lock-unlock';
35
30
 
36
31
  const { ValidatedInputControl } = unlock( componentsPrivateApis );
37
32
 
33
+ const noop = () => {};
34
+
38
35
  /**
39
36
  * Whether the argument is a function.
40
37
  *
@@ -45,99 +42,75 @@ function isFunction( maybeFunc ) {
45
42
  return typeof maybeFunc === 'function';
46
43
  }
47
44
 
48
- class URLInput extends Component {
49
- constructor( props ) {
50
- super( props );
51
-
52
- this.onChange = this.onChange.bind( this );
53
- this.onFocus = this.onFocus.bind( this );
54
- this.onKeyDown = this.onKeyDown.bind( this );
55
- this.selectLink = this.selectLink.bind( this );
56
- this.handleOnClick = this.handleOnClick.bind( this );
57
- this.bindSuggestionNode = this.bindSuggestionNode.bind( this );
58
- this.autocompleteRef = props.autocompleteRef || createRef();
59
- this.inputRef = props.inputRef || createRef();
60
- this.hasRenderedValidation = { current: false };
61
- this.updateSuggestions = debounce(
62
- this.updateSuggestions.bind( this ),
63
- 200
64
- );
65
-
66
- this.suggestionNodes = [];
67
-
68
- this.suggestionsRequest = null;
69
-
70
- this.state = {
71
- suggestions: [],
72
- showSuggestions: false,
73
- suggestionsValue: null,
74
- selectedSuggestion: null,
75
- suggestionsListboxId: '',
76
- suggestionOptionIdPrefix: '',
77
- };
78
- }
79
-
80
- componentDidUpdate( prevProps ) {
81
- const { showSuggestions, selectedSuggestion } = this.state;
82
- const { value, __experimentalShowInitialSuggestions = false } =
83
- this.props;
84
-
85
- // Only have to worry about scrolling selected suggestion into view
86
- // when already expanded.
87
- if (
88
- showSuggestions &&
89
- selectedSuggestion !== null &&
90
- this.suggestionNodes[ selectedSuggestion ]
91
- ) {
92
- this.suggestionNodes[ selectedSuggestion ].scrollIntoView( {
93
- behavior: 'instant',
94
- block: 'nearest',
95
- inline: 'nearest',
96
- } );
97
- }
98
-
99
- // Update suggestions when the value changes.
100
- if ( prevProps.value !== value && ! this.props.disableSuggestions ) {
101
- if ( value?.length ) {
102
- // If the new value is not empty we need to update with suggestions for it.
103
- this.updateSuggestions( value );
104
- } else if ( __experimentalShowInitialSuggestions ) {
105
- // If the new value is empty and we can show initial suggestions, then show initial suggestions.
106
- this.updateSuggestions();
107
- }
108
- }
109
- }
110
-
111
- componentDidMount() {
112
- if ( this.shouldShowInitialSuggestions() ) {
113
- this.updateSuggestions();
114
- }
115
- }
116
-
117
- componentWillUnmount() {
118
- this.suggestionsRequest?.cancel?.();
119
- this.suggestionsRequest = null;
120
- }
121
-
122
- bindSuggestionNode( index ) {
123
- return ( ref ) => {
124
- this.suggestionNodes[ index ] = ref;
125
- };
126
- }
127
-
128
- shouldShowInitialSuggestions() {
129
- const { __experimentalShowInitialSuggestions = false, value } =
130
- this.props;
131
- return (
132
- __experimentalShowInitialSuggestions && ! ( value && value.length )
133
- );
134
- }
135
-
136
- updateSuggestions( value = '' ) {
137
- const {
138
- __experimentalFetchLinkSuggestions: fetchLinkSuggestions,
139
- __experimentalHandleURLSuggestions: handleURLSuggestions,
140
- } = this.props;
45
+ /**
46
+ * Text field for entering a URL, with an autocomplete list of matching posts,
47
+ * pages and other link suggestions.
48
+ *
49
+ * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/url-input/README.md
50
+ *
51
+ * @param {Object} props Component props.
52
+ */
53
+ export default function URLInput( props ) {
54
+ const {
55
+ __experimentalFetchLinkSuggestions: fetchLinkSuggestionsProp,
56
+ __experimentalHandleURLSuggestions: handleURLSuggestions,
57
+ __experimentalRenderControl: renderControl,
58
+ __experimentalRenderSuggestions: renderSuggestions,
59
+ __experimentalShowInitialSuggestions: showInitialSuggestions = false,
60
+ autocompleteRef,
61
+ className,
62
+ customValidity,
63
+ disableSuggestions,
64
+ disabled = false,
65
+ help = null,
66
+ hideLabelFromVision = false,
67
+ inputRef,
68
+ isFullWidth,
69
+ label = null,
70
+ markWhenOptional,
71
+ onChange,
72
+ onKeyDown,
73
+ onSubmit,
74
+ placeholder = __( 'Paste URL or type to search' ),
75
+ required = true,
76
+ suffix,
77
+ value = '',
78
+ } = props;
79
+
80
+ const instanceId = useInstanceId( URLInput );
81
+ const { getSettings } = useSelect( blockEditorStore );
82
+ const debouncedSpeak = useDebounce( speak, 500 );
83
+
84
+ const [ suggestions, setSuggestions ] = useState( [] );
85
+ const [ suggestionsValue, setSuggestionsValue ] = useState( null );
86
+ const [ selectedSuggestion, setSelectedSuggestion ] = useState( null );
87
+ const [ isSuggestionsListOpen, setIsSuggestionsListOpen ] =
88
+ useState( false );
89
+ const [ isLoading, setIsLoading ] = useState( false );
90
+
91
+ const fallbackInputRef = useRef();
92
+ const suggestionNodesRef = useRef( [] );
93
+ // A fetch Promise can't be aborted. It's mimicked by holding on to the
94
+ // pending request so that responses of superseded requests can be ignored.
95
+ const suggestionsRequestRef = useRef( null );
96
+
97
+ const controlInputRef = inputRef ?? fallbackInputRef;
98
+
99
+ const inputId = `url-input-control-${ instanceId }`;
100
+ const suggestionsListboxId = `block-editor-url-input-suggestions-${ instanceId }`;
101
+ const suggestionOptionIdPrefix = `block-editor-url-input-suggestion-${ instanceId }`;
102
+
103
+ // The suggestions are hidden rather than discarded, so that returning focus
104
+ // to a field that already has results doesn't trigger a new search.
105
+ const showSuggestions =
106
+ isSuggestionsListOpen &&
107
+ disableSuggestions !== true &&
108
+ ( showInitialSuggestions || !! value.length );
109
+
110
+ const updateSuggestions = useEvent( ( searchValue = '' ) => {
111
+ const fetchLinkSuggestions = isFunction( fetchLinkSuggestionsProp )
112
+ ? fetchLinkSuggestionsProp
113
+ : getSettings().__experimentalFetchLinkSuggestions;
141
114
 
142
115
  if ( ! fetchLinkSuggestions ) {
143
116
  return;
@@ -145,11 +118,11 @@ class URLInput extends Component {
145
118
 
146
119
  // Initial suggestions may only show if there is no value
147
120
  // (note: this includes whitespace).
148
- const isInitialSuggestions = ! value?.length;
121
+ const isInitialSuggestions = ! searchValue?.length;
149
122
 
150
123
  // Trim only now we've determined whether or not it originally had a "length"
151
124
  // (even if that value was all whitespace).
152
- value = value.trim();
125
+ const search = searchValue.trim();
153
126
 
154
127
  // Allow a suggestions request if:
155
128
  // - there are at least 2 characters in the search input (except manual searches where
@@ -157,119 +130,152 @@ class URLInput extends Component {
157
130
  // - this is a direct entry (eg: a URL)
158
131
  if (
159
132
  ! isInitialSuggestions &&
160
- ( value.length < 2 || ( ! handleURLSuggestions && isURL( value ) ) )
133
+ ( search.length < 2 ||
134
+ ( ! handleURLSuggestions && isURL( search ) ) )
161
135
  ) {
162
- this.suggestionsRequest?.cancel?.();
163
- this.suggestionsRequest = null;
164
-
165
- this.setState( {
166
- suggestions: [],
167
- showSuggestions: false,
168
- suggestionsValue: value,
169
- selectedSuggestion: null,
170
- loading: false,
171
- } );
136
+ suggestionsRequestRef.current?.cancel?.();
137
+ suggestionsRequestRef.current = null;
138
+
139
+ setSuggestions( [] );
140
+ setIsSuggestionsListOpen( false );
141
+ setSuggestionsValue( search );
142
+ setSelectedSuggestion( null );
143
+ setIsLoading( false );
172
144
 
173
145
  return;
174
146
  }
175
147
 
176
- this.setState( {
177
- selectedSuggestion: null,
178
- loading: true,
179
- } );
148
+ setSelectedSuggestion( null );
149
+ setIsLoading( true );
180
150
 
181
- const request = fetchLinkSuggestions( value, {
151
+ const request = fetchLinkSuggestions( search, {
182
152
  isInitialSuggestions,
183
153
  } );
154
+ suggestionsRequestRef.current = request;
184
155
 
185
156
  request
186
- .then( ( suggestions ) => {
187
- // A fetch Promise doesn't have an abort option. It's mimicked by
188
- // comparing the request reference in on the instance, which is
189
- // reset or deleted on subsequent requests or unmounting.
190
- if ( this.suggestionsRequest !== request ) {
157
+ .then( ( nextSuggestions ) => {
158
+ if ( suggestionsRequestRef.current !== request ) {
191
159
  return;
192
160
  }
193
161
 
194
- this.setState( {
195
- suggestions,
196
- suggestionsValue: value,
197
- loading: false,
198
- showSuggestions: !! suggestions.length,
199
- } );
162
+ setSuggestions( nextSuggestions );
163
+ setSuggestionsValue( search );
164
+ setIsLoading( false );
165
+ setIsSuggestionsListOpen( !! nextSuggestions.length );
200
166
 
201
- if ( !! suggestions.length ) {
202
- this.props.debouncedSpeak(
167
+ if ( nextSuggestions.length ) {
168
+ debouncedSpeak(
203
169
  sprintf(
204
170
  /* translators: %d: number of results. */
205
171
  _n(
206
172
  '%d result found, use up and down arrow keys to navigate.',
207
173
  '%d results found, use up and down arrow keys to navigate.',
208
- suggestions.length
174
+ nextSuggestions.length
209
175
  ),
210
- suggestions.length
176
+ nextSuggestions.length
211
177
  ),
212
178
  'assertive'
213
179
  );
214
180
  } else {
215
- this.props.debouncedSpeak(
216
- __( 'No results.' ),
217
- 'assertive'
218
- );
181
+ debouncedSpeak( __( 'No results.' ), 'assertive' );
219
182
  }
220
183
  } )
221
184
  .catch( () => {
222
- if ( this.suggestionsRequest !== request ) {
185
+ if ( suggestionsRequestRef.current !== request ) {
223
186
  return;
224
187
  }
225
188
 
226
- this.setState( {
227
- loading: false,
228
- } );
189
+ setIsLoading( false );
229
190
  } )
230
191
  .finally( () => {
231
- // If this is the current promise then reset the reference
232
- // to allow for checking if a new request is made.
233
- if ( this.suggestionsRequest === request ) {
234
- this.suggestionsRequest = null;
192
+ if ( suggestionsRequestRef.current === request ) {
193
+ suggestionsRequestRef.current = null;
235
194
  }
236
195
  } );
196
+ } );
197
+
198
+ const debouncedUpdateSuggestions = useDebounce( updateSuggestions, 200 );
199
+
200
+ // Keep the suggestions in sync with the value being searched for. An empty
201
+ // value requests the initial suggestions, when those are enabled.
202
+ useEffect( () => {
203
+ if (
204
+ ! disableSuggestions &&
205
+ ( value.length || showInitialSuggestions )
206
+ ) {
207
+ debouncedUpdateSuggestions( value );
208
+ }
209
+ }, [
210
+ value,
211
+ disableSuggestions,
212
+ showInitialSuggestions,
213
+ debouncedUpdateSuggestions,
214
+ ] );
215
+
216
+ // Persist the hidden state, so that the list can't reappear with stale
217
+ // results once the value or the props allow suggestions again.
218
+ useEffect( () => {
219
+ if ( ! showSuggestions ) {
220
+ setIsSuggestionsListOpen( false );
221
+ }
222
+ }, [ showSuggestions ] );
223
+
224
+ useEffect( () => {
225
+ if ( showSuggestions && selectedSuggestion !== null ) {
226
+ suggestionNodesRef.current[ selectedSuggestion ]?.scrollIntoView( {
227
+ behavior: 'instant',
228
+ block: 'nearest',
229
+ inline: 'nearest',
230
+ } );
231
+ }
232
+ }, [ showSuggestions, selectedSuggestion ] );
237
233
 
238
- // Note that this assignment is handled *before* the async search request
239
- // as a Promise always resolves on the next tick of the event loop.
240
- this.suggestionsRequest = request;
234
+ useEffect( () => {
235
+ return () => {
236
+ suggestionsRequestRef.current?.cancel?.();
237
+ suggestionsRequestRef.current = null;
238
+ };
239
+ }, [] );
240
+
241
+ function selectLink( suggestion ) {
242
+ onChange( suggestion.url, suggestion );
243
+ setSelectedSuggestion( null );
244
+ setIsSuggestionsListOpen( false );
241
245
  }
242
246
 
243
- onChange( newValue ) {
244
- this.props.onChange( newValue );
247
+ function handleSuggestionClick( suggestion ) {
248
+ selectLink( suggestion );
249
+ // Move focus to the input field when a link suggestion is clicked.
250
+ controlInputRef.current.focus();
245
251
  }
246
252
 
247
- onFocus() {
248
- const { suggestions } = this.state;
249
- const { disableSuggestions, value } = this.props;
253
+ function handleChange( newValue ) {
254
+ // `InputControl` passes an `{ event }` object as its second argument,
255
+ // which callers would mistake for a selected suggestion.
256
+ onChange( newValue );
257
+ }
250
258
 
259
+ function handleFocus() {
251
260
  // When opening the link editor, if there's a value present, we want to load the suggestions pane with the results for this input search value
252
261
  // Don't re-run the suggestions on focus if there are already suggestions present (prevents searching again when tabbing between the input and buttons)
253
262
  // or there is already a request in progress.
254
263
  if (
255
264
  value &&
256
265
  ! disableSuggestions &&
257
- ! ( suggestions && suggestions.length ) &&
258
- this.suggestionsRequest === null
266
+ ! suggestions.length &&
267
+ suggestionsRequestRef.current === null
259
268
  ) {
260
- // Ensure the suggestions are updated with the current input value.
261
- this.updateSuggestions( value );
269
+ debouncedUpdateSuggestions( value );
262
270
  }
263
271
  }
264
272
 
265
- onKeyDown( event ) {
266
- this.props.onKeyDown?.( event );
267
- const { showSuggestions, selectedSuggestion, suggestions, loading } =
268
- this.state;
273
+ function handleKeyDown( event ) {
274
+ onKeyDown?.( event );
269
275
 
270
276
  // If the suggestions are not shown or loading, we shouldn't handle the arrow keys
271
277
  // We shouldn't preventDefault to allow block arrow keys navigation.
272
- if ( ! showSuggestions || ! suggestions.length || loading ) {
278
+ if ( ! showSuggestions || ! suggestions.length || isLoading ) {
273
279
  // In the Windows version of Firefox the up and down arrows don't move the caret
274
280
  // within an input field like they do for Mac Firefox/Chrome/Safari. This causes
275
281
  // a form of focus trapping that is disruptive to the user experience. This disruption
@@ -290,15 +296,13 @@ class URLInput extends Component {
290
296
  // When DOWN is pressed, if the caret is not at the end of the text, move it to the
291
297
  // last position.
292
298
  case DOWN: {
293
- if (
294
- this.props.value.length !== event.target.selectionStart
295
- ) {
299
+ if ( value.length !== event.target.selectionStart ) {
296
300
  event.preventDefault();
297
301
 
298
302
  // Set the input caret to the last position.
299
303
  event.target.setSelectionRange(
300
- this.props.value.length,
301
- this.props.value.length
304
+ value.length,
305
+ value.length
302
306
  );
303
307
  }
304
308
  break;
@@ -306,9 +310,9 @@ class URLInput extends Component {
306
310
 
307
311
  // Submitting while loading should trigger onSubmit.
308
312
  case ENTER: {
309
- if ( this.props.onSubmit ) {
313
+ if ( onSubmit ) {
310
314
  event.preventDefault();
311
- this.props.onSubmit( null, event );
315
+ onSubmit( null, event );
312
316
  }
313
317
  break;
314
318
  }
@@ -317,50 +321,46 @@ class URLInput extends Component {
317
321
  return;
318
322
  }
319
323
 
320
- const suggestion =
321
- this.state.suggestions[ this.state.selectedSuggestion ];
324
+ const suggestion = suggestions[ selectedSuggestion ];
322
325
 
323
326
  switch ( event.keyCode ) {
324
327
  case UP: {
325
328
  event.preventDefault();
326
- const previousIndex = ! selectedSuggestion
327
- ? suggestions.length - 1
328
- : selectedSuggestion - 1;
329
- this.setState( {
330
- selectedSuggestion: previousIndex,
331
- } );
329
+ setSelectedSuggestion(
330
+ ! selectedSuggestion
331
+ ? suggestions.length - 1
332
+ : selectedSuggestion - 1
333
+ );
332
334
  break;
333
335
  }
334
336
  case DOWN: {
335
337
  event.preventDefault();
336
- const nextIndex =
338
+ setSelectedSuggestion(
337
339
  selectedSuggestion === null ||
338
- selectedSuggestion === suggestions.length - 1
340
+ selectedSuggestion === suggestions.length - 1
339
341
  ? 0
340
- : selectedSuggestion + 1;
341
- this.setState( {
342
- selectedSuggestion: nextIndex,
343
- } );
342
+ : selectedSuggestion + 1
343
+ );
344
344
  break;
345
345
  }
346
346
  case TAB: {
347
- if ( this.state.selectedSuggestion !== null ) {
348
- this.selectLink( suggestion );
347
+ if ( selectedSuggestion !== null ) {
348
+ selectLink( suggestion );
349
349
  // Announce a link has been selected when tabbing away from the input field.
350
- this.props.speak( __( 'Link selected.' ) );
350
+ speak( __( 'Link selected.' ) );
351
351
  }
352
352
  break;
353
353
  }
354
354
  case ENTER: {
355
355
  event.preventDefault();
356
- if ( this.state.selectedSuggestion !== null ) {
357
- this.selectLink( suggestion );
356
+ if ( selectedSuggestion !== null ) {
357
+ selectLink( suggestion );
358
358
 
359
- if ( this.props.onSubmit ) {
360
- this.props.onSubmit( suggestion, event );
359
+ if ( onSubmit ) {
360
+ onSubmit( suggestion, event );
361
361
  }
362
- } else if ( this.props.onSubmit ) {
363
- this.props.onSubmit( null, event );
362
+ } else if ( onSubmit ) {
363
+ onSubmit( null, event );
364
364
  }
365
365
 
366
366
  break;
@@ -368,252 +368,184 @@ class URLInput extends Component {
368
368
  }
369
369
  }
370
370
 
371
- selectLink( suggestion ) {
372
- this.props.onChange( suggestion.url, suggestion );
373
- this.setState( {
374
- selectedSuggestion: null,
375
- showSuggestions: false,
376
- } );
377
- }
378
-
379
- handleOnClick( suggestion ) {
380
- this.selectLink( suggestion );
381
- // Move focus to the input field when a link suggestion is clicked.
382
- this.inputRef.current.focus();
383
- }
384
-
385
- static getDerivedStateFromProps(
386
- {
387
- value,
388
- instanceId,
389
- disableSuggestions,
390
- __experimentalShowInitialSuggestions = false,
391
- },
392
- { showSuggestions }
393
- ) {
394
- let shouldShowSuggestions = showSuggestions;
395
-
396
- const hasValue = value && value.length;
397
-
398
- if ( ! __experimentalShowInitialSuggestions && ! hasValue ) {
399
- shouldShowSuggestions = false;
400
- }
401
-
402
- if ( disableSuggestions === true ) {
403
- shouldShowSuggestions = false;
404
- }
371
+ const controlProps = {
372
+ id: inputId, // Passes attribute to label for the for attribute
373
+ label,
374
+ className: clsx( 'block-editor-url-input', className, {
375
+ 'is-full-width': isFullWidth,
376
+ } ),
377
+ hideLabelFromVision,
378
+ };
379
+
380
+ const inputProps = {
381
+ id: inputId,
382
+ value,
383
+ required,
384
+ type: 'text',
385
+ name: inputId,
386
+ autoComplete: 'off',
387
+ onChange: disabled ? noop : handleChange,
388
+ onFocus: disabled ? noop : handleFocus,
389
+ onKeyDown: disabled ? noop : handleKeyDown,
390
+ placeholder,
391
+ role: 'combobox',
392
+ 'aria-label': label ? undefined : __( 'URL' ), // Ensure input always has an accessible label
393
+ 'aria-expanded': showSuggestions,
394
+ 'aria-autocomplete': 'list',
395
+ 'aria-owns': suggestionsListboxId,
396
+ 'aria-activedescendant':
397
+ selectedSuggestion !== null
398
+ ? `${ suggestionOptionIdPrefix }-${ selectedSuggestion }`
399
+ : undefined,
400
+ ref: controlInputRef,
401
+ disabled,
402
+ suffix,
403
+ help,
404
+ };
405
+
406
+ return (
407
+ <>
408
+ <Control
409
+ controlProps={ controlProps }
410
+ inputProps={ inputProps }
411
+ isLoading={ isLoading }
412
+ customValidity={ customValidity }
413
+ markWhenOptional={ markWhenOptional }
414
+ renderControl={ renderControl }
415
+ />
416
+ { showSuggestions && suggestions.length > 0 && (
417
+ <Suggestions
418
+ autocompleteRef={ autocompleteRef }
419
+ className={ className }
420
+ handleSuggestionClick={ handleSuggestionClick }
421
+ isLoading={ isLoading }
422
+ renderSuggestions={ renderSuggestions }
423
+ selectedSuggestion={ selectedSuggestion }
424
+ suggestionNodesRef={ suggestionNodesRef }
425
+ suggestionOptionIdPrefix={ suggestionOptionIdPrefix }
426
+ suggestions={ suggestions }
427
+ suggestionsListboxId={ suggestionsListboxId }
428
+ suggestionsValue={ suggestionsValue }
429
+ />
430
+ ) }
431
+ </>
432
+ );
433
+ }
405
434
 
406
- return {
407
- showSuggestions: shouldShowSuggestions,
408
- suggestionsListboxId: `block-editor-url-input-suggestions-${ instanceId }`,
409
- suggestionOptionIdPrefix: `block-editor-url-input-suggestion-${ instanceId }`,
410
- };
435
+ function Control( {
436
+ controlProps,
437
+ inputProps,
438
+ isLoading,
439
+ customValidity,
440
+ markWhenOptional,
441
+ renderControl,
442
+ } ) {
443
+ // Once a validity has been reported, keep using the validated control, so
444
+ // that clearing the validity doesn't remount (and blur) the input.
445
+ const [ isValidated, setIsValidated ] = useState(
446
+ customValidity !== undefined
447
+ );
448
+
449
+ if ( customValidity !== undefined && ! isValidated ) {
450
+ setIsValidated( true );
411
451
  }
412
452
 
413
- render() {
414
- return (
415
- <>
416
- { this.renderControl() }
417
- { this.renderSuggestions() }
418
- </>
419
- );
453
+ if ( renderControl ) {
454
+ return renderControl( controlProps, inputProps, isLoading );
420
455
  }
421
456
 
422
- renderControl() {
423
- const {
424
- label = null,
425
- className,
426
- isFullWidth,
427
- instanceId,
428
- placeholder = __( 'Paste URL or type to search' ),
429
- __experimentalRenderControl: renderControl,
430
- value = '',
431
- hideLabelFromVision = false,
432
- help = null,
433
- disabled = false,
434
- customValidity,
435
- markWhenOptional,
436
- } = this.props;
437
-
438
- const {
439
- loading,
440
- showSuggestions,
441
- selectedSuggestion,
442
- suggestionsListboxId,
443
- suggestionOptionIdPrefix,
444
- } = this.state;
445
-
446
- const inputId = `url-input-control-${ instanceId }`;
447
-
448
- const controlProps = {
449
- id: inputId, // Passes attribute to label for the for attribute
450
- label,
451
- className: clsx( 'block-editor-url-input', className, {
452
- 'is-full-width': isFullWidth,
453
- } ),
454
- hideLabelFromVision,
455
- };
456
-
457
- const inputProps = {
458
- id: inputId,
459
- value,
460
- required: this.props.required ?? true,
461
- type: 'text',
462
- name: inputId,
463
- autoComplete: 'off',
464
- onChange: disabled ? () => {} : this.onChange, // Disable onChange when disabled
465
- onFocus: disabled ? () => {} : this.onFocus, // Disable onFocus when disabled
466
- placeholder,
467
- onKeyDown: disabled ? () => {} : this.onKeyDown, // Disable onKeyDown when disabled
468
- role: 'combobox',
469
- 'aria-label': label ? undefined : __( 'URL' ), // Ensure input always has an accessible label
470
- 'aria-expanded': showSuggestions,
471
- 'aria-autocomplete': 'list',
472
- 'aria-owns': suggestionsListboxId,
473
- 'aria-activedescendant':
474
- selectedSuggestion !== null
475
- ? `${ suggestionOptionIdPrefix }-${ selectedSuggestion }`
476
- : undefined,
477
- ref: this.inputRef,
478
- disabled,
479
- suffix: this.props.suffix,
480
- help,
481
- };
457
+ const MaybeValidatedInputControl = isValidated
458
+ ? ValidatedInputControl
459
+ : InputControl;
460
+
461
+ return (
462
+ <BaseControl { ...controlProps }>
463
+ <MaybeValidatedInputControl
464
+ { ...inputProps }
465
+ { ...( isValidated && {
466
+ customValidity,
467
+ // Suppress the "(Required)" indicator in the label.
468
+ // The field is still required for validation, but the indicator
469
+ // can be hidden when markWhenOptional is set to true.
470
+ ...( markWhenOptional !== undefined && {
471
+ markWhenOptional,
472
+ } ),
473
+ } ) }
474
+ />
475
+ { isLoading && <Spinner /> }
476
+ </BaseControl>
477
+ );
478
+ }
482
479
 
483
- const validationProps = {
484
- customValidity,
485
- // Suppress the "(Required)" indicator in the label.
486
- // The field is still required for validation, but the indicator
487
- // can be hidden when markWhenOptional is set to true.
488
- ...( markWhenOptional !== undefined && {
489
- markWhenOptional,
490
- } ),
480
+ function Suggestions( {
481
+ autocompleteRef,
482
+ className,
483
+ handleSuggestionClick,
484
+ isLoading,
485
+ renderSuggestions,
486
+ selectedSuggestion,
487
+ suggestionNodesRef,
488
+ suggestionOptionIdPrefix,
489
+ suggestions,
490
+ suggestionsListboxId,
491
+ suggestionsValue,
492
+ } ) {
493
+ const suggestionsListProps = {
494
+ id: suggestionsListboxId,
495
+ ref: autocompleteRef,
496
+ role: 'listbox',
497
+ };
498
+
499
+ const buildSuggestionItemProps = ( suggestion, index ) => {
500
+ return {
501
+ role: 'option',
502
+ tabIndex: '-1',
503
+ id: `${ suggestionOptionIdPrefix }-${ index }`,
504
+ ref: ( node ) => {
505
+ suggestionNodesRef.current[ index ] = node;
506
+ },
507
+ 'aria-selected': index === selectedSuggestion ? true : undefined,
491
508
  };
509
+ };
492
510
 
493
- if ( renderControl ) {
494
- return renderControl( controlProps, inputProps, loading );
495
- }
496
-
497
- // Use ValidatedInputControl if customValidity has ever had a non-undefined value.
498
- if ( customValidity !== undefined ) {
499
- this.hasRenderedValidation.current = true;
500
- }
501
-
502
- const MaybeValidatedInputControl = this.hasRenderedValidation.current
503
- ? ValidatedInputControl
504
- : InputControl;
505
-
506
- return (
507
- <BaseControl { ...controlProps }>
508
- <MaybeValidatedInputControl
509
- { ...inputProps }
510
- { ...( this.hasRenderedValidation.current
511
- ? validationProps
512
- : {} ) }
513
- __next40pxDefaultSize
514
- />
515
- { loading && <Spinner /> }
516
- </BaseControl>
517
- );
518
- }
519
-
520
- renderSuggestions() {
521
- const {
522
- className,
523
- __experimentalRenderSuggestions: renderSuggestions,
524
- } = this.props;
525
-
526
- const {
527
- showSuggestions,
511
+ if ( isFunction( renderSuggestions ) ) {
512
+ return renderSuggestions( {
528
513
  suggestions,
529
- suggestionsValue,
530
514
  selectedSuggestion,
531
- suggestionsListboxId,
532
- suggestionOptionIdPrefix,
533
- loading,
534
- } = this.state;
535
-
536
- if ( ! showSuggestions || suggestions.length === 0 ) {
537
- return null;
538
- }
539
-
540
- const suggestionsListProps = {
541
- id: suggestionsListboxId,
542
- ref: this.autocompleteRef,
543
- role: 'listbox',
544
- };
545
-
546
- const buildSuggestionItemProps = ( suggestion, index ) => {
547
- return {
548
- role: 'option',
549
- tabIndex: '-1',
550
- id: `${ suggestionOptionIdPrefix }-${ index }`,
551
- ref: this.bindSuggestionNode( index ),
552
- 'aria-selected':
553
- index === selectedSuggestion ? true : undefined,
554
- };
555
- };
556
-
557
- if ( isFunction( renderSuggestions ) ) {
558
- return renderSuggestions( {
559
- suggestions,
560
- selectedSuggestion,
561
- suggestionsListProps,
562
- buildSuggestionItemProps,
563
- isLoading: loading,
564
- handleSuggestionClick: this.handleOnClick,
565
- isInitialSuggestions: ! suggestionsValue?.length,
566
- currentInputValue: suggestionsValue,
567
- } );
568
- }
569
-
570
- return (
571
- <Popover placement="bottom" focusOnMount={ false }>
572
- <div
573
- { ...suggestionsListProps }
574
- className={ clsx( 'block-editor-url-input__suggestions', {
575
- [ `${ className }__suggestions` ]: className,
576
- } ) }
577
- >
578
- { suggestions.map( ( suggestion, index ) => (
579
- <Button
580
- __next40pxDefaultSize
581
- { ...buildSuggestionItemProps( suggestion, index ) }
582
- key={ suggestion.id }
583
- className={ clsx(
584
- 'block-editor-url-input__suggestion',
585
- {
586
- 'is-selected': index === selectedSuggestion,
587
- }
588
- ) }
589
- onClick={ () => this.handleOnClick( suggestion ) }
590
- >
591
- { suggestion.title }
592
- </Button>
593
- ) ) }
594
- </div>
595
- </Popover>
596
- );
515
+ suggestionsListProps,
516
+ buildSuggestionItemProps,
517
+ isLoading,
518
+ handleSuggestionClick,
519
+ isInitialSuggestions: ! suggestionsValue?.length,
520
+ currentInputValue: suggestionsValue,
521
+ } );
597
522
  }
598
- }
599
523
 
600
- /**
601
- * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/url-input/README.md
602
- */
603
- export default compose(
604
- withSafeTimeout,
605
- withSpokenMessages,
606
- withInstanceId,
607
- withSelect( ( select, props ) => {
608
- // If a link suggestions handler is already provided then
609
- // bail.
610
- if ( isFunction( props.__experimentalFetchLinkSuggestions ) ) {
611
- return;
612
- }
613
- const { getSettings } = select( blockEditorStore );
614
- return {
615
- __experimentalFetchLinkSuggestions:
616
- getSettings().__experimentalFetchLinkSuggestions,
617
- };
618
- } )
619
- )( URLInput );
524
+ return (
525
+ <Popover placement="bottom" focusOnMount={ false }>
526
+ <div
527
+ { ...suggestionsListProps }
528
+ className={ clsx( 'block-editor-url-input__suggestions', {
529
+ [ `${ className }__suggestions` ]: className,
530
+ } ) }
531
+ >
532
+ { suggestions.map( ( suggestion, index ) => (
533
+ <Button
534
+ __next40pxDefaultSize
535
+ { ...buildSuggestionItemProps( suggestion, index ) }
536
+ key={ suggestion.id }
537
+ className={ clsx(
538
+ 'block-editor-url-input__suggestion',
539
+ {
540
+ 'is-selected': index === selectedSuggestion,
541
+ }
542
+ ) }
543
+ onClick={ () => handleSuggestionClick( suggestion ) }
544
+ >
545
+ { suggestion.title }
546
+ </Button>
547
+ ) ) }
548
+ </div>
549
+ </Popover>
550
+ );
551
+ }