diff --git a/cypress/e2e/AnimationList/test-absolute-autoAdapt.cy.ts b/cypress/e2e/AnimationList/test-absolute-autoAdapt.cy.ts new file mode 100644 index 000000000..ad0757519 --- /dev/null +++ b/cypress/e2e/AnimationList/test-absolute-autoAdapt.cy.ts @@ -0,0 +1,59 @@ +it('下拉列表', () => { + cy.visit('/cn/components/Select?example=009-popupContainer-absolute-long') + + function getNearestPositionDom(target: HTMLElement): HTMLElement { + if (target) { + const { position } = getComputedStyle(target) + + if (position !== 'static') { + return target + } + + if (target.parentElement) { + return getNearestPositionDom(target.parentElement) + } + } + return document.body + } + + cy.get('#relative-app').as('App') + cy.get('#app-out-container').as('OutContainer') + cy.get('#app-root-container').as('Container') + + cy.viewport(1100, 500) + cy.scrollTo(0, 1000) + cy.get('#app-out-container').scrollTo(100, 150) + + cy.get('#app-root-container').as('Container') + + cy.get(`.select`).as('select') + cy.get(`@select`).click() + cy.get('@Container') + .find(`.so-select-options`) + .should('have.length', 1) + + cy.get(`.select`).then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get('@Container') + .find(`.so-select-options`) + .then($co => { + const coRect = $co[0].getBoundingClientRect() + // 3 为固定间距 + expect(Math.abs(coRect.top - rect.top - rect.height)).to.eq(3) + }) + }) + + cy.get('.so-select-options').then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get(`@select`).then($co => { + const coRect = $co[0].getBoundingClientRect() + expect(rect.width + coRect.left).to.be.greaterThan(1100) + }) + cy.get('@Container').then($container => { + const nearstPositionDom = getNearestPositionDom($container[0]) + const nearstPositionDomRect = nearstPositionDom.getBoundingClientRect() + const offset = 1100 - nearstPositionDomRect.left - nearstPositionDomRect.width + expect(getComputedStyle($el[0]).right).to.eq(`${offset * -1}px`) + }) + }) +}) diff --git a/cypress/e2e/AnimationList/test.absolute-relative.cy.ts b/cypress/e2e/AnimationList/test.absolute-relative.cy.ts new file mode 100644 index 000000000..69015cdaa --- /dev/null +++ b/cypress/e2e/AnimationList/test.absolute-relative.cy.ts @@ -0,0 +1,106 @@ +describe('Select[clickAway]', () => { + it('下拉列表', () => { + cy.visit('/cn/components/Select?example=008-popupContainer-absolute-relative') + + const components = [ + { + selector: 'select', + dropDown: 'so-select-options', + }, + { + selector: 'datepicker', + dropDown: 'so-datepicker-picker', + }, + { + selector: 'cascader', + dropDown: 'so-cascader-options', + }, + { + selector: 'treeselect', + dropDown: 'so-treeSelect-options', + }, + ] + + cy.get('#relative-app').as('App') + cy.get('#app-out-container').as('OutContainer') + cy.get('#app-root-container').as('Container') + + cy.get('#app-out-container').scrollTo(50, 50) + + components.forEach(async com => { + const { selector, dropDown } = com + + cy.get(`.${selector}`).as(selector) + cy.get(`@${selector}`).click() + cy.get('@Container') + .find(`.${dropDown}`) + .should('have.length', 1) + + cy.get(`.${selector}`).then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get('@Container') + .find(`.${dropDown}`) + .then($co => { + const coRect = $co[0].getBoundingClientRect() + // 3 为固定间距 + expect(Math.abs(coRect.top - rect.top - rect.height)).to.eq(3) + }) + cy.get('@Container').click({ force: true }) + }) + }) + }) + + it('上拉列表', () => { + cy.visit('/cn/components/Select?example=008-popupContainer-absolute-relative') + + const components = [ + { + selector: 'select', + dropDown: 'so-select-options', + }, + { + selector: 'datepicker', + dropDown: 'so-datepicker-picker', + }, + { + selector: 'cascader', + dropDown: 'so-cascader-options', + }, + { + selector: 'treeselect', + dropDown: 'so-treeSelect-options', + }, + ] + + cy.get('#relative-app').as('App') + cy.get('#app-out-container').as('OutContainer') + cy.get('#app-root-container').as('Container') + + cy.get('#app-out-container').scrollTo(50, 50) + + cy.get('#apis').invoke('css', { display: 'none' }) + cy.get('#api-Select').invoke('css', { display: 'none' }) + cy.get('.doc-api-table').invoke('css', { display: 'none' }) + + components.forEach(async com => { + const { selector, dropDown } = com + + cy.get(`.${selector}`).as(selector) + cy.get(`@${selector}`).click() + cy.get('@Container') + .find(`.${dropDown}`) + .should('have.length', 1) + + cy.get(`.${selector}`).then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get('@Container') + .find(`.${dropDown}`) + .then($co => { + const coRect = $co[0].getBoundingClientRect() + // 3 为固定间距 + expect(Math.abs(coRect.top - rect.top)).to.eq(coRect.height + 3) + }) + }) + }) + }) +}) diff --git a/cypress/e2e/AnimationList/test.absolute.cy.ts b/cypress/e2e/AnimationList/test.absolute.cy.ts new file mode 100644 index 000000000..c54d82076 --- /dev/null +++ b/cypress/e2e/AnimationList/test.absolute.cy.ts @@ -0,0 +1,105 @@ +describe('Select[clickAway]', () => { + it('下拉列表', () => { + cy.visit('/cn/components/Select?example=007-popupContainer-absolute') + + const components = [ + { + selector: 'select', + dropDown: 'so-select-options', + }, + { + selector: 'datepicker', + dropDown: 'so-datepicker-picker', + }, + { + selector: 'cascader', + dropDown: 'so-cascader-options', + }, + { + selector: 'treeselect', + dropDown: 'so-treeSelect-options', + }, + ] + + cy.get('#relative-app').as('App') + cy.get('#app-out-container').as('OutContainer') + cy.get('#app-root-container').as('Container') + + cy.get('#app-out-container').scrollTo(50, 50) + + components.forEach(async com => { + const { selector, dropDown } = com + + cy.get(`.${selector}`).as(selector) + cy.get(`@${selector}`).click() + cy.get('@Container') + .find(`.${dropDown}`) + .should('have.length', 1) + + cy.get(`.${selector}`).then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get('@Container') + .find(`.${dropDown}`) + .then($co => { + const coRect = $co[0].getBoundingClientRect() + // 3 为固定间距 + expect(Math.abs(coRect.top - rect.top - rect.height)).to.eq(3) + }) + }) + }) + }) + + it('上拉列表', () => { + cy.visit('/cn/components/Select?example=007-popupContainer-absolute') + + const components = [ + { + selector: 'select', + dropDown: 'so-select-options', + }, + { + selector: 'datepicker', + dropDown: 'so-datepicker-picker', + }, + { + selector: 'cascader', + dropDown: 'so-cascader-options', + }, + { + selector: 'treeselect', + dropDown: 'so-treeSelect-options', + }, + ] + + cy.get('#relative-app').as('App') + cy.get('#app-out-container').as('OutContainer') + cy.get('#app-root-container').as('Container') + + cy.get('#app-out-container').scrollTo(50, 50) + + cy.get('#apis').invoke('css', { display: 'none' }) + cy.get('#api-Select').invoke('css', { display: 'none' }) + cy.get('.doc-api-table').invoke('css', { display: 'none' }) + + components.forEach(async com => { + const { selector, dropDown } = com + + cy.get(`.${selector}`).as(selector) + cy.get(`@${selector}`).click() + cy.get('@Container') + .find(`.${dropDown}`) + .should('have.length', 1) + + cy.get(`.${selector}`).then($el => { + const rect = $el[0].getBoundingClientRect() + cy.get('@Container') + .find(`.${dropDown}`) + .then($co => { + const coRect = $co[0].getBoundingClientRect() + // 3 为固定间距 + expect(Math.abs(coRect.top - rect.top)).to.eq(coRect.height + 3) + }) + }) + }) + }) +}) diff --git a/site/chunks/Components/Select.js b/site/chunks/Components/Select.js index 6835b187f..5e3c4e9a4 100644 --- a/site/chunks/Components/Select.js +++ b/site/chunks/Components/Select.js @@ -416,6 +416,45 @@ const examples = [ parseTsText: require('!raw-loader!ts-loader!doc/pages/components/Select/test-006-open.tsx'), }, + { + name: 'test-007-popupContainer-absolute', + isTs: true, + isTest: true, + title: locate( + '指定无定位属性容器后,下拉菜单是否正常定位 \n 检查容器位置是否在下拉菜单之下。该示例需要单独运行,否则 setConfig 会干扰其他示例', + ' \n ' + ), + component: require('doc/pages/components/Select/test-007-popupContainer-absolute.tsx').default, + rawText: require('!raw-loader!doc/pages/components/Select/test-007-popupContainer-absolute.tsx'), + parseTsText: require('!raw-loader!ts-loader!doc/pages/components/Select/test-007-popupContainer-absolute.tsx'), + + }, + { + name: 'test-008-popupContainer-absolute-relative', + isTs: true, + isTest: true, + title: locate( + '指定容器后,下拉菜单是否正常定位 \n 检查容器位置是否在下拉菜单之下。该示例需要单独运行,否则 setConfig 会干扰其他示例', + ' \n ' + ), + component: require('doc/pages/components/Select/test-008-popupContainer-absolute-relative.tsx').default, + rawText: require('!raw-loader!doc/pages/components/Select/test-008-popupContainer-absolute-relative.tsx'), + parseTsText: require('!raw-loader!ts-loader!doc/pages/components/Select/test-008-popupContainer-absolute-relative.tsx'), + + }, + { + name: 'test-009-popupContainer-absolute-long', + isTs: true, + isTest: true, + title: locate( + '指定无定位属性容器后,超长下拉菜单是否正常定位 \n ', + ' \n options auto adapt width' + ), + component: require('doc/pages/components/Select/test-009-popupContainer-absolute-long.tsx').default, + rawText: require('!raw-loader!doc/pages/components/Select/test-009-popupContainer-absolute-long.tsx'), + parseTsText: require('!raw-loader!ts-loader!doc/pages/components/Select/test-009-popupContainer-absolute-long.tsx'), + + }, ] const codes = undefined diff --git a/site/pages/components/Select/test-007-popupContainer-absolute.tsx b/site/pages/components/Select/test-007-popupContainer-absolute.tsx new file mode 100644 index 000000000..1b5f4edb1 --- /dev/null +++ b/site/pages/components/Select/test-007-popupContainer-absolute.tsx @@ -0,0 +1,138 @@ +/** + * cn - 指定无定位属性容器后,下拉菜单是否正常定位 + * -- 检查容器位置是否在下拉菜单之下。该示例需要单独运行,否则 setConfig 会干扰其他示例 + * en - + * -- + */ + +import React, { useEffect, useState } from 'react' +import { Select, setConfig, DatePicker, Cascader, TreeSelect } from 'shineout' + +type SelectItem = string +const data: SelectItem[] = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'violet'] + +for (let i = 0; i < 50; i++) { + data.push(`color-${i}`) +} + +const cascaderData = [ + { + value: 'jiangsu', + children: [ + { + value: 'nanjing', + children: [ + { + value: 'jiangning', + }, + ], + }, + ], + }, + { + value: 'anhui', + children: [ + { + value: 'hefei', + children: [ + { + value: 'feidong', + }, + ], + }, + ], + }, +] + +const treeSelectData = [ + { + id: '1', + title: '1', + children: [ + { id: '1-1', title: '1-1', children: [{ id: '1-1-1', title: '1-1-1' }, { id: '1-1-2', title: '1-1-2' }] }, + { id: '1-2', title: '1-2' }, + ], + }, + { id: '2', title: '2', children: [{ id: '2-1', title: '2-1' }, { id: '2-2', title: '2-2' }] }, + { id: '3', title: '3', children: [{ id: '3-1', title: '3-1' }] }, +] + +const container: React.CSSProperties = { + padding: 10, + height: 100, + width: 300, + background: '#ebebeb', + overflow: 'auto', +} + +const style: React.CSSProperties = { + width: 200, + marginInlineEnd: 12, + marginBottom: 8, + marginLeft: 100, +} + +const App: React.FC = () => { + const [mount, setMount] = useState(false) + + useEffect(() => { + setConfig({ + popupContainer: document.getElementById('app-root-container') || document.body, + }) + + setMount(true) + }, []) + + return ( + <> +
+
+
+
+ {mount && ( + + )} +
+ {mount && ( + + )} + {mount && ( + `${n.value}`} + /> + )} + {mount && ( + `node ${node.title}`} + data={treeSelectData} + /> + )} +
+
+
+ +
+
+ + ) +} + +export default App diff --git a/site/pages/components/Select/test-009-popupContainer-absolute-long.tsx b/site/pages/components/Select/test-009-popupContainer-absolute-long.tsx new file mode 100644 index 000000000..589bc3f03 --- /dev/null +++ b/site/pages/components/Select/test-009-popupContainer-absolute-long.tsx @@ -0,0 +1,69 @@ +/** + * cn - 指定无定位属性容器后,超长下拉菜单是否正常定位 + * -- + * en - + * -- options auto adapt width + */ +import React, { useEffect, useState } from 'react' +import { Select, setConfig } from 'shineout' + +type SelectItem = string + +const data: SelectItem[] = [ + 'red', + 'orange', + 'this option is so long long long long long this option is so long long long long long this option is so long long long long long', + 'green', + 'cyan', + 'blue', + 'violet', +] + +const container: React.CSSProperties = { + padding: 10, + height: 100, + width: 300, + background: '#ebebeb', + overflow: 'auto', +} + +const App: React.FC = () => { + const [mount, setMount] = useState(false) + + useEffect(() => { + setConfig({ + popupContainer: document.getElementById('app-root-container') || document.body, + }) + + setMount(true) + }, []) + + // return + )} +
+
+ + +
+
+ + ) +} + +export default App diff --git a/src/AnimationList/AbsoluteList.tsx b/src/AnimationList/AbsoluteList.tsx index 689798ebd..a0a757bac 100644 --- a/src/AnimationList/AbsoluteList.tsx +++ b/src/AnimationList/AbsoluteList.tsx @@ -27,7 +27,8 @@ function getRoot() { return root } -const getOverDocStyle = (right: boolean) => (right ? { left: 0, right: 'auto' } : { right: 0, left: 'auto' }) +const getOverDocStyle = (isRight: boolean, offset: number) => + isRight ? { left: offset, right: 'auto' } : { right: offset, left: 'auto' } const listPosition = ['drop-down', 'drop-up'] const pickerPosition = ['left-bottom', 'left-top', 'right-bottom', 'right-top'] @@ -37,6 +38,7 @@ export default function(List: ComponentType) { class AbsoluteList extends Component { state = { overdoc: false, + overdocOffset: 0, } lastStyle: React.CSSProperties @@ -67,6 +69,7 @@ export default function(List: ComponentType) { props.getResetPosition(this.resetPosition.bind(this)) } this.zoomChangeHandler = this.zoomChangeHandler.bind(this) + this.getNearestPositionDom = this.getNearestPositionDom.bind(this) } componentDidMount() { @@ -125,20 +128,25 @@ export default function(List: ComponentType) { const { container } = this const defaultContainer = getDefaultContainer() const rootContainer = container === getRoot() || !container ? defaultContainer : container - const containerRect = rootContainer.getBoundingClientRect() + const realContainer = this.getNearestPositionDom(rootContainer) + const containerRect = realContainer.getBoundingClientRect() + const containerScroll = { left: rootContainer.scrollLeft, top: rootContainer.scrollTop, } + this.containerRect = containerRect this.containerScroll = containerScroll if (listPosition.includes(position)) { style.left = rect.left - containerRect.left + containerScroll.left + if (isRTL()) { style.right = containerRect.width - rect.width - style.left style.left = 'auto' } + if (position === 'drop-down') { style.top = rect.top - containerRect.top + rect.height + containerScroll.top } else { @@ -162,6 +170,21 @@ export default function(List: ComponentType) { return style } + getNearestPositionDom(target: HTMLElement): HTMLElement { + if (target) { + const { position } = getComputedStyle(target) + + if (position !== 'static') { + return target + } + + if (target.parentElement) { + return this.getNearestPositionDom(target.parentElement) + } + } + return document.body + } + getStyle() { const { parentElement, scrollElement, focus } = this.props const lazyResult = { focus, style: this.lastStyle } @@ -209,28 +232,33 @@ export default function(List: ComponentType) { resetPosition(clean?: boolean) { const { focus, parentElement } = this.props if (!this.el || !focus || (this.ajustdoc && !clean)) return - const width = this.el.offsetWidth - const pos = (parentElement && parentElement.getBoundingClientRect()) || { left: 0, right: 0 } + const pos = (parentElement && parentElement.getBoundingClientRect()) || ({ left: 0, right: 0 } as DOMRect) + const elRect = this.el.getBoundingClientRect() const containerRect = this.containerRect || { left: 0, width: 0 } const containerScroll = this.containerScroll || { left: 0 } let overdoc + if (this.isRight()) { if (isRTL() && containerScroll.left) { // this condition the style left: 0 will not meet expect so not set overdoc overdoc = false } else { - overdoc = pos.right - width < containerRect.left + // overdoc = pos.right - width < containerRect.left + overdoc = pos.left + pos.width < elRect.width } } else if (!isRTL() && containerScroll.left) { // this condition the style right: 0 will not meet expect so not set overdoc overdoc = false } else { - overdoc = pos.left - containerRect.left + width + containerScroll.left > (containerRect.width || docSize.width) + overdoc = pos.left + elRect.width > docSize.width } if (this.state.overdoc === overdoc) return this.ajustdoc = true this.setState({ overdoc, + overdocOffset: this.isRight() + ? -containerRect.left + : -(docSize.width - containerRect.left - containerRect.width), }) } @@ -259,7 +287,11 @@ export default function(List: ComponentType) { if (!Number.isNaN(parsed)) style.zIndex = parsed } - const mergeStyle = Object.assign({}, style, this.state.overdoc ? getOverDocStyle(this.isRight()) : undefined) + const mergeStyle = Object.assign( + {}, + style, + this.state.overdoc ? getOverDocStyle(this.isRight(), this.state.overdocOffset) : undefined + ) return } @@ -296,7 +328,7 @@ export default function(List: ComponentType) { {}, style, props.style, - this.state.overdoc ? getOverDocStyle(this.isRight()) : undefined + this.state.overdoc ? getOverDocStyle(this.isRight(), this.state.overdocOffset) : undefined ) if (zIndex || typeof zIndex === 'number') mergeStyle.zIndex = parseInt((zIndex as unknown) as string, 10) return ReactDOM.createPortal(