refactor(affix): set default listen-to to document. rename offset-* to trigger-*.

This commit is contained in:
07akioni 2021-05-27 11:39:44 +08:00
parent d464ec01a5
commit c5b7645f39
10 changed files with 176 additions and 103 deletions

View File

@ -2,10 +2,23 @@
## Pending
### Breaking Changes
- `n-affix`'s `listen-to` prop is `document` by default (first scrollable parent before).
### Feats
- `n-affix`'s `listen-to` prop support `Window | Document | HTMLElement`.
### Fixes
- Fix `n-input-number` not restore valid value after blur.
### Deprecated
- `n-affix`'s `offset-top` prop is deprecated, please use `trigger-top` instead.
- `n-affix`'s `offset-bottom` prop is deprecated, please use `trigger-bottom` instead.
## 2.10.0
### Breaking Changes

View File

@ -2,10 +2,23 @@
## Pending
### Breaking Changes
- `n-affix``listen-to` 属性默认为 `document` (曾为首个可滚动的父节点)
### Feats
- `n-affix``listen-to` 属性支持 `Window | Document | HTMLElement`
### Fixes
- 修正 `n-input-number` 在 blur 后不会恢复有效的值
### Deprecated
- `n-affix`'s `offset-top` prop is deprecated, please use `trigger-top` instead.
- `n-affix`'s `offset-bottom` prop is deprecated, please use `trigger-bottom` instead.
## 2.10.0
### Breaking Changes

View File

