@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.
- package/CHANGELOG.md +18 -0
- package/README.md +8 -0
- package/build/components/alignment-control/ui.cjs +2 -4
- package/build/components/alignment-control/ui.cjs.map +2 -2
- package/build/components/background-image-control/index.cjs +40 -10
- package/build/components/background-image-control/index.cjs.map +2 -2
- package/build/components/block-alignment-control/ui.cjs +5 -3
- package/build/components/block-alignment-control/ui.cjs.map +2 -2
- package/build/components/block-inspector/index.cjs +73 -46
- package/build/components/block-inspector/index.cjs.map +2 -2
- package/build/components/block-list/block-html.cjs +4 -1
- package/build/components/block-list/block-html.cjs.map +2 -2
- package/build/components/block-list/use-block-props/index.cjs +5 -2
- package/build/components/block-list/use-block-props/index.cjs.map +2 -2
- package/build/components/block-list/use-block-props/use-focus-handler.cjs +2 -3
- package/build/components/block-list/use-block-props/use-focus-handler.cjs.map +2 -2
- package/build/components/block-list/use-block-props/use-register-block-event-handlers.cjs +48 -0
- package/build/components/block-list/use-block-props/use-register-block-event-handlers.cjs.map +7 -0
- package/build/components/block-parent-selector/index.cjs +2 -2
- package/build/components/block-parent-selector/index.cjs.map +2 -2
- package/build/components/block-toolbar/switch-section-style.cjs +4 -2
- package/build/components/block-toolbar/switch-section-style.cjs.map +3 -3
- package/build/components/block-tools/empty-block-inserter.cjs +2 -7
- package/build/components/block-tools/empty-block-inserter.cjs.map +2 -2
- package/build/components/block-vertical-alignment-control/ui.cjs +3 -5
- package/build/components/block-vertical-alignment-control/ui.cjs.map +2 -2
- package/build/components/border-radius-control/utils.cjs +8 -58
- package/build/components/border-radius-control/utils.cjs.map +2 -2
- package/build/components/colors-gradients/control.cjs +5 -3
- package/build/components/colors-gradients/control.cjs.map +2 -2
- package/build/components/dimension-control/index.cjs +30 -20
- package/build/components/dimension-control/index.cjs.map +3 -3
- package/build/components/dimensions-tool/aspect-ratio-tool.cjs +9 -2
- package/build/components/dimensions-tool/aspect-ratio-tool.cjs.map +3 -3
- package/build/components/editor-styles/index.cjs +9 -1
- package/build/components/editor-styles/index.cjs.map +2 -2
- package/build/components/global-styles/background-panel.cjs +66 -25
- package/build/components/global-styles/background-panel.cjs.map +3 -3
- package/build/components/global-styles/border-panel.cjs +153 -55
- package/build/components/global-styles/border-panel.cjs.map +3 -3
- package/build/components/global-styles/color-gradient-dropdown-item.cjs +46 -14
- package/build/components/global-styles/color-gradient-dropdown-item.cjs.map +3 -3
- package/build/components/global-styles/color-panel.cjs +63 -17
- package/build/components/global-styles/color-panel.cjs.map +2 -2
- package/build/components/global-styles/dimensions-panel.cjs +242 -64
- package/build/components/global-styles/dimensions-panel.cjs.map +3 -3
- package/build/components/global-styles/filters-panel.cjs +59 -20
- package/build/components/global-styles/filters-panel.cjs.map +3 -3
- package/build/components/global-styles/hooks.cjs +4 -1
- package/build/components/global-styles/hooks.cjs.map +2 -2
- package/build/components/global-styles/inheritance/index.cjs +142 -0
- package/build/components/global-styles/inheritance/index.cjs.map +7 -0
- package/build/components/global-styles/inherited-value-context.cjs +173 -0
- package/build/components/global-styles/inherited-value-context.cjs.map +7 -0
- package/build/components/global-styles/shadow-panel-components.cjs +38 -20
- package/build/components/global-styles/shadow-panel-components.cjs.map +2 -2
- package/build/components/global-styles/state-control.cjs +1 -1
- package/build/components/global-styles/state-control.cjs.map +2 -2
- package/build/components/global-styles/typography-panel.cjs +271 -78
- package/build/components/global-styles/typography-panel.cjs.map +3 -3
- package/build/components/inspector-controls-tabs/styles-tab.cjs +10 -1
- package/build/components/inspector-controls-tabs/styles-tab.cjs.map +2 -2
- package/build/components/line-height-control/index.cjs +1 -1
- package/build/components/line-height-control/index.cjs.map +2 -2
- package/build/components/link-control/index.cjs +3 -1
- package/build/components/link-control/index.cjs.map +2 -2
- package/build/components/preset-input-control/index.cjs +2 -1
- package/build/components/preset-input-control/index.cjs.map +2 -2
- package/build/components/provider/block-refs-provider.cjs +8 -2
- package/build/components/provider/block-refs-provider.cjs.map +2 -2
- package/build/components/provider/index.cjs +1 -1
- package/build/components/provider/index.cjs.map +2 -2
- package/build/components/provider/use-block-sync.cjs +18 -6
- package/build/components/provider/use-block-sync.cjs.map +2 -2
- package/build/components/provider/use-media-upload-settings.cjs +3 -1
- package/build/components/provider/use-media-upload-settings.cjs.map +2 -2
- package/build/components/rich-text/index.cjs +23 -13
- package/build/components/rich-text/index.cjs.map +2 -2
- package/build/components/spacing-sizes-control/utils.cjs +6 -19
- package/build/components/spacing-sizes-control/utils.cjs.map +2 -2
- package/build/components/text-indent-control/index.cjs +8 -2
- package/build/components/text-indent-control/index.cjs.map +2 -2
- package/build/components/url-input/index.cjs +303 -344
- package/build/components/url-input/index.cjs.map +3 -3
- package/build/components/writing-flow/index.cjs +2 -0
- package/build/components/writing-flow/index.cjs.map +3 -3
- package/build/components/writing-flow/use-arrow-nav.cjs +24 -1
- package/build/components/writing-flow/use-arrow-nav.cjs.map +2 -2
- package/build/components/writing-flow/use-click-selection.cjs +28 -2
- package/build/components/writing-flow/use-click-selection.cjs.map +2 -2
- package/build/components/writing-flow/use-editable-root-event-handlers.cjs +179 -0
- package/build/components/writing-flow/use-editable-root-event-handlers.cjs.map +7 -0
- package/build/components/writing-flow/use-editable-root.cjs +5 -42
- package/build/components/writing-flow/use-editable-root.cjs.map +2 -2
- package/build/components/writing-flow/use-home-end.cjs +1 -1
- package/build/components/writing-flow/use-home-end.cjs.map +2 -2
- package/build/components/writing-flow/use-selection-observer.cjs +17 -13
- package/build/components/writing-flow/use-selection-observer.cjs.map +2 -2
- package/build/components/writing-flow/utils.cjs +3 -14
- package/build/components/writing-flow/utils.cjs.map +2 -2
- package/build/hooks/background.cjs +13 -10
- package/build/hooks/background.cjs.map +2 -2
- package/build/hooks/block-style-variation.cjs +4 -45
- package/build/hooks/block-style-variation.cjs.map +3 -3
- package/build/hooks/border.cjs +19 -4
- package/build/hooks/border.cjs.map +2 -2
- package/build/hooks/custom-css.cjs +6 -2
- package/build/hooks/custom-css.cjs.map +2 -2
- package/build/hooks/dimensions.cjs +15 -4
- package/build/hooks/dimensions.cjs.map +2 -2
- package/build/hooks/duotone.cjs +26 -14
- package/build/hooks/duotone.cjs.map +3 -3
- package/build/hooks/elements.cjs +14 -4
- package/build/hooks/elements.cjs.map +2 -2
- package/build/hooks/line-height.cjs +1 -2
- package/build/hooks/line-height.cjs.map +2 -2
- package/build/hooks/typography.cjs +13 -4
- package/build/hooks/typography.cjs.map +2 -2
- package/build/private-apis.cjs +9 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/store/actions.cjs +9 -4
- package/build/store/actions.cjs.map +2 -2
- package/build/store/private-selectors.cjs +28 -1
- package/build/store/private-selectors.cjs.map +2 -2
- package/build/store/reducer.cjs +6 -6
- package/build/store/reducer.cjs.map +2 -2
- package/build/store/selectors.cjs +23 -0
- package/build/store/selectors.cjs.map +2 -2
- package/build/utils/native-undo.cjs +55 -0
- package/build/utils/native-undo.cjs.map +7 -0
- package/build-module/components/alignment-control/ui.mjs +2 -4
- package/build-module/components/alignment-control/ui.mjs.map +2 -2
- package/build-module/components/background-image-control/index.mjs +43 -10
- package/build-module/components/background-image-control/index.mjs.map +2 -2
- package/build-module/components/block-alignment-control/ui.mjs +5 -3
- package/build-module/components/block-alignment-control/ui.mjs.map +2 -2
- package/build-module/components/block-inspector/index.mjs +74 -46
- package/build-module/components/block-inspector/index.mjs.map +2 -2
- package/build-module/components/block-list/block-html.mjs +4 -1
- package/build-module/components/block-list/block-html.mjs.map +2 -2
- package/build-module/components/block-list/use-block-props/index.mjs +5 -2
- package/build-module/components/block-list/use-block-props/index.mjs.map +2 -2
- package/build-module/components/block-list/use-block-props/use-focus-handler.mjs +2 -3
- package/build-module/components/block-list/use-block-props/use-focus-handler.mjs.map +2 -2
- package/build-module/components/block-list/use-block-props/use-register-block-event-handlers.mjs +28 -0
- package/build-module/components/block-list/use-block-props/use-register-block-event-handlers.mjs.map +7 -0
- package/build-module/components/block-parent-selector/index.mjs +2 -2
- package/build-module/components/block-parent-selector/index.mjs.map +2 -2
- package/build-module/components/block-toolbar/switch-section-style.mjs +4 -2
- package/build-module/components/block-toolbar/switch-section-style.mjs.map +2 -2
- package/build-module/components/block-tools/empty-block-inserter.mjs +2 -7
- package/build-module/components/block-tools/empty-block-inserter.mjs.map +2 -2
- package/build-module/components/block-vertical-alignment-control/ui.mjs +4 -6
- package/build-module/components/block-vertical-alignment-control/ui.mjs.map +2 -2
- package/build-module/components/border-radius-control/utils.mjs +11 -53
- package/build-module/components/border-radius-control/utils.mjs.map +2 -2
- package/build-module/components/colors-gradients/control.mjs +5 -3
- package/build-module/components/colors-gradients/control.mjs.map +2 -2
- package/build-module/components/dimension-control/index.mjs +30 -20
- package/build-module/components/dimension-control/index.mjs.map +2 -2
- package/build-module/components/dimensions-tool/aspect-ratio-tool.mjs +10 -6
- package/build-module/components/dimensions-tool/aspect-ratio-tool.mjs.map +2 -2
- package/build-module/components/editor-styles/index.mjs +9 -1
- package/build-module/components/editor-styles/index.mjs.map +2 -2
- package/build-module/components/global-styles/background-panel.mjs +71 -29
- package/build-module/components/global-styles/background-panel.mjs.map +2 -2
- package/build-module/components/global-styles/border-panel.mjs +157 -56
- package/build-module/components/global-styles/border-panel.mjs.map +2 -2
- package/build-module/components/global-styles/color-gradient-dropdown-item.mjs +51 -15
- package/build-module/components/global-styles/color-gradient-dropdown-item.mjs.map +2 -2
- package/build-module/components/global-styles/color-panel.mjs +63 -17
- package/build-module/components/global-styles/color-panel.mjs.map +2 -2
- package/build-module/components/global-styles/dimensions-panel.mjs +248 -66
- package/build-module/components/global-styles/dimensions-panel.mjs.map +2 -2
- package/build-module/components/global-styles/filters-panel.mjs +64 -21
- package/build-module/components/global-styles/filters-panel.mjs.map +2 -2
- package/build-module/components/global-styles/hooks.mjs +4 -1
- package/build-module/components/global-styles/hooks.mjs.map +2 -2
- package/build-module/components/global-styles/inheritance/index.mjs +108 -0
- package/build-module/components/global-styles/inheritance/index.mjs.map +7 -0
- package/build-module/components/global-styles/inherited-value-context.mjs +141 -0
- package/build-module/components/global-styles/inherited-value-context.mjs.map +7 -0
- package/build-module/components/global-styles/shadow-panel-components.mjs +38 -20
- package/build-module/components/global-styles/shadow-panel-components.mjs.map +2 -2
- package/build-module/components/global-styles/state-control.mjs +1 -1
- package/build-module/components/global-styles/state-control.mjs.map +2 -2
- package/build-module/components/global-styles/typography-panel.mjs +276 -79
- package/build-module/components/global-styles/typography-panel.mjs.map +2 -2
- package/build-module/components/inspector-controls-tabs/styles-tab.mjs +6 -1
- package/build-module/components/inspector-controls-tabs/styles-tab.mjs.map +2 -2
- package/build-module/components/line-height-control/index.mjs +1 -1
- package/build-module/components/line-height-control/index.mjs.map +2 -2
- package/build-module/components/link-control/index.mjs +4 -2
- package/build-module/components/link-control/index.mjs.map +2 -2
- package/build-module/components/preset-input-control/index.mjs +2 -1
- package/build-module/components/preset-input-control/index.mjs.map +2 -2
- package/build-module/components/provider/block-refs-provider.mjs +8 -2
- package/build-module/components/provider/block-refs-provider.mjs.map +2 -2
- package/build-module/components/provider/index.mjs +1 -1
- package/build-module/components/provider/index.mjs.map +2 -2
- package/build-module/components/provider/use-block-sync.mjs +18 -6
- package/build-module/components/provider/use-block-sync.mjs.map +2 -2
- package/build-module/components/provider/use-media-upload-settings.mjs +3 -1
- package/build-module/components/provider/use-media-upload-settings.mjs.map +2 -2
- package/build-module/components/rich-text/index.mjs +28 -14
- package/build-module/components/rich-text/index.mjs.map +2 -2
- package/build-module/components/spacing-sizes-control/utils.mjs +6 -18
- package/build-module/components/spacing-sizes-control/utils.mjs.map +2 -2
- package/build-module/components/text-indent-control/index.mjs +8 -2
- package/build-module/components/text-indent-control/index.mjs.map +2 -2
- package/build-module/components/url-input/index.mjs +306 -353
- package/build-module/components/url-input/index.mjs.map +2 -2
- package/build-module/components/writing-flow/index.mjs +2 -0
- package/build-module/components/writing-flow/index.mjs.map +2 -2
- package/build-module/components/writing-flow/use-arrow-nav.mjs +24 -1
- package/build-module/components/writing-flow/use-arrow-nav.mjs.map +2 -2
- package/build-module/components/writing-flow/use-click-selection.mjs +28 -2
- package/build-module/components/writing-flow/use-click-selection.mjs.map +2 -2
- package/build-module/components/writing-flow/use-editable-root-event-handlers.mjs +154 -0
- package/build-module/components/writing-flow/use-editable-root-event-handlers.mjs.map +7 -0
- package/build-module/components/writing-flow/use-editable-root.mjs +5 -37
- package/build-module/components/writing-flow/use-editable-root.mjs.map +2 -2
- package/build-module/components/writing-flow/use-home-end.mjs +1 -1
- package/build-module/components/writing-flow/use-home-end.mjs.map +2 -2
- package/build-module/components/writing-flow/use-selection-observer.mjs +18 -14
- package/build-module/components/writing-flow/use-selection-observer.mjs.map +2 -2
- package/build-module/components/writing-flow/utils.mjs +2 -11
- package/build-module/components/writing-flow/utils.mjs.map +2 -2
- package/build-module/hooks/background.mjs +13 -10
- package/build-module/hooks/background.mjs.map +2 -2
- package/build-module/hooks/block-style-variation.mjs +8 -44
- package/build-module/hooks/block-style-variation.mjs.map +2 -2
- package/build-module/hooks/border.mjs +19 -4
- package/build-module/hooks/border.mjs.map +2 -2
- package/build-module/hooks/custom-css.mjs +7 -3
- package/build-module/hooks/custom-css.mjs.map +2 -2
- package/build-module/hooks/dimensions.mjs +15 -4
- package/build-module/hooks/dimensions.mjs.map +2 -2
- package/build-module/hooks/duotone.mjs +20 -9
- package/build-module/hooks/duotone.mjs.map +2 -2
- package/build-module/hooks/elements.mjs +14 -4
- package/build-module/hooks/elements.mjs.map +2 -2
- package/build-module/hooks/line-height.mjs +1 -2
- package/build-module/hooks/line-height.mjs.map +2 -2
- package/build-module/hooks/typography.mjs +13 -4
- package/build-module/hooks/typography.mjs.map +2 -2
- package/build-module/private-apis.mjs +7 -3
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/store/actions.mjs +9 -4
- package/build-module/store/actions.mjs.map +2 -2
- package/build-module/store/private-selectors.mjs +29 -1
- package/build-module/store/private-selectors.mjs.map +2 -2
- package/build-module/store/reducer.mjs +6 -6
- package/build-module/store/reducer.mjs.map +2 -2
- package/build-module/store/selectors.mjs +23 -0
- package/build-module/store/selectors.mjs.map +2 -2
- package/build-module/utils/native-undo.mjs +29 -0
- package/build-module/utils/native-undo.mjs.map +7 -0
- package/build-style/content-rtl.css +10 -18
- package/build-style/content.css +10 -18
- package/build-style/style-rtl.css +150 -7
- package/build-style/style.css +150 -7
- package/package.json +40 -41
- package/src/components/alignment-control/test/__snapshots__/index.js.snap +1 -1
- package/src/components/alignment-control/ui.js +2 -4
- package/src/components/background-image-control/index.js +74 -37
- package/src/components/background-image-control/style.scss +13 -4
- package/src/components/block-alignment-control/test/__snapshots__/index.js.snap +3 -3
- package/src/components/block-alignment-control/test/index.js +1 -1
- package/src/components/block-alignment-control/ui.js +4 -2
- package/src/components/block-breadcrumb/test/__snapshots__/index.js.snap +1 -1
- package/src/components/block-compare/test/__snapshots__/block-view.js.snap +1 -1
- package/src/components/block-icon/test/__snapshots__/index.js.snap +1 -1
- package/src/components/block-inspector/index.js +60 -31
- package/src/components/block-list/block-html.js +5 -0
- package/src/components/block-list/content.scss +1 -11
- package/src/components/block-list/use-block-props/index.js +5 -1
- package/src/components/block-list/use-block-props/use-focus-handler.js +9 -8
- package/src/components/block-list/use-block-props/use-register-block-event-handlers.js +45 -0
- package/src/components/block-parent-selector/index.js +5 -2
- package/src/components/block-toolbar/switch-section-style.js +5 -2
- package/src/components/block-tools/empty-block-inserter.js +3 -7
- package/src/components/block-vertical-alignment-control/test/__snapshots__/index.js.snap +3 -3
- package/src/components/block-vertical-alignment-control/test/index.js +18 -2
- package/src/components/block-vertical-alignment-control/ui.js +3 -5
- package/src/components/border-radius-control/test/utils.js +0 -41
- package/src/components/border-radius-control/utils.js +16 -107
- package/src/components/button-block-appender/content.scss +6 -2
- package/src/components/color-palette/test/__snapshots__/control.js.snap +1 -1
- package/src/components/colors-gradients/control.js +6 -3
- package/src/components/default-block-appender/test/__snapshots__/index.js.snap +1 -1
- package/src/components/dimension-control/index.js +13 -1
- package/src/components/dimensions-tool/aspect-ratio-tool.js +13 -6
- package/src/components/editor-styles/index.js +16 -1
- package/src/components/global-styles/background-panel.js +107 -34
- package/src/components/global-styles/border-panel.js +219 -48
- package/src/components/global-styles/color-gradient-dropdown-item.js +93 -28
- package/src/components/global-styles/color-panel.js +92 -11
- package/src/components/global-styles/dimensions-panel.js +422 -71
- package/src/components/global-styles/filters-panel.js +88 -28
- package/src/components/global-styles/hooks.js +8 -1
- package/src/components/global-styles/inheritance/index.js +204 -0
- package/src/components/global-styles/inheritance/style.scss +184 -0
- package/src/components/global-styles/inheritance/test/index.js +281 -0
- package/src/components/global-styles/inherited-value-context.js +271 -0
- package/src/components/global-styles/shadow-panel-components.js +48 -26
- package/src/components/global-styles/state-control.js +2 -2
- package/src/components/global-styles/test/background-panel.js +338 -1
- package/src/components/global-styles/test/border-panel.js +441 -0
- package/src/components/global-styles/test/color-panel.js +182 -2
- package/src/components/global-styles/test/dimensions-panel.js +753 -0
- package/src/components/global-styles/test/filters-panel.js +194 -0
- package/src/components/global-styles/test/inherited-value-context-core.js +98 -0
- package/src/components/global-styles/test/inherited-value-context.js +454 -0
- package/src/components/global-styles/test/typography-panel-core.js +185 -0
- package/src/components/global-styles/test/typography-panel.js +849 -12
- package/src/components/global-styles/typography-panel.js +449 -100
- package/src/components/image-editor/style.scss +33 -0
- package/src/components/inner-blocks/test/__snapshots__/index.js.snap +1 -1
- package/src/components/inspector-controls-tabs/styles-tab.js +5 -1
- package/src/components/line-height-control/index.js +1 -1
- package/src/components/link-control/index.js +7 -2
- package/src/components/preset-input-control/index.js +4 -1
- package/src/components/provider/block-refs-provider.js +12 -2
- package/src/components/provider/index.js +2 -2
- package/src/components/provider/use-block-sync.js +18 -6
- package/src/components/provider/use-media-upload-settings.js +2 -0
- package/src/components/rich-text/index.js +37 -16
- package/src/components/spacing-sizes-control/test/utils.js +16 -5
- package/src/components/spacing-sizes-control/utils.js +11 -28
- package/src/components/text-indent-control/index.js +8 -0
- package/src/components/url-input/index.js +363 -431
- package/src/components/url-input/test/index.js +592 -0
- package/src/components/warning/test/__snapshots__/index.js.snap +1 -1
- package/src/components/writing-flow/index.js +2 -0
- package/src/components/writing-flow/use-arrow-nav.js +51 -1
- package/src/components/writing-flow/use-click-selection.js +54 -3
- package/src/components/writing-flow/use-editable-root-event-handlers.js +266 -0
- package/src/components/writing-flow/use-editable-root.js +7 -77
- package/src/components/writing-flow/use-home-end.js +17 -9
- package/src/components/writing-flow/use-selection-observer.js +32 -15
- package/src/components/writing-flow/utils.js +3 -24
- package/src/hooks/background.js +26 -21
- package/src/hooks/block-fields/rich-text/styles.scss +1 -1
- package/src/hooks/block-style-variation.js +8 -74
- package/src/hooks/border.js +19 -4
- package/src/hooks/custom-css.js +15 -6
- package/src/hooks/dimensions.js +17 -4
- package/src/hooks/duotone.js +26 -8
- package/src/hooks/elements.js +14 -3
- package/src/hooks/line-height.js +0 -1
- package/src/hooks/test/inherited-value-wiring.js +411 -0
- package/src/hooks/test/preset-round-trip.js +456 -0
- package/src/hooks/typography.js +35 -23
- package/src/private-apis.js +11 -2
- package/src/store/actions.js +10 -4
- package/src/store/private-selectors.js +57 -1
- package/src/store/reducer.js +9 -9
- package/src/store/selectors.js +56 -8
- package/src/store/test/private-selectors.js +160 -1
- package/src/style.scss +2 -0
- package/src/utils/native-undo.js +56 -0
- package/build/components/duotone/utils.cjs +0 -87
- package/build/components/duotone/utils.cjs.map +0 -7
- package/build-module/components/duotone/utils.mjs +0 -59
- package/build-module/components/duotone/utils.mjs.map +0 -7
- package/src/components/duotone/utils.js +0 -90
- 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
|
+
} );
|
|
@@ -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 ) ) {
|