From 0b27e8c8ced075ef2e7a808e0ab8bbb2be44d480 Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Mon, 26 Oct 2026 19:25:17 +0000 Subject: [PATCH] Attach hidden connection action to labelled wordmark Keep the developer entry without a keyboard path by product decision. Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez --- .../pairing_page/pairing_welcome_view.dart | 176 +++++++++--------- .../onboarding/hosted_communities_test.dart | 33 +++- 2 files changed, 121 insertions(+), 88 deletions(-) diff --git a/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart b/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart index 70b8d1bd33..d49fc1ddf6 100644 --- a/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart +++ b/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart @@ -323,38 +323,42 @@ class _PairingWelcomeView extends HookWidget { setupContent == null && !signInPending && !isBusy; - final welcome = Semantics( - customSemanticsActions: canRevealOptions - ? { - const CustomSemanticsAction(label: 'More connection options'): - revealOptions, - } - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Grid.gutter, - vertical: Grid.sm, - ), - child: CustomScrollView( - // Stay fixed when the welcome content fits; allow only real overflow - // (such as the pairing form above the keyboard), with no rubber banding. - physics: const ClampingScrollPhysics(), - slivers: [ - SliverFillRemaining( - hasScrollBody: false, - child: Column( - children: [ - Expanded( - child: _TwoFingerDoubleTapSurface( - onDoubleTap: canRevealOptions ? revealOptions : null, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 320), - child: AspectRatio( - aspectRatio: 777 / 326, + final welcome = Padding( + padding: const EdgeInsets.symmetric( + horizontal: Grid.gutter, + vertical: Grid.sm, + ), + child: CustomScrollView( + // Stay fixed when the welcome content fits; allow only real overflow + // (such as the pairing form above the keyboard), with no rubber banding. + physics: const ClampingScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + Expanded( + child: _TwoFingerDoubleTapSurface( + onDoubleTap: canRevealOptions ? revealOptions : null, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: AspectRatio( + aspectRatio: 777 / 326, + child: Semantics( + // Hidden developer entry intentionally has no + // keyboard shortcut. Keep its assistive action + // on the existing labelled wordmark node. + customSemanticsActions: canRevealOptions + ? { + const CustomSemanticsAction( + label: 'More connection options', + ): revealOptions, + } + : null, child: OnboardingWordmark( key: const Key('pairing-buzz-wordmark'), color: context._onboardingIsDark @@ -363,73 +367,71 @@ class _PairingWelcomeView extends HookWidget { ), ), ), - const SizedBox(height: Grid.xxs), - Text( - 'Your people, your agents, your projects —\nall in one place.', - textAlign: TextAlign.center, - style: context.textTheme.bodyLarge?.copyWith( - color: context._onboardingInk, - ), + ), + const SizedBox(height: Grid.xxs), + Text( + 'Your people, your agents, your projects —\nall in one place.', + textAlign: TextAlign.center, + style: context.textTheme.bodyLarge?.copyWith( + color: context._onboardingInk, ), - ], - ), + ), + ], ), ), ), - const SizedBox(height: Grid.md), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 440), - child: SizedBox( - width: double.infinity, - child: setupContent != null - ? setupContent! - : !connectionLanding - ? actions(2) - : ClipRect( - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - for (var page = 1; page < 5; page++) - ExcludeFocus( + ), + const SizedBox(height: Grid.md), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: SizedBox( + width: double.infinity, + child: setupContent != null + ? setupContent! + : !connectionLanding + ? actions(2) + : ClipRect( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + for (var page = 1; page < 5; page++) + ExcludeFocus( + excluding: page != selectedPage, + child: ExcludeSemantics( excluding: page != selectedPage, - child: ExcludeSemantics( - excluding: page != selectedPage, - child: IgnorePointer( - ignoring: page != selectedPage, - child: AnimatedSlide( - key: ValueKey( - 'welcome-panel-$page', - ), - offset: Offset( - page == selectedPage - ? 0 - : page == 1 - ? -1 - : 1, - 0, - ), - duration: revealDuration, - curve: page == selectedPage - ? Curves.easeOutQuad - : Curves.easeOutCubic.flipped, - child: SizedBox( - width: double.infinity, - child: actions(page), - ), + child: IgnorePointer( + ignoring: page != selectedPage, + child: AnimatedSlide( + key: ValueKey('welcome-panel-$page'), + offset: Offset( + page == selectedPage + ? 0 + : page == 1 + ? -1 + : 1, + 0, + ), + duration: revealDuration, + curve: page == selectedPage + ? Curves.easeOutQuad + : Curves.easeOutCubic.flipped, + child: SizedBox( + width: double.infinity, + child: actions(page), ), ), ), ), - ], - ), + ), + ], ), - ), + ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ); return PopScope( diff --git a/mobile/test/features/onboarding/hosted_communities_test.dart b/mobile/test/features/onboarding/hosted_communities_test.dart index b7861a867d..8ca1c279d6 100644 --- a/mobile/test/features/onboarding/hosted_communities_test.dart +++ b/mobile/test/features/onboarding/hosted_communities_test.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:buzz/shared/hosted/hosted_community_creation.dart'; import 'package:buzz/shared/hosted/hosted_environment.dart'; import 'package:buzz/features/onboarding/hosted_connect_page.dart'; @@ -10,6 +12,7 @@ import 'package:buzz/shared/hosted/builderlab_account.dart'; import 'package:buzz/shared/hosted/hosted_community_connection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -46,7 +49,7 @@ void main() { ), ); await tester.pumpAndSettle(); - await revealOptions(tester); + await revealOptionsWithSemantics(tester); await tester.pumpAndSettle(); tester.view.physicalSize = const Size(420, 900); await tester.pumpAndSettle(); @@ -322,3 +325,31 @@ Future revealOptions(WidgetTester tester) async { await tester.pump(const Duration(milliseconds: 50)); } } + +Future revealOptionsWithSemantics(WidgetTester tester) async { + final handle = tester.ensureSemantics(); + try { + await tester.pump(); + final wordmark = tester.getSemantics(find.bySemanticsLabel('Buzz')); + final data = wordmark.getSemanticsData(); + final actionIds = data.customSemanticsActionIds!; + expect(actionIds, hasLength(1)); + expect( + CustomSemanticsAction.getAction(actionIds.single)!.label, + 'More connection options', + ); + // The action belongs to the wordmark, never an unlabelled page-sized stop. + expect(data.label, 'Buzz'); + expect(wordmark.rect.height, lessThan(200)); + wordmark.owner!.performAction( + wordmark.id, + ui.SemanticsAction.customAction, + actionIds.single, + ); + await tester.pumpAndSettle(); + expect(find.text('Enter a community link').hitTestable(), findsOneWidget); + expect(find.text('Use a pairing code').hitTestable(), findsOneWidget); + } finally { + handle.dispose(); + } +}