@ -1,21 +1,29 @@
# Basic
Affix has `offset-top`, `top`, `offset-bottom` and `bottom`. `offset-top` is top affixing trigger point. `top` is the style `top` value after top affixing is trigger. `offset-bottom` and `bottom` work in similar way.
Affix has `trigger-top`, `top`, `trigger-bottom` and `bottom`. `trigger-top` is top affixing trigger point. `top` is the style `top` value after top affixing is trigger. `trigger-bottom` and `bottom` work in similar way.
```html
<div class="container">
<div class="container" ref="container">
<div class="padding"></div>
<div class="content">
<n-row>
<n-col :span="12">
<n-affix :top="120" :offset-top="60"
><n-tag>Affix Trigger Top 60px</n-tag></n-affix
<n-affix
:top="120"
:trigger-top="60"
:listen-to="() => $refs.container"
>
<n-tag>Affix Trigger Top 60px</n-tag>
</n-affix>
</n-col>
<n-col :span="12">
<n-affix :bottom="120" :offset-bottom="60"
><n-tag>Affix Trigger Bottom 60px</n-tag></n-affix
<n-affix
:bottom="120"
:trigger-bottom="60"
:listen-to="() => $refs.container"
>
<n-tag>Affix Trigger Bottom 60px</n-tag>
</n-affix>
</n-col>
</n-row>
</div>

View File

@ -13,9 +13,9 @@ position
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| bottom | `number` | `undefined` | The css bottom property after trigger bottom affix. (if not set, use `offset-bottom` prop) |
| listen-to | `string \| HTMLElement` | `undefined` | The scrolling element to listen scrolling. If not set it will listen to the nearest scrollable ascendant element. |
| offset-bottom | `number` | `undefined` | The distance px to bottom of target to trigger bottom affix. (if not set, use `bottom` prop) |
| offset-top | `number` | `undefined` | The distance px to top of target to trigger top affix. (if not set, use `top` prop) |
| bottom | `number` | `undefined` | The css bottom property after trigger bottom affix. (if not set, use `trigger-bottom` prop) |
| listen-to | `string \| HTMLElement \| Document \| Window` | `document` | The scrolling element to listen scrolling. |
| trigger-bottom | `number` | `undefined` | The distance px to bottom of target to trigger bottom affix. (if not set, use `bottom` prop) |
| trigger-top | `number` | `undefined` | The distance px to top of target to trigger top affix. (if not set, use `top` prop) |
| position | `'fixed' \| 'absolute'` | `'fixed'` | |
| top | `number` | `undefined` | The css top property after trigger top affix. (if not set, use `offset-top` prop) |
| top | `number` | `undefined` | The css top property after trigger top affix. (if not set, use `trigger-top` prop) |

View File

@ -1,21 +1,29 @@
# Position
Affix can be `absolute` or `fixed` positioned. You may need some css tricks to make it works as following. By default position is set to `fixed`, because in most cases scrolled element is `#document`.
Affix can be `absolute` or `fixed` positioned. You may need some css tricks to make it works as following. By default position is set to `fixed`, because in most cases scrolled element is `document`.
```html
<div class="absolute-anchor-container">
<div class="container">
<div class="container" ref="container">
<div class="padding"></div>
<div class="content">
<div style="display: inline-block; width: 50%;">
<n-affix :offset-top="50" position="absolute"
><n-tag>Affix Trigger Top 50px</n-tag></n-affix
<n-affix
:trigger-top="50"
position="absolute"
:listen-to="() => $refs.container"
>
<n-tag>Affix Trigger Top 50px</n-tag>
</n-affix>
</div>
<div style="display: inline-block; width: 50%;">
<n-affix :offset-bottom="60" position="absolute"
><n-tag>Affix Trigger Bottom 60px</n-tag></n-affix
<n-affix
:trigger-bottom="60"
position="absolute"
:listen-to="() => $refs.container"
>
<n-tag>Affix Trigger Bottom 60px</n-tag>
</n-affix>
</div>
</div>
</div>

View File

@ -1,23 +1,31 @@
# 基础用法
Affix 有 `offset-top`、`top`、`offset-bottom` 和 `bottom` 属性。`offset-top` 是顶部固定的触发距离,`top` 是在触发顶部固定之后 CSS 的 `top` 值。`offset-bottom` 和 `bottom` 类似。
Affix 有 `trigger-top`、`top`、`trigger-bottom` 和 `bottom` 属性。`trigger-top` 是顶部固定的触发距离,`top` 是在触发顶部固定之后 CSS 的 `top` 值。`trigger-bottom` 和 `bottom` 类似。
```html
<div class="container">
<div class="container" ref="container">
<div class="padding"></div>
<div class="content">
<n-row>
<n-col :span="12">
<n-affix :top="120" :offset-top="60"
><n-tag>顶部触发距离 60px</n-tag></n-affix
<n-grid :cols="2">
<n-gi :span="1">
<n-affix
:top="120"
:trigger-top="60"
:listen-to="() => $refs.container"
>
</n-col>
<n-col :span="12">
<n-affix :bottom="120" :offset-bottom="60"
><n-tag>底部触发距离 60px</n-tag></n-affix
<n-tag>顶部触发距离 60px</n-tag>
</n-affix>
</n-gi>
<n-gi :span="1">
<n-affix
:bottom="120"
:trigger-bottom="60"
:listen-to="() => $refs.container"
>
</n-col>
</n-row>
<n-tag>底部触发距离 60px</n-tag>
</n-affix>
</n-gi>
</n-grid>
</div>
</div>
```

View File

@ -13,9 +13,9 @@ position
| 名称 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| bottom | `number` | `undefined` | 在触发顶部固定后 Affix 的 CSS bottom 属性(如果没设定,会使用 `offset-bottom` 代替) |
| listen-to | `string \| HTMLElement` | `undefined` | 需要监听滚动的元素,如果未设定则会监听最近的可滚动祖先元素 |
| offset-bottom | `number` | `undefined` | 触发底部固定时Affix 和目标元素元素的底部距离(如果没设定,会使用 `bottom` 代替) |
| offset-top | `number` | `undefined` | 触发顶部固定时Affix 和目标元素元素的顶部距离(如果没设定,会使用 `top` 代替) |
| bottom | `number` | `undefined` | 在触发顶部固定后 Affix 的 CSS bottom 属性(如果没设定,会使用 `trigger-bottom` 代替) |
| listen-to | `string \| HTMLElement \| Document \| Window` | `document` | 需要监听滚动的元素 |
| trigger-bottom | `number` | `undefined` | 触发底部固定时Affix 和目标元素元素的底部距离(如果没设定,会使用 `bottom` 代替) |
| trigger-top | `number` | `undefined` | 触发顶部固定时Affix 和目标元素元素的顶部距离(如果没设定,会使用 `top` 代替) |
| position | `'fixed' \| 'absolute'` | `'fixed'` | |
| top | `number` | `undefined` | 在触发顶部固定后 Affix 的 CSS top 属性(如果没设定,会使用 `offset-top` 代替) |
| top | `number` | `undefined` | 在触发顶部固定后 Affix 的 CSS top 属性(如果没设定,会使用 `trigger-top` 代替) |

View File

@ -1,21 +1,29 @@
# 位置
Affix 可以 `absolute` 或者 `fixed` 定位。你可能还需要写一些额外的 CSS 才能让达到例子的效果。 默认情况下位置是 `fixed`,因为大多数情况下,滚动的元素是 `#document`。
Affix 可以 `absolute` 或者 `fixed` 定位。你可能还需要写一些额外的 CSS 才能让达到例子的效果。 默认情况下位置是 `fixed`,因为大多数情况下,滚动的元素是 `document`。
```html
<div class="absolute-anchor-container">
<div class="container">
<div class="container" ref="container">
<div class="padding"></div>
<div class="content">
<div style="display: inline-block; width: 50%;">
<n-affix :offset-top="50" position="absolute"
><n-tag>顶部触发距离 50px</n-tag></n-affix
<n-affix
:trigger-top="50"
position="absolute"
:listen-to="() => $refs.container"
>
<n-tag>顶部触发距离 50px</n-tag>
</n-affix>
</div>
<div style="display: inline-block; width: 50%;">
<n-affix :offset-bottom="60" position="absolute"
><n-tag>底部触发距离 60px</n-tag></n-affix
<n-affix
:trigger-bottom="60"
position="absolute"
:listen-to="() => $refs.container"
>
<n-tag>底部触发距离 60px</n-tag>
</n-affix>
</div>
</div>
</div>

View File

@ -8,44 +8,59 @@ import {
PropType,
h
} from 'vue'
import { getScrollParent, unwrapElement, beforeNextFrameOnce } from 'seemly'
import { unwrapElement, beforeNextFrameOnce } from 'seemly'
import { useConfig, useStyle } from '../../_mixins'
import { warn, keysOf } from '../../_utils'
import type { ExtractPublicPropTypes } from '../../_utils'
import { getScrollTop, getRect } from './utils'
import type { ScrollTarget } from './utils'
import style from './styles/index.cssr'
export const affixProps = {
listenTo: {
type: [String, Object] as PropType<
string | (() => HTMLElement) | undefined
>,
default: undefined
},
offsetTop: {
type: Number,
default: undefined
},
top: {
type: Number,
default: undefined
},
offsetBottom: {
type: Number,
default: undefined
},
bottom: {
type: Number,
default: undefined
},
listenTo: [String, Object, Function] as PropType<
string | ScrollTarget | (() => HTMLElement) | undefined
>,
top: Number,
bottom: Number,
triggerTop: Number,
triggerBottom: Number,
position: {
type: String,
type: String as PropType<'fix' | 'absolute'>,
default: 'fix'
},
// deprecated
offsetTop: {
type: Number as PropType<number | undefined>,
validator: () => {
if (__DEV__) {
warn(
'affix',
'`offset-top` is deprecated, please use `trigger-top` instead.'
)
}
return true
},
default: undefined
},
offsetBottom: {
type: Number as PropType<number | undefined>,
validator: () => {
if (__DEV__) {
warn(
'affix',
'`offset-bottom` is deprecated, please use `trigger-bottom` instead.'
)
}
return true
},
default: undefined
},
target: {
type: (Function as unknown) as PropType<(() => HTMLElement) | undefined>,
validator: () => {
warn('affix', '`target` is deprecated, please use `listen-to` instead.')
if (__DEV__) {
warn('affix', '`target` is deprecated, please use `listen-to` instead.')
}
return true
},
default: undefined
@ -62,7 +77,7 @@ export default defineComponent({
setup (props) {
const { mergedClsPrefixRef } = useConfig(props)
useStyle('Affix', style, mergedClsPrefixRef)
const scrollElementRef = ref<HTMLElement | null>(null)
let scrollTarget: ScrollTarget | null = null
const stickToTopRef = ref(false)
const stickToBottomRef = ref(false)
const bottomAffixedTriggerScrollTopRef = ref<number | null>(null)
@ -71,69 +86,55 @@ export default defineComponent({
return stickToBottomRef.value || stickToTopRef.value
})
const mergedOffsetTopRef = computed(() => {
const { offsetTop, top } = props
return offsetTop === undefined ? top : offsetTop
return props.triggerTop ?? props.offsetTop ?? props.top
})
const mergedTopRef = computed(() => {
const { offsetTop, top } = props
return top === undefined ? offsetTop : top
return props.top ?? props.triggerTop ?? props.offsetTop
})
const mergedBottomRef = computed(() => {
const { offsetBottom, bottom } = props
return bottom === undefined ? offsetBottom : bottom
return props.bottom ?? props.triggerBottom ?? props.offsetBottom
})
const mergedOffsetBottomRef = computed(() => {
const { offsetBottom, bottom } = props
return offsetBottom === undefined ? bottom : offsetBottom
return props.triggerBottom ?? props.offsetBottom ?? props.bottom
})
const selfRef = ref<Element | null>(null)
const init = (): void => {
const { target: getScrollTarget, listenTo } = props
let scrollElement
if (getScrollTarget) {
// deprecated
scrollElement = getScrollTarget()
scrollTarget = getScrollTarget()
} else if (listenTo) {
scrollElement = unwrapElement(listenTo)
scrollTarget = unwrapElement(listenTo)
} else {
scrollElement = getScrollParent(selfRef.value)
scrollTarget = document
}
if (scrollElement) {
scrollElementRef.value = scrollElement
if (scrollTarget) {
scrollTarget.addEventListener('scroll', handleScroll)
handleScroll()
} else if (__DEV__) {
warn('affix', 'Target to be listened to is not valid.')
}
if (scrollElement) {
scrollElement.addEventListener('scroll', handleScroll)
handleScroll()
}
}
function handleScroll (): void {
beforeNextFrameOnce(_handleScroll)
}
function _handleScroll (): void {
const { value: containerEl } = scrollElementRef
const { value: selfEl } = selfRef
if (!containerEl || !selfEl) return
if (!scrollTarget || !selfEl) return
const scrollTop = getScrollTop(scrollTarget)
if (affixedRef.value) {
if (
containerEl.scrollTop <
(topAffixedTriggerScrollTopRef.value as number)
) {
if (scrollTop < (topAffixedTriggerScrollTopRef.value as number)) {
stickToTopRef.value = false
topAffixedTriggerScrollTopRef.value = null
}
if (
containerEl.scrollTop >
(bottomAffixedTriggerScrollTopRef.value as number)
) {
if (scrollTop > (bottomAffixedTriggerScrollTopRef.value as number)) {
stickToBottomRef.value = false
bottomAffixedTriggerScrollTopRef.value = null
}
return
}
const containerRect = containerEl.getBoundingClientRect()
const containerRect = getRect(scrollTarget)
const affixRect = selfEl.getBoundingClientRect()
const pxToTop = affixRect.top - containerRect.top
const pxToBottom = containerRect.bottom - affixRect.bottom
@ -142,7 +143,7 @@ export default defineComponent({
if (mergedOffsetTop !== undefined && pxToTop <= mergedOffsetTop) {
stickToTopRef.value = true
topAffixedTriggerScrollTopRef.value =
containerEl.scrollTop - (mergedOffsetTop - pxToTop)
scrollTop - (mergedOffsetTop - pxToTop)
} else {
stickToTopRef.value = false
topAffixedTriggerScrollTopRef.value = null
@ -153,7 +154,7 @@ export default defineComponent({
) {
stickToBottomRef.value = true
bottomAffixedTriggerScrollTopRef.value =
containerEl.scrollTop + mergedOffsetBottom - pxToBottom
scrollTop + mergedOffsetBottom - pxToBottom
} else {
stickToBottomRef.value = false
bottomAffixedTriggerScrollTopRef.value = null
@ -163,10 +164,8 @@ export default defineComponent({
init()
})
onBeforeUnmount(() => {
const scrollElement = scrollElementRef.value
if (scrollElement) {
scrollElement.removeEventListener('scroll', handleScroll)
}
if (!scrollTarget) return
scrollTarget.removeEventListener('scroll', handleScroll)
})
return {
selfRef,
@ -174,12 +173,17 @@ export default defineComponent({
mergedClsPrefix: mergedClsPrefixRef,
mergedstyle: computed<CSSProperties>(() => {
const style: CSSProperties = {}
if (stickToTopRef.value && mergedOffsetTopRef.value !== undefined) {
if (
stickToTopRef.value &&
mergedOffsetTopRef.value !== undefined &&
mergedTopRef.value !== undefined
) {
style.top = `${mergedTopRef.value}px`
}
if (
stickToBottomRef.value &&
mergedOffsetBottomRef.value !== undefined
mergedOffsetBottomRef.value !== undefined &&
mergedBottomRef.value !== undefined
) {
style.bottom = `${mergedBottomRef.value}px`
}

11
src/affix/src/utils.ts Normal file
View File

@ -0,0 +1,11 @@
export type ScrollTarget = Window | Document | HTMLElement
export function getScrollTop (target: ScrollTarget): number {
return target instanceof HTMLElement ? target.scrollTop : window.scrollY
}
export function getRect (target: ScrollTarget): { top: number, bottom: number } {
return target instanceof HTMLElement
? target.getBoundingClientRect()
: { top: 0, bottom: window.innerHeight }
}