diff --git a/src/components/App.tsx b/src/components/App.tsx index 4756f9e02..ecedc3ef9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -28,7 +28,7 @@ import LockScreen from './main/LockScreen.async'; import AppInactive from './main/AppInactive'; import Transition from './ui/Transition'; import UiLoader from './common/UiLoader'; -// import Test from './test/TestUpdateRef'; +// import Test from './test/TestSvg'; import styles from './App.module.scss'; diff --git a/src/components/test/TestSvg.tsx b/src/components/test/TestSvg.tsx new file mode 100644 index 000000000..9e016bdfe --- /dev/null +++ b/src/components/test/TestSvg.tsx @@ -0,0 +1,51 @@ +import React, { useState } from '../../lib/teact/teact'; + +export function App() { + const [stateValue, setStateValue] = useState(false); + + return ( +
{ + setStateValue((current) => !current); + }} + > + + + + + + + + + {stateValue && ( + + )} + + + +
+ ); +} + +export default App; diff --git a/src/lib/teact/dom-events.ts b/src/lib/teact/dom-events.ts index f303fba1a..605b7f49d 100644 --- a/src/lib/teact/dom-events.ts +++ b/src/lib/teact/dom-events.ts @@ -1,15 +1,15 @@ import { DEBUG } from '../../config'; type Handler = (e: Event) => void; -type DelegationRegistry = Map; +type DelegationRegistry = Map; const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave', 'load']); const documentEventCounters: Record = {}; const delegationRegistryByEventType: Record = {}; -const delegatedEventTypesByElement = new Map>(); +const delegatedEventTypesByElement = new Map>(); -export function addEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { +export function addEventListener(element: Element, propName: string, handler: Handler, asCapture = false) { const eventType = resolveEventType(propName, element); if (canUseEventDelegation(eventType, element, asCapture)) { addDelegatedListener(eventType, element, handler); @@ -18,7 +18,7 @@ export function addEventListener(element: HTMLElement, propName: string, handler } } -export function removeEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { +export function removeEventListener(element: Element, propName: string, handler: Handler, asCapture = false) { const eventType = resolveEventType(propName, element); if (canUseEventDelegation(eventType, element, asCapture)) { removeDelegatedListener(eventType, element); @@ -27,7 +27,7 @@ export function removeEventListener(element: HTMLElement, propName: string, hand } } -function resolveEventType(propName: string, element: HTMLElement) { +function resolveEventType(propName: string, element: Element) { const eventType = propName .replace(/^on/, '') .replace(/Capture$/, '').toLowerCase(); @@ -54,7 +54,7 @@ function resolveEventType(propName: string, element: HTMLElement) { return eventType; } -function canUseEventDelegation(realEventType: string, element: HTMLElement, asCapture: boolean) { +function canUseEventDelegation(realEventType: string, element: Element, asCapture: boolean) { return ( !asCapture && !NON_BUBBLEABLE_EVENTS.has(realEventType) @@ -63,7 +63,7 @@ function canUseEventDelegation(realEventType: string, element: HTMLElement, asCa ); } -function addDelegatedListener(eventType: string, element: HTMLElement, handler: Handler) { +function addDelegatedListener(eventType: string, element: Element, handler: Handler) { if (!documentEventCounters[eventType]) { documentEventCounters[eventType] = 0; document.addEventListener(eventType, handleEvent); @@ -74,7 +74,7 @@ function addDelegatedListener(eventType: string, element: HTMLElement, handler: documentEventCounters[eventType]++; } -function removeDelegatedListener(eventType: string, element: HTMLElement) { +function removeDelegatedListener(eventType: string, element: Element) { documentEventCounters[eventType]--; if (!documentEventCounters[eventType]) { // Synchronous deletion on 0 will cause perf degradation in the case of 1 element @@ -86,7 +86,7 @@ function removeDelegatedListener(eventType: string, element: HTMLElement) { delegatedEventTypesByElement.get(element)!.delete(eventType); } -export function removeAllDelegatedListeners(element: HTMLElement) { +export function removeAllDelegatedListeners(element: Element) { const eventTypes = delegatedEventTypesByElement.get(element); if (!eventTypes) { return; @@ -101,7 +101,7 @@ function handleEvent(realEvent: Event) { if (events) { let furtherCallsPrevented = false; - let current: HTMLElement = realEvent.target as HTMLElement; + let current: Element = realEvent.target as Element; const stopPropagation = () => { furtherCallsPrevented = true; @@ -138,7 +138,7 @@ function handleEvent(realEvent: Event) { } } - current = current.parentNode as HTMLElement; + current = current.parentNode as Element; } } } @@ -151,7 +151,7 @@ function resolveDelegationRegistry(eventType: string) { return delegationRegistryByEventType[eventType]; } -function resolveDelegatedEventTypes(element: HTMLElement) { +function resolveDelegatedEventTypes(element: Element) { const existing = delegatedEventTypesByElement.get(element); if (existing) { return existing; diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 99dd0ca0b..6c4321d1d 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -32,6 +32,8 @@ interface SelectionState { isCaretAtEnd: boolean; } +type DOMElement = HTMLElement | SVGElement; + const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']); const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; @@ -71,7 +73,7 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) { } function renderWithVirtual( - parentEl: HTMLElement, + parentEl: DOMElement, $current: VirtualElement | undefined, $new: T, $parent: VirtualElementParent | VirtualDomHead, @@ -81,17 +83,22 @@ function renderWithVirtual( nextSibling?: ChildNode; forceMoveToEnd?: boolean; fragment?: DocumentFragment; + isSvg?: true; } = {}, ): T { const { skipComponentUpdate, fragment } = options; - let { nextSibling } = options; + let { nextSibling, isSvg } = options; - const isCurrentComponent = $current && $current.type === VirtualType.Component; - const isNewComponent = $new && $new.type === VirtualType.Component; + const isCurrentComponent = $current?.type === VirtualType.Component; + const isNewComponent = $new?.type === VirtualType.Component; const $newAsReal = $new as VirtualElementReal; - const isCurrentFragment = $current && !isCurrentComponent && $current.type === VirtualType.Fragment; - const isNewFragment = $new && !isNewComponent && $new.type === VirtualType.Fragment; + const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment; + const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment; + + if ($new?.type === VirtualType.Tag && $new.tag === 'svg') { + isSvg = true; + } if ( !skipComponentUpdate @@ -127,7 +134,9 @@ function renderWithVirtual( $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new; } - mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { + nextSibling, fragment, isSvg, + }); } else { const canSetTextContent = !fragment && !nextSibling @@ -139,12 +148,12 @@ function renderWithVirtual( parentEl.textContent = $newAsReal.value; $newAsReal.target = parentEl.firstChild!; } else { - const node = createNode($newAsReal); + const node = createNode($newAsReal, isSvg); $newAsReal.target = node; insertBefore(fragment || parentEl, node, nextSibling); if ($newAsReal.type === VirtualType.Tag) { - setElementRef($newAsReal, node as HTMLElement); + setElementRef($newAsReal, node as DOMElement); } } } @@ -162,14 +171,16 @@ function renderWithVirtual( } remount(parentEl, $current, undefined); - mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { + nextSibling, fragment, isSvg, + }); } else { - const node = createNode($newAsReal); + const node = createNode($newAsReal, isSvg); $newAsReal.target = node; remount(parentEl, $current, node, nextSibling); if ($newAsReal.type === VirtualType.Tag) { - setElementRef($newAsReal, node as HTMLElement); + setElementRef($newAsReal, node as DOMElement); } } } else { @@ -196,14 +207,14 @@ function renderWithVirtual( const $newAsTag = $new as VirtualElementTag; setElementRef($current, undefined); - setElementRef($newAsTag, currentTarget as HTMLElement); + setElementRef($newAsTag, currentTarget as DOMElement); if (nextSibling || options.forceMoveToEnd) { insertBefore(parentEl, currentTarget, nextSibling); } - updateAttributes($current, $newAsTag, currentTarget as HTMLElement); - renderChildren($current, $newAsTag, currentTarget as HTMLElement); + updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg); + renderChildren($current, $newAsTag, currentTarget as DOMElement, undefined, undefined, isSvg); } } } @@ -213,7 +224,7 @@ function renderWithVirtual( } function initComponent( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, index: number, @@ -235,7 +246,7 @@ function updateComponent($current: VirtualElementComponent, $new: VirtualElement } function setupComponentUpdateListener( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, index: number, @@ -255,11 +266,12 @@ function setupComponentUpdateListener( } function mountChildren( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment, options: { nextSibling?: ChildNode; fragment?: DocumentFragment; + isSvg?: true; }, ) { const { children } = $element; @@ -272,13 +284,13 @@ function mountChildren( } } -function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) { +function unmountChildren(parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment) { for (const $child of $element.children) { renderWithVirtual(parentEl, $child, undefined, $element, -1); } } -function createNode($element: VirtualElementReal): Node { +function createNode($element: VirtualElementReal, isSvg?: true): Node { if ($element.type === VirtualType.Empty) { return document.createTextNode(''); } @@ -288,7 +300,7 @@ function createNode($element: VirtualElementReal): Node { } const { tag, props, children } = $element; - const element = document.createElement(tag); + const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag); processControlled(tag, props); @@ -297,7 +309,7 @@ function createNode($element: VirtualElementReal): Node { if (!props.hasOwnProperty(key)) continue; if (props[key] !== undefined) { - setAttribute(element, key, props[key]); + setAttribute(element, key, props[key], isSvg); } } @@ -305,7 +317,7 @@ function createNode($element: VirtualElementReal): Node { for (let i = 0, l = children.length; i < l; i++) { const $child = children[i]; - const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i); + const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i, { isSvg }); if ($renderedChild !== $child) { children[i] = $renderedChild; } @@ -315,7 +327,7 @@ function createNode($element: VirtualElementReal): Node { } function remount( - parentEl: HTMLElement, + parentEl: DOMElement, $current: VirtualElement, node: Node | undefined, componentNextSibling?: ChildNode, @@ -366,7 +378,7 @@ function unmountRealTree($element: VirtualElement) { } } -function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { +function insertBefore(parentEl: DOMElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { if (nextSibling) { parentEl.insertBefore(node, nextSibling); } else { @@ -386,9 +398,10 @@ function getNextSibling($current: VirtualElement): ChildNode | undefined { function renderChildren( $current: VirtualElementParent, $new: VirtualElementParent, - currentEl: HTMLElement, + currentEl: DOMElement, nextSibling?: ChildNode, forceMoveToEnd = false, + isSvg?: true, ) { if (DEBUG) { DEBUG_checkKeyUniqueness($new.children); @@ -419,7 +432,7 @@ function renderChildren( newChildren[i], $new, i, - i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd }, + i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg }, ); if ($renderedChild && $renderedChild !== newChildren[i]) { @@ -434,7 +447,7 @@ function renderChildren( // This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones. // It also allows to selectively move particular preserved nodes within their DOM list. -function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) { +function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: DOMElement) { const currentChildren = $current.children; const newChildren = $new.children; @@ -549,7 +562,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle } function renderFragment( - fragmentIndex: number, fragmentSize: number, parentEl: HTMLElement, $parent: VirtualElementParent, + fragmentIndex: number, fragmentSize: number, parentEl: DOMElement, $parent: VirtualElementParent, ) { const nextSibling = parentEl.childNodes[fragmentIndex]; @@ -576,13 +589,13 @@ function renderFragment( insertBefore(parentEl, fragment, nextSibling); } -function setElementRef($element: VirtualElementTag, htmlElement: HTMLElement | undefined) { +function setElementRef($element: VirtualElementTag, DOMElement: DOMElement | undefined) { const { ref } = $element.props; if (typeof ref === 'object') { - ref.current = htmlElement; + ref.current = DOMElement; } else if (typeof ref === 'function') { - ref(htmlElement); + ref(DOMElement); } } @@ -629,7 +642,7 @@ function processControlled(tag: string, props: AnyLiteral) { }; } -function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { +function processUncontrolledOnMount(element: DOMElement, props: AnyLiteral) { if (!CONTROLLABLE_TAGS.includes(element.tagName)) { return; } @@ -643,7 +656,7 @@ function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { } } -function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: HTMLElement) { +function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, isSvg?: true) { processControlled(element.tagName, $new.props); const currentEntries = Object.entries($current.props); @@ -667,14 +680,14 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, const currentValue = $current.props[key]; if (newValue !== undefined && newValue !== currentValue) { - setAttribute(element, key, newValue); + setAttribute(element, key, newValue, isSvg); } } } -function setAttribute(element: HTMLElement, key: string, value: any) { +function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true) { if (key === 'className') { - updateClassName(element, value); + updateClassName(element, value, isSvg); } else if (key === 'value') { const inputEl = element as HTMLInputElement; @@ -701,14 +714,14 @@ function setAttribute(element: HTMLElement, key: string, value: any) { element.innerHTML = value.__html; } else if (key.startsWith('on')) { addEventListener(element, key, value, key.endsWith('Capture')); - } else if (key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { + } else if (isSvg || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { element.setAttribute(key, value); } else if (!FILTERED_ATTRIBUTES.has(key)) { (element as any)[MAPPED_ATTRIBUTES[key] || key] = value; } } -function removeAttribute(element: HTMLElement, key: string, value: any) { +function removeAttribute(element: DOMElement, key: string, value: any) { if (key === 'className') { updateClassName(element, ''); } else if (key === 'value') { @@ -724,10 +737,16 @@ function removeAttribute(element: HTMLElement, key: string, value: any) { } } -function updateClassName(element: HTMLElement, value: string) { +function updateClassName(element: DOMElement, value: string, isSvg?: true) { + if (isSvg) { + element.setAttribute('class', value); + return; + } + + const htmlElement = element as HTMLElement; const extra = extraClasses.get(element); if (!extra) { - element.className = value; + htmlElement.className = value; return; } @@ -736,10 +755,10 @@ function updateClassName(element: HTMLElement, value: string) { extraArray.push(value); } - element.className = extraArray.join(' '); + htmlElement.className = extraArray.join(' '); } -function updateStyle(element: HTMLElement, value: string) { +function updateStyle(element: DOMElement, value: string) { element.style.cssText = value; const extraObject = extraStyles.get(element); @@ -748,7 +767,7 @@ function updateStyle(element: HTMLElement, value: string) { } } -export function addExtraClass(element: Element, className: string, forceSingle = false) { +export function addExtraClass(element: DOMElement, className: string, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -770,7 +789,7 @@ export function addExtraClass(element: Element, className: string, forceSingle = } } -export function removeExtraClass(element: Element, className: string, forceSingle = false) { +export function removeExtraClass(element: DOMElement, className: string, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -794,7 +813,7 @@ export function removeExtraClass(element: Element, className: string, forceSingl } } -export function toggleExtraClass(element: Element, className: string, force?: boolean, forceSingle = false) { +export function toggleExtraClass(element: DOMElement, className: string, force?: boolean, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -815,12 +834,12 @@ export function toggleExtraClass(element: Element, className: string, force?: bo } } -export function setExtraStyles(element: HTMLElement, styles: Partial & AnyLiteral) { +export function setExtraStyles(element: DOMElement, styles: Partial & AnyLiteral) { extraStyles.set(element, styles); applyExtraStyles(element); } -function applyExtraStyles(element: HTMLElement) { +function applyExtraStyles(element: DOMElement) { const standardStyles = Object.entries(extraStyles.get(element)!).reduce>( (acc, [prop, value]) => { if (prop.startsWith('--')) { diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index b65269259..57ed281d2 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -37,7 +37,7 @@ interface VirtualElementText { export interface VirtualElementTag { type: VirtualType.Tag; - target?: HTMLElement; + target?: HTMLElement | SVGElement; tag: string; props: Props; children: VirtualElementChildren;