refactor(placable): use a measure element for better

positioning robustness
This commit is contained in:
07akioni 2020-01-30 16:00:38 +08:00
parent 3d2708ebdc
commit f8e76dd7cd
17 changed files with 126 additions and 72 deletions

View File

@ -6,7 +6,6 @@
'n-base-select-menu--multiple': multiple, 'n-base-select-menu--multiple': multiple,
[`n-${theme}-theme`]: theme [`n-${theme}-theme`]: theme
}" }"
:style="{ :style="{
width: width && (width + 'px') width: width && (width + 'px')
}" }"

View File

@ -11,20 +11,20 @@
</slot> </slot>
<div <div
ref="contentContainer" ref="contentContainer"
v-clickoutside="handleClickOutsideMenu" class="n-positioning-container"
class="n-detached-content-container n-select-detached-content-container"
:class="{ :class="{
[namespace]: namespace [namespace]: namespace
}" }"
> >
<div <div
ref="content" ref="content"
class="n-detached-content-content" class="n-positioning-content"
> >
<transition name="n-select-menu--transition"> <transition name="n-select-menu--transition">
<n-base-select-menu <n-base-select-menu
v-if="active" v-if="active"
ref="contentInner" ref="contentInner"
v-clickoutside="handleClickOutsideMenu"
auto-pending-first-option auto-pending-first-option
class="n-select-menu" class="n-select-menu"
:theme="synthesizedTheme" :theme="synthesizedTheme"

View File

@ -1,8 +1,8 @@
<template> <template>
<div <div
class="n-detached-content-container" class="n-positioning-container"
> >
<div ref="content" class="n-detached-content"> <div ref="content" class="n-positioning-content">
<transition name="n-cascader-menu--transition"> <transition name="n-cascader-menu--transition">
<div <div
v-if="active" v-if="active"

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="n-detached-content-container"> <div class="n-positioning-container">
<div ref="content" class="n-detached-content"> <div ref="content" class="n-positioning-content">
<transition name="n-cascader-menu--transition"> <transition name="n-cascader-menu--transition">
<n-base-select-menu <n-base-select-menu
v-if="active" v-if="active"
@ -15,6 +15,7 @@
:is-option-selected="isSelected" :is-option-selected="isSelected"
@mousedown.native.prevent="() => {}" @mousedown.native.prevent="() => {}"
@menu-toggle-option="handleSelectMenuToggleOption" @menu-toggle-option="handleSelectMenuToggleOption"
@menu-visible="handleMenuVisible"
/> />
</transition> </transition>
</div> </div>
@ -188,6 +189,9 @@ export default {
const pendingOptionData = this.$refs.contentInner.getPendingOptionData() const pendingOptionData = this.$refs.contentInner.getPendingOptionData()
this.handleSelectOptionCheck(pendingOptionData) this.handleSelectOptionCheck(pendingOptionData)
} }
},
handleMenuVisible () {
this.updatePosition()
} }
} }
} }

View File

@ -54,19 +54,19 @@
</n-input> </n-input>
<div <div
ref="contentContainer" ref="contentContainer"
class="n-detached-content-container n-date-picker-detached-content-container" class="n-positioning-container"
:class="{ :class="{
[namespace]: namespace [namespace]: namespace
}" }"
> >
<div <div
ref="content" ref="content"
v-clickoutside="handleClickOutside" class="n-positioning-content"
class="n-dateched-content"
> >
<datetime-panel <datetime-panel
v-if="type === 'datetime'" v-if="type === 'datetime'"
ref="panel" ref="panel"
v-clickoutside="handleClickOutside"
:value="value" :value="value"
:active="active" :active="active"
:actions="actions" :actions="actions"
@ -80,6 +80,7 @@
<date-panel <date-panel
v-else-if="type === 'date'" v-else-if="type === 'date'"
ref="panel" ref="panel"
v-clickoutside="handleClickOutside"
:value="value" :value="value"
:active="active" :active="active"
:actions="actions" :actions="actions"
@ -92,6 +93,7 @@
<daterange-panel <daterange-panel
v-else-if="type === 'daterange'" v-else-if="type === 'daterange'"
ref="panel" ref="panel"
v-clickoutside="handleClickOutside"
:value="value" :value="value"
:active="active" :active="active"
:actions="actions" :actions="actions"
@ -105,6 +107,7 @@
<datetimerange-panel <datetimerange-panel
v-else-if="type === 'datetimerange'" v-else-if="type === 'datetimerange'"
ref="panel" ref="panel"
v-clickoutside="handleClickOutside"
:value="value" :value="value"
:active="active" :active="active"
:actions="actions" :actions="actions"

