Skip to content

Commit e1aed07

Browse files
expo-tuft[bot]ide
authored andcommitted
rn-tester: a screen for how links behave on each platform
Four cases, and they are not variations on a theme: the first three are the three SHAPES an anchor takes — a run of glyphs inside a sentence, a box around an image, and a block when an author writes `display: block` — each of which hit-tests differently and each of which was broken separately at some point. The fourth shows `preventDefault()` cancelling the navigation. It lives on the RNTester side rather than in the shared HTML document because everything it demonstrates is an INTERACTION, and the web-comparison report renders that document in a browser, where it would be pretending a long press had happened. The padding is symmetric here, unlike the other HTML screens' 48pt bottom. That figure is scroll clearance for long documents; these cases are short, so the screen never scrolls and the space was simply lopsided — the last paragraph's own bottom margin lands on top of it, because a container with padding stops a child's margin collapsing out, giving about 65pt below against 16 above. The text takes its colour from the renderer's initial value, CanvasText, as a page does from `html { color: CanvasText }`, so nothing on the screen states one. Co-authored-by: ide <379606+ide@users.noreply.github.com>
1 parent f583c9a commit e1aed07

3 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {RNTesterModule} from '../../types/RNTesterTypes';
14+
15+
import {SECONDARY_COLOR} from './themed';
16+
import * as React from 'react';
17+
import {useEffect, useRef, useState} from 'react';
18+
import {Animated, Easing, Platform, ScrollView, Text} from 'react-native';
19+
20+
import '@react-native/expo-intrinsics-poc';
21+
22+
/*
23+
* `<a href>` on each platform's terms.
24+
*
25+
* This screen lives on the RNTester side rather than in the shared document,
26+
* because everything it demonstrates is an INTERACTION — and the web-comparison
27+
* report renders the shared document in a browser, where it would be pretending
28+
* that a long press had happened.
29+
*
30+
* The three cases are the three shapes an anchor takes. They are not variations
31+
* on a theme: each is a different thing to hit-test, and each was broken
32+
* separately at some point.
33+
*/
34+
35+
function Note({children}: {children: React.Node}): React.Node {
36+
return (
37+
<Text style={{fontSize: 13, color: SECONDARY_COLOR, marginBottom: 12}}>
38+
{children}
39+
</Text>
40+
);
41+
}
42+
43+
function Screen({children}: {children: React.Node}): React.Node {
44+
return (
45+
/*
46+
* Symmetric padding, unlike the other HTML screens' `paddingBottom: 48`.
47+
*
48+
* That 48 is scroll clearance for long documents, and these cases are
49+
* short — so it never scrolls and the space is simply lopsided: the last
50+
* paragraph's own bottom margin lands on top of it (a container with
51+
* padding stops a child's margin collapsing out), making the gap under the
52+
* text about 65pt against 16 above it.
53+
*/
54+
<ScrollView contentContainerStyle={{padding: 16}}>{children}</ScrollView>
55+
);
56+
}
57+
58+
function InlineLink(): React.Node {
59+
return (
60+
<Screen>
61+
<Note>
62+
{Platform.OS === 'ios'
63+
? 'Tap to open. Press and hold: the glyphs lift out of the sentence into the system link menu.'
64+
: 'Tap to open. Links take the theme’s colour and Android’s underline.'}
65+
</Note>
66+
<p>
67+
A paragraph with {/* $FlowExpectedError[not-a-component] intrinsic */}
68+
<a href="https://reactnative.dev/">a link inside it</a> and ordinary
69+
text after, so the link has to be found among the glyphs rather than by
70+
hit-testing a view.
71+
</p>
72+
<p>
73+
A link long enough to{' '}
74+
{/* $FlowExpectedError[not-a-component] intrinsic */}
75+
<a href="https://reactnative.dev/docs/getting-started">
76+
wrap across lines, each lifting as its own shape
77+
</a>{' '}
78+
— the lift hugs the link's glyphs on each line and leaves the words
79+
around it alone.
80+
</p>
81+
</Screen>
82+
);
83+
}
84+
85+
function LinkedImage(): React.Node {
86+
return (
87+
<Screen>
88+
<Note>
89+
An anchor can wrap content, not just text. The image is the link.
90+
</Note>
91+
{/* $FlowExpectedError[not-a-component] intrinsic */}
92+
<a href="https://reactnative.dev/">
93+
{/* $FlowExpectedError[not-a-component] intrinsic */}
94+
<img
95+
src="https://reactnative.dev/img/tiny_logo.png"
96+
width={72}
97+
height={72}
98+
/>
99+
</a>
100+
<Note>
101+
The anchor stays inline, so the image rides in the text as an attachment
102+
and carries the link with it.
103+
</Note>
104+
</Screen>
105+
);
106+
}
107+
108+
function LiveLink(): React.Node {
109+
const spin = useRef(new Animated.Value(0)).current;
110+
useEffect(() => {
111+
const loop = Animated.loop(
112+
Animated.timing(spin, {
113+
toValue: 1,
114+
duration: 1600,
115+
easing: Easing.linear,
116+
// Driven natively, so the square keeps turning even while JavaScript is
117+
// idle and while a menu is up.
118+
useNativeDriver: true,
119+
}),
120+
);
121+
loop.start();
122+
return () => loop.stop();
123+
}, [spin]);
124+
125+
return (
126+
<Screen>
127+
<Note>
128+
The square keeps turning under the menu. The lift is rendered once, so
129+
the chip is a still of the moment you pressed.
130+
</Note>
131+
{/* $FlowExpectedError[not-a-component] intrinsic */}
132+
<a href="https://reactnative.dev/">
133+
<Animated.View
134+
style={{
135+
width: 72,
136+
height: 72,
137+
borderRadius: 10,
138+
backgroundColor: '#2563eb',
139+
transform: [
140+
{
141+
rotate: spin.interpolate({
142+
inputRange: [0, 1],
143+
outputRange: ['0deg', '360deg'],
144+
}),
145+
},
146+
],
147+
}}
148+
/>
149+
</a>
150+
</Screen>
151+
);
152+
}
153+
154+
function BlockLink(): React.Node {
155+
return (
156+
<Screen>
157+
<Note>
158+
{Platform.OS === 'ios'
159+
? 'With display:block the anchor is a box, so holding lifts the whole box — the way iOS lifts a row.'
160+
: 'With display:block the anchor is a box rather than a run of glyphs. Tapping it still follows the link.'}
161+
</Note>
162+
{/* $FlowExpectedError[not-a-component] intrinsic */}
163+
<a
164+
href="https://reactnative.dev/blog"
165+
style={{
166+
display: 'block',
167+
padding: 16,
168+
borderRadius: 12,
169+
backgroundColor: '#00000010',
170+
}}>
171+
A whole block that is a link
172+
</a>
173+
</Screen>
174+
);
175+
}
176+
177+
function CancellingLink(): React.Node {
178+
const [count, setCount] = useState(0);
179+
return (
180+
<Screen>
181+
<Note>
182+
preventDefault() cancels the navigation, exactly as on the web — this
183+
one counts taps instead of opening.
184+
</Note>
185+
<p>
186+
{/* $FlowExpectedError[not-a-component] intrinsic */}
187+
<a
188+
href="https://reactnative.dev/"
189+
onClick={(event: $FlowFixMe) => {
190+
event.preventDefault();
191+
setCount(c => c + 1);
192+
}}>
193+
Handled in JavaScript
194+
</a>{' '}
195+
— tapped {count} {count === 1 ? 'time' : 'times'}, and Safari never
196+
opened.
197+
</p>
198+
</Screen>
199+
);
200+
}
201+
202+
export default {
203+
title: 'HTML: links',
204+
category: 'UI',
205+
description:
206+
'How <a href> behaves on each platform: the system tint, the platform’s own ' +
207+
'underline convention, and press-and-hold through the OS’s own link menu.',
208+
examples: [
209+
{
210+
name: 'inline',
211+
title: 'A link inside a sentence',
212+
render: (): React.Node => <InlineLink />,
213+
},
214+
{
215+
name: 'image',
216+
title: 'A link wrapping an image',
217+
render: (): React.Node => <LinkedImage />,
218+
},
219+
{
220+
name: 'live',
221+
title: 'A link wrapping a moving view',
222+
description: 'What the OS lifts when the content is animating.',
223+
render: (): React.Node => <LiveLink />,
224+
},
225+
{
226+
name: 'block',
227+
title: 'A link that is a block',
228+
render: (): React.Node => <BlockLink />,
229+
},
230+
{
231+
name: 'cancel',
232+
title: 'Cancelling the navigation',
233+
render: (): React.Node => <CancellingLink />,
234+
},
235+
],
236+
} as RNTesterModule;

‎packages/rn-tester/js/utils/RNTesterList.android.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ const Components: Array<RNTesterModuleInfo> = [
7474
module: require('../examples/HTMLElements/HTMLTextLevelExample').default,
7575
category: 'UI',
7676
},
77+
{
78+
key: 'LinkBehaviorExample',
79+
module: require('../examples/HTMLElements/LinkBehaviorExample').default,
80+
category: 'UI',
81+
},
7782
{
7883
key: 'HTMLGroupingExample',
7984
module: require('../examples/HTMLElements/HTMLGroupingExample').default,

‎packages/rn-tester/js/utils/RNTesterList.ios.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ const Components: Array<RNTesterModuleInfo> = [
7272
module: require('../examples/HTMLElements/HTMLTextLevelExample').default,
7373
category: 'UI',
7474
},
75+
{
76+
key: 'LinkBehaviorExample',
77+
module: require('../examples/HTMLElements/LinkBehaviorExample').default,
78+
category: 'UI',
79+
},
7580
{
7681
key: 'HTMLGroupingExample',
7782
module: require('../examples/HTMLElements/HTMLGroupingExample').default,

0 commit comments

Comments
 (0)