Skip to content

Commit

Permalink
Merge pull request #108 from js-tool-pack/transition-group-tag-null
Browse files Browse the repository at this point in the history
TransitionGroup tag receive null
  • Loading branch information
mengxinssfd authored Jun 1, 2024
2 parents dfab3fb + 73244ce commit f3c4f39
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 138 deletions.
14 changes: 6 additions & 8 deletions packages/components/src/transition-group/TransitionGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { TransitionGroupProps } from './transition-group.types';
import { useChildMap, useWrapper, useFlips } from './hooks';
import { RequiredPart } from '@tool-pack/types';
import { useForwardRef } from '@pkg/shared';
import React from 'react';
import { forwardRef, memo } from 'react';
import type { FC } from 'react';

/**
* v1 版有部分 bug 不好解决。
Expand All @@ -19,23 +19,21 @@ const defaultProps = {
tag: 'div',
} satisfies TransitionGroupProps;

const TransitionGroup: React.FC<TransitionGroupProps> = React.forwardRef<
const TransitionGroup: FC<TransitionGroupProps> = forwardRef<
HTMLDivElement,
TransitionGroupProps
>((props, _ref) => {
>((props, ref) => {
const { children, name, ...rest } = props as RequiredPart<
TransitionGroupProps,
keyof typeof defaultProps
>;

const ref = useForwardRef(_ref);
const childMap = useChildMap(children, name);
const wrapper = useWrapper(childMap, rest, ref);
useFlips(ref, childMap, name);
useFlips(childMap, name);
return wrapper;
});

TransitionGroup.defaultProps = defaultProps;
TransitionGroup.displayName = 'TransitionGroup';

export default React.memo(TransitionGroup);
export default memo(TransitionGroup);
66 changes: 66 additions & 0 deletions packages/components/src/transition-group/demo/tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* title: 标签
* description: 默认标签为 div,可设置 tag 为其它标签,当 tag 为 null 时,移除包裹元素
*/

import {
TransitionGroup,
Transition,
Button,
Space,
} from '@tool-pack/react-ui';
import React, { useCallback, useState, useRef } from 'react';
import styles from './list.module.scss';

const App: React.FC = () => {
const [, update] = useState({});
const forceUpdate = useCallback(() => update({}), []);

const children = useRef<number[]>([...Array.from({ length: 10 }).keys()]);

const index = useRef(children.current.length);
function addChild() {
const list = children.current;
const splice = list.splice(~~(Math.random() * list.length), list.length);
list.push(index.current);
list.push(...splice);
forceUpdate();
index.current++;
}
function removeChild(item: number) {
const index = children.current.indexOf(item);
if (index === -1) return;
children.current.splice(index, 1);
forceUpdate();
}
function removeRandomChild() {
removeChild(children.current[~~(Math.random() * children.current.length)]!);
}

return (
<div className={styles['root']}>
<Space style={{ justifyContent: 'center' }}>
<Button onClick={addChild} type="primary">
添加
</Button>
<Button onClick={removeRandomChild} type="warning" plain>
移除
</Button>
</Space>
<br />
<div className="group-container">
<TransitionGroup name="group" tag={null}>
{children.current.map((item) => {
return (
<Transition key={item}>
<div>{item}</div>
</Transition>
);
})}
</TransitionGroup>
</div>
</div>
);
};

