Teact: Introduce extra classes manipulation

This commit is contained in:
Alexander Zinchuk 2023-04-15 13:50:13 +02:00
parent 18fe9abbc2
commit 289694c13b
4 changed files with 99 additions and 41 deletions

View File

@ -638,8 +638,7 @@ export type ApiUpdate = (
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiRequestInitApi
ApiRequestInitApi
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -1,7 +1,9 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { useLayoutEffect, useRef } from '../../lib/teact/teact'; import React, { useLayoutEffect, useRef } from '../../lib/teact/teact';
import { addExtraClass, removeExtraClass, toggleExtraClass } from '../../lib/teact/teact-dom';
import { getGlobal } from '../../global'; import { getGlobal } from '../../global';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import { ANIMATION_LEVEL_MIN } from '../../config'; import { ANIMATION_LEVEL_MIN } from '../../config';
@ -36,10 +38,10 @@ export type TransitionProps = {
children: React.ReactNode | ChildrenFn; children: React.ReactNode | ChildrenFn;
}; };
const classNames = { const FALLBACK_ANIMATION_END = 1000;
const CLASSES = {
active: 'Transition__slide--active', active: 'Transition__slide--active',
}; };
const FALLBACK_ANIMATION_END = 1000;
const Transition: FC<TransitionProps> = ({ const Transition: FC<TransitionProps> = ({
ref, ref,
@ -92,21 +94,21 @@ const Transition: FC<TransitionProps> = ({
} }
const container = containerRef.current!; const container = containerRef.current!;
const childElements = container.children; const childElements = container.children;
if (childElements.length === 1 && !activeKeyChanged) { if (childElements.length === 1 && !activeKeyChanged) {
const firstChild = childElements[0] as HTMLElement;
if (name.startsWith('slide-optimized')) { if (name.startsWith('slide-optimized')) {
(childElements[0] as HTMLElement).style.transition = 'none'; firstChild.style.transition = 'none';
(childElements[0] as HTMLElement).style.transform = 'translate3d(0, 0, 0)'; firstChild.style.transform = 'translate3d(0, 0, 0)';
} }
childElements[0].classList.add(classNames.active); addExtraClass(firstChild, CLASSES.active);
return; return;
} }
const childNodes = Array.from(container.childNodes); const childNodes = Array.from(container.childNodes);
if (!activeKeyChanged || !childNodes.length) { if (!activeKeyChanged || !childNodes.length) {
return; return;
} }
@ -142,14 +144,16 @@ const Transition: FC<TransitionProps> = ({
return; return;
} }
container.classList.remove('animating'); removeExtraClass(container, 'animating');
container.classList.toggle('backwards', isBackwards); toggleExtraClass(container, 'backwards', isBackwards);
if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) { if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) {
childNodes.forEach((node, i) => { childNodes.forEach((node, i) => {
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
node.classList.remove('from', 'through', 'to'); removeExtraClass(node, 'from');
node.classList.toggle(classNames.active, i === activeIndex); removeExtraClass(node, 'through');
removeExtraClass(node, 'to');
toggleExtraClass(node, CLASSES.active, i === activeIndex);
} }
}); });
@ -160,19 +164,19 @@ const Transition: FC<TransitionProps> = ({
childNodes.forEach((node, i) => { childNodes.forEach((node, i) => {
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
node.classList.remove(classNames.active); removeExtraClass(node, CLASSES.active);
node.classList.toggle('from', i === prevActiveIndex);
node.classList.toggle('through', ( toggleExtraClass(node, 'from', i === prevActiveIndex);
(i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex) const isThrough = (i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex);
)); toggleExtraClass(node, 'through', isThrough);
node.classList.toggle('to', i === activeIndex); toggleExtraClass(node, 'to', i === activeIndex);
} }
}); });
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
requestAnimationFrame(() => { requestAnimationFrame(() => {
container.classList.add('animating'); addExtraClass(container, 'animating');
onStart?.(); onStart?.();
@ -182,17 +186,20 @@ const Transition: FC<TransitionProps> = ({
return; return;
} }
container.classList.remove('animating', 'backwards'); removeExtraClass(container, 'animating');
removeExtraClass(container, 'backwards');
childNodes.forEach((node, i) => { childNodes.forEach((node, i) => {
if (node instanceof HTMLElement) { if (node instanceof HTMLElement) {
node.classList.remove('from', 'through', 'to'); removeExtraClass(node, 'from');
node.classList.toggle(classNames.active, i === activeIndex); removeExtraClass(node, 'through');
removeExtraClass(node, 'to');
toggleExtraClass(node, CLASSES.active, i === activeIndex);
} }
}); });
if (shouldRestoreHeight) { if (shouldRestoreHeight) {
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`); const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`);
if (activeElement) { if (activeElement) {
activeElement.style.height = 'auto'; activeElement.style.height = 'auto';
@ -237,7 +244,7 @@ const Transition: FC<TransitionProps> = ({
useLayoutEffect(() => { useLayoutEffect(() => {
if (shouldRestoreHeight) { if (shouldRestoreHeight) {
const container = containerRef.current!; const container = containerRef.current!;
const activeElement = container.querySelector<HTMLDivElement>(`.${classNames.active}`) const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`)
|| container.querySelector<HTMLDivElement>('.from'); || container.querySelector<HTMLDivElement>('.from');
const clientHeight = activeElement?.clientHeight; const clientHeight = activeElement?.clientHeight;
if (!clientHeight) { if (!clientHeight) {
@ -250,6 +257,7 @@ const Transition: FC<TransitionProps> = ({
} }
}, [shouldRestoreHeight, children]); }, [shouldRestoreHeight, children]);
const asFastList = !renderCount;
const renders = rendersRef.current; const renders = rendersRef.current;
const renderKeys = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number); const renderKeys = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number);
const contents = renderKeys.map((key) => { const contents = renderKeys.map((key) => {
@ -273,7 +281,7 @@ const Transition: FC<TransitionProps> = ({
ref={containerRef} ref={containerRef}
id={id} id={id}
className={buildClassName('Transition', className, name)} className={buildClassName('Transition', className, name)}
teactFastList={!renderCount} teactFastList={asFastList}
> >
{contents} {contents}
</div> </div>
@ -303,11 +311,11 @@ function performSlideOptimized(
if (animationLevel === ANIMATION_LEVEL_MIN) { if (animationLevel === ANIMATION_LEVEL_MIN) {
fromSlide.style.transition = 'none'; fromSlide.style.transition = 'none';
fromSlide.style.transform = ''; fromSlide.style.transform = '';
fromSlide.classList.remove(classNames.active); removeExtraClass(fromSlide, CLASSES.active);
toSlide.style.transition = 'none'; toSlide.style.transition = 'none';
toSlide.style.transform = 'translate3d(0, 0, 0)'; toSlide.style.transform = 'translate3d(0, 0, 0)';
toSlide.classList.add(classNames.active); addExtraClass(toSlide, CLASSES.active);
cleanup(); cleanup();
@ -337,8 +345,8 @@ function performSlideOptimized(
toSlide.style.transition = ''; toSlide.style.transition = '';
toSlide.style.transform = 'translate3d(0, 0, 0)'; toSlide.style.transform = 'translate3d(0, 0, 0)';
fromSlide.classList.remove(classNames.active); removeExtraClass(fromSlide, CLASSES.active);
toSlide.classList.add(classNames.active); addExtraClass(toSlide, CLASSES.active);
waitForTransitionEnd(fromSlide, () => { waitForTransitionEnd(fromSlide, () => {
if (activeKey !== currentKeyRef.current) { if (activeKey !== currentKeyRef.current) {

View File

@ -43,6 +43,8 @@ const MAPPED_ATTRIBUTES: { [k: string]: string } = {
const INDEX_KEY_PREFIX = '__indexKey#'; const INDEX_KEY_PREFIX = '__indexKey#';
const headsByElement = new WeakMap<HTMLElement, VirtualDomHead>(); const headsByElement = new WeakMap<HTMLElement, VirtualDomHead>();
const extraClasses = new WeakMap<HTMLElement, Set<string>>();
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_virtualTreeSize = 1; let DEBUG_virtualTreeSize = 1;
@ -319,15 +321,18 @@ function remount(
} }
} }
export function unmountRealTree($element: VirtualElement) { function unmountRealTree($element: VirtualElement) {
if (isComponentElement($element)) { if (isComponentElement($element)) {
unmountComponent($element.componentInstance); unmountComponent($element.componentInstance);
} else { } else if (!isFragmentElement($element)) {
if (isTagElement($element)) { if (isTagElement($element)) {
if ($element.target) { const { target } = $element;
removeAllDelegatedListeners($element.target as HTMLElement);
if ($element.props.ref?.current === $element.target) { if (target) {
extraClasses.delete(target);
removeAllDelegatedListeners(target);
if ($element.props.ref?.current === target) {
$element.props.ref.current = undefined; $element.props.ref.current = undefined;
} }
} }
@ -629,10 +634,8 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag,
} }
function setAttribute(element: HTMLElement, key: string, value: any) { function setAttribute(element: HTMLElement, key: string, value: any) {
// An optimization attempt
if (key === 'className') { if (key === 'className') {
element.className = value; updateClassName(element, value);
// An optimization attempt
} else if (key === 'value') { } else if (key === 'value') {
const inputEl = element as HTMLInputElement; const inputEl = element as HTMLInputElement;
@ -668,7 +671,7 @@ function setAttribute(element: HTMLElement, key: string, value: any) {
function removeAttribute(element: HTMLElement, key: string, value: any) { function removeAttribute(element: HTMLElement, key: string, value: any) {
if (key === 'className') { if (key === 'className') {
element.className = ''; updateClassName(element, '');
} else if (key === 'value') { } else if (key === 'value') {
(element as HTMLInputElement).value = ''; (element as HTMLInputElement).value = '';
} else if (key === 'style') { } else if (key === 'style') {
@ -682,6 +685,55 @@ function removeAttribute(element: HTMLElement, key: string, value: any) {
} }
} }
function updateClassName(element: HTMLElement, value: string) {
const extra = extraClasses.get(element);
if (!extra) {
element.className = value;
return;
}
const extraArray = Array.from(extra);
if (value) {
extraArray.push(value);
}
element.className = extraArray.join(' ');
}
export function addExtraClass(element: HTMLElement, className: string) {
element.classList.add(className);
const classList = extraClasses.get(element);
if (classList) {
classList.add(className);
} else {
extraClasses.set(element, new Set([className]));
}
}
export function removeExtraClass(element: HTMLElement, className: string) {
element.classList.remove(className);
const classList = extraClasses.get(element);
if (classList) {
classList.delete(className);
if (!classList.size) {
extraClasses.delete(element);
}
}
}
export function toggleExtraClass(element: HTMLElement, className: string, force?: boolean) {
element.classList.toggle(className, force);
if (element.classList.contains(className)) {
addExtraClass(element, className);
} else {
removeExtraClass(element, className);
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomHead) { function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomHead) {
DEBUG_virtualTreeSize += $current.children.length; DEBUG_virtualTreeSize += $current.children.length;

View File

@ -35,7 +35,7 @@ interface VirtualElementText {
export interface VirtualElementTag { export interface VirtualElementTag {
type: VirtualElementTypesEnum.Tag; type: VirtualElementTypesEnum.Tag;
target?: Node; target?: HTMLElement;
tag: string; tag: string;
props: Props; props: Props;
children: VirtualElementChildren; children: VirtualElementChildren;
@ -50,7 +50,6 @@ export interface VirtualElementComponent {
export interface VirtualElementFragment { export interface VirtualElementFragment {
type: VirtualElementTypesEnum.Fragment; type: VirtualElementTypesEnum.Fragment;
target?: Node;
children: VirtualElementChildren; children: VirtualElementChildren;
} }