Skip to content

Commit 365c031

Browse files
authored
Workaround against display: inline bug in Safari (#32822)
Safari has a bug where if you put a block element inside an inline element and the inline element has a `view-transition-name` assigned it finds it as duplicate names. https://bugs.webkit.org/show_bug.cgi?id=290923 This adds a warning if we detect this scenario in dev mode. For the case where it renders into a single block, we can model this by making the parent either `block` or `inline-block` automatically to fix the issue. So we do that to automatically cover simple cases like `<a><div>...</div></a>`. This unfortunately causes layout/styling thrash so we might want to delete it once the bug has been fixed in enough Safari versions.
1 parent a9d63f3 commit 365c031

File tree

3 files changed

+122
-6
lines changed

3 files changed

+122
-6
lines changed

Diff for: fixtures/view-transition/src/components/Page.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ export default function Page({url, navigate}) {
113113

114114
const exclamation = (
115115
<ViewTransition name="exclamation" onShare={onTransition}>
116-
<span>!</span>
116+
<span>
117+
<div>!</div>
118+
</span>
117119
</ViewTransition>
118120
);
119121
return (

Diff for: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+116-2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ export type Props = {
137137
'view-transition-name'?: string,
138138
viewTransitionClass?: string,
139139
'view-transition-class'?: string,
140+
margin?: string,
141+
marginTop?: string,
142+
'margin-top'?: string,
143+
marginBottom?: string,
144+
'margin-bottom'?: string,
140145
...
141146
},
142147
bottom?: null | number,
@@ -1161,6 +1166,59 @@ export function unhideTextInstance(
11611166
textInstance.nodeValue = text;
11621167
}
11631168

1169+
function warnForBlockInsideInline(instance: HTMLElement) {
1170+
if (__DEV__) {
1171+
let nextNode = instance.firstChild;
1172+
outer: while (nextNode != null) {
1173+
let node: Node = nextNode;
1174+
if (
1175+
node.nodeType === ELEMENT_NODE &&
1176+
getComputedStyle((node: any)).display === 'block'
1177+
) {
1178+
console.error(
1179+
"You're about to start a <ViewTransition> around a display: inline " +
1180+
'element <%s>, which itself has a display: block element <%s> inside it. ' +
1181+
'This might trigger a bug in Safari which causes the View Transition to ' +
1182+
'be skipped with a duplicate name error.\n' +
1183+
'https://bugs.webkit.org/show_bug.cgi?id=290923',
1184+
instance.tagName.toLocaleLowerCase(),
1185+
(node: any).tagName.toLocaleLowerCase(),
1186+
);
1187+
break;
1188+
}
1189+
if (node.firstChild != null) {
1190+
nextNode = node.firstChild;
1191+
continue;
1192+
}
1193+
if (node === instance) {
1194+
break;
1195+
}
1196+
while (node.nextSibling == null) {
1197+
if (node.parentNode == null || node.parentNode === instance) {
1198+
break;
1199+
}
1200+
node = node.parentNode;
1201+
}
1202+
nextNode = node.nextSibling;
1203+
}
1204+
}
1205+
}
1206+
1207+
function countClientRects(rects: Array<ClientRect>): number {
1208+
if (rects.length === 1) {
1209+
return 1;
1210+
}
1211+
// Count non-zero rects.
1212+
let count = 0;
1213+
for (let i = 0; i < rects.length; i++) {
1214+
const rect = rects[i];
1215+
if (rect.width > 0 && rect.height > 0) {
1216+
count++;
1217+
}
1218+
}
1219+
return count;
1220+
}
1221+
11641222
export function applyViewTransitionName(
11651223
instance: Instance,
11661224
name: string,
@@ -1173,13 +1231,42 @@ export function applyViewTransitionName(
11731231
// $FlowFixMe[prop-missing]
11741232
instance.style.viewTransitionClass = className;
11751233
}
1234+
const computedStyle = getComputedStyle(instance);
1235+
if (computedStyle.display === 'inline') {
1236+
// WebKit has a bug where assigning a name to display: inline elements errors
1237+
// if they have display: block children. We try to work around this bug in the
1238+
// simple case by converting it automatically to display: inline-block.
1239+
// https://bugs.webkit.org/show_bug.cgi?id=290923
1240+
const rects = instance.getClientRects();
1241+
if (countClientRects(rects) === 1) {
1242+
// If the instance has a single client rect, that means that it can be
1243+
// expressed as a display: inline-block or block.
1244+
// This will cause layout thrash but we live with it since inline view transitions
1245+
// are unusual.
1246+
const style = instance.style;
1247+
// If there's literally only one rect, then it's likely on a single line like an
1248+
// inline-block. If it's multiple rects but all but one of them are empty it's
1249+
// likely because it's a single block that caused a line break.
1250+
style.display = rects.length === 1 ? 'inline-block' : 'block';
1251+
// Margin doesn't apply to inline so should be zero. However, padding top/bottom
1252+
// applies to inline-block positioning which we can offset by setting the margin
1253+
// to the negative padding to get it back into original position.
1254+
style.marginTop = '-' + computedStyle.paddingTop;
1255+
style.marginBottom = '-' + computedStyle.paddingBottom;
1256+
} else {
1257+
// This case cannot be easily fixed if it has blocks but it's also fine if
1258+
// it doesn't have blocks. So we only warn in DEV about this being an issue.
1259+
warnForBlockInsideInline(instance);
1260+
}
1261+
}
11761262
}
11771263

11781264
export function restoreViewTransitionName(
11791265
instance: Instance,
11801266
props: Props,
11811267
): void {
11821268
instance = ((instance: any): HTMLElement);
1269+
const style = instance.style;
11831270
const styleProp = props[STYLE];
11841271
const viewTransitionName =
11851272
styleProp != null
@@ -1190,7 +1277,7 @@ export function restoreViewTransitionName(
11901277
: null
11911278
: null;
11921279
// $FlowFixMe[prop-missing]
1193-
instance.style.viewTransitionName =
1280+
style.viewTransitionName =
11941281
viewTransitionName == null || typeof viewTransitionName === 'boolean'
11951282
? ''
11961283
: // The value would've errored already if it wasn't safe.
@@ -1205,12 +1292,39 @@ export function restoreViewTransitionName(
12051292
: null
12061293
: null;
12071294
// $FlowFixMe[prop-missing]
1208-
instance.style.viewTransitionClass =
1295+
style.viewTransitionClass =
12091296
viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
12101297
? ''
12111298
: // The value would've errored already if it wasn't safe.
12121299
// eslint-disable-next-line react-internal/safe-string-coercion
12131300
('' + viewTransitionClass).trim();
1301+
if (style.display === 'inline-block') {
1302+
// We might have overridden the style. Reset it to what it should be.
1303+
if (styleProp == null) {
1304+
style.display = style.margin = '';
1305+
} else {
1306+
const display = styleProp.display;
1307+
style.display =
1308+
display == null || typeof display === 'boolean' ? '' : display;
1309+
const margin = styleProp.margin;
1310+
if (margin != null) {
1311+
style.margin = margin;
1312+
} else {
1313+
const marginTop = styleProp.hasOwnProperty('marginTop')
1314+
? styleProp.marginTop
1315+
: styleProp['margin-top'];
1316+
style.marginTop =
1317+
marginTop == null || typeof marginTop === 'boolean' ? '' : marginTop;
1318+
const marginBottom = styleProp.hasOwnProperty('marginBottom')
1319+
? styleProp.marginBottom
1320+
: styleProp['margin-bottom'];
1321+
style.marginBottom =
1322+
marginBottom == null || typeof marginBottom === 'boolean'
1323+
? ''
1324+
: marginBottom;
1325+
}
1326+
}
1327+
}
12141328
}
12151329

12161330
export function cancelViewTransitionName(

Diff for: packages/react-reconciler/src/ReactFiberApplyGesture.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -834,18 +834,18 @@ function insertDestinationClonesOfFiber(
834834
}
835835

836836
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
837+
appendChild(hostParentClone, clone);
838+
unhideInstance(clone, finishedWork.memoizedProps);
837839
recursivelyInsertClones(
838840
finishedWork,
839841
clone,
840842
null,
841843
CLONE_APPEARING_PAIR,
842844
);
843-
appendChild(hostParentClone, clone);
844-
unhideInstance(clone, finishedWork.memoizedProps);
845845
trackHostMutation();
846846
} else {
847-
recursivelyInsertClones(finishedWork, clone, null, visitPhase);
848847
appendChild(hostParentClone, clone);
848+
recursivelyInsertClones(finishedWork, clone, null, visitPhase);
849849
}
850850
if (parentViewTransition !== null) {
851851
if (parentViewTransition.clones === null) {

0 commit comments

Comments
 (0)