Skip to content

Commit 35eeb89

Browse files
authored
feat: Support onVisibleChanged (#7)
* feat: support onVisibleChanged * wrap an useState * fix trigger visible change logic * fix test case
1 parent 793429a commit 35eeb89

10 files changed

Lines changed: 148 additions & 60 deletions

File tree

examples/CSSMotion.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ class Demo extends React.Component<{}, DemoState> {
140140
onLeaveActive={this.onCollapse}
141141
onEnterEnd={this.skipColorTransition}
142142
onLeaveEnd={this.skipColorTransition}
143+
onVisibleChanged={(visible) => {
144+
console.log('Visible Changed:', visible);
145+
}}
143146
>
144147
{({ style, className }, ref) => (
145148
<Div

examples/CSSMotionList.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Demo extends React.Component<{}, DemoState> {
3333
}
3434
}
3535

36-
keyList = keyList.map(key => {
36+
keyList = keyList.map((key) => {
3737
if (key === 3) {
3838
return { key, background: 'orange' };
3939
}
@@ -92,6 +92,9 @@ class Demo extends React.Component<{}, DemoState> {
9292
onAppearStart={this.onCollapse}
9393
onEnterStart={this.onCollapse}
9494
onLeaveActive={this.onCollapse}
95+
onVisibleChanged={(changedVisible, info) => {
96+
console.log('Visible Changed >>>', changedVisible, info);
97+
}}
9598
>
9699
{({ key, background, className, style }) => {
97100
return (

src/CSSMotion.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export interface CSSMotionProps {
6767
onEnterEnd?: MotionEndEventHandler;
6868
onLeaveEnd?: MotionEndEventHandler;
6969

70+
// Special
71+
/** This will always trigger after final visible changed. Even if no motion configured. */
72+
onVisibleChanged?: (visible: boolean) => void;
73+
7074
internalRef?: React.Ref<any>;
7175

7276
children?: (

src/CSSMotionList.tsx

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
parseKeys,
1212
KeyObject,
1313
} from './util/diff';
14-
import { ListMotionEndEventHandler } from './interface';
1514

1615
const MOTION_PROP_NAMES = [
1716
'eventProps',
@@ -36,16 +35,24 @@ const MOTION_PROP_NAMES = [
3635
'onLeaveEnd',
3736
];
3837

39-
export interface CSSMotionListProps extends Omit<CSSMotionProps, 'onLeaveEnd'> {
38+
export interface CSSMotionListProps
39+
extends Omit<CSSMotionProps, 'onVisibleChanged'> {
4040
keys: (React.Key | { key: React.Key; [name: string]: any })[];
4141
component?: string | React.ComponentType | false;
42-
onLeaveEnd?: ListMotionEndEventHandler;
42+
43+
/** This will always trigger after final visible changed. Even if no motion configured. */
44+
onVisibleChanged?: (visible: boolean, info: { key: React.Key }) => void;
4345
}
4446

4547
export interface CSSMotionListState {
4648
keyEntities: KeyObject[];
4749
}
4850

51+
/**
52+
* Generate a CSSMotionList component with config
53+
* @param transitionSupport No need since CSSMotionList no longer depends on transition support
54+
* @param CSSMotion CSSMotion component
55+
*/
4956
export function genCSSMotionList(
5057
transitionSupport: boolean,
5158
CSSMotion = OriginCSSMotion,
@@ -67,31 +74,11 @@ export function genCSSMotionList(
6774
{ keyEntities }: CSSMotionListState,
6875
) {
6976
const parsedKeyObjects = parseKeys(keys);
70-
71-
// Always as keep when motion not support
72-
if (!transitionSupport) {
73-
return {
74-
keyEntities: parsedKeyObjects.map(obj => ({
75-
...obj,
76-
status: STATUS_KEEP,
77-
})),
78-
};
79-
}
80-
8177
const mixedKeyEntities = diffKeys(keyEntities, parsedKeyObjects);
8278

83-
const keyEntitiesLen = keyEntities.length;
8479
return {
85-
keyEntities: mixedKeyEntities.filter(entity => {
86-
// IE 9 not support Array.prototype.find
87-
let prevEntity = null;
88-
for (let i = 0; i < keyEntitiesLen; i += 1) {
89-
const currentEntity = keyEntities[i];
90-
if (currentEntity.key === entity.key) {
91-
prevEntity = currentEntity;
92-
break;
93-
}
94-
}
80+
keyEntities: mixedKeyEntities.filter((entity) => {
81+
const prevEntity = keyEntities.find(({ key }) => entity.key === key);
9582

9683
// Remove if already mark as removed
9784
if (
@@ -108,7 +95,7 @@ export function genCSSMotionList(
10895

10996
removeKey = (removeKey: React.Key) => {
11097
this.setState(({ keyEntities }) => ({
111-
keyEntities: keyEntities.map(entity => {
98+
keyEntities: keyEntities.map((entity) => {
11299
if (entity.key !== removeKey) return entity;
113100
return {
114101
...entity,
@@ -120,12 +107,17 @@ export function genCSSMotionList(
120107

121108
render() {
122109
const { keyEntities } = this.state;
123-
const { component, children, onLeaveEnd, ...restProps } = this.props;
110+
const {
111+
component,
112+
children,
113+
onVisibleChanged,
114+
...restProps
115+
} = this.props;
124116

125117
const Component = component || React.Fragment;
126118

127119
const motionProps: CSSMotionProps = {};
128-
MOTION_PROP_NAMES.forEach(prop => {
120+
MOTION_PROP_NAMES.forEach((prop) => {
129121
motionProps[prop] = restProps[prop];
130122
delete restProps[prop];
131123
});
@@ -141,11 +133,12 @@ export function genCSSMotionList(
141133
key={eventProps.key}
142134
visible={visible}
143135
eventProps={eventProps}
144-
onLeaveEnd={(...args) => {
145-
if (onLeaveEnd) {
146-
onLeaveEnd(...args, { key: eventProps.key });
136+
onVisibleChanged={(changedVisible) => {
137+
onVisibleChanged?.(changedVisible, { key: eventProps.key });
138+
139+
if (!changedVisible) {
140+
this.removeKey(eventProps.key);
147141
}
148-
this.removeKey(eventProps.key);
149142
}}
150143
>
151144
{children}

src/hooks/useState.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useEffect, useState, useRef } from 'react';
2+
3+
export default function useMountStatus<T>(
4+
defaultValue?: T,
5+
): [T, (next: T | (() => T)) => void] {
6+
const destroyRef = useRef(false);
7+
const [val, setVal] = useState<T>(defaultValue);
8+
9+
function setValue(next: T | (() => T)) {
10+
if (!destroyRef.current) {
11+
setVal(next);
12+
}
13+
}
14+
15+
useEffect(
16+
() => () => {
17+
destroyRef.current = true;
18+
},
19+
[],
20+
);
21+
22+
return [val, setValue];
23+
}

src/hooks/useStatus.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { useState, useRef, useEffect } from 'react';
2+
import { useRef, useEffect } from 'react';
33
import {
44
STATUS_APPEAR,
55
STATUS_NONE,
@@ -14,11 +14,11 @@ import {
1414
MotionPrepareEventHandler,
1515
StepStatus,
1616
} from '../interface';
17+
import useState from './useState';
1718
import { CSSMotionProps } from '../CSSMotion';
1819
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
1920
import useStepQueue, { DoStep, SkipStep, isActive } from './useStepQueue';
2021
import useDomMotionEvents from './useDomMotionEvents';
21-
// import useFrameStep, { StepMap, StepCell } from './useFrameStep';
2222

2323
export default function useStatus(
2424
supportMotion: boolean,
@@ -42,10 +42,11 @@ export default function useStatus(
4242
onAppearEnd,
4343
onEnterEnd,
4444
onLeaveEnd,
45+
onVisibleChanged,
4546
}: CSSMotionProps,
4647
): [MotionStatus, StepStatus, React.CSSProperties, boolean] {
4748
// Used for outer render usage to avoid `visible: false & status: none` to render nothing
48-
const [asyncVisible, setAsyncVisible] = useState(visible);
49+
const [asyncVisible, setAsyncVisible] = useState<boolean>();
4950
const [status, setStatus] = useState<MotionStatus>(STATUS_NONE);
5051
const [style, setStyle] = useState<React.CSSProperties | undefined>(null);
5152

@@ -220,6 +221,13 @@ export default function useStatus(
220221
[],
221222
);
222223

224+
// Trigger `onVisibleChanged`
225+
useEffect(() => {
226+
if (asyncVisible !== undefined && status === STATUS_NONE) {
227+
onVisibleChanged?.(asyncVisible);
228+
}
229+
}, [asyncVisible, status]);
230+
223231
// ============================ Styles ============================
224232
let mergedStyle = style;
225233
if (eventHandlers[STEP_PREPARE] && step === STEP_START) {
@@ -229,5 +237,5 @@ export default function useStatus(
229237
};
230238
}
231239

232-
return [status, step, mergedStyle, asyncVisible];
240+
return [status, step, mergedStyle, asyncVisible ?? visible];
233241
}

src/hooks/useStepQueue.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ export default (
4040
}
4141

4242
useIsomorphicLayoutEffect(() => {
43-
// if (step === STEP_START) {
44-
// return;
45-
// }
46-
4743
if (step !== STEP_NONE && step !== STEP_ACTIVATED) {
4844
const index = STEP_QUEUE.indexOf(step);
4945
const nextStep = STEP_QUEUE[index + 1];
@@ -55,7 +51,7 @@ export default (
5551
setStep(nextStep);
5652
} else {
5753
// Do as frame for step update
58-
nextFrame(info => {
54+
nextFrame((info) => {
5955
function doNext() {
6056
// Skip since current queue is ood
6157
if (info.isCanceled()) return;

src/interface.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,3 @@ export type MotionEndEventHandler = (
3737
element: HTMLElement,
3838
event: MotionEvent,
3939
) => boolean | void;
40-
41-
export type ListMotionEndEventHandler = (
42-
element: HTMLElement,
43-
event: MotionEvent,
44-
info: { key: React.Key },
45-
) => void;

tests/CSSMotion.spec.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,10 @@ describe('CSSMotion', () => {
246246
});
247247
}
248248

249-
test('without ref', React.forwardRef(props => <div {...props} />));
249+
test(
250+
'without ref',
251+
React.forwardRef((props) => <div {...props} />),
252+
);
250253

251254
test(
252255
'FC with ref',
@@ -506,7 +509,7 @@ describe('CSSMotion', () => {
506509
let lockResolve: Function;
507510
const onAppearPrepare = jest.fn(
508511
() =>
509-
new Promise(resolve => {
512+
new Promise((resolve) => {
510513
lockResolve = resolve;
511514
}),
512515
);
@@ -538,8 +541,6 @@ describe('CSSMotion', () => {
538541
wrapper.update();
539542
});
540543

541-
console.log(wrapper.html());
542-
543544
expect(
544545
wrapper.find('.motion-box').hasClass('bamboo-appear-prepare'),
545546
).toBeFalsy();

0 commit comments

Comments
 (0)