Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
500aa56
feat(preview): opt-in accessible view for streamed browser tabs
Oct 26, 2026
c06988a
fix(preview): show accessibility action refusals
Oct 26, 2026
ac80fad
fix(preview): keep accessible view edits, trees, frames and backlog i…
Oct 26, 2026
f267683
fix(preview): preserve drafts and stop edits after navigation
Oct 26, 2026
0ebffcc
Merge main into issue-watch/25271
Oct 26, 2026
8b8e33a
Merge main into issue-watch/25271
Oct 26, 2026
5dc3174
Merge main into issue-watch/25271
Oct 26, 2026
6c19128
Merge main and preserve accessibility subscriptions
Oct 26, 2026
d53ad39
Merge remote-tracking branch 'github/main' into takeover-25308
maria-rcks Oct 27, 2026
d58a98a
fix(preview): preserve accessible fields and report incomplete pages
maria-rcks Oct 27, 2026
6e0ed16
fix(preview): keep accessible keyboard navigation in the viewer
maria-rcks Oct 27, 2026
50a12e6
Merge main into issue-watch/25271
Oct 27, 2026
7d758a4
chore: merge main into accessibility preview branch
Oct 29, 2026
d54e042
fix(preview): use current effect lint rule
Oct 29, 2026
cbaa694
chore(preview): merge current main for accessible view
maria-rcks Oct 30, 2026
b7476e2
chore(preview): merge current main for accessible view
Oct 30, 2026
4a67180
chore(preview): merge main and retain browser profile tests
Oct 30, 2026
1704b21
chore(preview): merge main and retain mobile test registrations
Oct 30, 2026
63c8128
test(preview): include accessibility in browser stream mock
Oct 30, 2026
00835a8
chore(preview): merge main and preserve browser clipboard tests
Oct 31, 2026
348d14b
chore(preview): merge main and retain mobile test registrations
Nov 1, 2026
e2a528d
chore(preview): merge main and retain mobile test registrations
Nov 1, 2026
3c86930
chore(preview): merge main into accessible preview view
shivamhwp Nov 3, 2026
22011ad
chore(preview): address accessibility review conventions
shivamhwp Nov 3, 2026
fd507f1
fix(preview): accessible view keeps multi-select listboxes and flags …
shivamhwp Nov 3, 2026
da5d8f6
chore(preview): merge main into accessible preview view
shivamhwp Nov 3, 2026
3ff8eb7
fix(preview): preserve unedited and oversized field values
shivamhwp Nov 4, 2026
5f91be7
fix(preview): accessible view reports a failed first read and leaks n…
shivamhwp Nov 4, 2026
13adfc9
Merge commit '43bf0babd4b41d22466640e5d9c8d08e190fff9c' into issue-wa…
shivamhwp Nov 4, 2026
8bd84d2
fix(preview): keep oversized edits and explain the field limit
shivamhwp Nov 4, 2026
20ec7fc
fix(preview): preserve oversized pasted drafts
shivamhwp Nov 4, 2026
9258f69
fix(mobile): keep accessible view dismissible over PDFs
shivamhwp Nov 4, 2026
68d82de
fix(preview): retain accessibility notices under backpressure
shivamhwp Nov 4, 2026
df24a07
fix(web): allow dismissing accessible view over PDFs
shivamhwp Nov 4, 2026
8155b5f
fix(preview): refresh accessible state across PDF transitions
shivamhwp Nov 4, 2026
65325db
fix(preview): isolate accessibility subscriptions and PDF resets
shivamhwp Nov 4, 2026
11f6a26
Merge origin/main into issue-watch/25271
shivamhwp Nov 4, 2026
33ec2bd
Merge origin/main into issue-watch/25271
shivamhwp Nov 5, 2026
8658c81
Merge origin/main into issue-watch/25271
shivamhwp Nov 10, 2026
7affa78
fix(server): check accessibility control errors with Schema.is after …
shivamhwp Nov 10, 2026
817cccb
Merge commit 'd1909d68f8a8864d13fcb214c116777a651e6684' into issue-wa…
shivamhwp Nov 10, 2026
faf6498
Merge branch 'main' into issue-watch/25271
shivamhwp Nov 12, 2026
1e4d422
test(preview): keep heading parsing coverage in the parser
shivamhwp Nov 12, 2026
d4babad
fix(preview): withdraw a navigated page's accessible view when its su…
shivamhwp Nov 12, 2026
2b25454
Merge main into accessibility preview branch
shivamhwp Nov 12, 2026
703791d
Merge main into accessibility preview branch
shivamhwp Nov 13, 2026
6849400
fix(mobile): mark accessible browser documents ready
shivamhwp Nov 13, 2026
e76fb92
fix(mobile): keep browser controls above the loading overlay
shivamhwp Nov 13, 2026
111d9fe
Merge main into accessibility preview branch
shivamhwp Nov 13, 2026
b1a96ce
fix(preview): clicking a tree item's wrapped label activates it
shivamhwp Nov 13, 2026
9333036
Merge remote-tracking branch 'origin/main' into issue-watch/25271
juliusmarminge Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async function prepareStreamScripts() {
["packages/client-runtime/src/device", ["stream.ts", "hubAccess.ts"], generateAll],
[
"packages/client-runtime/src/preview",
["serverBrowserStream.ts"],
["serverBrowserStream.ts", "accessibleDocument.ts"],
generatePreviewStreamScript,
],
]) {
Expand Down
125 changes: 77 additions & 48 deletions apps/mobile/src/features/browser/PreviewStreamWebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@t3tools/client-runtime/preview/server-browser-stream";
import type { EnvironmentId, PreviewStreamHostSetup } from "@t3tools/contracts";
import {
useCallback,
useEffectEvent,
useImperativeHandle,
useLayoutEffect,
Expand Down Expand Up @@ -164,6 +165,8 @@ function AuthorizedPreviewStream({
...props
}: PreviewStreamConfiguration & NativeStreamBridge) {
const [attempt, setAttempt] = useState(0);
// Outlives reconnects, which remount the document view.
const [accessible, setAccessible] = useState(false);
const [previousAccess, setPreviousAccess] = useState(props.access);
// Wait for refreshed access, including cookie credentials with unchanged JSON.
if (props.access !== previousAccess) {
Expand All @@ -185,6 +188,8 @@ function AuthorizedPreviewStream({
{...props}
ref={ref}
configuration={configuration}
accessible={accessible}
onAccessibleChange={setAccessible}
onUnauthorized={() => {
const refusals = ++unauthorized.current;
if (refusals >= MAX_REFUSALS) return false;
Expand Down Expand Up @@ -225,8 +230,12 @@ function PreviewStreamDocumentView({
onRetry,
onStreaming,
onRecoverProcess,
accessible,
onAccessibleChange,
}: Omit<NativeStreamBridge, "onUnauthorized" | "onRefused"> & {
readonly configuration: string;
readonly accessible: boolean;
readonly onAccessibleChange: (accessible: boolean) => void;
readonly background: string;
readonly onUnauthorized: () => boolean;
readonly onRetry: () => void;
Expand Down Expand Up @@ -260,6 +269,15 @@ function PreviewStreamDocumentView({
webView.current?.injectJavaScript(
`window.T3PreviewStream?.command(${JSON.stringify(input)}); true;`,
);
const showAccessible = useCallback(
(next: boolean) =>
webView.current?.injectJavaScript(`window.T3PreviewStream?.accessibility(${next}); true;`),
[],
);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- PDF transitions release the WebView's semantic stream and cached document without changing the user's preference.
useLayoutEffect(() => {
showAccessible(accessible && pdfUri == null);
}, [accessible, pdfUri, showAccessible]);
// Reads here rather than in the screen: a tab switch during the iOS paste prompt unmounts
// this view and nulls its WebView ref, so the text can never type into another tab.
const paste = () =>
Expand Down Expand Up @@ -328,16 +346,26 @@ function PreviewStreamDocumentView({
<AppText className="text-xs text-foreground-muted">
{previewStreamControlLabel(control)}
</AppText>
{control?.canOperate ? (
<View className="flex-row items-center gap-2">
<Button
label={control.controller === "you" ? "Release control" : "Take control"}
label={accessible ? "Hide accessible view" : "Accessible view"}
tone="secondary"
disabled={control.controller === "another-viewer"}
onPress={() =>
command({ type: control.controller === "you" ? "releaseControl" : "takeControl" })
}
disabled={pdfUri != null && !accessible}
onPress={() => {
onAccessibleChange(!accessible);
}}
/>
) : null}
{control?.canOperate ? (
<Button
label={control.controller === "you" ? "Release control" : "Take control"}
tone="secondary"
disabled={control.controller === "another-viewer"}
onPress={() =>
command({ type: control.controller === "you" ? "releaseControl" : "takeControl" })
}
/>
) : null}
</View>
</View>
) : null}
<View className="relative flex-1">
Expand Down Expand Up @@ -405,13 +433,14 @@ function PreviewStreamDocumentView({
onPictureInPicture?.(message, message.detail);
return;
case "status":
if (message.status === "connecting") showAccessible(accessible && pdfUri == null);
if (message.status !== "connecting") clearBootstrapTimer();
if (message.status === "error") {
fail(message.detail ?? "Browser stream failed.");
return;
}
setStatus(message.status);
if (message.status === "connecting") {
if (message.status === "connecting" && !message.preserveControl) {
// Dialogs may have closed and reopened while this viewer was disconnected.
dialogRef.current = null;
setControl(null);
Expand All @@ -427,6 +456,46 @@ function PreviewStreamDocumentView({
<PdfPreview key={pdfUri} uri={pdfUri} />
</View>
) : null}
{status !== "streaming" ? (
<View
className="absolute inset-0 items-center justify-center gap-4 px-6"
style={{ backgroundColor: background }}
>
{status === "connecting" ? <AppActivityIndicator colorClassName="accent-icon" /> : null}
{compact ? null : (
<AppText
accessibilityLiveRegion="polite"
className="text-center text-sm text-foreground-muted"
>
{status === "error" ? error : "Connecting to browser..."}
</AppText>
)}
{status === "error" && hostSetup && !compact ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Copy ${hostSetup.command}`}
className="w-full flex-row items-center gap-2 rounded-xl border border-secondary-border bg-secondary px-4 py-3"
onPress={() => {
void Clipboard.setStringAsync(hostSetup.command).catch(() => undefined);
}}
>
<AppText selectable className="flex-1 font-mono text-sm text-secondary-foreground">
{hostSetup.command}
</AppText>
<AppText className="text-xs text-foreground-muted">Copy</AppText>
</Pressable>
) : null}
{status === "error" && !gone && !compact ? (
<View className="items-center">
<Button
label={hostSetup ? "Try again" : "Reconnect"}
tone="secondary"
onPress={onRetry}
/>
</View>
) : null}
</View>
) : null}
</View>
{!compact && fileChooser && control?.controller === "you" ? (
<View className="absolute inset-x-3 top-16 gap-3 rounded-xl border border-secondary-border bg-secondary p-4">
Expand Down Expand Up @@ -482,46 +551,6 @@ function PreviewStreamDocumentView({
)}
</View>
) : null}
{status !== "streaming" ? (
<View
className="absolute inset-0 items-center justify-center gap-4 px-6"
style={{ backgroundColor: background }}
>
{status === "connecting" ? <AppActivityIndicator colorClassName="accent-icon" /> : null}
{compact ? null : (
<AppText
accessibilityLiveRegion="polite"
className="text-center text-sm text-foreground-muted"
>
{status === "error" ? error : "Connecting to browser..."}
</AppText>
)}
{status === "error" && hostSetup && !compact ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Copy ${hostSetup.command}`}
className="w-full flex-row items-center gap-2 rounded-xl border border-secondary-border bg-secondary px-4 py-3"
onPress={() => {
void Clipboard.setStringAsync(hostSetup.command).catch(() => undefined);
}}
>
<AppText selectable className="flex-1 font-mono text-sm text-secondary-foreground">
{hostSetup.command}
</AppText>
<AppText className="text-xs text-foreground-muted">Copy</AppText>
</Pressable>
) : null}
{status === "error" && !gone && !compact ? (
<View className="items-center">
<Button
label={hostSetup ? "Try again" : "Reconnect"}
tone="secondary"
onPress={onRetry}
/>
</View>
) : null}
</View>
) : null}
</View>
);
}
Loading
Loading