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);
+ }}
+ >
+
+
+ );
+}
+
+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;