generated from eternallycyf/ims-monorepo-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
85f6e7e
commit 893eb18
Showing
18 changed files
with
709 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { CSSProperties, useCallback, useMemo, useRef } from 'react'; | ||
import useEventListener from './hooks/useEventListener'; | ||
import { getRect } from './hooks/useRect'; | ||
import useScrollParent from './hooks/useScrollParent'; | ||
import useSetState from './hooks/useSetState'; | ||
import useUpdateEffect from './hooks/useUpdateEffect'; | ||
import useVisibilityChange from './hooks/useVisibilityChange'; | ||
import './index.less'; | ||
|
||
import { | ||
extend, | ||
getScrollTop, | ||
getZIndexStyle, | ||
isHidden, | ||
mergeProps, | ||
unitToPx, | ||
} from './hooks/utils'; | ||
import { StickyProps } from './interface'; | ||
|
||
const Sticky: React.FC<StickyProps> = (p) => { | ||
const props = mergeProps(p, { | ||
offsetTop: 0, | ||
offsetBottom: 0, | ||
position: 'top', | ||
}); | ||
const [state, updateState] = useSetState({ | ||
fixed: false, | ||
width: 0, // root width | ||
height: 0, // root height | ||
transform: 0, | ||
}); | ||
|
||
const root = useRef<HTMLDivElement>(null); | ||
const scrollParent = useScrollParent(root); | ||
|
||
const offset = useMemo<number>( | ||
() => unitToPx(props.position === 'top' ? props.offsetTop : props.offsetBottom), | ||
[props.position, props.offsetTop, props.offsetBottom], | ||
); | ||
|
||
const rootStyle = useMemo<CSSProperties | undefined>(() => { | ||
const { fixed, height, width } = state; | ||
if (fixed) { | ||
return { | ||
width: `${width}px`, | ||
height: `${height}px`, | ||
}; | ||
} | ||
return null; | ||
}, [state.fixed, state.height, state.width]); | ||
|
||
const stickyStyle = useMemo<CSSProperties | undefined>(() => { | ||
if (!state.fixed) { | ||
return null; | ||
} | ||
|
||
const style: CSSProperties = extend(getZIndexStyle(props.zIndex), { | ||
width: `${state.width}px`, | ||
height: `${state.height}px`, | ||
[props.position]: `${offset}px`, | ||
}); | ||
|
||
if (state.transform) { | ||
style.transform = `translate3d(0, ${state.transform}px, 0)`; | ||
} | ||
|
||
return style; | ||
}, [props.position, state.fixed, offset, state.width, state.height, state.transform]); | ||
|
||
const emitScroll = (scrollTop: number, isFixed: boolean) => { | ||
if (props.onScroll) { | ||
props.onScroll({ | ||
scrollTop, | ||
isFixed, | ||
}); | ||
} | ||
}; | ||
|
||
const onScroll = useCallback(() => { | ||
if (!root.current || isHidden(root.current)) { | ||
return; | ||
} | ||
|
||
const { container, position } = props; | ||
const rootRect = getRect(root.current); | ||
const scrollTop = getScrollTop(window); | ||
|
||
const newState = {} as typeof state; | ||
newState.width = rootRect.width; | ||
newState.height = rootRect.height; | ||
|
||
if (position === 'top') { | ||
// The sticky component should be kept inside the container element | ||
if (container) { | ||
const containerRect = getRect(container.current); | ||
const difference = containerRect.bottom - offset - newState.height; | ||
newState.fixed = offset > rootRect.top && containerRect.bottom > 0; | ||
newState.transform = difference < 0 ? difference : 0; | ||
} else { | ||
newState.fixed = offset > rootRect.top; | ||
} | ||
} else { | ||
const { clientHeight } = document.documentElement; | ||
if (container) { | ||
const containerRect = getRect(container.current); | ||
const difference = clientHeight - containerRect.top - offset - newState.height; | ||
newState.fixed = | ||
clientHeight - offset < rootRect.bottom && clientHeight > containerRect.top; | ||
newState.transform = difference < 0 ? -difference : 0; | ||
} else { | ||
newState.fixed = clientHeight - offset < rootRect.bottom; | ||
} | ||
} | ||
updateState(newState); | ||
emitScroll(scrollTop, newState.fixed); | ||
}, [offset]); | ||
|
||
useEventListener('scroll', onScroll, { | ||
target: scrollParent, | ||
depends: [offset], | ||
}); | ||
useVisibilityChange(root, onScroll); | ||
useUpdateEffect(() => { | ||
props.onChange?.(state.fixed); | ||
}, [state.fixed]); | ||
|
||
return ( | ||
<div ref={root} style={rootStyle}> | ||
<div className="Sticky-fixed" style={stickyStyle}> | ||
{props.children} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Sticky; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.demo-sticky--wrapper { | ||
overflow: scroll; | ||
height: 1000px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Button } from 'antd'; | ||
import { Sticky } from 'ims-view-pc'; | ||
import React, { useRef } from 'react'; | ||
import './index.less'; | ||
|
||
export default () => { | ||
const container = useRef<HTMLDivElement>(null); | ||
|
||
return ( | ||
<div> | ||
<div className="demo-sticky--wrapper"> | ||
<Sticky> | ||
<Button type="primary" style={{ marginLeft: '15px' }}> | ||
基础用法 | ||
</Button> | ||
</Sticky> | ||
|
||
<Sticky offsetTop={50}> | ||
<Button style={{ marginLeft: '115px' }}>吸顶距离</Button> | ||
</Sticky> | ||
|
||
<div ref={container} style={{ height: '300px', backgroundColor: '#fff' }}> | ||
<Sticky container={container}> | ||
<Button style={{ marginLeft: '215px' }}>指定容器</Button> | ||
</Sticky> | ||
</div> | ||
|
||
<div style={{ height: '70vh' }} /> | ||
<Sticky position="bottom" offsetBottom={50}> | ||
<Button style={{ marginLeft: '15px' }}>吸底距离</Button> | ||
</Sticky> | ||
</div> | ||
</div> | ||
); | ||
}; |
67 changes: 67 additions & 0 deletions
67
packages/ims-view-pc/src/components/Sticky/hooks/useEventListener.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* eslint-disable react-hooks/rules-of-hooks */ | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
import { useEffect } from 'react'; | ||
import { BasicTarget, getTargetElement, inBrowser, TargetElement } from './utils'; | ||
|
||
// https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener#使用_passive_改善的滚屏性能 | ||
export let supportsPassive = false; | ||
|
||
if (inBrowser) { | ||
try { | ||
const opts = {}; | ||
Object.defineProperty(opts, 'passive', { | ||
get() { | ||
supportsPassive = true; | ||
}, | ||
}); | ||
window.addEventListener('test-passive', null, opts); | ||
// eslint-disable-next-line no-empty | ||
} catch (e) {} | ||
} | ||
|
||
type Target = BasicTarget<TargetElement>; | ||
|
||
export type UseEventListenerOptions = { | ||
target?: Target; | ||
capture?: boolean; | ||
passive?: boolean; | ||
depends?: Array<unknown>; | ||
}; | ||
|
||
function useEventListener( | ||
type: string, | ||
listener: EventListener, | ||
options: UseEventListenerOptions = {}, | ||
): void { | ||
if (!inBrowser) { | ||
return; | ||
} | ||
const { target = window, passive = false, capture = false, depends = [] } = options; | ||
let attached: boolean; | ||
|
||
const add = () => { | ||
const element = getTargetElement(target); | ||
|
||
if (element && !attached) { | ||
element.addEventListener(type, listener, supportsPassive ? { capture, passive } : capture); | ||
attached = true; | ||
} | ||
}; | ||
|
||
const remove = () => { | ||
const element = getTargetElement(target); | ||
|
||
if (element && attached) { | ||
element.removeEventListener(type, listener, capture); | ||
attached = false; | ||
} | ||
}; | ||
|
||
// https://stackoverflow.com/questions/55265255/react-usestate-hook-event-handler-using-initial-state | ||
useEffect(() => { | ||
add(); | ||
return () => remove(); | ||
}, [target, ...depends]); | ||
} | ||
|
||
export default useEventListener; |
46 changes: 46 additions & 0 deletions
46
packages/ims-view-pc/src/components/Sticky/hooks/useRect.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
function isWindow(val: unknown): val is Window { | ||
return val === window; | ||
} | ||
interface Rect { | ||
top: number; | ||
left: number; | ||
right: number; | ||
bottom: number; | ||
width: number; | ||
height: number; | ||
} | ||
|
||
const useRect = (elementRef: Element | Window): Rect => { | ||
const element = elementRef; | ||
|
||
if (isWindow(element)) { | ||
const width = element.innerWidth; | ||
const height = element.innerHeight; | ||
|
||
return { | ||
top: 0, | ||
left: 0, | ||
right: width, | ||
bottom: height, | ||
width, | ||
height, | ||
}; | ||
} | ||
|
||
if (element && element.getBoundingClientRect) { | ||
return element.getBoundingClientRect(); | ||
} | ||
|
||
return { | ||
top: 0, | ||
left: 0, | ||
right: 0, | ||
bottom: 0, | ||
width: 0, | ||
height: 0, | ||
}; | ||
}; | ||
|
||
export { useRect as getRect }; | ||
|
||
export default useRect; |
22 changes: 22 additions & 0 deletions
22
packages/ims-view-pc/src/components/Sticky/hooks/useRefState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; | ||
import { useCallback, useRef, useState } from 'react'; | ||
import { isFunction } from './utils'; | ||
|
||
type StateType<T> = T | (() => T); | ||
|
||
export default function useRefState<T>( | ||
initialState: StateType<T>, | ||
): [T, Dispatch<SetStateAction<T>>, MutableRefObject<T>] { | ||
const [state, setState] = useState<T>(initialState); | ||
const ref = useRef(state); | ||
const setRafState = useCallback( | ||
(patch) => { | ||
setState((prevState) => { | ||
// eslint-disable-next-line no-return-assign | ||
return (ref.current = isFunction(patch) ? patch(prevState) : patch); | ||
}); | ||
}, | ||
[state], | ||
); | ||
return [state, setRafState, ref]; | ||
} |
52 changes: 52 additions & 0 deletions
52
packages/ims-view-pc/src/components/Sticky/hooks/useScrollParent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* eslint-disable no-param-reassign */ | ||
import { MutableRefObject, useEffect, useState } from 'react'; | ||
import { BasicTarget, getTargetElement, inBrowser } from './utils'; | ||
|
||
type ScrollElement = Element | HTMLElement | Window; | ||
|
||
const overflowScrollReg = /scroll|auto/i; | ||
const defaultRoot = inBrowser ? window : undefined; | ||
|
||
function isElement(node: Element) { | ||
const ELEMENT_NODE_TYPE = 1; | ||
return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE; | ||
} | ||
|
||
export function getScrollParent(el: Element, root: ScrollElement = defaultRoot): ScrollElement { | ||
if (root === undefined) { | ||
root = window; | ||
} | ||
let node = el; | ||
while (node && node !== root && isElement(node)) { | ||
const { overflowY } = window.getComputedStyle(node); | ||
if (overflowScrollReg.test(overflowY)) { | ||
if (node.tagName !== 'BODY') { | ||
return node; | ||
} | ||
|
||
const htmlOverflowY = window.getComputedStyle(node.parentNode as Element).overflowY; | ||
if (overflowScrollReg.test(htmlOverflowY)) { | ||
return node; | ||
} | ||
} | ||
node = node.parentNode as Element; | ||
} | ||
return root; | ||
} | ||
|
||
function useScrollParent( | ||
el: BasicTarget<HTMLElement | Element | Window | Document>, | ||
): Element | Window { | ||
const [scrollParent, setScrollParent] = useState<Element | Window>(); | ||
|
||
useEffect(() => { | ||
if (el) { | ||
const element = getTargetElement(el) as Element; | ||
setScrollParent(getScrollParent(element)); | ||
} | ||
}, []); | ||
|
||
return scrollParent; | ||
} | ||
|
||
export default useScrollParent; |
Oops, something went wrong.