@wordpress/components 20.0.1-next.d6164808d3.0 → 20.0.2-next.957ca95e4c.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 +29 -1
- package/CONTRIBUTING.md +12 -12
- package/build/alignment-matrix-control/styles/alignment-matrix-control-styles.js +12 -12
- package/build/alignment-matrix-control/styles/alignment-matrix-control-styles.js.map +1 -1
- package/build/autocomplete/get-default-use-items.js +7 -1
- package/build/autocomplete/get-default-use-items.js.map +1 -1
- package/build/autocomplete/index.js +3 -1
- package/build/autocomplete/index.js.map +1 -1
- package/build/card/card/component.js +6 -11
- package/build/card/card/component.js.map +1 -1
- package/build/card/card/hook.js +0 -10
- package/build/card/card/hook.js.map +1 -1
- package/build/card/card/index.js.map +1 -1
- package/build/card/card-body/component.js +7 -8
- package/build/card/card-body/component.js.map +1 -1
- package/build/card/card-body/hook.js +0 -4
- package/build/card/card-body/hook.js.map +1 -1
- package/build/card/card-body/index.js.map +1 -1
- package/build/card/card-divider/component.js +7 -8
- package/build/card/card-divider/component.js.map +1 -1
- package/build/card/card-divider/hook.js +0 -4
- package/build/card/card-divider/hook.js.map +1 -1
- package/build/card/card-divider/index.js.map +1 -1
- package/build/card/card-footer/component.js +7 -8
- package/build/card/card-footer/component.js.map +1 -1
- package/build/card/card-footer/hook.js +0 -4
- package/build/card/card-footer/hook.js.map +1 -1
- package/build/card/card-footer/index.js.map +1 -1
- package/build/card/card-header/component.js +7 -8
- package/build/card/card-header/component.js.map +1 -1
- package/build/card/card-header/hook.js +0 -4
- package/build/card/card-header/hook.js.map +1 -1
- package/build/card/card-header/index.js.map +1 -1
- package/build/card/card-media/component.js +7 -7
- package/build/card/card-media/component.js.map +1 -1
- package/build/card/card-media/hook.js +0 -4
- package/build/card/card-media/hook.js.map +1 -1
- package/build/card/card-media/index.js.map +1 -1
- package/build/card/context.js.map +1 -1
- package/build/card/index.js.map +1 -1
- package/build/card/styles.js +17 -17
- package/build/card/styles.js.map +1 -1
- package/build/clipboard-button/index.js +16 -1
- package/build/clipboard-button/index.js.map +1 -1
- package/build/color-palette/index.js +6 -2
- package/build/color-palette/index.js.map +1 -1
- package/build/custom-gradient-picker/index.js +11 -0
- package/build/custom-gradient-picker/index.js.map +1 -1
- package/build/date-time/date/index.js +25 -6
- package/build/date-time/date/index.js.map +1 -1
- package/build/date-time/date/styles.js +22 -12
- package/build/date-time/date/styles.js.map +1 -1
- package/build/date-time/date-time/index.js +1 -3
- package/build/date-time/date-time/index.js.map +1 -1
- package/build/date-time/date-time/styles.js +19 -5
- package/build/date-time/date-time/styles.js.map +1 -1
- package/build/date-time/time/styles.js +12 -12
- package/build/date-time/time/styles.js.map +1 -1
- package/build/drop-zone/index.js +2 -4
- package/build/drop-zone/index.js.map +1 -1
- package/build/dropdown-menu/index.js +1 -3
- package/build/dropdown-menu/index.js.map +1 -1
- package/build/dropdown-menu/index.native.js +0 -17
- package/build/dropdown-menu/index.native.js.map +1 -1
- package/build/focal-point-picker/controls.js +4 -4
- package/build/focal-point-picker/controls.js.map +1 -1
- package/build/focal-point-picker/focal-point.js +4 -6
- package/build/focal-point-picker/focal-point.js.map +1 -1
- package/build/focal-point-picker/grid.js +6 -35
- package/build/focal-point-picker/grid.js.map +1 -1
- package/build/focal-point-picker/index.js +160 -330
- package/build/focal-point-picker/index.js.map +1 -1
- package/build/focal-point-picker/media.js +4 -34
- package/build/focal-point-picker/media.js.map +1 -1
- package/build/focal-point-picker/styles/focal-point-picker-style.js +14 -14
- package/build/focal-point-picker/styles/focal-point-picker-style.js.map +1 -1
- package/build/focal-point-picker/utils.js +2 -6
- package/build/focal-point-picker/utils.js.map +1 -1
- package/build/focusable-iframe/index.js +6 -0
- package/build/focusable-iframe/index.js.map +1 -1
- package/build/form-token-field/index.js +18 -15
- package/build/form-token-field/index.js.map +1 -1
- package/build/gradient-picker/index.js +12 -1
- package/build/gradient-picker/index.js.map +1 -1
- package/build/guide/index.js +8 -6
- package/build/guide/index.js.map +1 -1
- package/build/higher-order/with-constrained-tabbing/index.js +1 -1
- package/build/higher-order/with-constrained-tabbing/index.js.map +1 -1
- package/build/higher-order/with-spoken-messages/index.js +2 -0
- package/build/higher-order/with-spoken-messages/index.js.map +1 -1
- package/build/isolated-event-container/index.js +4 -0
- package/build/isolated-event-container/index.js.map +1 -1
- package/build/mobile/global-styles-context/utils.native.js +1 -1
- package/build/mobile/global-styles-context/utils.native.js.map +1 -1
- package/build/navigable-container/menu.js +3 -9
- package/build/navigable-container/menu.js.map +1 -1
- package/build/navigation/menu/menu-title-search.js +1 -3
- package/build/navigation/menu/menu-title-search.js.map +1 -1
- package/build/palette-edit/index.js +6 -2
- package/build/palette-edit/index.js.map +1 -1
- package/build/popover/index.js +15 -35
- package/build/popover/index.js.map +1 -1
- package/build/text-highlight/index.js +4 -4
- package/build/text-highlight/index.js.map +1 -1
- package/build/toggle-group-control/toggle-group-control/component.js +1 -1
- package/build/toggle-group-control/toggle-group-control/component.js.map +1 -1
- package/build/toggle-group-control/toggle-group-control/styles.js +23 -8
- package/build/toggle-group-control/toggle-group-control/styles.js.map +1 -1
- package/build/tooltip/index.js +1 -7
- package/build/tooltip/index.js.map +1 -1
- package/build/tree-grid/index.js +4 -10
- package/build/tree-grid/index.js.map +1 -1
- package/build/utils/strings.js +13 -0
- package/build/utils/strings.js.map +1 -1
- package/build-module/alignment-matrix-control/styles/alignment-matrix-control-styles.js +12 -12
- package/build-module/alignment-matrix-control/styles/alignment-matrix-control-styles.js.map +1 -1
- package/build-module/autocomplete/get-default-use-items.js +6 -1
- package/build-module/autocomplete/get-default-use-items.js.map +1 -1
- package/build-module/autocomplete/index.js +2 -1
- package/build-module/autocomplete/index.js.map +1 -1
- package/build-module/card/card/component.js +5 -10
- package/build-module/card/card/component.js.map +1 -1
- package/build-module/card/card/hook.js +0 -9
- package/build-module/card/card/hook.js.map +1 -1
- package/build-module/card/card/index.js.map +1 -1
- package/build-module/card/card-body/component.js +7 -8
- package/build-module/card/card-body/component.js.map +1 -1
- package/build-module/card/card-body/hook.js +0 -4
- package/build-module/card/card-body/hook.js.map +1 -1
- package/build-module/card/card-body/index.js.map +1 -1
- package/build-module/card/card-divider/component.js +7 -8
- package/build-module/card/card-divider/component.js.map +1 -1
- package/build-module/card/card-divider/hook.js +0 -4
- package/build-module/card/card-divider/hook.js.map +1 -1
- package/build-module/card/card-divider/index.js.map +1 -1
- package/build-module/card/card-footer/component.js +7 -8
- package/build-module/card/card-footer/component.js.map +1 -1
- package/build-module/card/card-footer/hook.js +0 -4
- package/build-module/card/card-footer/hook.js.map +1 -1
- package/build-module/card/card-footer/index.js.map +1 -1
- package/build-module/card/card-header/component.js +7 -8
- package/build-module/card/card-header/component.js.map +1 -1
- package/build-module/card/card-header/hook.js +0 -4
- package/build-module/card/card-header/hook.js.map +1 -1
- package/build-module/card/card-header/index.js.map +1 -1
- package/build-module/card/card-media/component.js +7 -7
- package/build-module/card/card-media/component.js.map +1 -1
- package/build-module/card/card-media/hook.js +0 -4
- package/build-module/card/card-media/hook.js.map +1 -1
- package/build-module/card/card-media/index.js.map +1 -1
- package/build-module/card/context.js.map +1 -1
- package/build-module/card/index.js.map +1 -1
- package/build-module/card/styles.js +17 -17
- package/build-module/card/styles.js.map +1 -1
- package/build-module/clipboard-button/index.js +17 -1
- package/build-module/clipboard-button/index.js.map +1 -1
- package/build-module/color-palette/index.js +5 -2
- package/build-module/color-palette/index.js.map +1 -1
- package/build-module/custom-gradient-picker/index.js +10 -0
- package/build-module/custom-gradient-picker/index.js.map +1 -1
- package/build-module/date-time/date/index.js +27 -8
- package/build-module/date-time/date/index.js.map +1 -1
- package/build-module/date-time/date/styles.js +21 -5
- package/build-module/date-time/date/styles.js.map +1 -1
- package/build-module/date-time/date-time/index.js +2 -3
- package/build-module/date-time/date-time/index.js.map +1 -1
- package/build-module/date-time/date-time/styles.js +20 -1
- package/build-module/date-time/date-time/styles.js.map +1 -1
- package/build-module/date-time/time/styles.js +12 -12
- package/build-module/date-time/time/styles.js.map +1 -1
- package/build-module/drop-zone/index.js +2 -3
- package/build-module/drop-zone/index.js.map +1 -1
- package/build-module/dropdown-menu/index.js +1 -2
- package/build-module/dropdown-menu/index.js.map +1 -1
- package/build-module/dropdown-menu/index.native.js +0 -16
- package/build-module/dropdown-menu/index.native.js.map +1 -1
- package/build-module/focal-point-picker/controls.js +4 -4
- package/build-module/focal-point-picker/controls.js.map +1 -1
- package/build-module/focal-point-picker/focal-point.js +4 -6
- package/build-module/focal-point-picker/focal-point.js.map +1 -1
- package/build-module/focal-point-picker/grid.js +6 -34
- package/build-module/focal-point-picker/grid.js.map +1 -1
- package/build-module/focal-point-picker/index.js +158 -325
- package/build-module/focal-point-picker/index.js.map +1 -1
- package/build-module/focal-point-picker/media.js +4 -36
- package/build-module/focal-point-picker/media.js.map +1 -1
- package/build-module/focal-point-picker/styles/focal-point-picker-style.js +13 -14
- package/build-module/focal-point-picker/styles/focal-point-picker-style.js.map +1 -1
- package/build-module/focal-point-picker/utils.js +2 -6
- package/build-module/focal-point-picker/utils.js.map +1 -1
- package/build-module/focusable-iframe/index.js +6 -0
- package/build-module/focusable-iframe/index.js.map +1 -1
- package/build-module/form-token-field/index.js +18 -14
- package/build-module/form-token-field/index.js.map +1 -1
- package/build-module/gradient-picker/index.js +11 -1
- package/build-module/gradient-picker/index.js.map +1 -1
- package/build-module/guide/index.js +8 -5
- package/build-module/guide/index.js.map +1 -1
- package/build-module/higher-order/with-constrained-tabbing/index.js +1 -1
- package/build-module/higher-order/with-constrained-tabbing/index.js.map +1 -1
- package/build-module/higher-order/with-spoken-messages/index.js +2 -0
- package/build-module/higher-order/with-spoken-messages/index.js.map +1 -1
- package/build-module/isolated-event-container/index.js +3 -0
- package/build-module/isolated-event-container/index.js.map +1 -1
- package/build-module/mobile/global-styles-context/utils.native.js +2 -2
- package/build-module/mobile/global-styles-context/utils.native.js.map +1 -1
- package/build-module/navigable-container/menu.js +3 -8
- package/build-module/navigable-container/menu.js.map +1 -1
- package/build-module/navigation/menu/menu-title-search.js +1 -2
- package/build-module/navigation/menu/menu-title-search.js.map +1 -1
- package/build-module/palette-edit/index.js +6 -2
- package/build-module/palette-edit/index.js.map +1 -1
- package/build-module/popover/index.js +15 -35
- package/build-module/popover/index.js.map +1 -1
- package/build-module/text-highlight/index.js +2 -5
- package/build-module/text-highlight/index.js.map +1 -1
- package/build-module/toggle-group-control/toggle-group-control/component.js +2 -1
- package/build-module/toggle-group-control/toggle-group-control/component.js.map +1 -1
- package/build-module/toggle-group-control/toggle-group-control/styles.js +20 -6
- package/build-module/toggle-group-control/toggle-group-control/styles.js.map +1 -1
- package/build-module/tooltip/index.js +1 -6
- package/build-module/tooltip/index.js.map +1 -1
- package/build-module/tree-grid/index.js +4 -9
- package/build-module/tree-grid/index.js.map +1 -1
- package/build-module/utils/strings.js +11 -0
- package/build-module/utils/strings.js.map +1 -1
- package/build-style/style-rtl.css +2 -21
- package/build-style/style.css +2 -21
- package/build-types/animation/index.d.ts +2 -0
- package/build-types/animation/index.d.ts.map +1 -0
- package/build-types/card/card/component.d.ts +3 -3
- package/build-types/card/card/component.d.ts.map +1 -1
- package/build-types/card/card/hook.d.ts +7 -2
- package/build-types/card/card/hook.d.ts.map +1 -1
- package/build-types/card/card/index.d.ts +2 -2
- package/build-types/card/card/index.d.ts.map +1 -1
- package/build-types/card/card-body/component.d.ts +3 -3
- package/build-types/card/card-body/component.d.ts.map +1 -1
- package/build-types/card/card-body/hook.d.ts +5 -2
- package/build-types/card/card-body/hook.d.ts.map +1 -1
- package/build-types/card/card-body/index.d.ts +2 -2
- package/build-types/card/card-body/index.d.ts.map +1 -1
- package/build-types/card/card-divider/component.d.ts +3 -3
- package/build-types/card/card-divider/component.d.ts.map +1 -1
- package/build-types/card/card-divider/hook.d.ts +5 -2
- package/build-types/card/card-divider/hook.d.ts.map +1 -1
- package/build-types/card/card-divider/index.d.ts +2 -2
- package/build-types/card/card-divider/index.d.ts.map +1 -1
- package/build-types/card/card-footer/component.d.ts +3 -3
- package/build-types/card/card-footer/component.d.ts.map +1 -1
- package/build-types/card/card-footer/hook.d.ts +5 -2
- package/build-types/card/card-footer/hook.d.ts.map +1 -1
- package/build-types/card/card-footer/index.d.ts +2 -2
- package/build-types/card/card-footer/index.d.ts.map +1 -1
- package/build-types/card/card-header/component.d.ts +3 -3
- package/build-types/card/card-header/component.d.ts.map +1 -1
- package/build-types/card/card-header/hook.d.ts +5 -2
- package/build-types/card/card-header/hook.d.ts.map +1 -1
- package/build-types/card/card-header/index.d.ts +2 -2
- package/build-types/card/card-header/index.d.ts.map +1 -1
- package/build-types/card/card-media/component.d.ts +3 -4
- package/build-types/card/card-media/component.d.ts.map +1 -1
- package/build-types/card/card-media/hook.d.ts +6 -5
- package/build-types/card/card-media/hook.d.ts.map +1 -1
- package/build-types/card/card-media/index.d.ts +2 -2
- package/build-types/card/card-media/index.d.ts.map +1 -1
- package/build-types/card/context.d.ts +3 -2
- package/build-types/card/context.d.ts.map +1 -1
- package/build-types/card/index.d.ts +6 -6
- package/build-types/card/index.d.ts.map +1 -1
- package/build-types/card/stories/index.d.ts +12 -0
- package/build-types/card/stories/index.d.ts.map +1 -0
- package/build-types/card/styles.d.ts +20 -22
- package/build-types/card/styles.d.ts.map +1 -1
- package/build-types/card/test/index.d.ts +2 -0
- package/build-types/card/test/index.d.ts.map +1 -0
- package/build-types/card/types.d.ts +7 -1
- package/build-types/card/types.d.ts.map +1 -1
- package/build-types/clipboard-button/index.d.ts +16 -0
- package/build-types/clipboard-button/index.d.ts.map +1 -0
- package/build-types/color-palette/index.d.ts.map +1 -1
- package/build-types/composite/index.d.ts +2 -0
- package/build-types/composite/index.d.ts.map +1 -0
- package/build-types/date-time/date/index.d.ts +1 -1
- package/build-types/date-time/date/index.d.ts.map +1 -1
- package/build-types/date-time/date/styles.d.ts +4 -0
- package/build-types/date-time/date/styles.d.ts.map +1 -1
- package/build-types/date-time/date-time/index.d.ts.map +1 -1
- package/build-types/date-time/date-time/styles.d.ts +13 -0
- package/build-types/date-time/date-time/styles.d.ts.map +1 -1
- package/build-types/date-time/time/styles.d.ts.map +1 -1
- package/build-types/disclosure/index.d.ts +2 -0
- package/build-types/disclosure/index.d.ts.map +1 -0
- package/build-types/dropdown-menu/index.d.ts.map +1 -1
- package/build-types/focusable-iframe/index.d.ts +8 -0
- package/build-types/focusable-iframe/index.d.ts.map +1 -0
- package/build-types/form-token-field/index.d.ts.map +1 -1
- package/build-types/form-token-field/test/index.d.ts +2 -0
- package/build-types/form-token-field/test/index.d.ts.map +1 -0
- package/build-types/higher-order/with-constrained-tabbing/index.d.ts +3 -0
- package/build-types/higher-order/with-constrained-tabbing/index.d.ts.map +1 -0
- package/build-types/higher-order/with-spoken-messages/index.d.ts +4 -0
- package/build-types/higher-order/with-spoken-messages/index.d.ts.map +1 -0
- package/build-types/isolated-event-container/index.d.ts +3 -0
- package/build-types/isolated-event-container/index.d.ts.map +1 -0
- package/build-types/mobile/inserter-button/sparkles.d.ts +3 -0
- package/build-types/mobile/inserter-button/sparkles.d.ts.map +1 -0
- package/build-types/navigable-container/menu.d.ts.map +1 -1
- package/build-types/popover/index.d.ts +0 -1
- package/build-types/popover/index.d.ts.map +1 -1
- package/build-types/radio-context/index.d.ts +6 -0
- package/build-types/radio-context/index.d.ts.map +1 -0
- package/build-types/text-highlight/index.d.ts +0 -3
- package/build-types/text-highlight/index.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/component.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/styles.d.ts +4 -0
- package/build-types/toggle-group-control/toggle-group-control/styles.d.ts.map +1 -1
- package/build-types/tooltip/index.d.ts.map +1 -1
- package/build-types/utils/strings.d.ts +8 -0
- package/build-types/utils/strings.d.ts.map +1 -1
- package/package.json +17 -17
- package/src/alignment-matrix-control/styles/alignment-matrix-control-styles.js +1 -0
- package/src/autocomplete/get-default-use-items.js +6 -1
- package/src/autocomplete/index.js +2 -1
- package/src/card/card/{component.js → component.tsx} +13 -9
- package/src/card/card/{hook.js → hook.ts} +11 -11
- package/src/card/card/{index.js → index.ts} +0 -0
- package/src/card/card-body/{component.js → component.tsx} +13 -9
- package/src/card/card-body/{hook.js → hook.ts} +5 -5
- package/src/card/card-body/{index.js → index.ts} +0 -0
- package/src/card/card-divider/{component.js → component.tsx} +16 -10
- package/src/card/card-divider/{hook.js → hook.ts} +5 -5
- package/src/card/card-divider/{index.js → index.ts} +0 -0
- package/src/card/card-footer/{component.js → component.tsx} +13 -9
- package/src/card/card-footer/{hook.js → hook.ts} +5 -5
- package/src/card/card-footer/{index.js → index.ts} +0 -0
- package/src/card/card-header/{component.js → component.tsx} +13 -9
- package/src/card/card-header/{hook.js → hook.ts} +5 -5
- package/src/card/card-header/{index.js → index.ts} +0 -0
- package/src/card/card-media/{component.js → component.tsx} +13 -8
- package/src/card/card-media/{hook.js → hook.ts} +5 -5
- package/src/card/card-media/{index.js → index.ts} +0 -0
- package/src/card/{context.js → context.ts} +0 -0
- package/src/card/{index.js → index.ts} +0 -0
- package/src/card/stories/index.tsx +75 -0
- package/src/card/{styles.js → styles.ts} +0 -0
- package/src/card/test/__snapshots__/{index.js.snap → index.tsx.snap} +0 -0
- package/src/card/test/{index.js → index.tsx} +3 -3
- package/src/card/types.ts +8 -1
- package/src/clipboard-button/index.js +13 -0
- package/src/color-palette/index.js +8 -5
- package/src/color-palette/style.scss +0 -14
- package/src/color-palette/test/__snapshots__/index.js.snap +11 -4
- package/src/custom-gradient-picker/index.js +12 -0
- package/src/custom-gradient-picker/stories/index.js +3 -0
- package/src/date-time/date/index.tsx +26 -6
- package/src/date-time/date/styles.ts +6 -0
- package/src/date-time/date/test/index.tsx +6 -2
- package/src/date-time/date-time/index.tsx +3 -4
- package/src/date-time/date-time/styles.ts +9 -0
- package/src/date-time/time/styles.ts +1 -0
- package/src/drop-zone/index.js +2 -3
- package/src/dropdown-menu/index.js +1 -2
- package/src/dropdown-menu/index.native.js +0 -13
- package/src/dropdown-menu/test/index.js +54 -58
- package/src/focal-point-picker/README.md +3 -6
- package/src/focal-point-picker/controls.js +4 -4
- package/src/focal-point-picker/focal-point.js +2 -8
- package/src/focal-point-picker/grid.js +5 -41
- package/src/focal-point-picker/index.js +161 -303
- package/src/focal-point-picker/media.js +4 -28
- package/src/focal-point-picker/styles/focal-point-picker-style.js +5 -8
- package/src/focal-point-picker/test/index.js +1 -1
- package/src/focal-point-picker/utils.js +2 -6
- package/src/focusable-iframe/index.js +5 -0
- package/src/form-token-field/index.tsx +17 -23
- package/src/form-token-field/test/index.tsx +2106 -0
- package/src/gradient-picker/README.md +9 -0
- package/src/gradient-picker/index.js +9 -0
- package/src/gradient-picker/stories/index.js +1 -0
- package/src/guide/index.js +6 -3
- package/src/guide/test/index.js +138 -1
- package/src/higher-order/with-constrained-tabbing/index.js +1 -1
- package/src/higher-order/with-spoken-messages/index.js +2 -0
- package/src/isolated-event-container/index.js +3 -0
- package/src/mobile/global-styles-context/utils.native.js +7 -2
- package/src/navigable-container/menu.js +3 -7
- package/src/navigation/menu/menu-title-search.js +1 -2
- package/src/palette-edit/index.js +14 -10
- package/src/palette-edit/style.scss +3 -11
- package/src/placeholder/style.scss +1 -4
- package/src/popover/index.js +17 -35
- package/src/popover/stories/index.js +0 -1
- package/src/text-highlight/index.tsx +1 -5
- package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +62 -44
- package/src/toggle-group-control/toggle-group-control/component.tsx +3 -2
- package/src/toggle-group-control/toggle-group-control/styles.ts +5 -0
- package/src/tooltip/index.js +1 -5
- package/src/tree-grid/index.js +4 -9
- package/src/utils/strings.ts +11 -0
- package/tsconfig.json +45 -76
- package/tsconfig.tsbuildinfo +1 -1
- package/build/ui/__storybook-utils/example-grid.js +0 -88
- package/build/ui/__storybook-utils/example-grid.js.map +0 -1
- package/build/ui/__storybook-utils/index.js +0 -19
- package/build/ui/__storybook-utils/index.js.map +0 -1
- package/build/ui/__storybook-utils/page.js +0 -43
- package/build/ui/__storybook-utils/page.js.map +0 -1
- package/build/utils/keyboard.js +0 -41
- package/build/utils/keyboard.js.map +0 -1
- package/build-module/ui/__storybook-utils/example-grid.js +0 -69
- package/build-module/ui/__storybook-utils/example-grid.js.map +0 -1
- package/build-module/ui/__storybook-utils/index.js +0 -2
- package/build-module/ui/__storybook-utils/index.js.map +0 -1
- package/build-module/ui/__storybook-utils/page.js +0 -32
- package/build-module/ui/__storybook-utils/page.js.map +0 -1
- package/build-module/utils/keyboard.js +0 -33
- package/build-module/utils/keyboard.js.map +0 -1
- package/build-types/form-token-field/test/lib/fixtures.d.ts +0 -26
- package/build-types/form-token-field/test/lib/fixtures.d.ts.map +0 -1
- package/build-types/form-token-field/test/lib/token-field-wrapper.d.ts +0 -21
- package/build-types/form-token-field/test/lib/token-field-wrapper.d.ts.map +0 -1
- package/build-types/utils/keyboard.d.ts +0 -12
- package/build-types/utils/keyboard.d.ts.map +0 -1
- package/src/card/stories/index.js +0 -209
- package/src/form-token-field/test/index.js +0 -442
- package/src/form-token-field/test/lib/fixtures.js +0 -89
- package/src/form-token-field/test/lib/token-field-wrapper.tsx +0 -71
- package/src/guide/test/page-control.js +0 -40
- package/src/ui/__storybook-utils/example-grid.js +0 -61
- package/src/ui/__storybook-utils/index.js +0 -1
- package/src/ui/__storybook-utils/page.js +0 -29
- package/src/utils/keyboard.js +0 -28
- package/src/utils/test/keyboard.js +0 -34
|
@@ -0,0 +1,2106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
within,
|
|
8
|
+
getDefaultNormalizer,
|
|
9
|
+
waitFor,
|
|
10
|
+
} from '@testing-library/react';
|
|
11
|
+
import userEvent from '@testing-library/user-event';
|
|
12
|
+
import type { ComponentProps } from 'react';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* WordPress dependencies
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from '@wordpress/element';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal dependencies
|
|
21
|
+
*/
|
|
22
|
+
import FormTokenField from '../';
|
|
23
|
+
|
|
24
|
+
const FormTokenFieldWithState = ( {
|
|
25
|
+
onChange,
|
|
26
|
+
value,
|
|
27
|
+
initialValue = [],
|
|
28
|
+
...props
|
|
29
|
+
}: ComponentProps< typeof FormTokenField > & {
|
|
30
|
+
initialValue?: ComponentProps< typeof FormTokenField >[ 'value' ];
|
|
31
|
+
} ) => {
|
|
32
|
+
const [ selectedValue, setSelectedValue ] =
|
|
33
|
+
useState< ComponentProps< typeof FormTokenField >[ 'value' ] >(
|
|
34
|
+
initialValue
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<FormTokenField
|
|
39
|
+
{ ...props }
|
|
40
|
+
value={ selectedValue }
|
|
41
|
+
onChange={ ( tokens ) => {
|
|
42
|
+
setSelectedValue( tokens );
|
|
43
|
+
onChange?.( tokens );
|
|
44
|
+
} }
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const expectTokensToBeInTheDocument = ( tokensText: string[] ) => {
|
|
50
|
+
tokensText.forEach( ( tokenText, tokenIndex, tokensArray ) => {
|
|
51
|
+
// Each token has 2 tags rendered in the DOM:
|
|
52
|
+
// - one with the format "takenName (X of Y)", which is visibly hidden,
|
|
53
|
+
// and is used for assistive technology;
|
|
54
|
+
// - one with the format "tokenName", which is visible but hidden to
|
|
55
|
+
// assistive technology.
|
|
56
|
+
const assistiveTechnologyToken = screen.getByText(
|
|
57
|
+
`${ tokenText } (${ tokenIndex + 1 } of ${ tokensArray.length })`,
|
|
58
|
+
{
|
|
59
|
+
normalizer: getDefaultNormalizer( {
|
|
60
|
+
collapseWhitespace: false,
|
|
61
|
+
trim: false,
|
|
62
|
+
} ),
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
// The "exact" flag is necessary in order no to match the element
|
|
66
|
+
// used for assistive technology.
|
|
67
|
+
const visibleToken = screen.getByText( tokenText, {
|
|
68
|
+
exact: true,
|
|
69
|
+
normalizer: getDefaultNormalizer( {
|
|
70
|
+
collapseWhitespace: false,
|
|
71
|
+
trim: false,
|
|
72
|
+
} ),
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
expect( assistiveTechnologyToken ).toBeInTheDocument();
|
|
76
|
+
expect( visibleToken ).toBeVisible();
|
|
77
|
+
expect( visibleToken ).toHaveAttribute( 'aria-hidden', 'true' );
|
|
78
|
+
} );
|
|
79
|
+
};
|
|
80
|
+
const expectTokensNotToBeInTheDocument = ( tokensText: string[] ) => {
|
|
81
|
+
tokensText.forEach( ( tokenText ) =>
|
|
82
|
+
expect( screen.queryByText( tokenText ) ).not.toBeInTheDocument()
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const expectVisibleSuggestionsToBe = (
|
|
87
|
+
listElement: HTMLElement,
|
|
88
|
+
suggestionsText: string[]
|
|
89
|
+
) => {
|
|
90
|
+
const allVisibleOptions = within( listElement ).queryAllByRole( 'option' );
|
|
91
|
+
|
|
92
|
+
expect( allVisibleOptions ).toHaveLength( suggestionsText.length );
|
|
93
|
+
|
|
94
|
+
allVisibleOptions.forEach( ( matchedOption, index ) => {
|
|
95
|
+
expect( matchedOption ).toHaveAccessibleName(
|
|
96
|
+
suggestionsText[ index ]
|
|
97
|
+
);
|
|
98
|
+
} );
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function unescapeAndFormatSpaces( str: string ) {
|
|
102
|
+
const nbsp = String.fromCharCode( 160 );
|
|
103
|
+
const escaped = new DOMParser().parseFromString( str, 'text/html' );
|
|
104
|
+
return escaped.documentElement.textContent?.replace( / /g, nbsp ) ?? '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe( 'FormTokenField', () => {
|
|
108
|
+
describe( 'basic usage', () => {
|
|
109
|
+
it( "should add tokens with the input's value when pressing the enter key", async () => {
|
|
110
|
+
const user = userEvent.setup( {
|
|
111
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
const onChangeSpy = jest.fn();
|
|
115
|
+
|
|
116
|
+
render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
|
|
117
|
+
|
|
118
|
+
const input = screen.getByRole( 'combobox' );
|
|
119
|
+
|
|
120
|
+
// Add 'apple' token by typing it and pressing enter to tokenize it.
|
|
121
|
+
await user.type( input, 'apple[Enter]' );
|
|
122
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
123
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'apple' ] );
|
|
124
|
+
expectTokensToBeInTheDocument( [ 'apple' ] );
|
|
125
|
+
|
|
126
|
+
// Add 'pear' token by typing it and pressing enter to tokenize it.
|
|
127
|
+
await user.type( input, 'pear[Enter]' );
|
|
128
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
129
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [
|
|
130
|
+
'apple',
|
|
131
|
+
'pear',
|
|
132
|
+
] );
|
|
133
|
+
expectTokensToBeInTheDocument( [ 'apple', 'pear' ] );
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
it( "should add a token with the input's value when pressing the comma key", async () => {
|
|
137
|
+
const user = userEvent.setup( {
|
|
138
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
139
|
+
} );
|
|
140
|
+
|
|
141
|
+
const onChangeSpy = jest.fn();
|
|
142
|
+
|
|
143
|
+
render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
|
|
144
|
+
|
|
145
|
+
const input = screen.getByRole( 'combobox' );
|
|
146
|
+
|
|
147
|
+
// Add 'orange' token by typing it and pressing enter to tokenize it.
|
|
148
|
+
await user.type( input, 'orange,' );
|
|
149
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
150
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'orange' ] );
|
|
151
|
+
expectTokensToBeInTheDocument( [ 'orange' ] );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'should add a token with the input value when pressing the space key and the `tokenizeOnSpace` prop is `true`', async () => {
|
|
155
|
+
const user = userEvent.setup( {
|
|
156
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
const onChangeSpy = jest.fn();
|
|
160
|
+
|
|
161
|
+
const { rerender } = render(
|
|
162
|
+
<FormTokenFieldWithState onChange={ onChangeSpy } />
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const input = screen.getByRole( 'combobox' );
|
|
166
|
+
|
|
167
|
+
// Add 'dragon fruit' token by typing it and pressing enter to tokenize it.
|
|
168
|
+
await user.type( input, 'dragon fruit[Enter]' );
|
|
169
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
170
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'dragon fruit' ] );
|
|
171
|
+
expectTokensToBeInTheDocument( [ 'dragon fruit' ] );
|
|
172
|
+
|
|
173
|
+
rerender(
|
|
174
|
+
<FormTokenFieldWithState
|
|
175
|
+
onChange={ onChangeSpy }
|
|
176
|
+
tokenizeOnSpace
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Add 'dragon fruit' token by typing it and pressing enter to tokenize it,
|
|
181
|
+
// this time two separate tokens should be added
|
|
182
|
+
await user.type( input, 'dragon fruit[Enter]' );
|
|
183
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
|
|
184
|
+
expect( onChangeSpy ).toHaveBeenNthCalledWith( 2, [
|
|
185
|
+
'dragon fruit',
|
|
186
|
+
'dragon',
|
|
187
|
+
] );
|
|
188
|
+
expect( onChangeSpy ).toHaveBeenNthCalledWith( 3, [
|
|
189
|
+
'dragon fruit',
|
|
190
|
+
'dragon',
|
|
191
|
+
'fruit',
|
|
192
|
+
] );
|
|
193
|
+
expectTokensToBeInTheDocument( [
|
|
194
|
+
'dragon fruit',
|
|
195
|
+
'dragon',
|
|
196
|
+
'fruit',
|
|
197
|
+
] );
|
|
198
|
+
} );
|
|
199
|
+
|
|
200
|
+
it( "should not add a token with the input's value when pressing the tab key", async () => {
|
|
201
|
+
const user = userEvent.setup( {
|
|
202
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
203
|
+
} );
|
|
204
|
+
|
|
205
|
+
const onChangeSpy = jest.fn();
|
|
206
|
+
|
|
207
|
+
render( <FormTokenFieldWithState onChange={ onChangeSpy } /> );
|
|
208
|
+
|
|
209
|
+
const input = screen.getByRole( 'combobox' );
|
|
210
|
+
|
|
211
|
+
// Add 'orange' token by typing it and pressing enter to tokenize it.
|
|
212
|
+
await user.type( input, 'grapefruit' );
|
|
213
|
+
await user.tab();
|
|
214
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
|
|
215
|
+
expectTokensNotToBeInTheDocument( [ 'grapefruit' ] );
|
|
216
|
+
} );
|
|
217
|
+
|
|
218
|
+
it( 'should remove the last token when pressing the backspace key', async () => {
|
|
219
|
+
const user = userEvent.setup( {
|
|
220
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
221
|
+
} );
|
|
222
|
+
|
|
223
|
+
const onChangeSpy = jest.fn();
|
|
224
|
+
|
|
225
|
+
render(
|
|
226
|
+
<FormTokenFieldWithState
|
|
227
|
+
onChange={ onChangeSpy }
|
|
228
|
+
initialValue={ [ 'banana', 'mango' ] }
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const input = screen.getByRole( 'combobox' );
|
|
233
|
+
|
|
234
|
+
// Press backspace to remove the last token ("mango")
|
|
235
|
+
await user.type( input, '[Backspace]' );
|
|
236
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
237
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'banana' ] );
|
|
238
|
+
expectTokensToBeInTheDocument( [ 'banana' ] );
|
|
239
|
+
expectTokensNotToBeInTheDocument( [ 'mango' ] );
|
|
240
|
+
|
|
241
|
+
// Press backspace to remove the last token ("banana")
|
|
242
|
+
await user.type( input, '[Backspace]' );
|
|
243
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
244
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [] );
|
|
245
|
+
expectTokensNotToBeInTheDocument( [ 'banana', 'mango' ] );
|
|
246
|
+
} );
|
|
247
|
+
|
|
248
|
+
it( 'should remove a token when clicking the token\'s "remove" button', async () => {
|
|
249
|
+
const user = userEvent.setup( {
|
|
250
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
251
|
+
} );
|
|
252
|
+
|
|
253
|
+
const onChangeSpy = jest.fn();
|
|
254
|
+
|
|
255
|
+
render(
|
|
256
|
+
<FormTokenFieldWithState
|
|
257
|
+
initialValue={ [ 'lemon', 'bergamot' ] }
|
|
258
|
+
onChange={ onChangeSpy }
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expectTokensToBeInTheDocument( [ 'lemon', 'bergamot' ] );
|
|
263
|
+
|
|
264
|
+
// There should be 2 "remove item" buttons, one per token
|
|
265
|
+
expect(
|
|
266
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )
|
|
267
|
+
).toHaveLength( 2 );
|
|
268
|
+
|
|
269
|
+
// Click the "X" button for the "lemon" token (the first one)
|
|
270
|
+
await user.click(
|
|
271
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
|
|
272
|
+
);
|
|
273
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
274
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'bergamot' ] );
|
|
275
|
+
expectTokensToBeInTheDocument( [ 'bergamot' ] );
|
|
276
|
+
expectTokensNotToBeInTheDocument( [ 'lemon' ] );
|
|
277
|
+
|
|
278
|
+
// There should be 1 "remove item" button for the "bergamot" token
|
|
279
|
+
expect(
|
|
280
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )
|
|
281
|
+
).toHaveLength( 1 );
|
|
282
|
+
|
|
283
|
+
// Click the "X" button for the "bergamot" token (the only one)
|
|
284
|
+
await user.click(
|
|
285
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
|
|
286
|
+
);
|
|
287
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
288
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [] );
|
|
289
|
+
expectTokensNotToBeInTheDocument( [ 'lemon', 'bergamot' ] );
|
|
290
|
+
} );
|
|
291
|
+
|
|
292
|
+
it( 'should remove a token when by focusing on the token\'s "remove" button and pressing space bar', async () => {
|
|
293
|
+
const user = userEvent.setup( {
|
|
294
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
295
|
+
} );
|
|
296
|
+
|
|
297
|
+
const onChangeSpy = jest.fn();
|
|
298
|
+
|
|
299
|
+
render(
|
|
300
|
+
<FormTokenFieldWithState
|
|
301
|
+
onChange={ onChangeSpy }
|
|
302
|
+
initialValue={ [ 'persimmon', 'plum' ] }
|
|
303
|
+
/>
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const input = screen.getByRole( 'combobox' );
|
|
307
|
+
await user.click( input );
|
|
308
|
+
|
|
309
|
+
// Currently the focus in on the input. Pressing shift+tab twice should
|
|
310
|
+
// move focus on the "remove item" button of the first token ("persimmon")
|
|
311
|
+
await user.tab( { shift: true } );
|
|
312
|
+
await user.tab( { shift: true } );
|
|
313
|
+
|
|
314
|
+
expect(
|
|
315
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )
|
|
316
|
+
).toHaveLength( 2 );
|
|
317
|
+
expect(
|
|
318
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
|
|
319
|
+
).toHaveFocus();
|
|
320
|
+
|
|
321
|
+
// Pressing the "space" key on the button should remove the "persimmon" item
|
|
322
|
+
await user.keyboard( '[Space]' );
|
|
323
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
324
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'plum' ] );
|
|
325
|
+
expectTokensToBeInTheDocument( [ 'plum' ] );
|
|
326
|
+
expectTokensNotToBeInTheDocument( [ 'persimmon' ] );
|
|
327
|
+
} );
|
|
328
|
+
|
|
329
|
+
it( 'should not add a new token if a token with the same value already exists', async () => {
|
|
330
|
+
const user = userEvent.setup( {
|
|
331
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
332
|
+
} );
|
|
333
|
+
|
|
334
|
+
const onChangeSpy = jest.fn();
|
|
335
|
+
|
|
336
|
+
render(
|
|
337
|
+
<FormTokenFieldWithState
|
|
338
|
+
initialValue={ [ 'papaya' ] }
|
|
339
|
+
onChange={ onChangeSpy }
|
|
340
|
+
/>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const input = screen.getByRole( 'combobox' );
|
|
344
|
+
|
|
345
|
+
// Add 'guava' token by typing it and pressing enter to tokenize it.
|
|
346
|
+
await user.type( input, 'guava[Enter]' );
|
|
347
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
348
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'papaya', 'guava' ] );
|
|
349
|
+
expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
|
|
350
|
+
|
|
351
|
+
// Try to add a 'papaya' token by typing it and pressing enter to tokenize it,
|
|
352
|
+
// but the token won't be added because it already exists.
|
|
353
|
+
await user.type( input, 'papaya[Enter]' );
|
|
354
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
355
|
+
expectTokensToBeInTheDocument( [ 'papaya', 'guava' ] );
|
|
356
|
+
} );
|
|
357
|
+
|
|
358
|
+
it( 'should not add a new token if the text input is blank', async () => {
|
|
359
|
+
const user = userEvent.setup( {
|
|
360
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
361
|
+
} );
|
|
362
|
+
|
|
363
|
+
const onChangeSpy = jest.fn();
|
|
364
|
+
|
|
365
|
+
render(
|
|
366
|
+
<FormTokenFieldWithState
|
|
367
|
+
initialValue={ [ 'melon' ] }
|
|
368
|
+
onChange={ onChangeSpy }
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const input = screen.getByRole( 'combobox' );
|
|
373
|
+
|
|
374
|
+
// Press enter on an empty input, no token gets added
|
|
375
|
+
await user.type( input, '[Enter]' );
|
|
376
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
377
|
+
expectTokensToBeInTheDocument( [ 'melon' ] );
|
|
378
|
+
} );
|
|
379
|
+
|
|
380
|
+
it( 'should allow moving the cursor through the tokens when pressing the arrow keys, and should remove the token in front of the cursor when pressing the delete key', async () => {
|
|
381
|
+
const user = userEvent.setup( {
|
|
382
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
383
|
+
} );
|
|
384
|
+
|
|
385
|
+
const onChangeSpy = jest.fn();
|
|
386
|
+
|
|
387
|
+
render(
|
|
388
|
+
<FormTokenFieldWithState
|
|
389
|
+
initialValue={ [ 'kiwi', 'peach', 'nectarine', 'coconut' ] }
|
|
390
|
+
onChange={ onChangeSpy }
|
|
391
|
+
/>
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expectTokensToBeInTheDocument( [
|
|
395
|
+
'kiwi',
|
|
396
|
+
'peach',
|
|
397
|
+
'nectarine',
|
|
398
|
+
'coconut',
|
|
399
|
+
] );
|
|
400
|
+
|
|
401
|
+
const input = screen.getByRole( 'combobox' );
|
|
402
|
+
|
|
403
|
+
// Press "delete" to delete the token in front of the cursor, but since
|
|
404
|
+
// there's no token in front of the cursor, nothing happens
|
|
405
|
+
await user.type( input, '[Delete]' );
|
|
406
|
+
|
|
407
|
+
// Pressing the right arrow doesn't move the cursor because there are no
|
|
408
|
+
// tokens in front of it, and therefore pressing "delete" yields the same
|
|
409
|
+
// result as before — no tokens are deleted.
|
|
410
|
+
await user.type( input, '[ArrowRight][Delete]' );
|
|
411
|
+
|
|
412
|
+
// Proof that so far, all keyboard interactions didn't delete any tokens.
|
|
413
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
414
|
+
expectTokensToBeInTheDocument( [
|
|
415
|
+
'kiwi',
|
|
416
|
+
'peach',
|
|
417
|
+
'nectarine',
|
|
418
|
+
'coconut',
|
|
419
|
+
] );
|
|
420
|
+
|
|
421
|
+
// Press the left arrow 4 times, moving cursor between the "kiwi" and
|
|
422
|
+
// "peach" tokens. Pressing the "delete" key will delete the "peach"
|
|
423
|
+
// token, since it's in front of the cursor.
|
|
424
|
+
await user.type(
|
|
425
|
+
input,
|
|
426
|
+
'[ArrowLeft][ArrowLeft][ArrowLeft][ArrowLeft][Delete]'
|
|
427
|
+
);
|
|
428
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
429
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
430
|
+
'peach',
|
|
431
|
+
'nectarine',
|
|
432
|
+
'coconut',
|
|
433
|
+
] );
|
|
434
|
+
expectTokensToBeInTheDocument( [
|
|
435
|
+
'peach',
|
|
436
|
+
'nectarine',
|
|
437
|
+
'coconut',
|
|
438
|
+
] );
|
|
439
|
+
expectTokensNotToBeInTheDocument( [ 'kiwi' ] );
|
|
440
|
+
|
|
441
|
+
// Press backspace to delete the token before the cursor, but since
|
|
442
|
+
// there's no token before the cursor, nothing happens
|
|
443
|
+
await user.type( input, '[Backspace]' );
|
|
444
|
+
|
|
445
|
+
// Pressing the left arrow doesn't move the cursor because there are no
|
|
446
|
+
// tokens before it, and therefore pressing backspace yields the same
|
|
447
|
+
// result as before — no tokens are deleted.
|
|
448
|
+
await user.type( input, '[ArrowLeft][Backspace]' );
|
|
449
|
+
|
|
450
|
+
// Proof that pressing backspace hasn't caused any further token deletion.
|
|
451
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
452
|
+
|
|
453
|
+
// Press the right arrow, moving cursor between the "kiwi" and
|
|
454
|
+
// "nectarine" tokens. Pressing the "delete" key will delete the "nectarine"
|
|
455
|
+
// token, since it's in front of the cursor.
|
|
456
|
+
await user.type( input, '[ArrowRight][Delete]' );
|
|
457
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
458
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
459
|
+
'peach',
|
|
460
|
+
'coconut',
|
|
461
|
+
] );
|
|
462
|
+
expectTokensToBeInTheDocument( [ 'peach', 'coconut' ] );
|
|
463
|
+
expectTokensNotToBeInTheDocument( [ 'kiwi', 'nectarine' ] );
|
|
464
|
+
|
|
465
|
+
// Add 'starfruit' token while the cursor is in between the "peach" and
|
|
466
|
+
// "coconut" tokens.
|
|
467
|
+
await user.type( input, 'starfruit[Enter]' );
|
|
468
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
|
|
469
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
470
|
+
'peach',
|
|
471
|
+
// Notice that starfruit is added in between "peach" and "coconut"
|
|
472
|
+
'starfruit',
|
|
473
|
+
'coconut',
|
|
474
|
+
] );
|
|
475
|
+
expectTokensToBeInTheDocument( [
|
|
476
|
+
'peach',
|
|
477
|
+
'starfruit',
|
|
478
|
+
'coconut',
|
|
479
|
+
] );
|
|
480
|
+
} );
|
|
481
|
+
|
|
482
|
+
it( "should add additional classnames passed via the `className` prop to the input element's 2nd level wrapper", () => {
|
|
483
|
+
render( <FormTokenFieldWithState className="test-classname" /> );
|
|
484
|
+
|
|
485
|
+
const input = screen.getByRole( 'combobox' );
|
|
486
|
+
|
|
487
|
+
// This is testing implementation details, but I'm not sure there's
|
|
488
|
+
// a better way.
|
|
489
|
+
expect( input.parentElement?.parentElement ).toHaveClass(
|
|
490
|
+
'test-classname'
|
|
491
|
+
);
|
|
492
|
+
} );
|
|
493
|
+
|
|
494
|
+
it( 'should label the input correctly via the `label` prop', () => {
|
|
495
|
+
const { rerender } = render( <FormTokenFieldWithState /> );
|
|
496
|
+
|
|
497
|
+
expect(
|
|
498
|
+
screen.getByRole( 'combobox', { name: 'Add item' } )
|
|
499
|
+
).toBeVisible();
|
|
500
|
+
|
|
501
|
+
rerender( <FormTokenFieldWithState label="Test label" /> );
|
|
502
|
+
|
|
503
|
+
expect(
|
|
504
|
+
screen.getByRole( 'combobox', { name: 'Test label' } )
|
|
505
|
+
).toBeVisible();
|
|
506
|
+
} );
|
|
507
|
+
|
|
508
|
+
it( 'should fire the `onFocus` callback when the input is focused', async () => {
|
|
509
|
+
const user = userEvent.setup( {
|
|
510
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
511
|
+
} );
|
|
512
|
+
|
|
513
|
+
const onFocusSpy = jest.fn();
|
|
514
|
+
|
|
515
|
+
render( <FormTokenFieldWithState onFocus={ onFocusSpy } /> );
|
|
516
|
+
|
|
517
|
+
const input = screen.getByRole( 'combobox' );
|
|
518
|
+
|
|
519
|
+
await user.click( input );
|
|
520
|
+
|
|
521
|
+
expect( onFocusSpy ).toHaveBeenCalledTimes( 1 );
|
|
522
|
+
expect( onFocusSpy ).toHaveBeenCalledWith(
|
|
523
|
+
expect.objectContaining( {
|
|
524
|
+
type: 'focus',
|
|
525
|
+
target: input,
|
|
526
|
+
} )
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect( input ).toHaveFocus();
|
|
530
|
+
} );
|
|
531
|
+
|
|
532
|
+
it( "should fire the `onInputChange` callback when the input's value changes", async () => {
|
|
533
|
+
const user = userEvent.setup( {
|
|
534
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
535
|
+
} );
|
|
536
|
+
|
|
537
|
+
const onInputChangeSpy = jest.fn();
|
|
538
|
+
|
|
539
|
+
render(
|
|
540
|
+
<FormTokenFieldWithState onInputChange={ onInputChangeSpy } />
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const input = screen.getByRole( 'combobox' );
|
|
544
|
+
|
|
545
|
+
await user.type( input, 'strawberry[Enter]' );
|
|
546
|
+
|
|
547
|
+
expect( onInputChangeSpy ).toHaveBeenCalledTimes(
|
|
548
|
+
'strawberry'.length
|
|
549
|
+
);
|
|
550
|
+
expect( onInputChangeSpy ).toHaveBeenNthCalledWith(
|
|
551
|
+
5,
|
|
552
|
+
'strawberry'.slice( 0, 5 )
|
|
553
|
+
);
|
|
554
|
+
} );
|
|
555
|
+
|
|
556
|
+
it( 'should show extra instructions when the `__experimentalShowHowTo` prop is set to `true`', () => {
|
|
557
|
+
const instructionsTokenizeSpace =
|
|
558
|
+
'Separate with commas, spaces, or the Enter key.';
|
|
559
|
+
const instructionsDefault =
|
|
560
|
+
'Separate with commas or the Enter key.';
|
|
561
|
+
|
|
562
|
+
// The __experimentalShowHowTo prop is `true` by default
|
|
563
|
+
const { rerender } = render( <FormTokenFieldWithState /> );
|
|
564
|
+
|
|
565
|
+
expect( screen.getByText( instructionsDefault ) ).toBeVisible();
|
|
566
|
+
|
|
567
|
+
// The "show how to" text is used to aria-describedby the input
|
|
568
|
+
expect(
|
|
569
|
+
screen.getByRole( 'combobox' )
|
|
570
|
+
).toHaveAccessibleDescription( instructionsDefault );
|
|
571
|
+
|
|
572
|
+
rerender( <FormTokenFieldWithState tokenizeOnSpace /> );
|
|
573
|
+
|
|
574
|
+
expect(
|
|
575
|
+
screen.getByText( instructionsTokenizeSpace )
|
|
576
|
+
).toBeVisible();
|
|
577
|
+
|
|
578
|
+
// The "show how to" text is used to aria-describedby the input
|
|
579
|
+
expect(
|
|
580
|
+
screen.getByRole( 'combobox' )
|
|
581
|
+
).toHaveAccessibleDescription( instructionsTokenizeSpace );
|
|
582
|
+
|
|
583
|
+
rerender(
|
|
584
|
+
<FormTokenFieldWithState
|
|
585
|
+
tokenizeOnSpace
|
|
586
|
+
__experimentalShowHowTo={ false }
|
|
587
|
+
/>
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(
|
|
591
|
+
screen.queryByText( instructionsDefault )
|
|
592
|
+
).not.toBeInTheDocument();
|
|
593
|
+
expect(
|
|
594
|
+
screen.queryByText( instructionsTokenizeSpace )
|
|
595
|
+
).not.toBeInTheDocument();
|
|
596
|
+
expect(
|
|
597
|
+
screen.getByRole( 'combobox' )
|
|
598
|
+
).not.toHaveAccessibleDescription();
|
|
599
|
+
} );
|
|
600
|
+
|
|
601
|
+
it( "should use the value of the `placeholder` prop as the input's placeholder only when there are no tokens", async () => {
|
|
602
|
+
const user = userEvent.setup( {
|
|
603
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
604
|
+
} );
|
|
605
|
+
|
|
606
|
+
const onChangeSpy = jest.fn();
|
|
607
|
+
|
|
608
|
+
render(
|
|
609
|
+
<FormTokenFieldWithState
|
|
610
|
+
onChange={ onChangeSpy }
|
|
611
|
+
placeholder="Test placeholder"
|
|
612
|
+
/>
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
expect(
|
|
616
|
+
screen.getByPlaceholderText( 'Test placeholder' )
|
|
617
|
+
).toBeVisible();
|
|
618
|
+
|
|
619
|
+
const input = screen.getByRole( 'combobox' );
|
|
620
|
+
|
|
621
|
+
// Add 'blueberry' token. The placeholder text should not be shown anymore
|
|
622
|
+
await user.type( input, 'blueberry[Enter]' );
|
|
623
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
624
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'blueberry' ] );
|
|
625
|
+
expectTokensToBeInTheDocument( [ 'blueberry' ] );
|
|
626
|
+
|
|
627
|
+
expect(
|
|
628
|
+
screen.queryByPlaceholderText( 'Test placeholder' )
|
|
629
|
+
).not.toBeInTheDocument();
|
|
630
|
+
} );
|
|
631
|
+
|
|
632
|
+
it( 'should handle accents and special characters in tokens and input value', async () => {
|
|
633
|
+
const user = userEvent.setup( {
|
|
634
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
635
|
+
} );
|
|
636
|
+
|
|
637
|
+
const onChangeSpy = jest.fn();
|
|
638
|
+
|
|
639
|
+
render(
|
|
640
|
+
<FormTokenFieldWithState
|
|
641
|
+
onChange={ onChangeSpy }
|
|
642
|
+
initialValue={ [ 'français', 'español', '日本', 'עברית' ] }
|
|
643
|
+
/>
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const input = screen.getByRole( 'combobox' );
|
|
647
|
+
|
|
648
|
+
// Add 'عربى' token by typing it and pressing enter to tokenize it.
|
|
649
|
+
await user.type( input, 'عربى[Enter]' );
|
|
650
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
651
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
652
|
+
'français',
|
|
653
|
+
'español',
|
|
654
|
+
'日本',
|
|
655
|
+
'עברית',
|
|
656
|
+
'عربى',
|
|
657
|
+
] );
|
|
658
|
+
expectTokensToBeInTheDocument( [
|
|
659
|
+
'français',
|
|
660
|
+
'español',
|
|
661
|
+
'日本',
|
|
662
|
+
'עברית',
|
|
663
|
+
'عربى',
|
|
664
|
+
] );
|
|
665
|
+
} );
|
|
666
|
+
} );
|
|
667
|
+
|
|
668
|
+
describe( 'suggestions', () => {
|
|
669
|
+
it( 'should not render suggestions in its default state', () => {
|
|
670
|
+
render(
|
|
671
|
+
<FormTokenFieldWithState
|
|
672
|
+
suggestions={ [ 'Red', 'Magenta', 'Vermilion' ] }
|
|
673
|
+
/>
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
677
|
+
} );
|
|
678
|
+
|
|
679
|
+
it( 'should render suggestions when receiving focus if the `__experimentalExpandOnFocus` prop is set to `true`', async () => {
|
|
680
|
+
const user = userEvent.setup( {
|
|
681
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
682
|
+
} );
|
|
683
|
+
|
|
684
|
+
const onFocusSpy = jest.fn();
|
|
685
|
+
|
|
686
|
+
const suggestions = [ 'Cobalt', 'Blue', 'Octane' ];
|
|
687
|
+
|
|
688
|
+
render(
|
|
689
|
+
<>
|
|
690
|
+
<FormTokenFieldWithState
|
|
691
|
+
onFocus={ onFocusSpy }
|
|
692
|
+
suggestions={ suggestions }
|
|
693
|
+
__experimentalExpandOnFocus
|
|
694
|
+
/>
|
|
695
|
+
</>
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const input = screen.getByRole( 'combobox' );
|
|
699
|
+
|
|
700
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
701
|
+
|
|
702
|
+
// Click the input, focusing it.
|
|
703
|
+
await user.click( input );
|
|
704
|
+
|
|
705
|
+
const suggestionList = screen.getByRole( 'listbox' );
|
|
706
|
+
|
|
707
|
+
expect( onFocusSpy ).toHaveBeenCalledTimes( 1 );
|
|
708
|
+
expect( suggestionList ).toBeVisible();
|
|
709
|
+
|
|
710
|
+
expectVisibleSuggestionsToBe(
|
|
711
|
+
screen.getByRole( 'listbox' ),
|
|
712
|
+
suggestions
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
// Minimum length limitations don't affect the search text when the
|
|
716
|
+
// `__experimentalExpandOnFocus` is `true`
|
|
717
|
+
await user.keyboard( 'c' );
|
|
718
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
719
|
+
'Cobalt',
|
|
720
|
+
'Octane',
|
|
721
|
+
] );
|
|
722
|
+
} );
|
|
723
|
+
|
|
724
|
+
it( 'should not render suggestions if the text input is not matching any of the suggestions', async () => {
|
|
725
|
+
const user = userEvent.setup( {
|
|
726
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
727
|
+
} );
|
|
728
|
+
|
|
729
|
+
const suggestions = [ 'White', 'Pearl', 'Alabaster' ];
|
|
730
|
+
|
|
731
|
+
render( <FormTokenFieldWithState suggestions={ suggestions } /> );
|
|
732
|
+
|
|
733
|
+
const input = screen.getByRole( 'combobox' );
|
|
734
|
+
|
|
735
|
+
// Type 'Snow' which doesn't match any of the suggestions
|
|
736
|
+
await user.type( input, 'Snow' );
|
|
737
|
+
|
|
738
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
739
|
+
} );
|
|
740
|
+
|
|
741
|
+
it( 'should render the matching suggestions only if the text input has the minimum length', async () => {
|
|
742
|
+
const user = userEvent.setup( {
|
|
743
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
744
|
+
} );
|
|
745
|
+
|
|
746
|
+
const suggestions = [ 'Yellow', 'Canary', 'Gold', 'Blonde' ];
|
|
747
|
+
|
|
748
|
+
render( <FormTokenFieldWithState suggestions={ suggestions } /> );
|
|
749
|
+
|
|
750
|
+
const input = screen.getByRole( 'combobox' );
|
|
751
|
+
|
|
752
|
+
// Despite 'l' matches some suggestions, the search text needs to be
|
|
753
|
+
// at least 2 characters
|
|
754
|
+
await user.type( input, ' l ' );
|
|
755
|
+
|
|
756
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
757
|
+
|
|
758
|
+
// The trimmed search text is now 2 characters long (`lo`), which is
|
|
759
|
+
// enough to show matching suggestions ('Yellow' and 'Blonde')
|
|
760
|
+
await user.type( input, '[ArrowLeft][ArrowLeft][ArrowLeft]o' );
|
|
761
|
+
|
|
762
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
763
|
+
'Yellow',
|
|
764
|
+
'Blonde',
|
|
765
|
+
] );
|
|
766
|
+
} );
|
|
767
|
+
|
|
768
|
+
it( 'should not render a matching suggestion if a token with the same value has already been added', async () => {
|
|
769
|
+
const user = userEvent.setup( {
|
|
770
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
771
|
+
} );
|
|
772
|
+
|
|
773
|
+
const suggestions = [ 'Green', 'Emerald', 'Seaweed' ];
|
|
774
|
+
|
|
775
|
+
render(
|
|
776
|
+
<FormTokenFieldWithState
|
|
777
|
+
suggestions={ suggestions }
|
|
778
|
+
initialValue={ [ 'Green' ] }
|
|
779
|
+
/>
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const input = screen.getByRole( 'combobox' );
|
|
783
|
+
|
|
784
|
+
// Despite 'ee' matches both the "Green" and "Seaweed", "Green" won't be
|
|
785
|
+
// displayed because there's already a token with the same value
|
|
786
|
+
await user.type( input, 'ee' );
|
|
787
|
+
|
|
788
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
789
|
+
'Seaweed',
|
|
790
|
+
] );
|
|
791
|
+
} );
|
|
792
|
+
|
|
793
|
+
it( 'should allow the user to use the keyboard to navigate and select suggestions (which are marked with the `aria-selected` attribute)', async () => {
|
|
794
|
+
const user = userEvent.setup( {
|
|
795
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
796
|
+
} );
|
|
797
|
+
|
|
798
|
+
const onChangeSpy = jest.fn();
|
|
799
|
+
|
|
800
|
+
const suggestions = [
|
|
801
|
+
'Pink',
|
|
802
|
+
'Salmon',
|
|
803
|
+
'Flamingo',
|
|
804
|
+
'Carnation',
|
|
805
|
+
'Neon',
|
|
806
|
+
];
|
|
807
|
+
|
|
808
|
+
render(
|
|
809
|
+
<FormTokenFieldWithState
|
|
810
|
+
onChange={ onChangeSpy }
|
|
811
|
+
suggestions={ suggestions }
|
|
812
|
+
/>
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const input = screen.getByRole( 'combobox' );
|
|
816
|
+
|
|
817
|
+
// Typing "on" will show the "Salmon", "Carnation" and "Neon" suggestions
|
|
818
|
+
await user.type( input, 'on' );
|
|
819
|
+
|
|
820
|
+
const suggestionList = screen.getByRole( 'listbox' );
|
|
821
|
+
|
|
822
|
+
expectVisibleSuggestionsToBe( suggestionList, [
|
|
823
|
+
'Salmon',
|
|
824
|
+
'Carnation',
|
|
825
|
+
'Neon',
|
|
826
|
+
] );
|
|
827
|
+
|
|
828
|
+
// Currently, none of the suggestions are selected
|
|
829
|
+
expect(
|
|
830
|
+
within( suggestionList ).queryAllByRole( 'option', {
|
|
831
|
+
selected: true,
|
|
832
|
+
} )
|
|
833
|
+
).toHaveLength( 0 );
|
|
834
|
+
|
|
835
|
+
// Pressing the down arrow will select "Salmon"
|
|
836
|
+
await user.keyboard( '[ArrowDown]' );
|
|
837
|
+
|
|
838
|
+
expect(
|
|
839
|
+
within( suggestionList ).getByRole( 'option', {
|
|
840
|
+
selected: true,
|
|
841
|
+
} )
|
|
842
|
+
).toHaveAccessibleName( 'Salmon' );
|
|
843
|
+
|
|
844
|
+
// Pressing the up arrow will select "Neon" (the selection wraps around
|
|
845
|
+
// the list)
|
|
846
|
+
await user.keyboard( '[ArrowUp]' );
|
|
847
|
+
|
|
848
|
+
expect(
|
|
849
|
+
within( suggestionList ).getByRole( 'option', {
|
|
850
|
+
selected: true,
|
|
851
|
+
} )
|
|
852
|
+
).toHaveAccessibleName( 'Neon' );
|
|
853
|
+
|
|
854
|
+
// Pressing the down arrow twice will select "Carnation" (the selection
|
|
855
|
+
// wraps around the list)
|
|
856
|
+
await user.keyboard( '[ArrowDown][ArrowDown]' );
|
|
857
|
+
|
|
858
|
+
expect(
|
|
859
|
+
within( suggestionList ).getByRole( 'option', {
|
|
860
|
+
selected: true,
|
|
861
|
+
} )
|
|
862
|
+
).toHaveAccessibleName( 'Carnation' );
|
|
863
|
+
|
|
864
|
+
// Pressing enter will add "Carnation" as a token and close the suggestion list
|
|
865
|
+
await user.keyboard( '[Enter]' );
|
|
866
|
+
|
|
867
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
868
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'Carnation' ] );
|
|
869
|
+
expectTokensToBeInTheDocument( [ 'Carnation' ] );
|
|
870
|
+
|
|
871
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
872
|
+
} );
|
|
873
|
+
|
|
874
|
+
it( 'should allow the user to use the mouse to navigate and select suggestions (which are marked with the `aria-selected` attribute)', async () => {
|
|
875
|
+
const user = userEvent.setup( {
|
|
876
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
877
|
+
} );
|
|
878
|
+
|
|
879
|
+
const onChangeSpy = jest.fn();
|
|
880
|
+
|
|
881
|
+
const suggestions = [ 'Tiger', 'Tangerine', 'Orange' ];
|
|
882
|
+
|
|
883
|
+
render(
|
|
884
|
+
<FormTokenFieldWithState
|
|
885
|
+
onChange={ onChangeSpy }
|
|
886
|
+
suggestions={ suggestions }
|
|
887
|
+
/>
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const input = screen.getByRole( 'combobox' );
|
|
891
|
+
|
|
892
|
+
// Typing "er" will show the "Tiger" and "Tangerine" suggestions
|
|
893
|
+
await user.type( input, 'er' );
|
|
894
|
+
|
|
895
|
+
const suggestionList = screen.getByRole( 'listbox' );
|
|
896
|
+
expectVisibleSuggestionsToBe( suggestionList, [
|
|
897
|
+
'Tiger',
|
|
898
|
+
'Tangerine',
|
|
899
|
+
] );
|
|
900
|
+
|
|
901
|
+
// Currently, none of the suggestions are selected
|
|
902
|
+
expect(
|
|
903
|
+
within( suggestionList ).queryAllByRole( 'option', {
|
|
904
|
+
selected: true,
|
|
905
|
+
} )
|
|
906
|
+
).toHaveLength( 0 );
|
|
907
|
+
|
|
908
|
+
const tigerOption = within( suggestionList ).getByRole( 'option', {
|
|
909
|
+
name: 'Tiger',
|
|
910
|
+
} );
|
|
911
|
+
const tangerineOption = within( suggestionList ).getByRole(
|
|
912
|
+
'option',
|
|
913
|
+
{
|
|
914
|
+
name: 'Tangerine',
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
// Hovering over each option will mark it as selected (via the
|
|
919
|
+
// `aria-selected` attribute)
|
|
920
|
+
await user.hover( tigerOption );
|
|
921
|
+
|
|
922
|
+
expect( tigerOption ).toHaveAttribute( 'aria-selected', 'true' );
|
|
923
|
+
expect( tangerineOption ).toHaveAttribute(
|
|
924
|
+
'aria-selected',
|
|
925
|
+
'false'
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
await user.hover( tangerineOption );
|
|
929
|
+
|
|
930
|
+
expect( tigerOption ).toHaveAttribute( 'aria-selected', 'false' );
|
|
931
|
+
expect( tangerineOption ).toHaveAttribute(
|
|
932
|
+
'aria-selected',
|
|
933
|
+
'true'
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
// Clicking an option will add it as a token and close the list
|
|
937
|
+
await user.click( tangerineOption );
|
|
938
|
+
|
|
939
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
940
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'Tangerine' ] );
|
|
941
|
+
expectTokensToBeInTheDocument( [ 'Tangerine' ] );
|
|
942
|
+
|
|
943
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
944
|
+
} );
|
|
945
|
+
|
|
946
|
+
it( 'should hide the suggestion list when the Escape key is pressed', async () => {
|
|
947
|
+
const user = userEvent.setup( {
|
|
948
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
949
|
+
} );
|
|
950
|
+
|
|
951
|
+
const onChangeSpy = jest.fn();
|
|
952
|
+
|
|
953
|
+
const suggestions = [ 'Black', 'Ash', 'Onyx', 'Ebony' ];
|
|
954
|
+
|
|
955
|
+
render(
|
|
956
|
+
<FormTokenFieldWithState
|
|
957
|
+
onChange={ onChangeSpy }
|
|
958
|
+
suggestions={ suggestions }
|
|
959
|
+
/>
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
const input = screen.getByRole( 'combobox' );
|
|
963
|
+
|
|
964
|
+
// Typing "ony" will show the "Onyx" and "Ebony" suggestions
|
|
965
|
+
await user.type( input, 'ony' );
|
|
966
|
+
|
|
967
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
968
|
+
'Onyx',
|
|
969
|
+
'Ebony',
|
|
970
|
+
] );
|
|
971
|
+
|
|
972
|
+
expect( screen.getByRole( 'listbox' ) ).toBeVisible();
|
|
973
|
+
|
|
974
|
+
// Pressing the ESC key will close the suggestion list
|
|
975
|
+
await user.keyboard( '[Escape]' );
|
|
976
|
+
|
|
977
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
978
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
979
|
+
} );
|
|
980
|
+
|
|
981
|
+
it( 'matches the search text with the suggestions in a case-insensitive way', async () => {
|
|
982
|
+
const user = userEvent.setup( {
|
|
983
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
984
|
+
} );
|
|
985
|
+
|
|
986
|
+
const suggestions = [ 'Cinnamon', 'Tawny', 'Mocha' ];
|
|
987
|
+
|
|
988
|
+
render( <FormTokenFieldWithState suggestions={ suggestions } /> );
|
|
989
|
+
|
|
990
|
+
const input = screen.getByRole( 'combobox' );
|
|
991
|
+
|
|
992
|
+
// Because text-matching is case-insensitive, "mo" matches both
|
|
993
|
+
// "Mocha" and "Cinnamon"
|
|
994
|
+
await user.type( input, 'mo' );
|
|
995
|
+
|
|
996
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
997
|
+
'Mocha',
|
|
998
|
+
'Cinnamon',
|
|
999
|
+
] );
|
|
1000
|
+
} );
|
|
1001
|
+
|
|
1002
|
+
it( 'should show, at most, a number of suggestions equals to the value of the `maxSuggestions` prop', async () => {
|
|
1003
|
+
const user = userEvent.setup( {
|
|
1004
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1005
|
+
} );
|
|
1006
|
+
|
|
1007
|
+
const suggestions = [
|
|
1008
|
+
'Ablaze',
|
|
1009
|
+
'Ability',
|
|
1010
|
+
'Abandon',
|
|
1011
|
+
'Abdomen',
|
|
1012
|
+
'Abdicate',
|
|
1013
|
+
'Abortive',
|
|
1014
|
+
'Abundance',
|
|
1015
|
+
'Abashedly',
|
|
1016
|
+
'Abominable',
|
|
1017
|
+
'Absolutely',
|
|
1018
|
+
'Absorption',
|
|
1019
|
+
'Abnormality',
|
|
1020
|
+
];
|
|
1021
|
+
|
|
1022
|
+
const { rerender } = render(
|
|
1023
|
+
<FormTokenFieldWithState suggestions={ suggestions } />
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
const input = screen.getByRole( 'combobox' );
|
|
1027
|
+
|
|
1028
|
+
// Because text-matching is case-insensitive, "Ab" matches all suggestions
|
|
1029
|
+
await user.type( input, 'Ab' );
|
|
1030
|
+
|
|
1031
|
+
// By default, `maxSuggestions` has a value of 100, which means that
|
|
1032
|
+
// all matching suggestions will be shown.
|
|
1033
|
+
expectVisibleSuggestionsToBe(
|
|
1034
|
+
screen.getByRole( 'listbox' ),
|
|
1035
|
+
suggestions
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
rerender(
|
|
1039
|
+
<FormTokenFieldWithState
|
|
1040
|
+
suggestions={ suggestions }
|
|
1041
|
+
maxSuggestions={ 3 }
|
|
1042
|
+
/>
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
// Only the first 3 suggestions are shown, as per the `maxSuggestions` prop
|
|
1046
|
+
expectVisibleSuggestionsToBe(
|
|
1047
|
+
screen.getByRole( 'listbox' ),
|
|
1048
|
+
suggestions.slice( 0, 3 )
|
|
1049
|
+
);
|
|
1050
|
+
} );
|
|
1051
|
+
|
|
1052
|
+
it( 'should match the search text against the unescaped values of suggestions with special characters (including spaces)', async () => {
|
|
1053
|
+
const user = userEvent.setup( {
|
|
1054
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1055
|
+
} );
|
|
1056
|
+
|
|
1057
|
+
render(
|
|
1058
|
+
<FormTokenFieldWithState
|
|
1059
|
+
displayTransform={ unescapeAndFormatSpaces }
|
|
1060
|
+
suggestions={ [
|
|
1061
|
+
'<3',
|
|
1062
|
+
'Stuff & Things',
|
|
1063
|
+
'Tags & Stuff',
|
|
1064
|
+
'Tags & Stuff 2',
|
|
1065
|
+
] }
|
|
1066
|
+
/>
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
const input = screen.getByRole( 'combobox' );
|
|
1070
|
+
|
|
1071
|
+
// Should match against the escaped value
|
|
1072
|
+
await user.type( input, '& S' );
|
|
1073
|
+
|
|
1074
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1075
|
+
'Tags & Stuff',
|
|
1076
|
+
'Tags & Stuff 2',
|
|
1077
|
+
] );
|
|
1078
|
+
|
|
1079
|
+
// Should match against the escaped value
|
|
1080
|
+
await user.clear( input );
|
|
1081
|
+
await user.type( input, 's &' );
|
|
1082
|
+
|
|
1083
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1084
|
+
'Tags & Stuff',
|
|
1085
|
+
'Tags & Stuff 2',
|
|
1086
|
+
] );
|
|
1087
|
+
|
|
1088
|
+
// Should not match against the escaped value
|
|
1089
|
+
await user.clear( input );
|
|
1090
|
+
await user.type( input, 'amp' );
|
|
1091
|
+
|
|
1092
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
1093
|
+
} );
|
|
1094
|
+
|
|
1095
|
+
it( 'should re-render if suggestions change', async () => {
|
|
1096
|
+
const user = userEvent.setup( {
|
|
1097
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1098
|
+
} );
|
|
1099
|
+
|
|
1100
|
+
const suggestions = [ 'Aluminum', 'Silver', 'Bronze' ];
|
|
1101
|
+
|
|
1102
|
+
const { rerender } = render( <FormTokenFieldWithState /> );
|
|
1103
|
+
|
|
1104
|
+
// Type "sil", but there are no suggestions.
|
|
1105
|
+
await user.type( screen.getByRole( 'combobox' ), 'sil' );
|
|
1106
|
+
|
|
1107
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
1108
|
+
|
|
1109
|
+
// When suggestions change, the "sil" text is matched against the new
|
|
1110
|
+
// suggestions, which results in the "Silver" suggestion being shown.
|
|
1111
|
+
rerender( <FormTokenFieldWithState suggestions={ suggestions } /> );
|
|
1112
|
+
|
|
1113
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1114
|
+
'Silver',
|
|
1115
|
+
] );
|
|
1116
|
+
} );
|
|
1117
|
+
|
|
1118
|
+
it( 'should automatically select the first matching suggestions when the `__experimentalAutoSelectFirstMatch` prop is set to `true`', async () => {
|
|
1119
|
+
const user = userEvent.setup( {
|
|
1120
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1121
|
+
} );
|
|
1122
|
+
|
|
1123
|
+
const suggestions = [ 'Walnut', 'Hazelnut', 'Pecan' ];
|
|
1124
|
+
|
|
1125
|
+
const { rerender } = render(
|
|
1126
|
+
<FormTokenFieldWithState suggestions={ suggestions } />
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
const input = screen.getByRole( 'combobox' );
|
|
1130
|
+
|
|
1131
|
+
// Type "nut", which will match "Walnut" and "Hazelnut".
|
|
1132
|
+
await user.type( input, 'nut' );
|
|
1133
|
+
|
|
1134
|
+
const suggestionList = screen.getByRole( 'listbox' );
|
|
1135
|
+
|
|
1136
|
+
expectVisibleSuggestionsToBe( suggestionList, [
|
|
1137
|
+
'Walnut',
|
|
1138
|
+
'Hazelnut',
|
|
1139
|
+
] );
|
|
1140
|
+
|
|
1141
|
+
expect(
|
|
1142
|
+
within( suggestionList ).queryByRole( 'option', {
|
|
1143
|
+
selected: true,
|
|
1144
|
+
} )
|
|
1145
|
+
).not.toBeInTheDocument();
|
|
1146
|
+
|
|
1147
|
+
rerender(
|
|
1148
|
+
<FormTokenFieldWithState
|
|
1149
|
+
__experimentalAutoSelectFirstMatch
|
|
1150
|
+
suggestions={ suggestions }
|
|
1151
|
+
/>
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
expect(
|
|
1155
|
+
within( suggestionList ).getByRole( 'option', {
|
|
1156
|
+
selected: true,
|
|
1157
|
+
} )
|
|
1158
|
+
).toHaveAccessibleName( 'Walnut' );
|
|
1159
|
+
} );
|
|
1160
|
+
|
|
1161
|
+
it( 'should allow to render custom suggestion items via the `__experimentalRenderItem` prop', async () => {
|
|
1162
|
+
const user = userEvent.setup( {
|
|
1163
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1164
|
+
} );
|
|
1165
|
+
|
|
1166
|
+
const suggestions = [ 'Wood', 'Stone', 'Metal' ];
|
|
1167
|
+
|
|
1168
|
+
render(
|
|
1169
|
+
<FormTokenFieldWithState
|
|
1170
|
+
suggestions={ suggestions }
|
|
1171
|
+
__experimentalRenderItem={ ( { item } ) => (
|
|
1172
|
+
<>Suggestion: { item }</>
|
|
1173
|
+
) }
|
|
1174
|
+
/>
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
// Type "woo". Matching suggestion will be "Wood"
|
|
1178
|
+
await user.type( screen.getByRole( 'combobox' ), 'woo' );
|
|
1179
|
+
|
|
1180
|
+
// The `__experimentalRenderItem` only affects the rendered suggestion,
|
|
1181
|
+
// but doesn't change the underlying data `value`, nor the value
|
|
1182
|
+
// displayed in the added token.
|
|
1183
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1184
|
+
'Suggestion: Wood',
|
|
1185
|
+
] );
|
|
1186
|
+
|
|
1187
|
+
await user.keyboard( '[ArrowDown][Enter]' );
|
|
1188
|
+
|
|
1189
|
+
expectTokensToBeInTheDocument( [ 'Wood' ] );
|
|
1190
|
+
} );
|
|
1191
|
+
} );
|
|
1192
|
+
|
|
1193
|
+
describe( 'tokens as objects', () => {
|
|
1194
|
+
it( 'should accept tokens in their object format', async () => {
|
|
1195
|
+
const user = userEvent.setup( {
|
|
1196
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1197
|
+
} );
|
|
1198
|
+
|
|
1199
|
+
const onChangeSpy = jest.fn();
|
|
1200
|
+
|
|
1201
|
+
const { rerender } = render(
|
|
1202
|
+
<FormTokenFieldWithState
|
|
1203
|
+
onChange={ onChangeSpy }
|
|
1204
|
+
__experimentalExpandOnFocus
|
|
1205
|
+
initialValue={ [
|
|
1206
|
+
{ value: 'Italy' },
|
|
1207
|
+
{ value: 'Switzerland' },
|
|
1208
|
+
] }
|
|
1209
|
+
/>
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
expectTokensToBeInTheDocument( [ 'Italy', 'Switzerland' ] );
|
|
1213
|
+
|
|
1214
|
+
const input = screen.getByRole( 'combobox' );
|
|
1215
|
+
|
|
1216
|
+
await user.type( input, 'Italy[Enter]' );
|
|
1217
|
+
|
|
1218
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1219
|
+
|
|
1220
|
+
rerender(
|
|
1221
|
+
<FormTokenFieldWithState
|
|
1222
|
+
onChange={ onChangeSpy }
|
|
1223
|
+
__experimentalExpandOnFocus
|
|
1224
|
+
initialValue={ [
|
|
1225
|
+
{ value: 'Italy' },
|
|
1226
|
+
{ value: 'Switzerland' },
|
|
1227
|
+
] }
|
|
1228
|
+
suggestions={ [ 'Italy', 'Switzerland', 'Sweden' ] }
|
|
1229
|
+
/>
|
|
1230
|
+
);
|
|
1231
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1232
|
+
'Sweden',
|
|
1233
|
+
] );
|
|
1234
|
+
} );
|
|
1235
|
+
|
|
1236
|
+
it( 'should trigger mouse callbacks if the `onMouseEnter` and/or the `onMouseLeave` properties are set on a token data object', async () => {
|
|
1237
|
+
const user = userEvent.setup( {
|
|
1238
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1239
|
+
} );
|
|
1240
|
+
|
|
1241
|
+
const onMouseEnterSpy = jest.fn();
|
|
1242
|
+
const onMouseLeaveSpy = jest.fn();
|
|
1243
|
+
|
|
1244
|
+
render(
|
|
1245
|
+
<FormTokenFieldWithState
|
|
1246
|
+
initialValue={ [
|
|
1247
|
+
{
|
|
1248
|
+
value: 'Germany',
|
|
1249
|
+
onMouseEnter: onMouseEnterSpy,
|
|
1250
|
+
onMouseLeave: onMouseLeaveSpy,
|
|
1251
|
+
},
|
|
1252
|
+
{ value: 'Liechtenstein' },
|
|
1253
|
+
{ value: 'Austria' },
|
|
1254
|
+
] }
|
|
1255
|
+
/>
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
// Move mouse over the 'Germany' token, then over 'Austria', then over
|
|
1259
|
+
// 'Liechtenstein'. The mouse-related callbacks should fire only for
|
|
1260
|
+
// the 'Germany' token, since they are not defined for other tokens.
|
|
1261
|
+
await user.hover( screen.getByText( 'Germany', { exact: true } ) );
|
|
1262
|
+
|
|
1263
|
+
expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
|
|
1264
|
+
expect( onMouseLeaveSpy ).not.toHaveBeenCalled();
|
|
1265
|
+
|
|
1266
|
+
await user.hover( screen.getByText( 'Austria', { exact: true } ) );
|
|
1267
|
+
|
|
1268
|
+
expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
|
|
1269
|
+
expect( onMouseLeaveSpy ).toHaveBeenCalledTimes( 1 );
|
|
1270
|
+
|
|
1271
|
+
await user.hover(
|
|
1272
|
+
screen.getByText( 'Liechtenstein', { exact: true } )
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
expect( onMouseEnterSpy ).toHaveBeenCalledTimes( 1 );
|
|
1276
|
+
expect( onMouseLeaveSpy ).toHaveBeenCalledTimes( 1 );
|
|
1277
|
+
} );
|
|
1278
|
+
|
|
1279
|
+
it( 'should add an accessible `title` to a token when specified', () => {
|
|
1280
|
+
render(
|
|
1281
|
+
<FormTokenFieldWithState
|
|
1282
|
+
initialValue={ [
|
|
1283
|
+
{ value: 'France' },
|
|
1284
|
+
{ value: 'Spain', title: 'España' },
|
|
1285
|
+
] }
|
|
1286
|
+
/>
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
expect( screen.queryByTitle( 'France' ) ).not.toBeInTheDocument();
|
|
1290
|
+
expect( screen.getByTitle( 'España' ) ).toBeVisible();
|
|
1291
|
+
} );
|
|
1292
|
+
|
|
1293
|
+
it( 'should be still used to filter out duplicate suggestions', () => {
|
|
1294
|
+
render(
|
|
1295
|
+
<FormTokenFieldWithState
|
|
1296
|
+
__experimentalExpandOnFocus
|
|
1297
|
+
initialValue={ [ { value: 'France' }, { value: 'Spain' } ] }
|
|
1298
|
+
/>
|
|
1299
|
+
);
|
|
1300
|
+
} );
|
|
1301
|
+
} );
|
|
1302
|
+
|
|
1303
|
+
describe( 'saveTransform', () => {
|
|
1304
|
+
it( "by default, it should trim the input's value from extra white spaces before attempting to add it as a token", async () => {
|
|
1305
|
+
const user = userEvent.setup( {
|
|
1306
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1307
|
+
} );
|
|
1308
|
+
|
|
1309
|
+
const onChangeSpy = jest.fn();
|
|
1310
|
+
|
|
1311
|
+
const { rerender } = render(
|
|
1312
|
+
<FormTokenFieldWithState
|
|
1313
|
+
initialValue={ [ 'potato' ] }
|
|
1314
|
+
onChange={ onChangeSpy }
|
|
1315
|
+
/>
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
const input = screen.getByRole( 'combobox' );
|
|
1319
|
+
|
|
1320
|
+
// Press enter on an empty input, no token gets added
|
|
1321
|
+
await user.type( input, '[Enter]' );
|
|
1322
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1323
|
+
expectTokensToBeInTheDocument( [ 'potato' ] );
|
|
1324
|
+
|
|
1325
|
+
// Add the "carrot" token - white space gets trimmed
|
|
1326
|
+
await user.type( input, ' carrot [Enter]' );
|
|
1327
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1328
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
1329
|
+
'potato',
|
|
1330
|
+
'carrot',
|
|
1331
|
+
] );
|
|
1332
|
+
expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
|
|
1333
|
+
|
|
1334
|
+
// Press enter on an input containing a duplicate token but surrounded by
|
|
1335
|
+
// white space, no token gets added
|
|
1336
|
+
await user.type( input, ' potato [Enter]' );
|
|
1337
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1338
|
+
expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
|
|
1339
|
+
|
|
1340
|
+
// Press enter on an input containing only spaces, no token gets added
|
|
1341
|
+
await user.type( input, ' [Enter]' );
|
|
1342
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1343
|
+
expectTokensToBeInTheDocument( [ 'potato', 'carrot' ] );
|
|
1344
|
+
|
|
1345
|
+
rerender(
|
|
1346
|
+
<FormTokenFieldWithState
|
|
1347
|
+
initialValue={ [ 'potato' ] }
|
|
1348
|
+
onChange={ onChangeSpy }
|
|
1349
|
+
saveTransform={ ( text: string ) => text }
|
|
1350
|
+
/>
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
// If a custom `saveTransform` function is passed, it will be the new
|
|
1354
|
+
// function's duty to trim the whitespace if necessary.
|
|
1355
|
+
await user.clear( input );
|
|
1356
|
+
await user.type( input, ' parnsnip [Enter]' );
|
|
1357
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
1358
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
1359
|
+
'potato',
|
|
1360
|
+
'carrot',
|
|
1361
|
+
' parnsnip ',
|
|
1362
|
+
] );
|
|
1363
|
+
expectTokensToBeInTheDocument( [
|
|
1364
|
+
'potato',
|
|
1365
|
+
'carrot',
|
|
1366
|
+
' parnsnip ',
|
|
1367
|
+
] );
|
|
1368
|
+
} );
|
|
1369
|
+
|
|
1370
|
+
it( "should allow to modify the input's value when saving it as a token", async () => {
|
|
1371
|
+
const user = userEvent.setup( {
|
|
1372
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1373
|
+
} );
|
|
1374
|
+
|
|
1375
|
+
const onChangeSpy = jest.fn();
|
|
1376
|
+
|
|
1377
|
+
const { rerender } = render(
|
|
1378
|
+
<FormTokenFieldWithState
|
|
1379
|
+
onFocus={ onChangeSpy }
|
|
1380
|
+
initialValue={ [ 'small trousers', 'small shirt' ] }
|
|
1381
|
+
/>
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
expectTokensToBeInTheDocument( [
|
|
1385
|
+
'small trousers',
|
|
1386
|
+
'small shirt',
|
|
1387
|
+
] );
|
|
1388
|
+
|
|
1389
|
+
rerender(
|
|
1390
|
+
<FormTokenFieldWithState
|
|
1391
|
+
onChange={ onChangeSpy }
|
|
1392
|
+
initialValue={ [ 'small trousers', 'small shirt' ] }
|
|
1393
|
+
saveTransform={ ( tokenText: string ) =>
|
|
1394
|
+
tokenText.replace( /small/g, 'medium' )
|
|
1395
|
+
}
|
|
1396
|
+
/>
|
|
1397
|
+
);
|
|
1398
|
+
|
|
1399
|
+
// The `saveTransform` prop doesn't apply to existing tokens.
|
|
1400
|
+
expectTokensToBeInTheDocument( [
|
|
1401
|
+
'small trousers',
|
|
1402
|
+
'small shirt',
|
|
1403
|
+
] );
|
|
1404
|
+
expectTokensNotToBeInTheDocument( [
|
|
1405
|
+
'medium trousers',
|
|
1406
|
+
'medium shirt',
|
|
1407
|
+
] );
|
|
1408
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1409
|
+
|
|
1410
|
+
const input = screen.getByRole( 'combobox' );
|
|
1411
|
+
|
|
1412
|
+
// Add 'small jacket' token by typing it and pressing enter to tokenize it.
|
|
1413
|
+
// The saveTransform function will change its value to "medium jacket"
|
|
1414
|
+
// when tokenizing it, thus affecting both the onChange callback and
|
|
1415
|
+
// the text rendered in the document.
|
|
1416
|
+
await user.type( input, 'small jacket[Enter]' );
|
|
1417
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1418
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
1419
|
+
'small trousers',
|
|
1420
|
+
'small shirt',
|
|
1421
|
+
'medium jacket',
|
|
1422
|
+
] );
|
|
1423
|
+
expectTokensToBeInTheDocument( [
|
|
1424
|
+
'small trousers',
|
|
1425
|
+
'small shirt',
|
|
1426
|
+
'medium jacket',
|
|
1427
|
+
] );
|
|
1428
|
+
expectTokensNotToBeInTheDocument( [
|
|
1429
|
+
'medium trousers',
|
|
1430
|
+
'medium shirt',
|
|
1431
|
+
'small jacket',
|
|
1432
|
+
] );
|
|
1433
|
+
} );
|
|
1434
|
+
|
|
1435
|
+
it( 'is applied to the search value when matching it against the list of suggestions', async () => {
|
|
1436
|
+
const user = userEvent.setup( {
|
|
1437
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1438
|
+
} );
|
|
1439
|
+
|
|
1440
|
+
const onChangeSpy = jest.fn();
|
|
1441
|
+
|
|
1442
|
+
const suggestions = [ 'Expensive food', 'Free food' ];
|
|
1443
|
+
|
|
1444
|
+
render(
|
|
1445
|
+
<FormTokenFieldWithState
|
|
1446
|
+
onChange={ onChangeSpy }
|
|
1447
|
+
suggestions={ suggestions }
|
|
1448
|
+
saveTransform={ ( text: string ) =>
|
|
1449
|
+
text.replace( /cheap/gi, 'free' )
|
|
1450
|
+
}
|
|
1451
|
+
/>
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
const input = screen.getByRole( 'combobox' );
|
|
1455
|
+
|
|
1456
|
+
// "cheap" matches the "Free food" option, since the `saveTransform`
|
|
1457
|
+
// function transform "cheap" to "free"
|
|
1458
|
+
await user.type( input, 'cheap' );
|
|
1459
|
+
|
|
1460
|
+
// But the value shown in the suggestion is still "Cheap food"
|
|
1461
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1462
|
+
'Free food',
|
|
1463
|
+
] );
|
|
1464
|
+
|
|
1465
|
+
// Selecting the suggestion will add the transformed value as a token,
|
|
1466
|
+
// since the `saveTransform` function will be applied before tokenizing.
|
|
1467
|
+
await user.keyboard( '[ArrowDown][Enter]' );
|
|
1468
|
+
|
|
1469
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1470
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Free food' ] );
|
|
1471
|
+
} );
|
|
1472
|
+
} );
|
|
1473
|
+
|
|
1474
|
+
describe( 'displayTransform', () => {
|
|
1475
|
+
it( 'should allow to modify the text rendered in the browser for each token', async () => {
|
|
1476
|
+
const user = userEvent.setup( {
|
|
1477
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1478
|
+
} );
|
|
1479
|
+
|
|
1480
|
+
const onChangeSpy = jest.fn();
|
|
1481
|
+
|
|
1482
|
+
const { rerender } = render(
|
|
1483
|
+
<FormTokenFieldWithState
|
|
1484
|
+
onChange={ onChangeSpy }
|
|
1485
|
+
initialValue={ [ 'dark blue', 'dark green' ] }
|
|
1486
|
+
/>
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
expectTokensToBeInTheDocument( [ 'dark blue', 'dark green' ] );
|
|
1490
|
+
|
|
1491
|
+
rerender(
|
|
1492
|
+
<FormTokenFieldWithState
|
|
1493
|
+
onChange={ onChangeSpy }
|
|
1494
|
+
initialValue={ [ 'dark blue', 'dark green' ] }
|
|
1495
|
+
displayTransform={ ( tokenText: string ) =>
|
|
1496
|
+
tokenText.replace( /dark/g, 'light' )
|
|
1497
|
+
}
|
|
1498
|
+
/>
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
// The `displayTransform` prop applies also to the displayed text
|
|
1502
|
+
// of existing tokens
|
|
1503
|
+
expectTokensToBeInTheDocument( [ 'light blue', 'light green' ] );
|
|
1504
|
+
expectTokensNotToBeInTheDocument( [ 'dark blue', 'dark green' ] );
|
|
1505
|
+
|
|
1506
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1507
|
+
|
|
1508
|
+
const input = screen.getByRole( 'combobox' );
|
|
1509
|
+
|
|
1510
|
+
// Add 'dark red' token by typing it and pressing enter to tokenize it.
|
|
1511
|
+
// The displayTransform function will change its displayed value to
|
|
1512
|
+
// "light red", but the onChange callback will still receive "dark red" as
|
|
1513
|
+
// part of the component's new value.
|
|
1514
|
+
await user.type( input, 'dark red[Enter]' );
|
|
1515
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1516
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [
|
|
1517
|
+
'dark blue',
|
|
1518
|
+
'dark green',
|
|
1519
|
+
'dark red',
|
|
1520
|
+
] );
|
|
1521
|
+
expectTokensToBeInTheDocument( [
|
|
1522
|
+
'light blue',
|
|
1523
|
+
'light green',
|
|
1524
|
+
'light red',
|
|
1525
|
+
] );
|
|
1526
|
+
expectTokensNotToBeInTheDocument( [
|
|
1527
|
+
'dark blue',
|
|
1528
|
+
'dark green',
|
|
1529
|
+
'dark red',
|
|
1530
|
+
] );
|
|
1531
|
+
} );
|
|
1532
|
+
|
|
1533
|
+
it( "is applied to each suggestions, but doesn't influence the matching against the search value", async () => {
|
|
1534
|
+
const user = userEvent.setup( {
|
|
1535
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1536
|
+
} );
|
|
1537
|
+
|
|
1538
|
+
const onChangeSpy = jest.fn();
|
|
1539
|
+
|
|
1540
|
+
const suggestions = [ 'Hot coffee', 'Hot tea' ];
|
|
1541
|
+
|
|
1542
|
+
render(
|
|
1543
|
+
<FormTokenFieldWithState
|
|
1544
|
+
onChange={ onChangeSpy }
|
|
1545
|
+
suggestions={ suggestions }
|
|
1546
|
+
displayTransform={ ( text: string ) =>
|
|
1547
|
+
text.replace( /hot/gi, 'cold' )
|
|
1548
|
+
}
|
|
1549
|
+
/>
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
const input = screen.getByRole( 'combobox' );
|
|
1553
|
+
|
|
1554
|
+
// The `displayTransform` function is only applied to the value
|
|
1555
|
+
// rendered in the DOM, while the data behind is not modified.
|
|
1556
|
+
await user.type( input, 'hot' );
|
|
1557
|
+
|
|
1558
|
+
expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [
|
|
1559
|
+
'cold coffee',
|
|
1560
|
+
'cold tea',
|
|
1561
|
+
] );
|
|
1562
|
+
|
|
1563
|
+
await user.keyboard( '[ArrowDown][Enter]' );
|
|
1564
|
+
|
|
1565
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1566
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [ 'Hot coffee' ] );
|
|
1567
|
+
} );
|
|
1568
|
+
|
|
1569
|
+
it( 'should allow to pass a function that renders tokens with escaped special characters correctly', async () => {
|
|
1570
|
+
render(
|
|
1571
|
+
<FormTokenFieldWithState
|
|
1572
|
+
initialValue={ [
|
|
1573
|
+
'a b',
|
|
1574
|
+
'i <3 tags',
|
|
1575
|
+
'1&2&3&4',
|
|
1576
|
+
] }
|
|
1577
|
+
displayTransform={ unescapeAndFormatSpaces }
|
|
1578
|
+
/>
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1581
|
+
// This is hacky, but it's a way we can check exactly the output HTML
|
|
1582
|
+
[
|
|
1583
|
+
'a b',
|
|
1584
|
+
'i <3 tags',
|
|
1585
|
+
'1&2&3&4',
|
|
1586
|
+
].forEach( ( tokenHtml ) => {
|
|
1587
|
+
screen.getByText( ( _, node: Element | null ) => {
|
|
1588
|
+
if ( node === null ) {
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
return node.innerHTML === tokenHtml;
|
|
1593
|
+
} );
|
|
1594
|
+
} );
|
|
1595
|
+
} );
|
|
1596
|
+
|
|
1597
|
+
it( 'should allow to pass a function that renders tokens with special characters correctly', async () => {
|
|
1598
|
+
// This test is not as realistic as the previous one: if a WP site
|
|
1599
|
+
// contains tag names with special characters, the API will always
|
|
1600
|
+
// return the tag names already escaped. However, this is still
|
|
1601
|
+
// worth testing, so we can be sure that token values with
|
|
1602
|
+
// dangerous characters in them don't have these characters carried
|
|
1603
|
+
// through unescaped to the HTML.
|
|
1604
|
+
render(
|
|
1605
|
+
<FormTokenFieldWithState
|
|
1606
|
+
initialValue={ [ 'a b', 'i <3 tags', '1&2&3&4' ] }
|
|
1607
|
+
displayTransform={ unescapeAndFormatSpaces }
|
|
1608
|
+
/>
|
|
1609
|
+
);
|
|
1610
|
+
|
|
1611
|
+
// This is hacky, but it's a way we can check exactly the output HTML
|
|
1612
|
+
[
|
|
1613
|
+
'a b',
|
|
1614
|
+
'i <3 tags',
|
|
1615
|
+
'1&2&3&4',
|
|
1616
|
+
].forEach( ( tokenHtml ) => {
|
|
1617
|
+
screen.getByText( ( _, node: Element | null ) => {
|
|
1618
|
+
if ( node === null ) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
return node.innerHTML === tokenHtml;
|
|
1623
|
+
} );
|
|
1624
|
+
} );
|
|
1625
|
+
} );
|
|
1626
|
+
} );
|
|
1627
|
+
|
|
1628
|
+
describe( 'validation', () => {
|
|
1629
|
+
it( 'should add a token only if it passes the validation set via `__experimentalValidateInput`', async () => {
|
|
1630
|
+
const user = userEvent.setup( {
|
|
1631
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1632
|
+
} );
|
|
1633
|
+
|
|
1634
|
+
const onChangeSpy = jest.fn();
|
|
1635
|
+
const startsWithCapitalLetter = ( tokenText: string ) =>
|
|
1636
|
+
/^[A-Z]/.test( tokenText );
|
|
1637
|
+
|
|
1638
|
+
const { rerender } = render(
|
|
1639
|
+
<FormTokenFieldWithState onChange={ onChangeSpy } />
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
const input = screen.getByRole( 'combobox' );
|
|
1643
|
+
|
|
1644
|
+
// Add 'cherry' token by typing it and pressing enter to tokenize it.
|
|
1645
|
+
await user.type( input, 'cherry[Enter]' );
|
|
1646
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1647
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'cherry' ] );
|
|
1648
|
+
expectTokensToBeInTheDocument( [ 'cherry' ] );
|
|
1649
|
+
|
|
1650
|
+
rerender(
|
|
1651
|
+
<FormTokenFieldWithState
|
|
1652
|
+
onChange={ onChangeSpy }
|
|
1653
|
+
__experimentalValidateInput={ startsWithCapitalLetter }
|
|
1654
|
+
/>
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
// Add 'cranberry' token by typing it and pressing enter to tokenize it.
|
|
1658
|
+
// The validation function won't allow the value from being tokenized.
|
|
1659
|
+
// Note that the any token added before is still around, even if it
|
|
1660
|
+
// wouldn't pass the newly added validation — this is because the
|
|
1661
|
+
// validation happens when the input\'s value gets tokenized.
|
|
1662
|
+
await user.type( input, 'cranberry[Enter]' );
|
|
1663
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1664
|
+
expectTokensToBeInTheDocument( [ 'cherry' ] );
|
|
1665
|
+
expectTokensNotToBeInTheDocument( [ 'cranberry' ] );
|
|
1666
|
+
|
|
1667
|
+
// Retry, this time with capital letter. The value should be added.
|
|
1668
|
+
await user.clear( input );
|
|
1669
|
+
await user.type( input, 'Cranberry[Enter]' );
|
|
1670
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
1671
|
+
expectTokensToBeInTheDocument( [ 'cherry', 'Cranberry' ] );
|
|
1672
|
+
} );
|
|
1673
|
+
} );
|
|
1674
|
+
|
|
1675
|
+
describe( 'maxLength', () => {
|
|
1676
|
+
it( 'should not allow adding new tokens beyond the value defined by the `maxLength` prop', async () => {
|
|
1677
|
+
const user = userEvent.setup( {
|
|
1678
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1679
|
+
} );
|
|
1680
|
+
|
|
1681
|
+
const onChangeSpy = jest.fn();
|
|
1682
|
+
|
|
1683
|
+
render(
|
|
1684
|
+
<FormTokenFieldWithState
|
|
1685
|
+
onChange={ onChangeSpy }
|
|
1686
|
+
initialValue={ [ 'square', 'triangle', 'circle' ] }
|
|
1687
|
+
maxLength={ 3 }
|
|
1688
|
+
/>
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
expectTokensToBeInTheDocument( [ 'square', 'triangle', 'circle' ] );
|
|
1692
|
+
|
|
1693
|
+
const input = screen.getByRole( 'combobox' );
|
|
1694
|
+
|
|
1695
|
+
// Try to add the 'hexagon' token, but because the number of tokens already
|
|
1696
|
+
// matches `maxLength`, the token won't be added.
|
|
1697
|
+
await user.type( input, 'hexagon[Enter]' );
|
|
1698
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 0 );
|
|
1699
|
+
expectTokensToBeInTheDocument( [ 'square', 'triangle', 'circle' ] );
|
|
1700
|
+
expectTokensNotToBeInTheDocument( [ 'hexagon' ] );
|
|
1701
|
+
|
|
1702
|
+
// Delete the last token ("circle"), in order to make space for the
|
|
1703
|
+
// hexagon token
|
|
1704
|
+
await user.clear( input );
|
|
1705
|
+
await user.keyboard( '[Backspace]' );
|
|
1706
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1707
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [
|
|
1708
|
+
'square',
|
|
1709
|
+
'triangle',
|
|
1710
|
+
] );
|
|
1711
|
+
expectTokensToBeInTheDocument( [ 'square', 'triangle' ] );
|
|
1712
|
+
expectTokensNotToBeInTheDocument( [ 'circle' ] );
|
|
1713
|
+
|
|
1714
|
+
// Try to add the 'hexagon' token again. This time, the token will be
|
|
1715
|
+
// added because the current number of tokens is below the `maxLength`
|
|
1716
|
+
// threshold.
|
|
1717
|
+
await user.type( input, 'hexagon[Enter]' );
|
|
1718
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 2 );
|
|
1719
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [
|
|
1720
|
+
'square',
|
|
1721
|
+
'triangle',
|
|
1722
|
+
'hexagon',
|
|
1723
|
+
] );
|
|
1724
|
+
expectTokensToBeInTheDocument( [
|
|
1725
|
+
'square',
|
|
1726
|
+
'triangle',
|
|
1727
|
+
'hexagon',
|
|
1728
|
+
] );
|
|
1729
|
+
} );
|
|
1730
|
+
|
|
1731
|
+
it( "should not affect the number of tokens set via the `value` prop (ie. not caused by tokenizing the user's input)", () => {
|
|
1732
|
+
render(
|
|
1733
|
+
<FormTokenFieldWithState
|
|
1734
|
+
initialValue={ [ 'rectangle', 'ellipse', 'pentagon' ] }
|
|
1735
|
+
maxLength={ 2 }
|
|
1736
|
+
/>
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
expectTokensToBeInTheDocument( [
|
|
1740
|
+
'rectangle',
|
|
1741
|
+
'ellipse',
|
|
1742
|
+
'pentagon',
|
|
1743
|
+
] );
|
|
1744
|
+
} );
|
|
1745
|
+
|
|
1746
|
+
it( 'should not affect tokens that were added before the limit was imposed', async () => {
|
|
1747
|
+
const user = userEvent.setup( {
|
|
1748
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1749
|
+
} );
|
|
1750
|
+
|
|
1751
|
+
const onChangeSpy = jest.fn();
|
|
1752
|
+
|
|
1753
|
+
const { rerender } = render(
|
|
1754
|
+
<FormTokenFieldWithState onChange={ onChangeSpy } />
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
const input = screen.getByRole( 'combobox' );
|
|
1758
|
+
|
|
1759
|
+
await user.type( input, 'cube[Enter]sphere[Enter]cylinder[Enter]' );
|
|
1760
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
|
|
1761
|
+
expect( onChangeSpy ).toHaveBeenLastCalledWith( [
|
|
1762
|
+
'cube',
|
|
1763
|
+
'sphere',
|
|
1764
|
+
'cylinder',
|
|
1765
|
+
] );
|
|
1766
|
+
expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
|
|
1767
|
+
|
|
1768
|
+
// Add a `maxLength` after some tokens have already been added.
|
|
1769
|
+
rerender(
|
|
1770
|
+
<FormTokenFieldWithState
|
|
1771
|
+
onChange={ onChangeSpy }
|
|
1772
|
+
maxLength={ 1 }
|
|
1773
|
+
/>
|
|
1774
|
+
);
|
|
1775
|
+
|
|
1776
|
+
// Changing `maxLength` doesn't affect existing tokens, even if their
|
|
1777
|
+
// number exceeds the new limit.
|
|
1778
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
|
|
1779
|
+
expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
|
|
1780
|
+
|
|
1781
|
+
// Try to add the 'pyramid' token, but because the number of tokens already
|
|
1782
|
+
// exceeds `maxLength`, the token won't be added.
|
|
1783
|
+
await user.type( input, 'pyramid[Enter]' );
|
|
1784
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 3 );
|
|
1785
|
+
expectTokensToBeInTheDocument( [ 'cube', 'sphere', 'cylinder' ] );
|
|
1786
|
+
expectTokensNotToBeInTheDocument( [ 'pyramid' ] );
|
|
1787
|
+
} );
|
|
1788
|
+
} );
|
|
1789
|
+
|
|
1790
|
+
describe( 'disabled', () => {
|
|
1791
|
+
it( 'should not allow adding tokens when the `disabled` prop is `true`', async () => {
|
|
1792
|
+
const user = userEvent.setup( {
|
|
1793
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1794
|
+
} );
|
|
1795
|
+
|
|
1796
|
+
const onChangeSpy = jest.fn();
|
|
1797
|
+
|
|
1798
|
+
const { rerender } = render(
|
|
1799
|
+
<FormTokenFieldWithState onChange={ onChangeSpy } />
|
|
1800
|
+
);
|
|
1801
|
+
|
|
1802
|
+
const input = screen.getByRole( 'combobox' );
|
|
1803
|
+
|
|
1804
|
+
// Add 'sun' token by typing it and pressing enter to tokenize it.
|
|
1805
|
+
await user.type( input, 'sun[Enter]' );
|
|
1806
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1807
|
+
expect( onChangeSpy ).toHaveBeenCalledWith( [ 'sun' ] );
|
|
1808
|
+
expectTokensToBeInTheDocument( [ 'sun' ] );
|
|
1809
|
+
|
|
1810
|
+
rerender(
|
|
1811
|
+
<FormTokenFieldWithState onChange={ onChangeSpy } disabled />
|
|
1812
|
+
);
|
|
1813
|
+
|
|
1814
|
+
// Try to add 'moon' token. The token is not added because of the `disabled`
|
|
1815
|
+
// prop.
|
|
1816
|
+
await user.type( input, 'moon[Enter]' );
|
|
1817
|
+
expect( onChangeSpy ).toHaveBeenCalledTimes( 1 );
|
|
1818
|
+
expectTokensNotToBeInTheDocument( [ 'moon' ] );
|
|
1819
|
+
} );
|
|
1820
|
+
|
|
1821
|
+
it( 'should not allow removing tokens when the `disable` prop is `true`', async () => {
|
|
1822
|
+
const user = userEvent.setup( {
|
|
1823
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1824
|
+
} );
|
|
1825
|
+
|
|
1826
|
+
const onChangeSpy = jest.fn();
|
|
1827
|
+
|
|
1828
|
+
render(
|
|
1829
|
+
<FormTokenFieldWithState
|
|
1830
|
+
onChange={ onChangeSpy }
|
|
1831
|
+
initialValue={ [ 'sea', 'ocean' ] }
|
|
1832
|
+
disabled
|
|
1833
|
+
/>
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
const input = screen.getByRole( 'combobox' );
|
|
1837
|
+
|
|
1838
|
+
// Try to delete the last token with the keyboard. The token won't be
|
|
1839
|
+
// deleted, because of the `disabled` prop.
|
|
1840
|
+
await user.type( input, '[Backspace]' );
|
|
1841
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1842
|
+
expectTokensToBeInTheDocument( [ 'sea', 'ocean' ] );
|
|
1843
|
+
|
|
1844
|
+
// Try to delete the last token with the mouse. The token won't be
|
|
1845
|
+
// deleted, because of the `disabled` prop.
|
|
1846
|
+
await user.click(
|
|
1847
|
+
screen.getAllByRole( 'button', { name: 'Remove item' } )[ 0 ]
|
|
1848
|
+
);
|
|
1849
|
+
expect( onChangeSpy ).not.toHaveBeenCalled();
|
|
1850
|
+
expectTokensToBeInTheDocument( [ 'sea', 'ocean' ] );
|
|
1851
|
+
} );
|
|
1852
|
+
} );
|
|
1853
|
+
|
|
1854
|
+
describe( 'messages', () => {
|
|
1855
|
+
const defaultMessages = {
|
|
1856
|
+
added: 'Item added.',
|
|
1857
|
+
removed: 'Item removed.',
|
|
1858
|
+
remove: 'Remove item',
|
|
1859
|
+
__experimentalInvalid: 'Invalid item',
|
|
1860
|
+
};
|
|
1861
|
+
const customMessages = {
|
|
1862
|
+
added: 'Test message for new item.',
|
|
1863
|
+
removed: 'Test message for item delete.',
|
|
1864
|
+
remove: 'Test label for item delete button.',
|
|
1865
|
+
__experimentalInvalid:
|
|
1866
|
+
'Test message for when an item fails validation.',
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
it( 'should announce to assistive technology the addition of a new token', async () => {
|
|
1870
|
+
const user = userEvent.setup( {
|
|
1871
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1872
|
+
} );
|
|
1873
|
+
|
|
1874
|
+
render( <FormTokenFieldWithState /> );
|
|
1875
|
+
|
|
1876
|
+
const input = screen.getByRole( 'combobox' );
|
|
1877
|
+
|
|
1878
|
+
// Add 'cat' token, check that the aria-live region has been updated.
|
|
1879
|
+
await user.type( input, 'cat[Enter]' );
|
|
1880
|
+
|
|
1881
|
+
expect( screen.getByText( defaultMessages.added ) ).toHaveAttribute(
|
|
1882
|
+
'aria-live',
|
|
1883
|
+
'assertive'
|
|
1884
|
+
);
|
|
1885
|
+
} );
|
|
1886
|
+
|
|
1887
|
+
it( 'should announce to assistive technology the addition of a new token with a custom message', async () => {
|
|
1888
|
+
const user = userEvent.setup( {
|
|
1889
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1890
|
+
} );
|
|
1891
|
+
|
|
1892
|
+
render( <FormTokenFieldWithState messages={ customMessages } /> );
|
|
1893
|
+
|
|
1894
|
+
const input = screen.getByRole( 'combobox' );
|
|
1895
|
+
|
|
1896
|
+
// Add 'dog' token, check that the aria-live region has been updated.
|
|
1897
|
+
await user.type( input, 'dog[Enter]' );
|
|
1898
|
+
|
|
1899
|
+
expect( screen.getByText( customMessages.added ) ).toHaveAttribute(
|
|
1900
|
+
'aria-live',
|
|
1901
|
+
'assertive'
|
|
1902
|
+
);
|
|
1903
|
+
} );
|
|
1904
|
+
|
|
1905
|
+
it( 'should announce to assistive technology the removal of a token', async () => {
|
|
1906
|
+
const user = userEvent.setup( {
|
|
1907
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1908
|
+
} );
|
|
1909
|
+
|
|
1910
|
+
render( <FormTokenFieldWithState initialValue={ [ 'horse' ] } /> );
|
|
1911
|
+
|
|
1912
|
+
const input = screen.getByRole( 'combobox' );
|
|
1913
|
+
|
|
1914
|
+
// Delete "horse" token
|
|
1915
|
+
await user.type( input, '[Backspace]' );
|
|
1916
|
+
|
|
1917
|
+
expect(
|
|
1918
|
+
screen.getByText( defaultMessages.removed )
|
|
1919
|
+
).toHaveAttribute( 'aria-live', 'assertive' );
|
|
1920
|
+
} );
|
|
1921
|
+
|
|
1922
|
+
it( 'should announce to assistive technology the removal of a token with a custom message', async () => {
|
|
1923
|
+
const user = userEvent.setup( {
|
|
1924
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1925
|
+
} );
|
|
1926
|
+
|
|
1927
|
+
render(
|
|
1928
|
+
<FormTokenFieldWithState
|
|
1929
|
+
initialValue={ [ 'donkey' ] }
|
|
1930
|
+
messages={ customMessages }
|
|
1931
|
+
/>
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
const input = screen.getByRole( 'combobox' );
|
|
1935
|
+
|
|
1936
|
+
// Delete "donkey" token
|
|
1937
|
+
await user.type( input, '[Backspace]' );
|
|
1938
|
+
|
|
1939
|
+
expect(
|
|
1940
|
+
screen.getByText( customMessages.removed )
|
|
1941
|
+
).toHaveAttribute( 'aria-live', 'assertive' );
|
|
1942
|
+
} );
|
|
1943
|
+
|
|
1944
|
+
it( 'should announce to assistive technology the failure of a potential token to pass validation', async () => {
|
|
1945
|
+
const user = userEvent.setup( {
|
|
1946
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1947
|
+
} );
|
|
1948
|
+
|
|
1949
|
+
render(
|
|
1950
|
+
<FormTokenFieldWithState
|
|
1951
|
+
__experimentalValidateInput={ () => false }
|
|
1952
|
+
/>
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
const input = screen.getByRole( 'combobox' );
|
|
1956
|
+
|
|
1957
|
+
// Try to add "eagle" token, which won't be added because of the
|
|
1958
|
+
// __experimentalValidateInput prop.
|
|
1959
|
+
await user.type( input, 'eagle[Enter]' );
|
|
1960
|
+
|
|
1961
|
+
expect(
|
|
1962
|
+
screen.getByText( defaultMessages.__experimentalInvalid )
|
|
1963
|
+
).toHaveAttribute( 'aria-live', 'assertive' );
|
|
1964
|
+
} );
|
|
1965
|
+
|
|
1966
|
+
it( 'should announce to assistive technology the failure of a potential token to pass validation with a custom message', async () => {
|
|
1967
|
+
const user = userEvent.setup( {
|
|
1968
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1969
|
+
} );
|
|
1970
|
+
|
|
1971
|
+
render(
|
|
1972
|
+
<FormTokenFieldWithState
|
|
1973
|
+
__experimentalValidateInput={ () => false }
|
|
1974
|
+
messages={ customMessages }
|
|
1975
|
+
/>
|
|
1976
|
+
);
|
|
1977
|
+
|
|
1978
|
+
const input = screen.getByRole( 'combobox' );
|
|
1979
|
+
|
|
1980
|
+
// Try to add "crocodile" token, which won't be added because of the
|
|
1981
|
+
// __experimentalValidateInput prop.
|
|
1982
|
+
await user.type( input, 'crocodile[Enter]' );
|
|
1983
|
+
|
|
1984
|
+
expect(
|
|
1985
|
+
screen.getByText( customMessages.__experimentalInvalid )
|
|
1986
|
+
).toHaveAttribute( 'aria-live', 'assertive' );
|
|
1987
|
+
} );
|
|
1988
|
+
|
|
1989
|
+
it( 'should announce to assistive technology the result of the matching of the search text against the list of suggestions', async () => {
|
|
1990
|
+
const user = userEvent.setup( {
|
|
1991
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
1992
|
+
} );
|
|
1993
|
+
|
|
1994
|
+
render(
|
|
1995
|
+
<FormTokenFieldWithState
|
|
1996
|
+
suggestions={ [ 'Donkey', 'Horse', 'Dog' ] }
|
|
1997
|
+
/>
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
const input = screen.getByRole( 'combobox' );
|
|
2001
|
+
|
|
2002
|
+
// No matching suggestions.
|
|
2003
|
+
await user.type( input, 'cat' );
|
|
2004
|
+
|
|
2005
|
+
await waitFor( () =>
|
|
2006
|
+
expect( screen.getByText( 'No results.' ) ).toHaveAttribute(
|
|
2007
|
+
'aria-live',
|
|
2008
|
+
'assertive'
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
|
|
2012
|
+
// "Donkey" and "Dog" matching
|
|
2013
|
+
await user.clear( input );
|
|
2014
|
+
await user.type( input, 'do' );
|
|
2015
|
+
|
|
2016
|
+
await waitFor( () =>
|
|
2017
|
+
expect(
|
|
2018
|
+
screen.getByText(
|
|
2019
|
+
'2 results found, use up and down arrow keys to navigate.'
|
|
2020
|
+
)
|
|
2021
|
+
).toHaveAttribute( 'aria-live', 'assertive' )
|
|
2022
|
+
);
|
|
2023
|
+
|
|
2024
|
+
// Only "Donkey" matches
|
|
2025
|
+
await user.type( input, 'nk' );
|
|
2026
|
+
|
|
2027
|
+
await waitFor( () =>
|
|
2028
|
+
expect(
|
|
2029
|
+
screen.getByText(
|
|
2030
|
+
'1 result found, use up and down arrow keys to navigate.'
|
|
2031
|
+
)
|
|
2032
|
+
).toHaveAttribute( 'aria-live', 'assertive' )
|
|
2033
|
+
);
|
|
2034
|
+
} );
|
|
2035
|
+
|
|
2036
|
+
it( 'should update the label for the "delete" button of a token', async () => {
|
|
2037
|
+
render(
|
|
2038
|
+
<FormTokenFieldWithState
|
|
2039
|
+
initialValue={ [ 'bear', 'panda' ] }
|
|
2040
|
+
messages={ customMessages }
|
|
2041
|
+
/>
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
expect(
|
|
2045
|
+
screen.getAllByRole( 'button', { name: customMessages.remove } )
|
|
2046
|
+
).toHaveLength( 2 );
|
|
2047
|
+
} );
|
|
2048
|
+
} );
|
|
2049
|
+
|
|
2050
|
+
// This section is definitely testing things in a non-user centric way,
|
|
2051
|
+
// but I wasn't sure if there was a better way.
|
|
2052
|
+
describe( 'aria attributes', () => {
|
|
2053
|
+
it( 'should add the correct aria attributes to the input as the user interacts with it', async () => {
|
|
2054
|
+
const user = userEvent.setup( {
|
|
2055
|
+
advanceTimers: jest.advanceTimersByTime,
|
|
2056
|
+
} );
|
|
2057
|
+
|
|
2058
|
+
const suggestions = [ 'Pine', 'Pistachio', 'Sage' ];
|
|
2059
|
+
|
|
2060
|
+
render( <FormTokenFieldWithState suggestions={ suggestions } /> );
|
|
2061
|
+
|
|
2062
|
+
// No suggestions visible
|
|
2063
|
+
const input = screen.getByRole( 'combobox' );
|
|
2064
|
+
|
|
2065
|
+
expect( input ).toHaveAttribute( 'autoComplete', 'off' );
|
|
2066
|
+
expect( input ).toHaveAttribute( 'aria-autocomplete', 'list' );
|
|
2067
|
+
expect( input ).toHaveAttribute( 'aria-expanded', 'false' );
|
|
2068
|
+
expect( input ).not.toHaveAttribute( 'aria-owns' );
|
|
2069
|
+
expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
|
|
2070
|
+
|
|
2071
|
+
// Typing "Pi" will show the "Pistachio" and "Pine" suggestions.
|
|
2072
|
+
await user.type( input, 'Pi' );
|
|
2073
|
+
|
|
2074
|
+
const suggestionList = screen.getByRole( 'listbox' );
|
|
2075
|
+
expect( suggestionList ).toBeVisible();
|
|
2076
|
+
|
|
2077
|
+
expect( input ).toHaveAttribute( 'aria-expanded', 'true' );
|
|
2078
|
+
expect( input ).toHaveAttribute( 'aria-owns', suggestionList.id );
|
|
2079
|
+
expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
|
|
2080
|
+
|
|
2081
|
+
// Select the "Pine" suggestion
|
|
2082
|
+
await user.click( input );
|
|
2083
|
+
await user.keyboard( '[ArrowDown]' );
|
|
2084
|
+
|
|
2085
|
+
const pineSuggestion = within( suggestionList ).getByRole(
|
|
2086
|
+
'option',
|
|
2087
|
+
{ name: 'Pine', selected: true }
|
|
2088
|
+
);
|
|
2089
|
+
expect( input ).toHaveAttribute( 'aria-expanded', 'true' );
|
|
2090
|
+
expect( input ).toHaveAttribute( 'aria-owns', suggestionList.id );
|
|
2091
|
+
expect( input ).toHaveAttribute(
|
|
2092
|
+
'aria-activedescendant',
|
|
2093
|
+
pineSuggestion.id
|
|
2094
|
+
);
|
|
2095
|
+
|
|
2096
|
+
// Add the suggestion, which hides the list
|
|
2097
|
+
await user.keyboard( '[Enter]' );
|
|
2098
|
+
|
|
2099
|
+
expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument();
|
|
2100
|
+
|
|
2101
|
+
expect( input ).toHaveAttribute( 'aria-expanded', 'false' );
|
|
2102
|
+
expect( input ).not.toHaveAttribute( 'aria-owns' );
|
|
2103
|
+
expect( input ).not.toHaveAttribute( 'aria-activedescendant' );
|
|
2104
|
+
} );
|
|
2105
|
+
} );
|
|
2106
|
+
} );
|