From ed42f00efc587e3e04c8ec7cbde79955dcc872c5 Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Sat, 4 Jul 2026 16:36:14 -0400 Subject: [PATCH 1/3] feat(a11y): app-wide accessibility pass + registry-driven audit test New AccessibilityTest sweeps every registered native route (via NativeRouter::registeredRoutes(), so future screens are audited automatically) through assertAccessible(), with parameterized routes visited as representative instances and bridge-driven screens skipped. 327 violations fixed across ~35 screens/components: - Icon-browser tiles (240): Pressable::a11yLabel from the enum case name - Social clones: alt text on every clickable avatar, post photo, story, banner, and thumbnail, derived from fixture data (names, usernames, truncated captions); like/comment/save/repost/compose/back buttons labeled - Icon-only top-bar actions labeled via the new NavAction::a11yLabel() (search, notifications, cast, video/voice call, share, favorite, more options) across youtube, instagram, syncup, syncup-native, and native-chrome screens - Sliders, gesture areas, trailing menu buttons, stepper icons, and FABs labeled; conditional composer controls (mic/send behind @if) covered in a proactive pass beyond the initial-frame audit Pest.php now registers the native testing suite's PestExpectations (toBeAccessible() and friends). Suite: 75 passed. Co-Authored-By: Claude Fable 5 --- app/NativeComponents/ExploreIcons.php | 2 + app/NativeComponents/InstagramFeed.php | 4 +- .../Layouts/SyncUpTabsLayout.php | 1 + app/NativeComponents/NativeChromeDemo.php | 3 +- app/NativeComponents/NativeChromeDetail.php | 2 +- app/NativeComponents/SyncUpChat.php | 6 +- app/NativeComponents/SyncUpChats.php | 2 +- .../SyncUpNative/SyncUpNativeChat.php | 3 + .../SyncUpNative/SyncUpNativeChats.php | 2 +- app/NativeComponents/YouTubeHome.php | 6 +- resources/views/native/animate.blade.php | 3 +- .../views/native/compose-tweet.blade.php | 3 +- resources/views/native/counter.blade.php | 4 +- .../views/native/explore/forms.blade.php | 6 +- .../views/native/explore/menus.blade.php | 3 + .../views/native/facebook-create.blade.php | 3 +- .../views/native/facebook-feed.blade.php | 4 ++ .../views/native/facebook-post.blade.php | 6 +- .../views/native/facebook-profile.blade.php | 6 +- .../views/native/instagram-feed.blade.php | 10 ++- .../views/native/instagram-post.blade.php | 10 ++- .../views/native/instagram-profile.blade.php | 4 +- .../views/native/instagram-search.blade.php | 4 ++ .../views/native/syncup-native/chat.blade.php | 4 +- resources/views/native/syncup/chats.blade.php | 2 +- .../views/native/syncup/friends.blade.php | 2 +- .../views/native/syncup/profile.blade.php | 4 +- resources/views/native/test-layout.blade.php | 6 +- resources/views/native/tweet-detail.blade.php | 9 ++- resources/views/native/twitter-feed.blade.php | 5 +- .../views/native/twitter-profile.blade.php | 6 +- .../views/native/youtube-channel.blade.php | 4 +- resources/views/native/youtube-home.blade.php | 4 ++ .../views/native/youtube-search.blade.php | 4 +- .../views/native/youtube-video.blade.php | 5 +- tests/Feature/Native/AccessibilityTest.php | 66 +++++++++++++++++++ tests/Pest.php | 3 + 37 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 tests/Feature/Native/AccessibilityTest.php diff --git a/app/NativeComponents/ExploreIcons.php b/app/NativeComponents/ExploreIcons.php index ae11dd1..e9f5e03 100644 --- a/app/NativeComponents/ExploreIcons.php +++ b/app/NativeComponents/ExploreIcons.php @@ -4,6 +4,7 @@ use App\Icons\Android; use App\Icons\Ios; +use Illuminate\Support\Str; use Native\Mobile\Edge\Element; use Native\Mobile\Edge\Elements\Column; use Native\Mobile\Edge\Elements\Icon; @@ -174,6 +175,7 @@ private function iconRow(array $cases, int $rowIndex, bool $ios): Element $row->addChild( Pressable::make($icon) ->onPress("showIcon('{$case->name}')") + ->a11yLabel(Str::headline($case->name)) ->flexGrow(1)->flexBasis(0)->height(self::CELL_SIZE) ->center()->class('bg-theme-surface-variant rounded-lg') ); diff --git a/app/NativeComponents/InstagramFeed.php b/app/NativeComponents/InstagramFeed.php index 4bc1ea9..ad1ef1f 100644 --- a/app/NativeComponents/InstagramFeed.php +++ b/app/NativeComponents/InstagramFeed.php @@ -27,8 +27,8 @@ public function navTitle(): string public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() - ->action(NavAction::make('activity')->icon('favorite_border')->press('viewActivity')) - ->action(NavAction::make('messages')->icon('chat_bubble_outline')->press('viewMessages')); + ->action(NavAction::make('activity')->icon('favorite_border')->a11yLabel('Activity')->press('viewActivity')) + ->action(NavAction::make('messages')->icon('chat_bubble_outline')->a11yLabel('Messages')->press('viewMessages')); } public function viewActivity(): void diff --git a/app/NativeComponents/Layouts/SyncUpTabsLayout.php b/app/NativeComponents/Layouts/SyncUpTabsLayout.php index bf6f689..5aa62bd 100644 --- a/app/NativeComponents/Layouts/SyncUpTabsLayout.php +++ b/app/NativeComponents/Layouts/SyncUpTabsLayout.php @@ -41,6 +41,7 @@ public function navBar(NativeComponent $screen): ?NavBar ->elevation(8) // subtle shadow under bar ->action(NavAction::make('search') ->icon(ios: Ios::Magnifyingglass, android: Android::Search) + ->a11yLabel('Search') ->press('openSearch')); } diff --git a/app/NativeComponents/NativeChromeDemo.php b/app/NativeComponents/NativeChromeDemo.php index 74e4c86..616e70b 100644 --- a/app/NativeComponents/NativeChromeDemo.php +++ b/app/NativeComponents/NativeChromeDemo.php @@ -37,7 +37,7 @@ public function navigationOptions(): ?NavBarOptions return NavBarOptions::make() ->subtitle('NavigationStack toolbar') // A plain icon — fires its press handler directly. - ->action(NavAction::make('share')->icon('share')->press('shareIt')) + ->action(NavAction::make('share')->icon('share')->a11yLabel('Share')->press('shareIt')) // A pull-down menu — tap to reveal sub-items. Each sub-item // is itself a NavAction with its own icon / label / press // handler, plus an optional `destructive()` flag for red @@ -45,6 +45,7 @@ public function navigationOptions(): ?NavBarOptions ->action( NavAction::make('more') ->icon('ellipsis') + ->a11yLabel('More options') ->items([ NavAction::make('mark_read') ->icon('checkmark.circle') diff --git a/app/NativeComponents/NativeChromeDetail.php b/app/NativeComponents/NativeChromeDetail.php index 6fed201..ff23cf9 100644 --- a/app/NativeComponents/NativeChromeDetail.php +++ b/app/NativeComponents/NativeChromeDetail.php @@ -30,7 +30,7 @@ public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() ->subtitle('Pushed via NavigationStack') - ->action(NavAction::make('star')->icon('star')->press('toggleStar')); + ->action(NavAction::make('star')->icon('star')->a11yLabel('Favorite')->press('toggleStar')); } public bool $starred = false; diff --git a/app/NativeComponents/SyncUpChat.php b/app/NativeComponents/SyncUpChat.php index 9f39398..1e8bc85 100644 --- a/app/NativeComponents/SyncUpChat.php +++ b/app/NativeComponents/SyncUpChat.php @@ -43,9 +43,9 @@ public function navTitle(): string public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() - ->action(NavAction::make('video')->icon('video.fill')->press('startVideo')) - ->action(NavAction::make('call')->icon('phone.fill')->press('startCall')) - ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu')); + ->action(NavAction::make('video')->icon('video.fill')->a11yLabel('Video call')->press('startVideo')) + ->action(NavAction::make('call')->icon('phone.fill')->a11yLabel('Voice call')->press('startCall')) + ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu')); } public function setDraft(string $value): void diff --git a/app/NativeComponents/SyncUpChats.php b/app/NativeComponents/SyncUpChats.php index c5d6330..6ff3cfe 100644 --- a/app/NativeComponents/SyncUpChats.php +++ b/app/NativeComponents/SyncUpChats.php @@ -31,7 +31,7 @@ public function navTitle(): string public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() - ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu')); + ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu')); } public function setFilter(string $name): void diff --git a/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php b/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php index 83bca19..747ea70 100644 --- a/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php +++ b/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php @@ -50,16 +50,19 @@ public function navigationOptions(): ?NavBarOptions ->action( NavAction::make('video') ->icon(ios: Ios::Camera, android: AndroidOutlined::Camera) + ->a11yLabel('Video call') ->press('startVideo') ) ->action( NavAction::make('call') ->icon(ios: Ios::Phone, android: AndroidOutlined::Phone) + ->a11yLabel('Voice call') ->press('startCall') ) ->action( NavAction::make('more') ->icon(ios: Ios::Ellipsis, android: Android::MoreVert) + ->a11yLabel('More options') ->items([ NavAction::make('mark_read') ->icon(ios: Ios::CheckmarkCircle, android: Android::CheckCircle) diff --git a/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php b/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php index 737d1f4..e3f67d1 100644 --- a/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php +++ b/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php @@ -29,7 +29,7 @@ public function navTitle(): string public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() - ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu')); + ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu')); } public function setFilter(string $name): void diff --git a/app/NativeComponents/YouTubeHome.php b/app/NativeComponents/YouTubeHome.php index 8cafb7e..a1f9293 100644 --- a/app/NativeComponents/YouTubeHome.php +++ b/app/NativeComponents/YouTubeHome.php @@ -27,9 +27,9 @@ public function navTitle(): string public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() - ->action(NavAction::make('cast')->icon('cart')->press('castDevice')) - ->action(NavAction::make('alerts')->icon('notifications')->press('viewNotifications')) - ->action(NavAction::make('search')->icon('search')->press('viewSearch')); + ->action(NavAction::make('cast')->icon('cart')->a11yLabel('Cast')->press('castDevice')) + ->action(NavAction::make('alerts')->icon('notifications')->a11yLabel('Notifications')->press('viewNotifications')) + ->action(NavAction::make('search')->icon('search')->a11yLabel('Search')->press('viewSearch')); } public function castDevice(): void diff --git a/resources/views/native/animate.blade.php b/resources/views/native/animate.blade.php index 98d21de..26db5a9 100644 --- a/resources/views/native/animate.blade.php +++ b/resources/views/native/animate.blade.php @@ -476,6 +476,7 @@ class="w-[44] h-[44] rounded-full items-center justify-center bg-amber-500" {{-- Main FAB — rotates 45° when open so + becomes × --}} class="w-full h-full rounded-2xl bg-slate-800" :opacity="$drag->interpolate([0, 180], [0, 0.5])" /> - + - +