export default App;
194 changes: 127 additions & 67 deletions packages/components/src/transition-group/hooks/useChildMap.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import {
isValidElement,
cloneElement,
useEffect,
useState,
Children,
} from 'react';
import { type TransitionProps, transitionCBAdapter } from '@pkg/components';
import type { ChildMap } from '../transition-group.types';
import React, { useEffect, useState } from 'react';
import { useIsInitDep } from '@pkg/shared';
export function useChildMap(children: React.ReactNode, name: string) {
import type { RefAttributes, ReactElement, ReactNode, Key } from 'react';
import type { ChildMapValue, ChildMap } from '../transition-group.types';
import { useIsInitDep, forwardRefs } from '@pkg/shared';

export function useChildMap(children: ReactNode, name: string): ChildMap {
const isInit = useIsInitDep(children);

const [childMap, setChildMap] = useState((): ChildMap => {
Expand All @@ -16,7 +24,7 @@ export function useChildMap(children: React.ReactNode, name: string) {
});
});

const onLeaved = (key: React.Key) => {
const onLeaved = (key: Key) => {
// leave 可能会丢失
setChildMap((prevChildren) => {
const map = new Map(prevChildren);
Expand All @@ -34,66 +42,28 @@ export function useChildMap(children: React.ReactNode, name: string) {
}

const nextChildMap = (
children: React.ReactNode,
children: ReactNode,
prevChildMap: ChildMap,
name: string,
onLeaved: (key: React.Key) => void,
onLeaved: (key: Key) => void,
): ChildMap => {
const childMap = createMap(children);
return mergeMaps(prevChildMap, childMap, (child, key): React.ReactNode => {
if (!React.isValidElement(child)) return child;

const inNext = childMap.get(key) !== undefined;
const inPrev = prevChildMap.get(key) !== undefined;
const inBoth = inNext && inPrev;

const isAdd = inNext && !inPrev;
const isRemove = !inNext && inPrev;

if (isRemove) {
if (child.props.show === false) return child;
// 因为 remove 了的 child 是不存在于 next 的,所以这个 child 是旧的,是 clone 过的
// tips: 加了 on 就不会等待多个 remove 完才 move,而是 remove 一个 move 一个
return cloneTransition(child, { appear: false, show: false });
}

const on = transitionCBAdapter({
onAfterLeave: () => onLeaved(child.key || ''),
});

if (isAdd) {
// 旧的不存在,所以 child 是新创建的,是未 clone 过的
return cloneTransition(child, {
appear: true,
name: name,
show: true,
on,
});
}

if (inBoth) {
// 两者皆有取最新,所以 child 是新创建的,是未 clone 过的
return cloneTransition(child, {
appear: false,
show: true,
name: name,
on,
});
}
return child;
});
return mergeMaps(prevChildMap, childMap, name, onLeaved);
};

function createMap(
children: React.ReactNode,
callback: (child: React.ReactElement) => React.ReactNode = (v) => v,
children: ReactNode,
callback: (child: ReactElement) => ChildMapValue = (v) => ({
reactEl: v,
ref: null,
}),
): ChildMap {
const map: ChildMap = new Map();
if (!children) return map;

// 如果没有手动添加key, React.Children.map会自动添加key
React.Children.map(children, (c) => c)?.forEach((child) => {
if (!React.isValidElement(child)) return;
// 如果没有手动添加key, Children.map会自动添加key
Children.map(children, (c) => c)?.forEach((child) => {
if (!isValidElement(child)) return;
const key = child.key || '';
if (!key) return;
map.set(key, callback(child));
Expand All @@ -105,14 +75,14 @@ function createMap(
function mergeMaps(
prevMap: ChildMap,
nextMap: ChildMap,
callback: (child: React.ReactNode, key: React.Key) => React.ReactNode,
name: string,
onLeaved: (key: Key) => void,
): ChildMap {
const getValue = (key: React.Key) => nextMap.get(key) ?? prevMap.get(key);

let insertKeys: React.Key[] = [];
const insertKeysMap = new Map<React.Key, typeof insertKeys>();
let insertKeys: Key[] = [];
const insertKeysMap = new Map<Key, typeof insertKeys>();
const result: ChildMap = new Map();

prevMap.forEach((_, key) => {
prevMap.forEach((_, key): void => {
if (nextMap.has(key)) {
if (!insertKeys.length) return;
insertKeysMap.set(key, insertKeys);
Expand All @@ -121,23 +91,95 @@ function mergeMaps(
}
insertKeys.push(key);
});

const result: ChildMap = new Map();
const push = (k: React.Key) => result.set(k, callback(getValue(k), k));
nextMap.forEach((_, key) => {
nextMap.forEach((_, key): void => {
const keys = insertKeysMap.get(key);
if (keys) keys.forEach(push);
push(key);
});
insertKeys.forEach(push);

return result;

function push(k: Key): void {
const value = mergeMapValue(
prevMap.get(k),
nextMap.get(k),
k,
name,
onLeaved,
);
value && result.set(k, value);
}
}

function mergeMapValue(
prevValue: ChildMapValue | undefined,
nextValue: ChildMapValue | undefined,
key: Key,
name: string,
onLeaved: (key: Key) => void,
): ChildMapValue | void {
const inNext = nextValue?.reactEl !== undefined;
const inPrev = prevValue?.reactEl !== undefined;
const inBoth = inNext && inPrev;

const isAdd = inNext && !inPrev;
const isRemove = !inNext && inPrev;

if (isRemove) {
// noinspection PointlessBooleanExpressionJS
if (
(prevValue.reactEl as ReactElement<TransitionProps>).props.show === false
)
return prevValue;
// 因为 remove 了的 child 是不存在于 next 的,所以这个 child 是旧的,是 clone 过的
// tips: 加了 on 就不会等待多个 remove 完才 move,而是 remove 一个 move 一个
return cloneTransition(
prevValue.reactEl,
{ appear: false, show: false },
prevValue.ref,
);
}

const on = transitionCBAdapter({
onAfterLeave: () => onLeaved(key || ''),
});

if (isAdd) {
// 旧的不存在,所以 child 是新创建的,是未 clone 过的
return cloneTransition(
nextValue.reactEl,
{
appear: true,
name: name,
show: true,
on,
},
prevValue?.ref,
);
}

if (inBoth) {
// 两者皆有取最新,所以 child 是新创建的,是未 clone 过的
return cloneTransition(
nextValue.reactEl,
{
appear: false,
show: true,
name: name,
on,
},
prevValue.ref,
);
}
}

function cloneTransition(
transition: React.ReactElement,
transition: ReactElement,
props: Partial<TransitionProps>,
) {
ref: HTMLElement | null = null,
): ChildMapValue {
const result: ChildMapValue = { reactEl: transition, ref };
const _props = { ...props } as TransitionProps;
const nextOn = props.on;
if (nextOn) {
Expand All @@ -147,5 +189,23 @@ function cloneTransition(
nextOn(el, status, lifeCircle);
};
}
return React.cloneElement<TransitionProps>(transition, _props);

result.reactEl = cloneElement<TransitionProps>(
transition,
_props,
cloneChildren(),
);
return result;

function cloneChildren() {
const children = transition.props.children;
return isValidElement(children)
? cloneElement(children as ReactElement, {
ref: (el: HTMLElement) => {
forwardRefs(el, (children as RefAttributes<any>).ref);
el && (result.ref = el);
},
})
: undefined;
}
}
Loading

0 comments on commit f3c4f39

Please sign in to comment.