fix(components): [el-popper] append to body compatability (#5755)

* fix(components): [el-popper] append to body compatability

- Add a new hook for deprecation warning
- Add deprecation warnings for previous used `append-to-body` API
- Add test for the API.

* Address PR comments

* Update documentations for deprecated

* Fix formatting issue
This commit is contained in:
jeremywu
2022-02-04 14:59:58 +08:00
committed by GitHub
parent fb50b4ff8d
commit dbb00ef4b6
27 changed files with 652 additions and 199 deletions

View File

@@ -138,24 +138,25 @@ cascader/panel
## Cascader Attributes
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------------- | ------- |
| model-value / v-model | binding value | - | — | — |
| options | data of the optionsthe key of `value` and `label` can be customize by `Props`. | array | — | — |
| props | configuration options, see the following table. | object | — | — |
| size | size of input | string | large / default /small | — |
| placeholder | placeholder of input | string | — | Select |
| disabled | whether Cascader is disabled | boolean | — | false |
| clearable | whether selected value can be cleared | boolean | — | false |
| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
| separator | option label separator | string | — | ' / ' |
| filterable | whether the options can be searched | boolean | — | — |
| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
| popper-class | custom class name for Cascader's dropdown | string | — | — |
| popper-append-to-body | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | true |
| Attribute | Description | Type | Accepted Values | Default |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------------- | ------- |
| model-value / v-model | binding value | - | — | — |
| options | data of the optionsthe key of `value` and `label` can be customize by `Props`. | array | — | — |
| props | configuration options, see the following table. | object | — | — |
| size | size of input | string | large / default /small | — |
| placeholder | placeholder of input | string | — | Select |
| disabled | whether Cascader is disabled | boolean | — | false |
| clearable | whether selected value can be cleared | boolean | — | false |
| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
| separator | option label separator | string | — | ' / ' |
| filterable | whether the options can be searched | boolean | — | — |
| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
| popper-class | custom class name for Cascader's dropdown | string | — | — |
| teleported | whether cascader popup is teleported | boolean | true / false | true |
| popper-append-to-body(deprecated) | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | true |
## Cascader Events

View File

@@ -196,27 +196,28 @@ input/length-limiting
## Autocomplete Attributes
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------- | ------------ |
| placeholder | the placeholder of Autocomplete | string | — | — |
| clearable | whether to show clear button | boolean | — | false |
| disabled | whether Autocomplete is disabled | boolean | — | false |
| value-key | key name of the input suggestion object for display | string | — | value |
| icon | icon component | string / Component | — | — |
| model-value / v-model | binding value | string | — | — |
| debounce | debounce delay when typing, in milliseconds | number | — | 300 |
| placement | placement of the popup menu | string | top / top-start / top-end / bottom / bottom-start / bottom-end | bottom-start |
| fetch-suggestions | a method to fetch input suggestions. When suggestions are ready, invoke `callback(data:[])` to return them to Autocomplete | Function(queryString, callback) | — | — |
| popper-class | custom class name for autocomplete's dropdown | string | — | — |
| trigger-on-focus | whether show suggestions when input focus | boolean | — | true |
| name | same as `name` in native input | string | — | — |
| select-when-unmatched | whether to emit a `select` event on enter when there is no autocomplete match | boolean | — | false |
| label | label text | string | — | — |
| prefix-icon | prefix icon class | string | — | — |
| suffix-icon | suffix icon class | string | — | — |
| hide-loading | whether to hide the loading icon in remote search | boolean | — | false |
| popper-append-to-body | whether to append the dropdown to body. If the positioning of the dropdown is wrong, you can try to set this prop to false | boolean | - | false |
| highlight-first-item | whether to highlight first item in remote search suggestions by default | boolean | | false |
| Attribute | Description | Type | Accepted Values | Default |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------- | ------------ |
| placeholder | the placeholder of Autocomplete | string | — | — |
| clearable | whether to show clear button | boolean | — | false |
| disabled | whether Autocomplete is disabled | boolean | — | false |
| value-key | key name of the input suggestion object for display | string | — | value |
| icon | icon component | string / Component | — | — |
| model-value / v-model | binding value | string | — | — |
| debounce | debounce delay when typing, in milliseconds | number | — | 300 |
| placement | placement of the popup menu | string | top / top-start / top-end / bottom / bottom-start / bottom-end | bottom-start |
| fetch-suggestions | a method to fetch input suggestions. When suggestions are ready, invoke `callback(data:[])` to return them to Autocomplete | Function(queryString, callback) | — | — |
| popper-class | custom class name for autocomplete's dropdown | string | — | — |
| trigger-on-focus | whether show suggestions when input focus | boolean | — | true |
| name | same as `name` in native input | string | — | — |
| select-when-unmatched | whether to emit a `select` event on enter when there is no autocomplete match | boolean | — | false |
| label | label text | string | — | — |
| prefix-icon | prefix icon class | string | — | — |
| suffix-icon | suffix icon class | string | — | — |
| hide-loading | whether to hide the loading icon in remote search | boolean | — | false |
| popper-append-to-body(deprecated) | whether to append the dropdown to body. If the positioning of the dropdown is wrong, you can try to set this prop to false | boolean | - | false |
| teleported | whether select dropdown is teleported to the body | boolean | true / false | true |
| highlight-first-item | whether to highlight first item in remote search suggestions by default | boolean | — | false |
## Autocomplete Slots

View File

@@ -77,14 +77,14 @@ menu/collapse
## SubMenu Attributes
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- | ------- | --------------- | ----------------------------------------------- |
| index | unique identification | string | — | — |
| popper-class | custom class name for the popup menu | string | — | — |
| show-timeout | timeout before showing a sub-menu | number | — | 300 |
| hide-timeout | timeout before hiding a sub-menu | number | — | 300 |
| disabled | whether the sub-menu is disabled | boolean | — | false |
| popper-append-to-body | whether to append the popup menu to body. If the positioning of the menu is wrong, you can try setting this prop | boolean | - | level one SubMenu: true / other SubMenus: false |
| Attribute | Description | Type | Accepted Values | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------- | --------------- | ----------------------------------------------- |
| index | unique identification | string | — | — |
| popper-class | custom class name for the popup menu | string | — | — |
| show-timeout | timeout before showing a sub-menu | number | — | 300 |
| hide-timeout | timeout before hiding a sub-menu | number | — | 300 |
| disabled | whether the sub-menu is disabled | boolean | — | false |
| popper-append-to-body(deprecated) | whether to append the popup menu to body. If the positioning of the menu is wrong, you can try setting this prop | boolean | - | level one SubMenu: true / other SubMenus: false |
## SubMenu Slots

View File

@@ -124,33 +124,34 @@ select-v2/remote-search
## SelectV2 Attributes
| Param | Description | Type | Accepted Values | Default |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------- | ------------- |
| model-value / v-model | biding value | string / number / boolean / object | — | — |
| multiple | is multiple | boolean | — | false |
| disabled | is disabled | boolean | — | false |
| value-key | unique identity key name for value, required when value is an object | string | — | value |
| size | input box size | string | large/default/small | default |
| clearable | whether select can be cleared | boolean | — | false |
| clear-icon | custom clear icon | string / component | — | CircleClose |
| collapse-tags | whether to collapse tags to a text when multiple selecting | boolean | — | false |
| multiple-limit | maximum number of options user can select when multiple is true. No limit when set to 0 | number | — | 0 |
| name | the name attribute of select input | string | — | — |
| autocomplete | autocomplete of select input | string | — | off |
| placeholder | the autocomplete attribute of select input | string | — | Please select |
| filterable | is filterable | boolean | — | false |
| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | boolean | — | false |
| reserve-keyword | whether reserve the keyword after select filtered option. | boolean | — | true |
| no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data |
| popper-class | custom class name for Select's dropdown | string | — | — |
| popper-append-to-body | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | false |
| popper-options | Customized popper option see more at [popper.js](https://popper.js.org/documentation.html) | object | - | - |
| automatic-dropdown | for non-filterable Select, this prop decides if the option menu pops up when the input is focused | boolean | - | false |
| clear-icon | Customized clear icon component | string / Component | — | CircleClose |
| height | The height of the dropdown panel, 34px for each item | number | - | 170 |
| scrollbar-always-on | Controls whether the scrollbar is always displayed | boolean | - | false |
| remote | whether search data from server | boolean | | false |
| remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | function(keyword: string) | — | — |
| Param | Description | Type | Accepted Values | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------- | ------------- |
| model-value / v-model | biding value | string / number / boolean / object | — | — |
| multiple | is multiple | boolean | — | false |
| disabled | is disabled | boolean | — | false |
| value-key | unique identity key name for value, required when value is an object | string | — | value |
| size | input box size | string | large/default/small | default |
| clearable | whether select can be cleared | boolean | — | false |
| clear-icon | custom clear icon | string / component | — | CircleClose |
| collapse-tags | whether to collapse tags to a text when multiple selecting | boolean | — | false |
| multiple-limit | maximum number of options user can select when multiple is true. No limit when set to 0 | number | — | 0 |
| name | the name attribute of select input | string | — | — |
| autocomplete | autocomplete of select input | string | — | off |
| placeholder | the autocomplete attribute of select input | string | — | Please select |
| filterable | is filterable | boolean | — | false |
| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | boolean | — | false |
| reserve-keyword | whether reserve the keyword after select filtered option. | boolean | — | true |
| no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data |
| popper-class | custom class name for Select's dropdown | string | — | — |
| popper-append-to-body(deprecated) | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | false |
| teleported | whether select dropdown is teleported to the body | boolean | true / false | true |
| popper-options | Customized popper option see more at [popper.js](https://popper.js.org/documentation.html) | object | - | - |
| automatic-dropdown | for non-filterable Select, this prop decides if the option menu pops up when the input is focused | boolean | - | false |
| clear-icon | Customized clear icon component | string / Component | — | CircleClose |
| height | The height of the dropdown panel, 34px for each item | number | - | 170 |
| scrollbar-always-on | Controls whether the scrollbar is always displayed | boolean | - | false |
| remote | whether search data from server | boolean | — | false |
| remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | function(keyword: string) | — | — |
<span style="display: none;">
<!-- | default-first-option | 在输入框按下回车,选择第一个匹配项。需配合 `filterable``remote` 使用 | boolean | - | false |

View File

@@ -111,37 +111,38 @@ If the binding value of Select is an object, make sure to assign `value-key` as
## Select Attributes
| Attribute | Description | Type | Accepted Values | Default |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------- | ---------------- |
| model-value / v-model | binding value | string / number / boolean / object | — | — |
| multiple | whether multiple-select is activated | boolean | — | false |
| disabled | whether Select is disabled | boolean | — | false |
| value-key | unique identity key name for value, required when value is an object | string | — | value |
| size | size of Input | string | large/default/small | default |
| clearable | whether select can be cleared | boolean | — | false |
| collapse-tags | whether to collapse tags to a text when multiple selecting | boolean | — | false |
| multiple-limit | maximum number of options user can select when `multiple` is `true`. No limit when set to 0 | number | — | 0 |
| name | the name attribute of select input | string | — | — |
| autocomplete | the autocomplete attribute of select input | string | — | off |
| placeholder | placeholder | string | — | Select |
| filterable | whether Select is filterable | boolean | — | false |
| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | boolean | — | false |
| filter-method | custom filter method | function | — | — |
| remote | whether options are loaded from server | boolean | — | false |
| remote-method | custom remote search method | function | — | — |
| loading | whether Select is loading data from server | boolean | — | false |
| loading-text | displayed text while loading data from server | string | — | Loading |
| no-match-text | displayed text when no data matches the filtering query, you can also use slot `empty` | string | — | No matching data |
| no-data-text | displayed text when there is no options, you can also use slot `empty` | string | — | No data |
| popper-class | custom class name for Select's dropdown | string | — | — |
| reserve-keyword | when `multiple` and `filter` is true, whether to reserve current keyword after selecting an option | boolean | — | true |
| default-first-option | select first matching option on enter key. Use with `filterable` or `remote` | boolean | - | false |
| popper-append-to-body | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | true |
| automatic-dropdown | for non-filterable Select, this prop decides if the option menu pops up when the input is focused | boolean | - | false |
| clear-icon | Custom clear icon component | string / Component | — | CircleClose |
| fit-input-width | whether the width of the dropdown is the same as the input | boolean | — | false |
| suffix-icon | Custom suffix icon component | string / Component | — | ArrowUp |
| tag-type | tag type | string | success/info/warning/danger | info |
| Attribute | Description | Type | Accepted Values | Default |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------- | ---------------- |
| model-value / v-model | binding value | string / number / boolean / object | — | — |
| multiple | whether multiple-select is activated | boolean | — | false |
| disabled | whether Select is disabled | boolean | — | false |
| value-key | unique identity key name for value, required when value is an object | string | — | value |
| size | size of Input | string | large/default/small | default |
| clearable | whether select can be cleared | boolean | — | false |
| collapse-tags | whether to collapse tags to a text when multiple selecting | boolean | — | false |
| multiple-limit | maximum number of options user can select when `multiple` is `true`. No limit when set to 0 | number | — | 0 |
| name | the name attribute of select input | string | — | — |
| autocomplete | the autocomplete attribute of select input | string | — | off |
| placeholder | placeholder | string | — | Select |
| filterable | whether Select is filterable | boolean | — | false |
| allow-create | whether creating new items is allowed. To use this, `filterable` must be true | boolean | — | false |
| filter-method | custom filter method | function | — | — |
| remote | whether options are loaded from server | boolean | — | false |
| remote-method | custom remote search method | function | — | — |
| loading | whether Select is loading data from server | boolean | — | false |
| loading-text | displayed text while loading data from server | string | — | Loading |
| no-match-text | displayed text when no data matches the filtering query, you can also use slot `empty` | string | — | No matching data |
| no-data-text | displayed text when there is no options, you can also use slot `empty` | string | — | No data |
| popper-class | custom class name for Select's dropdown | string | — | — |
| reserve-keyword | when `multiple` and `filter` is true, whether to reserve current keyword after selecting an option | boolean | — | true |
| default-first-option | select first matching option on enter key. Use with `filterable` or `remote` | boolean | - | false |
| popper-append-to-body(deprecated) | whether to append the popper menu to body. If the positioning of the popper is wrong, you can try to set this prop to false | boolean | - | true |
| teleported | whether select dropdown is teleported to the body | boolean | true / false | true |
| automatic-dropdown | for non-filterable Select, this prop decides if the option menu pops up when the input is focused | boolean | - | false |
| clear-icon | Custom clear icon component | string / Component | — | CircleClose |
| fit-input-width | whether the width of the dropdown is the same as the input | boolean | — | false |
| suffix-icon | Custom suffix icon component | string / Component | | ArrowUp |
| tag-type | tag type | string | success/info/warning/danger | info |
## Select Events

View File

@@ -142,29 +142,32 @@ tooltip/animations
## Attributes
| Attribute | Description | Type | Accepted Values | Default |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| append-to-body | whether to append Dialog itself to body. A nested Dialog should have this attribute set to `true` | boolean | — | true |
| effect | Tooltip theme, built-in theme: `dark` / `light` | string | string | dark |
| content | display content, can be overridden by `slot#content` | String | — | |
| raw-content | whether `content` is treated as HTML string | boolean | — | false |
| placement | position of Tooltip | string | top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end | bottom |
| visible / v-model:visible | visibility of Tooltip | boolean | — | false |
| disabled | whether Tooltip is disabled | boolean | — | false |
| offset | offset of the Tooltip | number | — | 0 |
| transition | animation name | string | — | el-fade-in-linear |
| visible-arrow | whether an arrow is displayed. For more information, check [ElPopper](https://github.com/element-plus/element-plus/tree/dev/packages/components/popper) page | boolean | — | true |
| popper-options | [popper.js](https://popper.js.org/documentation.html) parameters | Object | refer to [popper.js](https://popper.js.org/documentation.html) doc | `{ boundariesElement: 'body', gpuAcceleration: false }` |
| show-after | delay of appearance, in millisecond | number | — | 0 |
| hide-after | delay of disappear, in millisecond | number | — | 0 |
| auto-close | timeout in milliseconds to hide tooltip | number | — | 0 |
| manual | whether to control Tooltip manually. `mouseenter` and `mouseleave` won't have effects if set to `true` | boolean | — | false |
| popper-class | custom class name for Tooltip's popper | string | — | |
| enterable | whether the mouse can enter the tooltip | Boolean | — | true |
| tabindex | [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of Tooltip | number | — | 0 |
| trigger | How should the tooltip be triggered (to show) | string | hover / click / focus / contextmenu | hover |
| virtual-triggering | Indicates whether virtual triggering is enabled | boolean | — | false |
| virtual-ref | Indicates the reference element to which the tooltip is attached | HTMLElement | — | |
| Attribute | Description | Type | Accepted Values | Default |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| append-to | which element the tooltip CONTENT appends to | CSSSelector \| HTMLElement | — | #el-popper-container-[randomValue] |
| append-to-body (deprecated) | indicates whether the tooltip contents will be append to the document.body element | boolean | true / false | true |
| effect | Tooltip theme, built-in theme: `dark` / `light` | string | string | dark |
| content | display content, can be overridden by `slot#content` | String | — | |
| raw-content | whether `content` is treated as HTML string | boolean | — | false |
| placement | position of Tooltip | string | top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end | bottom |
| visible / v-model:visible | visibility of Tooltip | boolean | — | false |
| disabled | whether Tooltip is disabled | boolean | — | false |
| offset | offset of the Tooltip | number | — | 0 |
| transition | animation name | string | — | el-fade-in-linear |
| visible-arrow (will deprecate in 2.1.0 ) | whether an arrow is displayed. For more information, check [ElPopper](https://github.com/element-plus/element-plus/tree/dev/packages/components/popper) page | boolean | — | true |
| popper-options | [popper.js](https://popper.js.org/documentation.html) parameters | Object | refer to [popper.js](https://popper.js.org/documentation.html) doc | `{ boundariesElement: 'body', gpuAcceleration: false }` |
| show-after | delay of appearance, in millisecond | number | — | 0 |
| show-arrow | whether the tooltip content has an arrow | boolean | true / false | true |
| hide-after | delay of disappear, in millisecond | number | — | 0 |
| auto-close | timeout in milliseconds to hide tooltip | number | — | 0 |
| manual | whether to control Tooltip manually. `mouseenter` and `mouseleave` won't have effects if set to `true` | boolean | — | false |
| popper-class | custom class name for Tooltip's popper | string | — | |
| enterable | whether the mouse can enter the tooltip | Boolean | — | true |
| tabindex | [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of Tooltip | number | — | 0 |
| teleported | whether tooltip content is teleported, if `true` it will be teleported to where `append-to` sets | boolean | true / false | true |
| trigger | How should the tooltip be triggered (to show) | string | hover / click / focus / contextmenu | hover |
| virtual-triggering | Indicates whether virtual triggering is enabled | boolean | — | false |
| virtual-ref | Indicates the reference element to which the tooltip is attached | HTMLElement | — | — |
## Slots

View File

@@ -1,7 +1,8 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { NOOP } from '@vue/shared'
import { sleep } from '@element-plus/test-utils'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Autocomplete from '../src/index.vue'
jest.unmock('lodash/debounce')
@@ -177,4 +178,28 @@ describe('Autocomplete.vue', () => {
await sleep(30)
expect(document.body.querySelector('.highlighted')).toBeDefined()
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount()
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount({
teleported: false,
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})

View File

@@ -5,7 +5,7 @@
:placement="placement"
:fallback-placements="['bottom-start', 'top-start']"
:popper-class="`${ns.e('popper')} ${popperClass}`"
:append-to-body="popperAppendToBody"
:teleported="compatTeleported"
:gpu-acceleration="false"
pure
manual-mode
@@ -99,15 +99,19 @@ import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { throwError } from '@element-plus/utils/error'
import ElInput from '@element-plus/components/input'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTooltip from '@element-plus/components/tooltip'
import ElTooltip, {
useTooltipContentProps,
} from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import ElIcon from '@element-plus/components/icon'
import { Loading } from '@element-plus/icons-vue'
import type { Placement } from '@element-plus/components/popper'
import type { PropType } from 'vue'
const COMPONENT_NAME = 'ElAutocomplete'
export default defineComponent({
name: 'ElAutocomplete',
name: COMPONENT_NAME,
components: {
ElTooltip,
ElInput,
@@ -170,8 +174,9 @@ export default defineComponent({
},
popperAppendToBody: {
type: Boolean,
default: true,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
highlightFirstItem: {
type: Boolean,
default: false,
@@ -188,6 +193,10 @@ export default defineComponent({
],
setup(props, ctx) {
const ns = useNamespace('autocomplete')
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
const attrs = useAttrs()
const suggestions = ref<any[]>([])
const highlightedIndex = ref(-1)
@@ -364,6 +373,9 @@ export default defineComponent({
suggestionVisible,
suggestionLoading,
// deprecation in 2.1.0
compatTeleported,
getData,
handleInput,
handleChange,

View File

@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { triggerEvent } from '@element-plus/test-utils'
import { ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Cascader from '../src/index.vue'
const OPTIONS = [
@@ -367,4 +368,55 @@ describe('Cascader.vue', () => {
await nextTick()
expect(wrapper.vm.value).toEqual(['zhejiang', 'hangzhou'])
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount({
template: `
<cascader
v-model="value"
:options="options"
filterable
/>
`,
data() {
return {
options: OPTIONS,
value: [],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount({
template: `
<cascader
v-model="value"
:options="options"
:teleported="false"
filterable
/>
`,
data() {
return {
options: OPTIONS,
value: [],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})

View File

@@ -2,7 +2,7 @@
<el-tooltip
ref="tooltipRef"
v-model:visible="popperVisible"
:append-to-body="popperAppendToBody"
:teleported="compatTeleported"
:popper-class="`el-cascader__dropdown ${popperClass}`"
:popper-options="popperOptions"
:fallback-placements="[
@@ -172,7 +172,10 @@ import ElCascaderPanel, {
CommonProps,
} from '@element-plus/components/cascader-panel'
import ElInput from '@element-plus/components/input'
import ElTooltip from '@element-plus/components/tooltip'
import ElTooltip, {
useTooltipContentProps,
} from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTag from '@element-plus/components/tag'
import ElIcon from '@element-plus/components/icon'
@@ -229,9 +232,9 @@ const popperOptions: Partial<Options> = {
},
],
}
const COMPONENT_NAME = 'ElCascader'
export default defineComponent({
name: 'ElCascader',
name: COMPONENT_NAME,
components: {
ElCascaderPanel,
@@ -291,8 +294,9 @@ export default defineComponent({
},
popperAppendToBody: {
type: Boolean,
default: true,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
},
emits: [
@@ -309,6 +313,10 @@ export default defineComponent({
let inputInitialHeight = 0
let pressDeleteCount = 0
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
const { t } = useLocale()
const elForm = inject(elFormKey, {} as ElFormContext)
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
@@ -692,6 +700,9 @@ export default defineComponent({
multiple,
readonly,
clearBtnVisible,
// deprecation in ver 2.1.0
compatTeleported,
t,
togglePopperVisible,
hideSuggestionPanel,

View File

@@ -1,6 +1,7 @@
import { h, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { rAF } from '@element-plus/test-utils/tick'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import ElPopconfirm from '../src/popconfirm.vue'
const AXIOM = 'rem is the best girl'
@@ -48,4 +49,26 @@ describe('Popconfirm.vue', () => {
document.querySelector(selector)!.getAttribute('style')
).not.toContain('display: none')
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount()
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
_mount({ teleported: false })
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR)!.innerHTML
).toBe('')
})
})
})

View File

@@ -1,50 +1,54 @@
import { buttonType } from '@element-plus/components/button'
import { QuestionFilled } from '@element-plus/icons-vue'
import { buildProps, definePropType } from '@element-plus/utils/props'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import type { Component, ExtractPropTypes } from 'vue'
export const popconfirmProps = buildProps({
title: {
type: String,
},
confirmButtonText: {
type: String,
},
cancelButtonText: {
type: String,
},
confirmButtonType: {
type: String,
values: buttonType,
default: 'primary',
},
cancelButtonType: {
type: String,
values: buttonType,
default: 'text',
},
icon: {
type: definePropType<string | Component>([String, Object]),
default: QuestionFilled as any,
},
iconColor: {
type: String,
default: '#f90',
},
hideIcon: {
type: Boolean,
default: false,
},
hideAfter: {
type: Number,
default: 200,
},
onConfirm: {
type: definePropType<(e: Event) => Promise<boolean> | boolean>(Function),
},
onCancel: {
type: definePropType<(e: Event) => Promise<boolean> | boolean>(Function),
},
} as const)
export const popconfirmProps = {
...buildProps({
title: {
type: String,
},
confirmButtonText: {
type: String,
},
cancelButtonText: {
type: String,
},
confirmButtonType: {
type: String,
values: buttonType,
default: 'primary',
},
cancelButtonType: {
type: String,
values: buttonType,
default: 'text',
},
icon: {
type: definePropType<string | Component>([String, Object]),
default: QuestionFilled as any,
},
iconColor: {
type: String,
default: '#f90',
},
hideIcon: {
type: Boolean,
default: false,
},
hideAfter: {
type: Number,
default: 200,
},
onConfirm: {
type: definePropType<(e: Event) => Promise<boolean> | boolean>(Function),
},
onCancel: {
type: definePropType<(e: Event) => Promise<boolean> | boolean>(Function),
},
} as const),
teleported: useTooltipContentProps.teleported,
}
export type PopconfirmProps = ExtractPropTypes<typeof popconfirmProps>

View File

@@ -5,7 +5,7 @@
trigger="click"
effect="light"
popper-class="el-popover"
append-to-body
:teleported="compatTeleported"
:fallback-placements="['bottom', 'top', 'right', 'left']"
:hide-after="hideAfter"
persistent
@@ -43,11 +43,13 @@ import { defineComponent, ref, computed, unref } from 'vue'
import ElButton from '@element-plus/components/button'
import ElIcon from '@element-plus/components/icon'
import ElTooltip from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import { useLocale } from '@element-plus/hooks'
import { popconfirmProps } from './popconfirm'
const COMPONENT_NAME = 'Elpopperconfirm'
export default defineComponent({
name: 'ElPopconfirm',
name: COMPONENT_NAME,
components: {
ElButton,
@@ -58,6 +60,10 @@ export default defineComponent({
props: popconfirmProps,
setup(props) {
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'appendToBody'
)
const { t } = useLocale()
const tooltipRef = ref<{ onClose: () => void }>()
@@ -90,6 +96,9 @@ export default defineComponent({
finalCancelButtonText,
tooltipRef,
// Deprecation in 2.1.0
compatTeleported,
confirm,
cancel,
}

View File

@@ -2,6 +2,7 @@ import { h, nextTick } from 'vue'
import { PopupManager } from '@element-plus/utils/popup-manager'
import makeMount from '@element-plus/test-utils/make-mount'
import { rAF } from '@element-plus/test-utils/tick'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import { ElPopperTrigger } from '@element-plus/components/popper'
import Popover from '../src/index.vue'
@@ -12,9 +13,7 @@ const mount = makeMount(Popover, {
default: () => AXIOM,
reference: () => h('button', 'click me'),
},
props: {
appendToBody: false,
},
props: {},
global: {
attachTo: document.body,
},
@@ -125,4 +124,28 @@ describe('Popover.vue', () => {
await rAF()
expect(wrapper.emitted()).toHaveProperty('after-leave')
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
mount()
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
mount({
props: { teleported: false },
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})

View File

@@ -7,6 +7,7 @@
:enterable="enterable"
:popper-class="kls"
:popper-style="style"
:teleported="compatTeleported"
persistent
@show="afterEnter"
@hide="afterLeave"
@@ -28,6 +29,7 @@
<script lang="ts">
import { defineComponent, computed, ref, unref } from 'vue'
import ElTooltip from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import { isString } from '@element-plus/utils/util'
import { usePopoverProps } from './popover'
@@ -35,10 +37,10 @@ import type { StyleValue } from 'vue'
const emits = ['update:visible', 'after-enter', 'after-leave']
const NAME = 'ElPopover'
const COMPONENT_NAME = 'ElPopover'
export default defineComponent({
name: NAME,
name: COMPONENT_NAME,
components: {
ElTooltip,
},
@@ -73,6 +75,11 @@ export default defineComponent({
]
})
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'appendToBody'
)
const hide = () => {
tooltipRef.value?.hide()
}
@@ -86,6 +93,7 @@ export default defineComponent({
}
return {
compatTeleported,
kls,
style,
tooltipRef,

View File

@@ -2,6 +2,7 @@ import { buildProps } from '@element-plus/utils/props'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
export const usePopoverProps = {
appendToBody: { type: Boolean, default: undefined },
content: useTooltipContentProps.content,
popperStyle: useTooltipContentProps.popperStyle,
popperClass: useTooltipContentProps.popperClass,
@@ -13,6 +14,7 @@ export const usePopoverProps = {
...useTooltipContentProps.effect,
default: 'light',
},
teleported: useTooltipContentProps.teleported,
...buildProps({
title: String,

View File

@@ -10,5 +10,6 @@ export default ElPopper
export * from './src/popper'
export * from './src/tokens'
export { useDeprecateAppendToBody } from './src/deprecation'
export type { Placement, Options } from '@popperjs/core'

View File

@@ -0,0 +1,28 @@
import { computed, getCurrentInstance } from 'vue'
import { useDeprecated } from '@element-plus/hooks'
import { isBool } from '@element-plus/utils/util'
export function useDeprecateAppendToBody(scope: string, from: string) {
const vm = getCurrentInstance()!
const compatTeleported = computed(() => {
return (
isBool(vm.props[from]) ? vm.props[from] : vm.props.teleported
) as boolean
})
useDeprecated(
{
scope,
from,
replacement: 'teleported',
version: '2.1.0',
ref: 'https://element-plus.org/en-US/component/tooltip.html#attributes',
},
computed(() => isBool(vm.props[from]))
)
return {
compatTeleported,
}
}

View File

@@ -5,6 +5,7 @@ import { makeMountFunc } from '@element-plus/test-utils/make-mount'
import { rAF } from '@element-plus/test-utils/tick'
import { CircleClose } from '@element-plus/icons-vue'
import { hasClass } from '@element-plus/utils/dom'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Select from '../src/select.vue'
jest.useFakeTimers()
@@ -97,6 +98,7 @@ const createSelect = (
:remote="remote"
:reserve-keyword="reserveKeyword"
:scrollbar-always-on="scrollbarAlwaysOn"
:teleported="teleported"
${
options.methods && options.methods.filterMethod
? `:filter-method="filterMethod"`
@@ -132,7 +134,6 @@ const createSelect = (
filterable: false,
reserveKeyword: false,
multipleLimit: 0,
popperAppendToBody: true,
placeholder: DEFAULT_PLACEHOLDER,
scrollbarAlwaysOn: false,
...(options.data && options.data()),
@@ -1363,4 +1364,85 @@ describe('Select', () => {
done()
})
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
createSelect({
data() {
return {
options: [
{
value: '选项1',
label:
'黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕',
},
{
value: '选项2',
label:
'双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶',
},
{
value: '选项3',
label: '蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
createSelect({
data() {
return {
teleported: false,
options: [
{
value: '选项1',
label:
'黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕黄金糕',
},
{
value: '选项2',
label:
'双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶双皮奶',
},
{
value: '选项3',
label: '蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
}
},
})
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})

View File

@@ -1,4 +1,5 @@
import { isValidComponentSize } from '@element-plus/utils/validators'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import { CircleClose } from '@element-plus/icons-vue'
import type { PropType, Component } from 'vue'
@@ -64,8 +65,9 @@ export const SelectProps = {
},
popperAppendToBody: {
type: Boolean,
default: true,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
popperClass: {
type: String,
default: '',

View File

@@ -11,7 +11,7 @@
<el-tooltip
ref="popper"
v-model:visible="dropdownMenuVisible"
:append-to-body="popperAppendToBody"
:teleported="compatTeleported"
:popper-class="`el-select-v2__popper ${popperClass}`"
:gpu-acceleration="false"
:stop-popper-mouse-event="false"

View File

@@ -18,7 +18,7 @@ import {
removeResizeListener,
} from '@element-plus/utils/resize-event'
import { getValueByPath } from '@element-plus/utils/util'
import { Effect } from '@element-plus/components/popper'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import { ArrowUp } from '@element-plus/icons-vue'
import { useAllowCreate } from './useAllowCreate'
@@ -38,11 +38,16 @@ const TAG_BASE_WIDTH = {
default: 42,
small: 33,
}
const COMPONENT_NAME = 'ElSelectV2'
const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
// inject
const { t } = useLocale()
const { form: elForm, formItem: elFormItem } = useFormItem()
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
const states = reactive({
inputValue: DEFAULT_INPUT_PLACEHOLDER,
@@ -782,8 +787,8 @@ const useSelect = (props: ExtractPropTypes<typeof SelectProps>, emit) => {
validateState,
validateIcon,
Effect,
// deprecations
compatTeleported,
// methods exports
debouncedOnInputChange,

View File

@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import { sleep } from '@element-plus/test-utils'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { CircleClose, ArrowUp, CaretTop } from '@element-plus/icons-vue'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Select from '../src/select.vue'
import Group from '../src/option-group.vue'
import Option from '../src/option.vue'
@@ -1612,4 +1613,66 @@ describe('Select', () => {
await testAfterSearch({ multiple: true, remote: true, remoteMethod })
})
})
describe('teleported API', () => {
it('should mount on popper container', async () => {
expect(document.body.innerHTML).toBe('')
wrapper = _mount(
`
<el-select v-model="modelValue" multiple>
<el-option
v-for="option in options"
:key="option.value"
:value="option.value"
:label="option.label"
>
</el-option>
</el-select>`,
() => ({
modelValue: [1],
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
],
})
)
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).not.toBe('')
})
it('should not mount on the popper container', async () => {
expect(document.body.innerHTML).toBe('')
wrapper = _mount(
`
<el-select v-model="modelValue" multiple :teleported="false">
<el-option
v-for="option in options"
:key="option.value"
:value="option.value"
:label="option.label"
>
</el-option>
</el-select>`,
() => ({
modelValue: [1],
options: [
{ label: 'Test 1', value: 1 },
{ label: 'Test 2', value: 2 },
{ label: 'Test 3', value: 3 },
{ label: 'Test 4', value: 4 },
],
})
)
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
).toBe('')
})
})
})

View File

@@ -9,7 +9,7 @@
ref="tooltipRef"
v-model:visible="dropMenuVisible"
placement="bottom-start"
:append-to-body="popperAppendToBody"
:teleported="compatTeleported"
:popper-class="`el-select__popper ${popperClass}`"
:fallback-placements="['bottom-start', 'top-start', 'right', 'left']"
effect="light"
@@ -224,10 +224,13 @@ import {
import { ClickOutside } from '@element-plus/directives'
import { useFocus, useLocale } from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import ElTooltip from '@element-plus/components/tooltip'
import ElTooltip, {
useTooltipContentProps,
} from '@element-plus/components/tooltip'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTag from '@element-plus/components/tag'
import ElIcon from '@element-plus/components/icon'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
import {
addResizeListener,
@@ -244,9 +247,10 @@ import type { PropType, Component } from 'vue'
import type { ComponentSize } from '@element-plus/utils/types'
import type { SelectContext } from './token'
const COMPONENT_NAME = 'ElSelect'
export default defineComponent({
name: 'ElSelect',
componentName: 'ElSelect',
name: COMPONENT_NAME,
componentName: COMPONENT_NAME,
components: {
ElInput,
ElSelectMenu,
@@ -308,8 +312,9 @@ export default defineComponent({
collapseTags: Boolean,
popperAppendToBody: {
type: Boolean,
default: true,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
clearIcon: {
type: [String, Object] as PropType<string | Component>,
default: CircleClose,
@@ -513,6 +518,11 @@ export default defineComponent({
return tooltipRef.value?.popperRef?.contentRef
})
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
return {
tagInMultiLine,
prefixWidth,
@@ -576,6 +586,7 @@ export default defineComponent({
wrapperKls,
selectTagsStyle,
compatTeleported,
}
},
})

View File

@@ -0,0 +1,50 @@
import { computed, defineComponent, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { debugWarn } from '@element-plus/utils/error'
import { useDeprecated } from '../use-deprecated'
jest.mock('@element-plus/utils/error', () => {
return {
debugWarn: jest.fn(),
}
})
const DummyComponent = defineComponent({
props: {
shouldWarn: Boolean,
},
setup(props) {
useDeprecated(
{
from: 'oldApi',
replacement: 'newApi',
scope: 'dummyComponent',
version: 'some version',
ref: '',
},
computed(() => props.shouldWarn)
)
},
template: `<div>Rem is the best girl</div>`,
})
describe('useDeprecated', () => {
beforeEach(() => {
;(debugWarn as jest.Mock).mockClear()
})
it('should warn when condition is true', async () => {
mount(DummyComponent, {
props: {
shouldWarn: true,
},
})
await nextTick()
expect(debugWarn).toHaveBeenCalled()
})
it('should not warn when condition is false', async () => {
mount(DummyComponent)
await nextTick()
expect(debugWarn).not.toHaveBeenCalled()
})
})

View File

@@ -1,5 +1,6 @@
export * from './use-attrs'
export * from './use-common-props'
export * from './use-deprecated'
export * from './use-draggable'
export * from './use-focus'
export * from './use-form-item'

View File

@@ -0,0 +1,34 @@
import { unref, watch } from 'vue'
import { debugWarn } from '@element-plus/utils/error'
import type { MaybeRef } from '@vueuse/core'
type DeprecationParam = {
from: string
replacement: string
scope: string
version: string
ref: string
}
export const useDeprecated = (
{ from, replacement, scope, version, ref }: DeprecationParam,
condition: MaybeRef<boolean>
) => {
watch(
() => unref(condition),
(val) => {
if (val) {
debugWarn(
scope,
`API ${from} is about to be deprecated in version ${version}, please use ${replacement} instead.
For more detail, please visit: ${ref}
`
)
}
},
{
immediate: true,
}
)
}