Skip to content

Commit

Permalink
fix(affix): fix affix scroll out of container with target is non-window
Browse files Browse the repository at this point in the history
  • Loading branch information
xiefenga committed Dec 11, 2023
1 parent 0e25a55 commit 15dbfad
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 18 deletions.
3 changes: 2 additions & 1 deletion docs/src/app/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ a {

td {
max-width: 240px;
padding: 10px 16px;
padding: 8px 16px;
line-height: 2;
text-align: left;
border-top: 1px solid var(--yike-line-color-1);
transition: border 0.3s;
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/demos/affix/with-target.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default () => {
}}
>
<div style={{ height: 100 }} />
<Affix offset={10} target={() => targetRef.current}>
<Affix offset={10} target={() => targetRef.current} targetContainer={() => window}>
<Button>Affix top 10px in container</Button>
</Affix>
</div>
Expand Down
13 changes: 7 additions & 6 deletions docs/src/content/docs/components/affix.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ order: 2

## API

| 参数 | 描述 | 类型 | 默认值 |
| -------- | ------------------ | ----------------------------- | -------------- |
| offset | 偏移量 | `number` | 0 |
| position | 偏移方向 | `'top' \| 'bottom'` | `'top'` |
| target | 滚动容器 | `() => HTMLElement` | `() => window` |
| onChange | 固定状态改变时触发 | `(affixed?: boolean) => void` | - |
| 参数 | 描述 | 类型 | 默认值 |
|-----------------|-----------------------------------------------------------------|-------------------------------|----------------|
| offset | 偏移量 | `number` | 0 |
| position | 偏移方向 | `'top' \| 'bottom'` | `'top'` |
| target | 滚动容器 | `() => HTMLElement \| Window` | `() => window` |
| targetContainer | `target` 的外层滚动元素,用于解决 `target` 为非 `window` 时,外层元素滚动会导致固钉跑出容器问题。 | `() => HTMLElement \| Window` | - |
| onChange | 固定状态改变时触发 | `(affixed?: boolean) => void` | - |
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ export const useNormalizedContainer = (container: ComponentContainer) => {
});
return () => clearTimeout(timer);
}, [container]);
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(() => normalizedContainer, [container, normalizedContainer]);
return React.useCallback(() => normalizedContainer, [normalizedContainer]);
};
51 changes: 43 additions & 8 deletions packages/yike-design/src/components/affix/Affix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface AffixProps {
offset?: number;
position?: 'top' | 'bottom';
target?: ComponentContainer;
targetContainer?: ComponentContainer;
onChange?: (affixed: boolean) => void;
children: React.ReactNode;
}
Expand All @@ -25,10 +26,12 @@ interface AffixRef {
}

const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
const { offset = 0, position = 'top', target = getDefaultContainer, onChange } = props;
const { offset = 0, position = 'top', target = getDefaultContainer, onChange, targetContainer = target } = props;

const getTargetFunc = useNormalizedContainer(target);

const getTargetContainerFunc = useNormalizedContainer(targetContainer);

const fixedNodeRef = React.useRef<HTMLDivElement>(null);
const affixPlaceholderRef = React.useRef<HTMLDivElement>(null);

Expand All @@ -37,15 +40,25 @@ const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {

const measurePosition = React.useCallback(() => {
const target = getTargetFunc();
if (!affixPlaceholderRef.current || !target) {
if (!affixPlaceholderRef.current || !target || !fixedNodeRef.current) {
return;
}
const fixedNodeRect = fixedNodeRef.current.getBoundingClientRect();
const placeholderRect = affixPlaceholderRef.current.getBoundingClientRect();
const { top: currentScreenTop } = placeholderRect;
const currentScreenBottom = window.innerHeight - placeholderRect.bottom;
const placeholderScreenTop = placeholderRect.top;
const placeholderScreenBottom = window.innerHeight - placeholderRect.bottom;

const fixedNodeScreenTop = fixedNodeRect.top;
const fixedNodeScreenBottom = window.innerHeight - fixedNodeRect.bottom;

if (position === 'top') {
const fixedTop = getFixedTop(target, offset);
if (currentScreenTop <= fixedTop && !affixStyle) {
if (affixStyle && fixedNodeScreenTop !== fixedTop) {
setAffixStyle({
...affixStyle,
top: fixedTop,
});
} else if (placeholderScreenTop <= fixedTop && !affixStyle) {
setAffixStyle({
position: 'fixed',
top: fixedTop,
Expand All @@ -55,14 +68,19 @@ const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
height: fixedNodeRef.current?.offsetHeight,
});
onChange?.(true);
} else if (currentScreenTop > fixedTop && affixStyle) {
} else if (placeholderScreenTop > fixedTop && affixStyle) {
setAffixStyle(undefined);
setPlaceholderStyle(undefined);
onChange?.(false);
}
} else {
const fixedBottom = getFixedBottom(target, offset);
if (currentScreenBottom <= fixedBottom && !affixStyle) {
if (affixStyle && fixedNodeScreenBottom !== fixedBottom) {
setAffixStyle({
...affixStyle,
bottom: fixedBottom,
});
} else if (placeholderScreenBottom <= fixedBottom && !affixStyle) {
setAffixStyle({
position: 'fixed',
bottom: fixedBottom,
Expand All @@ -72,7 +90,7 @@ const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
height: fixedNodeRef.current?.offsetHeight,
});
onChange?.(true);
} else if (currentScreenBottom > fixedBottom && affixStyle) {
} else if (placeholderScreenBottom > fixedBottom && affixStyle) {
setAffixStyle(undefined);
setPlaceholderStyle(undefined);
onChange?.(false);
Expand Down Expand Up @@ -100,6 +118,23 @@ const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
};
}, [getTargetFunc, updatePosition]);

React.useEffect(() => {
const target = getTargetFunc();
const targetContainer = getTargetContainerFunc();
if (!target || !targetContainer || target === targetContainer) {
return;
}
TRIGGER_EVENTS.forEach(event => {
targetContainer.addEventListener(event, updatePosition);
});
return () => {
TRIGGER_EVENTS.forEach(event => {
targetContainer.removeEventListener(event, updatePosition);
});
updatePosition.cancel();
};
}, [getTargetFunc, getTargetContainerFunc, updatePosition]);

React.useEffect(() => {
measurePosition();
}, [measurePosition]);
Expand Down

0 comments on commit 15dbfad

Please sign in to comment.