Compare commits

...

3 Commits

Author SHA1 Message Date
Hristo Terezov
303f81f427 fix(participants-pane): replace :first-child with :first-of-type to silence emotion SSR warning
emotion/stylis emits a console error for :first-child selectors as they can
cause hydration mismatches in SSR. All three occurrences that trigger on
conference load are in ParticipantsPane. The semantics are equivalent since
all sibling elements are the same type.
2026-02-18 08:30:23 -06:00
Hristo Terezov
23a65fbf2a fix(ui): migrate ReactDOM.render to createRoot for React 18 compatibility
ReactDOM.render is deprecated in React 18 and causes console errors on every
render. Migrate to createRoot API across the main entry point and the legacy
video layout modules, storing root references to avoid recreating them on
repeated calls.
2026-02-18 08:25:25 -06:00
Hristo Terezov
5fee521938 fix(ui): replace kebab-case CSS properties with camelCase in style objects
Vendor-prefixed CSS properties like -webkit-appearance and -moz-appearance
used as object keys in emotion/tss-react style objects cause console errors
because JavaScript CSS-in-JS libraries require camelCase property names.

Replace all occurrences across Input, Chat, Filmstrip, and Slider components.
2026-02-18 08:25:08 -06:00
8 changed files with 43 additions and 33 deletions

View File

@@ -3,7 +3,7 @@
import Logger from '@jitsi/logger';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
@@ -166,7 +166,8 @@ export default class LargeVideoManager {
this.removePresenceLabel();
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
this._avatarRoot?.unmount();
this._avatarRoot = null;
this.container.style.display = 'none';
}
@@ -518,14 +519,16 @@ export default class LargeVideoManager {
* Updates the src of the dominant speaker avatar
*/
updateAvatar() {
ReactDOM.render(
if (!this._avatarRoot) {
this._avatarRoot = createRoot(this._dominantSpeakerAvatarContainer);
}
this._avatarRoot.render(
<Provider store = { APP.store }>
<Avatar
id = "dominantSpeakerAvatar"
participantId = { this.id }
size = { 200 } />
</Provider>,
this._dominantSpeakerAvatarContainer
</Provider>
);
}
@@ -559,15 +562,18 @@ export default class LargeVideoManager {
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
if (presenceLabelContainer) {
ReactDOM.render(
if (!this._presenceLabelRoot) {
this._presenceLabelRoot = createRoot(presenceLabelContainer);
}
this._presenceLabelRoot.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
</Provider>
);
}
}
@@ -577,11 +583,8 @@ export default class LargeVideoManager {
* @returns {void}
*/
removePresenceLabel() {
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
this._presenceLabelRoot?.unmount();
this._presenceLabelRoot = null;
}
/**

View File

@@ -4,7 +4,7 @@
import Logger from '@jitsi/logger';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants';
@@ -659,7 +659,12 @@ export class VideoContainer extends LargeContainer {
return;
}
ReactDOM.render(
const container = document.getElementById('largeVideoBackgroundContainer');
if (!this._backgroundRoot) {
this._backgroundRoot = createRoot(container);
}
this._backgroundRoot.render(
<LargeVideoBackground
hidden = { this._hideBackground || this._isHidden }
mirror = {
@@ -669,8 +674,7 @@ export class VideoContainer extends LargeContainer {
}
orientationFit = { this._backgroundOrientation }
videoElement = { this.video }
videoTrack = { this.stream } />,
document.getElementById('largeVideoBackgroundContainer')
videoTrack = { this.stream } />
);
}
}

View File

@@ -107,12 +107,12 @@ const useStyles = makeStyles()(theme => {
},
'input::-webkit-outer-spin-button, input::-webkit-inner-spin-button': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
margin: 0
},
'input[type=number]': {
'-moz-appearance': 'textfield'
MozAppearance: 'textfield'
},
icon: {

View File

@@ -132,7 +132,7 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
'*': {
userSelect: 'text',
'-webkit-user-select': 'text'
WebkitUserSelect: 'text'
}
},

View File

@@ -94,7 +94,7 @@ function styles(theme: Theme, props: IProps) {
margin: 0,
border: 'none',
'-webkit-appearance': 'none',
WebkitAppearance: 'none' as const,
'& svg': {
fill: theme.palette.icon01

View File

@@ -54,7 +54,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
fontWeight: 600,
height: '100%',
[[ '& > *:first-child', '& > *:last-child' ] as any]: {
[[ '& > *:first-of-type', '& > *:last-of-type' ] as any]: {
flexShrink: 0
},
@@ -116,11 +116,11 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
antiCollapse: {
fontSize: 0,
'&:first-child': {
'&:first-of-type': {
display: 'none'
},
'&:first-child + *': {
'&:first-of-type + *': {
marginTop: 0
}
},

View File

@@ -86,7 +86,7 @@ const useStyles = makeStyles()(theme => {
slider: {
// Use an additional class here to override global CSS specificity
'&.custom-slider': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
background: 'transparent',
height,
left: 0,
@@ -104,11 +104,11 @@ const useStyles = makeStyles()(theme => {
},
'&::-webkit-slider-runnable-track': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
...inputTrack
},
'&::-webkit-slider-thumb': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
position: 'relative',
top: -6,
...inputThumb

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { App } from './features/app/components/App.web';
import { getLogger } from './features/base/logging/functions';
@@ -72,14 +72,17 @@ globalNS.entryPoints = {
WHITEBOARD: WhiteboardApp
};
const _roots = new Map();
globalNS.renderEntryPoint = ({
Component,
props = {},
elementId = 'react'
}) => {
/* eslint-disable-next-line react/no-deprecated */
ReactDOM.render(
<Component { ...props } />,
document.getElementById(elementId)
);
const element = document.getElementById(elementId);
if (!_roots.has(elementId)) {
_roots.set(elementId, createRoot(element));
}
_roots.get(elementId).render(<Component { ...props } />);
};