@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
@@ -0,0 +1,592 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ /**
8
+ * WordPress dependencies
9
+ */
10
+ import { speak } from '@wordpress/a11y';
11
+ import { useState } from '@wordpress/element';
12
+ import { dispatch } from '@wordpress/data';
13
+ import { UP, DOWN, ENTER, TAB } from '@wordpress/keycodes';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import URLInput from '../';
19
+ import { store as blockEditorStore } from '../../../store';
20
+
21
+ jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) );
22
+
23
+ const SUGGESTIONS = [
24
+ {
25
+ id: 1,
26
+ title: 'Hello world',
27
+ type: 'post',
28
+ url: 'https://example.com/hello-world',
29
+ },
30
+ {
31
+ id: 2,
32
+ title: 'Sample page',
33
+ type: 'page',
34
+ url: 'https://example.com/sample-page',
35
+ },
36
+ ];
37
+
38
+ // `@wordpress/keycodes` matches on `event.keyCode`, which `userEvent` does not set.
39
+ const KEY_EVENTS = {
40
+ up: { key: 'ArrowUp', keyCode: UP },
41
+ down: { key: 'ArrowDown', keyCode: DOWN },
42
+ enter: { key: 'Enter', keyCode: ENTER },
43
+ tab: { key: 'Tab', keyCode: TAB },
44
+ };
45
+
46
+ /**
47
+ * Waits long enough for the suggestions request debounce to elapse, so that
48
+ * assertions about a request _not_ being made are meaningful.
49
+ */
50
+ function flushDebounce() {
51
+ return new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
52
+ }
53
+
54
+ /**
55
+ * `URLInput` is a controlled component, so most interactions require an owner
56
+ * that holds on to the value.
57
+ *
58
+ * @param {Object} props Props passed through to `URLInput`, with `value` used
59
+ * as the initial value.
60
+ */
61
+ function ControlledURLInput( props ) {
62
+ const { onChange, value: initialValue = '', ...restProps } = props;
63
+ const [ value, setValue ] = useState( initialValue );
64
+
65
+ return (
66
+ <URLInput
67
+ { ...restProps }
68
+ value={ value }
69
+ onChange={ ( newValue, suggestion ) => {
70
+ setValue( newValue );
71
+ onChange?.( newValue, suggestion );
72
+ } }
73
+ />
74
+ );
75
+ }
76
+
77
+ describe( 'URLInput', () => {
78
+ let fetchLinkSuggestions;
79
+
80
+ beforeEach( () => {
81
+ fetchLinkSuggestions = jest.fn().mockResolvedValue( SUGGESTIONS );
82
+ } );
83
+
84
+ afterEach( () => {
85
+ jest.clearAllMocks();
86
+ } );
87
+
88
+ function renderURLInput( props = {} ) {
89
+ const user = userEvent.setup();
90
+ const onChange = jest.fn();
91
+
92
+ render(
93
+ <ControlledURLInput
94
+ onChange={ onChange }
95
+ __experimentalFetchLinkSuggestions={ fetchLinkSuggestions }
96
+ { ...props }
97
+ />
98
+ );
99
+
100
+ return { user, onChange, input: screen.getByRole( 'combobox' ) };
101
+ }
102
+
103
+ // Mounting with a value requests suggestions for it, so the list is open
104
+ // by the time this resolves.
105
+ async function renderWithSuggestions( props = {} ) {
106
+ const utils = renderURLInput( { value: 'hello', ...props } );
107
+ await screen.findByRole( 'listbox' );
108
+ return utils;
109
+ }
110
+
111
+ describe( 'rendering', () => {
112
+ it( 'should fall back to a generic accessible name when no label is provided', () => {
113
+ const { rerender } = render(
114
+ <URLInput value="" onChange={ () => {} } />
115
+ );
116
+
117
+ const input = screen.getByRole( 'combobox', { name: 'URL' } );
118
+
119
+ expect( input ).toBeVisible();
120
+ expect( input ).toHaveAttribute( 'aria-expanded', 'false' );
121
+
122
+ rerender(
123
+ <URLInput label="Link" value="" onChange={ () => {} } />
124
+ );
125
+
126
+ expect(
127
+ screen.getByRole( 'combobox', { name: /Link/ } )
128
+ ).toBeVisible();
129
+ } );
130
+
131
+ it( 'should call `onChange` with the new value only', async () => {
132
+ const { user, input, onChange } = renderURLInput();
133
+
134
+ await user.type( input, 'abc' );
135
+
136
+ expect( onChange ).toHaveBeenCalledTimes( 3 );
137
+ // The second argument is reserved for a selected suggestion, so the
138
+ // `{ event }` object `InputControl` passes must not reach callers.
139
+ expect( onChange ).toHaveBeenLastCalledWith( 'abc', undefined );
140
+ } );
141
+ } );
142
+
143
+ describe( 'fetching suggestions', () => {
144
+ it( 'should display suggestions for the typed value', async () => {
145
+ const { user, input } = renderURLInput();
146
+
147
+ await user.type( input, 'hello' );
148
+
149
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
150
+ expect( screen.getAllByRole( 'option' ) ).toHaveLength( 2 );
151
+ expect( input ).toHaveAttribute( 'aria-expanded', 'true' );
152
+ // Typing five characters is debounced into a single request.
153
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 1 );
154
+ expect( fetchLinkSuggestions ).toHaveBeenCalledWith( 'hello', {
155
+ isInitialSuggestions: false,
156
+ } );
157
+ } );
158
+
159
+ it( 'should not fetch suggestions for fewer than two characters', async () => {
160
+ const { user, input } = renderURLInput();
161
+
162
+ await user.type( input, 'h' );
163
+ await flushDebounce();
164
+
165
+ expect( fetchLinkSuggestions ).not.toHaveBeenCalled();
166
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
167
+ } );
168
+
169
+ it( 'should not fetch suggestions for a direct URL entry', async () => {
170
+ const { user, input } = renderURLInput();
171
+
172
+ await user.type( input, 'https://example.com' );
173
+ await flushDebounce();
174
+
175
+ expect( fetchLinkSuggestions ).not.toHaveBeenCalled();
176
+ } );
177
+
178
+ it( 'should fetch suggestions for a direct URL entry when `__experimentalHandleURLSuggestions` is set', async () => {
179
+ const { user, input } = renderURLInput( {
180
+ __experimentalHandleURLSuggestions: true,
181
+ } );
182
+
183
+ await user.type( input, 'https://example.com' );
184
+
185
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
186
+ expect( fetchLinkSuggestions ).toHaveBeenCalledWith(
187
+ 'https://example.com',
188
+ { isInitialSuggestions: false }
189
+ );
190
+ } );
191
+
192
+ it( 'should fetch initial suggestions on mount when `__experimentalShowInitialSuggestions` is set', async () => {
193
+ renderURLInput( { __experimentalShowInitialSuggestions: true } );
194
+
195
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
196
+ expect( fetchLinkSuggestions ).toHaveBeenCalledWith( '', {
197
+ isInitialSuggestions: true,
198
+ } );
199
+ } );
200
+
201
+ it( 'should fetch suggestions on mount for a value that is already present', async () => {
202
+ renderURLInput( { value: 'hello' } );
203
+
204
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
205
+ expect( fetchLinkSuggestions ).toHaveBeenCalledWith( 'hello', {
206
+ isInitialSuggestions: false,
207
+ } );
208
+ } );
209
+
210
+ it( 'should not fetch initial suggestions on mount when `disableSuggestions` is set', async () => {
211
+ renderURLInput( {
212
+ __experimentalShowInitialSuggestions: true,
213
+ disableSuggestions: true,
214
+ } );
215
+
216
+ await flushDebounce();
217
+
218
+ expect( fetchLinkSuggestions ).not.toHaveBeenCalled();
219
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
220
+ } );
221
+
222
+ it( 'should fetch suggestions on focus when the previous search returned no results', async () => {
223
+ let resolveMountRequest;
224
+ fetchLinkSuggestions
225
+ .mockImplementationOnce(
226
+ () =>
227
+ new Promise( ( resolve ) => {
228
+ resolveMountRequest = resolve;
229
+ } )
230
+ )
231
+ .mockResolvedValue( SUGGESTIONS );
232
+
233
+ const { user, input } = renderURLInput( { value: 'hello' } );
234
+
235
+ await waitFor( () =>
236
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 1 )
237
+ );
238
+
239
+ // The request made on mount returns no results. The spinner is only
240
+ // removed once it has settled.
241
+ resolveMountRequest( [] );
242
+ await waitFor( () =>
243
+ expect(
244
+ screen.queryByRole( 'presentation' )
245
+ ).not.toBeInTheDocument()
246
+ );
247
+
248
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
249
+
250
+ await user.click( input );
251
+
252
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
253
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 2 );
254
+ } );
255
+
256
+ it( 'should not fetch suggestions again on refocus when suggestions are already displayed', async () => {
257
+ const { user, input } = await renderWithSuggestions();
258
+
259
+ await user.click( input );
260
+ await user.tab();
261
+ await user.click( input );
262
+ await flushDebounce();
263
+
264
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 1 );
265
+ } );
266
+
267
+ it( 'should hide the suggestions when the value is cleared', async () => {
268
+ const { user, input } = await renderWithSuggestions();
269
+
270
+ await user.clear( input );
271
+
272
+ await waitFor( () =>
273
+ expect(
274
+ screen.queryByRole( 'listbox' )
275
+ ).not.toBeInTheDocument()
276
+ );
277
+ } );
278
+
279
+ it( 'should ignore the response of a superseded request', async () => {
280
+ let resolveStaleRequest;
281
+ fetchLinkSuggestions
282
+ .mockImplementationOnce(
283
+ () =>
284
+ new Promise( ( resolve ) => {
285
+ resolveStaleRequest = resolve;
286
+ } )
287
+ )
288
+ .mockResolvedValue( SUGGESTIONS );
289
+
290
+ const { user, input } = renderURLInput( { value: 'hello' } );
291
+
292
+ await waitFor( () =>
293
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 1 )
294
+ );
295
+
296
+ await user.type( input, ' world' );
297
+ await waitFor( () =>
298
+ expect( fetchLinkSuggestions ).toHaveBeenCalledTimes( 2 )
299
+ );
300
+
301
+ resolveStaleRequest( [
302
+ {
303
+ id: 3,
304
+ title: 'Stale result',
305
+ type: 'post',
306
+ url: 'https://example.com/stale',
307
+ },
308
+ ] );
309
+
310
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
311
+ expect(
312
+ screen.queryByRole( 'option', { name: 'Stale result' } )
313
+ ).not.toBeInTheDocument();
314
+ } );
315
+
316
+ it( 'should fall back to the fetch handler from the block editor settings', async () => {
317
+ dispatch( blockEditorStore ).updateSettings( {
318
+ __experimentalFetchLinkSuggestions: fetchLinkSuggestions,
319
+ } );
320
+
321
+ try {
322
+ const user = userEvent.setup();
323
+ render( <ControlledURLInput /> );
324
+
325
+ await user.type( screen.getByRole( 'combobox' ), 'hello' );
326
+
327
+ expect( await screen.findByRole( 'listbox' ) ).toBeVisible();
328
+ expect( fetchLinkSuggestions ).toHaveBeenCalledWith( 'hello', {
329
+ isInitialSuggestions: false,
330
+ } );
331
+ } finally {
332
+ dispatch( blockEditorStore ).updateSettings( {
333
+ __experimentalFetchLinkSuggestions: undefined,
334
+ } );
335
+ }
336
+ } );
337
+ } );
338
+
339
+ describe( 'announcements', () => {
340
+ it( 'should announce the number of results', async () => {
341
+ await renderWithSuggestions();
342
+
343
+ await waitFor( () =>
344
+ expect( speak ).toHaveBeenCalledWith(
345
+ '2 results found, use up and down arrow keys to navigate.',
346
+ 'assertive'
347
+ )
348
+ );
349
+ } );
350
+
351
+ it( 'should announce when there are no results', async () => {
352
+ fetchLinkSuggestions.mockResolvedValue( [] );
353
+
354
+ renderURLInput( { value: 'hello' } );
355
+
356
+ await waitFor( () =>
357
+ expect( speak ).toHaveBeenCalledWith(
358
+ 'No results.',
359
+ 'assertive'
360
+ )
361
+ );
362
+ } );
363
+ } );
364
+
365
+ describe( 'keyboard interaction', () => {
366
+ it( 'should move the active suggestion with the down arrow key', async () => {
367
+ const { input } = await renderWithSuggestions();
368
+
369
+ expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
370
+
371
+ fireEvent.keyDown( input, KEY_EVENTS.down );
372
+
373
+ const [ firstOption, secondOption ] =
374
+ screen.getAllByRole( 'option' );
375
+
376
+ expect( input ).toHaveAttribute(
377
+ 'aria-activedescendant',
378
+ firstOption.id
379
+ );
380
+ expect( firstOption ).toHaveAttribute( 'aria-selected', 'true' );
381
+
382
+ fireEvent.keyDown( input, KEY_EVENTS.down );
383
+
384
+ expect( input ).toHaveAttribute(
385
+ 'aria-activedescendant',
386
+ secondOption.id
387
+ );
388
+
389
+ // Wraps back around to the first suggestion.
390
+ fireEvent.keyDown( input, KEY_EVENTS.down );
391
+
392
+ expect( input ).toHaveAttribute(
393
+ 'aria-activedescendant',
394
+ firstOption.id
395
+ );
396
+ } );
397
+
398
+ it( 'should move the active suggestion with the up arrow key', async () => {
399
+ const { input } = await renderWithSuggestions();
400
+
401
+ // Wraps around to the last suggestion.
402
+ fireEvent.keyDown( input, KEY_EVENTS.up );
403
+
404
+ const [ firstOption, secondOption ] =
405
+ screen.getAllByRole( 'option' );
406
+
407
+ expect( input ).toHaveAttribute(
408
+ 'aria-activedescendant',
409
+ secondOption.id
410
+ );
411
+
412
+ fireEvent.keyDown( input, KEY_EVENTS.up );
413
+
414
+ expect( input ).toHaveAttribute(
415
+ 'aria-activedescendant',
416
+ firstOption.id
417
+ );
418
+ } );
419
+
420
+ it( 'should select and submit the active suggestion when pressing Enter', async () => {
421
+ const onSubmit = jest.fn();
422
+ const { input, onChange } = await renderWithSuggestions( {
423
+ onSubmit,
424
+ } );
425
+
426
+ fireEvent.keyDown( input, KEY_EVENTS.down );
427
+ fireEvent.keyDown( input, KEY_EVENTS.enter );
428
+
429
+ expect( onChange ).toHaveBeenLastCalledWith(
430
+ SUGGESTIONS[ 0 ].url,
431
+ SUGGESTIONS[ 0 ]
432
+ );
433
+ expect( onSubmit ).toHaveBeenCalledWith(
434
+ SUGGESTIONS[ 0 ],
435
+ expect.anything()
436
+ );
437
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
438
+ } );
439
+
440
+ it( 'should submit without a suggestion when pressing Enter with no active suggestion', async () => {
441
+ const onSubmit = jest.fn();
442
+ const { input } = await renderWithSuggestions( { onSubmit } );
443
+
444
+ fireEvent.keyDown( input, KEY_EVENTS.enter );
445
+
446
+ expect( onSubmit ).toHaveBeenCalledWith( null, expect.anything() );
447
+ } );
448
+
449
+ it( 'should submit without a suggestion when pressing Enter and there are no suggestions', async () => {
450
+ const onSubmit = jest.fn();
451
+ const onKeyDown = jest.fn();
452
+ const { input } = renderURLInput( {
453
+ value: 'hello',
454
+ disableSuggestions: true,
455
+ onSubmit,
456
+ onKeyDown,
457
+ } );
458
+
459
+ fireEvent.keyDown( input, KEY_EVENTS.enter );
460
+
461
+ expect( onSubmit ).toHaveBeenCalledWith( null, expect.anything() );
462
+ expect( onKeyDown ).toHaveBeenCalled();
463
+ } );
464
+
465
+ it( 'should select and announce the active suggestion when pressing Tab', async () => {
466
+ const { input, onChange } = await renderWithSuggestions();
467
+
468
+ fireEvent.keyDown( input, KEY_EVENTS.down );
469
+ fireEvent.keyDown( input, KEY_EVENTS.tab );
470
+
471
+ expect( onChange ).toHaveBeenLastCalledWith(
472
+ SUGGESTIONS[ 0 ].url,
473
+ SUGGESTIONS[ 0 ]
474
+ );
475
+ expect( speak ).toHaveBeenCalledWith( 'Link selected.' );
476
+ } );
477
+
478
+ // Works around Firefox on Windows not moving the caret with the arrow
479
+ // keys. See https://github.com/WordPress/gutenberg/issues/5693.
480
+ it( 'should move the caret to either end of the input when there are no suggestions', async () => {
481
+ const { user, input } = renderURLInput( {
482
+ disableSuggestions: true,
483
+ } );
484
+
485
+ await user.type( input, 'hello' );
486
+ expect( input.selectionStart ).toBe( 5 );
487
+
488
+ fireEvent.keyDown( input, KEY_EVENTS.up );
489
+ expect( input.selectionStart ).toBe( 0 );
490
+
491
+ fireEvent.keyDown( input, KEY_EVENTS.down );
492
+ expect( input.selectionStart ).toBe( 5 );
493
+ } );
494
+ } );
495
+
496
+ describe( 'suggestion selection', () => {
497
+ it( 'should select a suggestion on click and return focus to the input', async () => {
498
+ const { user, input, onChange } = await renderWithSuggestions();
499
+
500
+ await user.click(
501
+ screen.getByRole( 'option', { name: 'Sample page' } )
502
+ );
503
+
504
+ expect( onChange ).toHaveBeenLastCalledWith(
505
+ SUGGESTIONS[ 1 ].url,
506
+ SUGGESTIONS[ 1 ]
507
+ );
508
+ expect( input ).toHaveFocus();
509
+ expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
510
+ } );
511
+ } );
512
+
513
+ describe( 'custom rendering', () => {
514
+ it( 'should render the control via `__experimentalRenderControl`', () => {
515
+ const renderControl = jest
516
+ .fn()
517
+ .mockReturnValue( <div>Custom control</div> );
518
+
519
+ render(
520
+ <URLInput
521
+ value=""
522
+ onChange={ () => {} }
523
+ __experimentalRenderControl={ renderControl }
524
+ />
525
+ );
526
+
527
+ expect( screen.getByText( 'Custom control' ) ).toBeVisible();
528
+ expect( renderControl ).toHaveBeenCalledWith(
529
+ expect.objectContaining( { label: null } ),
530
+ expect.objectContaining( { role: 'combobox', value: '' } ),
531
+ false
532
+ );
533
+ } );
534
+
535
+ it( 'should render suggestions via `__experimentalRenderSuggestions`', async () => {
536
+ const renderSuggestions = jest.fn( ( { suggestions } ) => (
537
+ <ul>
538
+ { suggestions.map( ( suggestion ) => (
539
+ <li key={ suggestion.id }>{ suggestion.title }</li>
540
+ ) ) }
541
+ </ul>
542
+ ) );
543
+
544
+ renderURLInput( {
545
+ value: 'hello',
546
+ __experimentalRenderSuggestions: renderSuggestions,
547
+ } );
548
+
549
+ expect(
550
+ await screen.findByText( 'Hello world' )
551
+ ).toBeInTheDocument();
552
+ expect( renderSuggestions ).toHaveBeenLastCalledWith(
553
+ expect.objectContaining( {
554
+ suggestions: SUGGESTIONS,
555
+ selectedSuggestion: null,
556
+ isLoading: false,
557
+ isInitialSuggestions: false,
558
+ currentInputValue: 'hello',
559
+ } )
560
+ );
561
+ } );
562
+ } );
563
+
564
+ describe( 'validation', () => {
565
+ it( 'should not remount the input when a custom validity is cleared', async () => {
566
+ const user = userEvent.setup();
567
+ const props = {
568
+ label: 'Link',
569
+ value: 'hello',
570
+ onChange: () => {},
571
+ };
572
+
573
+ const { rerender } = render(
574
+ <URLInput
575
+ { ...props }
576
+ customValidity={ {
577
+ type: 'invalid',
578
+ message: 'Invalid URL.',
579
+ } }
580
+ />
581
+ );
582
+
583
+ const input = screen.getByRole( 'combobox' );
584
+ await user.click( input );
585
+
586
+ rerender( <URLInput { ...props } customValidity={ undefined } /> );
587
+
588
+ expect( screen.getByRole( 'combobox' ) ).toBe( input );
589
+ expect( input ).toHaveFocus();
590
+ } );
591
+ } );
592
+ } );
@@ -1,4 +1,4 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
3
  exports[`Warning should match snapshot 1`] = `
4
4
  <div>
@@ -23,6 +23,7 @@ import { usePreviewModeNav } from './use-preview-mode-nav';
23
23
  import useSelectAll from './use-select-all';
24
24
  import useDragSelection from './use-drag-selection';
25
25
  import useSelectionObserver from './use-selection-observer';
26
+ import useEditableRootEventHandlers from './use-editable-root-event-handlers';
26
27
  import useClickSelection from './use-click-selection';
27
28
  import useInput from './use-input';
28
29
  import useClipboardHandler from './use-clipboard-handler';
@@ -39,6 +40,7 @@ export function useWritingFlow() {
39
40
  before,
40
41
  useMergeRefs( [
41
42
  ref,
43
+ useEditableRootEventHandlers(),
42
44
  useClipboardHandler(),
43
45
  useInput(),
44
46
  useEditableRoot(),
@@ -176,11 +176,15 @@ export default function useArrowNav() {
176
176
  const {
177
177
  getMultiSelectedBlocksStartClientId,
178
178
  getMultiSelectedBlocksEndClientId,
179
+ getNextBlockClientId,
180
+ getPreviousBlockClientId,
181
+ getSelectedBlockClientId,
182
+ getSelectionStart,
179
183
  getSettings,
180
184
  hasMultiSelection,
181
185
  __unstableIsFullySelected,
182
186
  } = useSelect( blockEditorStore );
183
- const { selectBlock } = useDispatch( blockEditorStore );
187
+ const { selectBlock, multiSelect } = useDispatch( blockEditorStore );
184
188
  return useRefEffect( ( node ) => {
185
189
  // Here a DOMRect is stored while moving the caret vertically so
186
190
  // vertical position of the start position can be restored. This is to
@@ -244,6 +248,32 @@ export default function useArrowNav() {
244
248
  // selection to the start or end of the selection.
245
249
  if ( hasMultiSelection() ) {
246
250
  if ( shiftKey ) {
251
+ // A fully selected multi-selection has no native
252
+ // selection to extend (use-multi-selection cleared it),
253
+ // so grow or shrink it by one block at the focus end.
254
+ // Only without a usable native selection: a selection
255
+ // that is fully selected because it resolves to a
256
+ // nesting ancestor keeps its native selection, which
257
+ // the browser extends natively (and the observer
258
+ // promotes to the common level).
259
+ const selection = defaultView.getSelection();
260
+ if (
261
+ __unstableIsFullySelected() &&
262
+ ( ! selection.rangeCount || selection.isCollapsed )
263
+ ) {
264
+ const anchorClientId =
265
+ getMultiSelectedBlocksStartClientId();
266
+ const focusClientId =
267
+ getMultiSelectedBlocksEndClientId();
268
+ const nextClientId = isReverse
269
+ ? getPreviousBlockClientId( focusClientId )
270
+ : getNextBlockClientId( focusClientId );
271
+
272
+ if ( nextClientId ) {
273
+ multiSelect( anchorClientId, nextClientId );
274
+ event.preventDefault();
275
+ }
276
+ }
247
277
  return;
248
278
  }
249
279
 
@@ -264,6 +294,26 @@ export default function useArrowNav() {
264
294
  return;
265
295
  }
266
296
 
297
+ // A block selected without a text selection within it (e.g. an
298
+ // image or spacer) has no native selection to extend: start a
299
+ // block multi-selection with the adjacent block.
300
+ if (
301
+ shiftKey &&
302
+ getSelectedBlockClientId() &&
303
+ ! getSelectionStart().attributeKey
304
+ ) {
305
+ const selectedClientId = getSelectedBlockClientId();
306
+ const nextClientId = isReverse
307
+ ? getPreviousBlockClientId( selectedClientId )
308
+ : getNextBlockClientId( selectedClientId );
309
+
310
+ if ( nextClientId ) {
311
+ multiSelect( selectedClientId, nextClientId );
312
+ event.preventDefault();
313
+ }
314
+ return;
315
+ }
316
+
267
317
  // Abort if our current target is not a candidate for navigation
268
318
  // (e.g. preserve native input behaviors).
269
319
  if ( ! isNavigationCandidate( target, keyCode, hasModifier ) ) {