View File

@ -1,6 +1,6 @@
<template> <template>
<n-base-portal ref="portal"> <n-base-portal ref="portal">
<div class="n-detached-content-container"> <div class="n-detached-content-container" style="z-index: 1000;">
<div <div
class="n-drawer-container" class="n-drawer-container"
:class="{ :class="{

View File

@ -97,17 +97,17 @@ export default {
} }
} }
}, },
created () { // created () {
console.log('dropdown menu created') // console.log('dropdown menu created')
}, // },
beforeDestroy () { // beforeDestroy () {
console.log('dropdown menu beforeDestroy') // console.log('dropdown menu beforeDestroy')
}, // },
destroyed () { // destroyed () {
console.log('dropdown menu destroyed') // console.log('dropdown menu destroyed')
}, // },
mounted () { mounted () {
console.log('dropdown menu mounted') // console.log('dropdown menu mounted')
if (this.defaultFocus && this.focusable) { if (this.defaultFocus && this.focusable) {
this.$el.focus() this.$el.focus()
} }
@ -220,6 +220,9 @@ export default {
isOptionSelected: () => false, isOptionSelected: () => false,
theme: this.synthesizedTheme theme: this.synthesizedTheme
}, },
style: {
overflow: 'visible'
},
on: { on: {
'menu-toggle-option': option => { 'menu-toggle-option': option => {
this.handleSelectOption(option.value) this.handleSelectOption(option.value)

View File

@ -227,7 +227,7 @@ export default {
}, },
render (h) { render (h) {
return h('div', { return h('div', {
staticClass: 'n-detached-content-container', staticClass: 'n-positioning-container',
class: { class: {
[this.containerClass]: true, [this.containerClass]: true,
[this.namespace]: this.namespace [this.namespace]: this.namespace
@ -235,7 +235,7 @@ export default {
ref: 'contentContainer' ref: 'contentContainer'
}, [ }, [
h('div', { h('div', {
staticClass: 'n-detached-content', staticClass: 'n-positioning-content',
ref: 'content' ref: 'content'
}, [ }, [
h('transition', { h('transition', {

View File

@ -41,15 +41,14 @@
/> />
<div <div
ref="contentContainer" ref="contentContainer"
v-clickoutside="handleClickOutsideMenu" class="n-positioning-container"
class="n-detached-content-container n-select-detached-content-container"
:class="{ :class="{
[namespace]: namespace [namespace]: namespace
}" }"
> >
<div <div
ref="content" ref="content"
class="n-detached-content-content" class="n-positioning-content"
> >
<transition <transition
name="n-select-menu--transition" name="n-select-menu--transition"
@ -58,6 +57,7 @@
<n-base-select-menu <n-base-select-menu
v-if="active" v-if="active"
ref="contentInner" ref="contentInner"
v-clickoutside="handleClickOutsideMenu"
class="n-select-menu" class="n-select-menu"
auto-pending-first-option auto-pending-first-option
:theme="synthesizedTheme" :theme="synthesizedTheme"

View File

@ -68,14 +68,14 @@
</div> </div>
<div <div
ref="contentContainer" ref="contentContainer"
class="n-detached-content-container n-slider-detached-content-container" class="n-positioning-container"
:class="{ :class="{
[namespace]: namespace [namespace]: namespace
}" }"
> >
<div <div
ref="content" ref="content"
class="n-detached-content" class="n-positioning-content"
> >
<transition name="n-fade-in-scale-up--transition"> <transition name="n-fade-in-scale-up--transition">
<div <div

View File

@ -21,9 +21,9 @@
/> />
<div <div
ref="contentContainer" ref="contentContainer"
class="n-detached-content-container n-time-picker-detached-content-container" class="n-positioning-container"
:class="{ :class="{
'n-detached-content-container--absolute-positioned': positionModeisAbsolute, 'n-positioning-container--absolute': positionModeisAbsolute,
[namespace]: namespace [namespace]: namespace
}" }"
> >

View File

@ -4,12 +4,33 @@ import getParentNode from '../utils/dom/getParentNode'
import getScrollParent from '../utils/dom/getScrollParent' import getScrollParent from '../utils/dom/getScrollParent'
import { getAdjustedPlacementOfTrackingElement, getTransformOriginByPlacement, getPosition } from '../utils/dom/calcPlacementTransform' import { getAdjustedPlacementOfTrackingElement, getTransformOriginByPlacement, getPosition } from '../utils/dom/calcPlacementTransform'
let viewMeasurerInitialized = false
let viewMeasurer = null
if (!viewMeasurerInitialized && !document.getElementById('n-view-measurer')) {
viewMeasurer = document.createElement('div')
viewMeasurer.id = 'n-view-measurer'
viewMeasurer.style = `
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
pointer-events: none;
visibility: hidden;
`
viewMeasurerInitialized = true
document.body.appendChild(viewMeasurer)
}
function getActivatorEl (componentInstance) { function getActivatorEl (componentInstance) {
return componentInstance.$refs.activator.$el || componentInstance.$refs.activator const refs = componentInstance.$refs
return refs.activator.$el || refs.activator
} }
function getContentEl (componentInstance) { function getContentEl (componentInstance) {
return componentInstance.$refs.content.$el || componentInstance.$refs.content const refs = componentInstance.$refs
return refs.content.$el || refs.content
} }
function getContentInner (instance) { function getContentInner (instance) {
@ -17,25 +38,37 @@ function getContentInner (instance) {
return (contentInnerRef && contentInnerRef.$el) || contentInnerRef || null return (contentInnerRef && contentInnerRef.$el) || contentInnerRef || null
} }
function getActivatorRect (manuallyPositioned, x, y, trackedElement) { function getViewBoundingRect () {
const containerBoundingRect = viewMeasurer.getBoundingClientRect()
return {
left: containerBoundingRect.left,
top: containerBoundingRect.top,
right: containerBoundingRect.right,
bottom: containerBoundingRect.bottom,
width: containerBoundingRect.width,
height: containerBoundingRect.height
}
}
function getActivatorRect (manuallyPositioned, x, y, trackedElement, viewBoundingRect) {
if (manuallyPositioned) { if (manuallyPositioned) {
return { return {
top: y, top: y,
left: x, left: x,
height: 0, height: 0,
width: 0, width: 0,
right: window.innerWidth - x, right: viewBoundingRect.width - x,
bottom: window.innerHeight - y bottom: viewBoundingRect.height - y
} }
} else { } else {
const activatorRect = trackedElement.getBoundingClientRect() const activatorRect = trackedElement.getBoundingClientRect()
return { return {
left: parseInt(activatorRect.left), left: activatorRect.left - viewBoundingRect.left,
top: parseInt(activatorRect.top), top: activatorRect.top - viewBoundingRect.top,
bottom: parseInt(window.innerHeight - activatorRect.bottom), bottom: viewBoundingRect.height + viewBoundingRect.top - activatorRect.bottom,
right: parseInt(window.innerWidth - activatorRect.right), right: viewBoundingRect.width + viewBoundingRect.left - activatorRect.right,
width: parseInt(activatorRect.width), width: activatorRect.width,
height: parseInt(activatorRect.height) height: activatorRect.height
} }
} }
} }
@ -177,14 +210,16 @@ export default {
}, },
methods: { methods: {
_getTrackingElement () { _getTrackingElement () {
if (this.$refs && this.$refs.content) { const refs = this.$refs
if (refs && refs.content) {
this.trackingElement = getContentEl(this) this.trackingElement = getContentEl(this)
} else if (this.getTrackingElement) { } else if (this.getTrackingElement) {
this.trackingElement = this.getTrackingElement() this.trackingElement = this.getTrackingElement()
} }
}, },
_getTrackedElement () { _getTrackedElement () {
if (this.$refs && this.$refs.activator) { const refs = this.$refs
if (refs && refs.activator) {
this.trackedElement = getActivatorEl(this) this.trackedElement = getActivatorEl(this)
} else if (this.getTrackedElement) { } else if (this.getTrackedElement) {
this.trackedElement = this.getTrackedElement() this.trackedElement = this.getTrackedElement()
@ -221,7 +256,8 @@ export default {
} }
} }
// debugger // debugger
const activatorRect = getActivatorRect(this.manuallyPositioned, this.x, this.y, this.trackedElement) const viewBoundingRect = getViewBoundingRect()
const activatorRect = getActivatorRect(this.manuallyPositioned, this.x, this.y, this.trackedElement, viewBoundingRect)
const contentInner = getContentInner(this) const contentInner = getContentInner(this)
if (this.widthMode === 'activator' && contentInner) { if (this.widthMode === 'activator' && contentInner) {
contentInner.style.minWidth = activatorRect.width + 'px' contentInner.style.minWidth = activatorRect.width + 'px'

28
playground/pg2.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<style>
.fixed-el {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: red;
}
.test-el {
position: absolute;
left: 400px;
top: 200px;
height: 100px;
width: 100px;
background-color: yellowgreen;
}
</style>
</head>
<body>
<div class="fixed-el">123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890-12345678901234567890123456789012345678901234567890
<div class="test-el"></div>
</div>
</body>
</html>

View File

@ -44,6 +44,7 @@
border-radius: 6px; border-radius: 6px;
transition: background-color .3s $--n-ease-in-out-cubic-bezier; transition: background-color .3s $--n-ease-in-out-cubic-bezier;
font-size: $--n-secondary-text-color; font-size: $--n-secondary-text-color;
overflow: hidden;
@include b(base-select-menu-option-wrapper) { @include b(base-select-menu-option-wrapper) {
position: relative; position: relative;
width: 100%; width: 100%;

View File

@ -42,9 +42,6 @@
font-size: 15px; font-size: 15px;
} }
} }
@include b(select-detached-content-container) {
transform: translateZ(0);
}
@include b(select-menu) { @include b(select-menu) {
@include fade-in-scale-up-transition(select-menu, $original-transition: (background-color .3s $--n-ease-in-out-cubic-bezier)); @include fade-in-scale-up-transition(select-menu, $original-transition: (background-color .3s $--n-ease-in-out-cubic-bezier));
} }

View File

@ -35,24 +35,20 @@ input {
font-size: inherit; font-size: inherit;
} }
.n-detached-content-container { .n-positioning-container {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 0; pointer-events: none;
overflow: visible; transform: translateZ(0);
} }
.n-detached-content-container.n-detached-content-container--absolute-positioned { .n-positioning-container.n-positioning-container--absolute {
position: absolute; position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
overflow: visible;
} }
.n-detached-content { .n-positioning-content {
position: fixed; pointer-events: all;
} }

View File

@ -125,17 +125,4 @@ $theme-names: "dark", "light";
@if not $common-css-attrs-generated { @if not $common-css-attrs-generated {
@content; @content;
} }
}
@mixin detachedContentWrapper {
position: fixed;
left: 0;
top: 0;
width: 0;
height: 0;
overflow: visible;
}
@mixin detachedContent {
position: fixed;
} }