-
-
Notifications
You must be signed in to change notification settings - Fork 669
feat(Stack v5, iOS): Support icon loading in header items and menu #4277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+890
−50
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6583597
feat(Stack v5, iOS): Support icon loading in header items and menu
kmichalikk ecc1141
Add scenario.md
kmichalikk b342d0a
fix copilot issues
kmichalikk 4404e7a
remove comment
kmichalikk 5014e91
apply CR suggestions
kmichalikk ba95310
feat(Stack v5, iOS): Add sizing to placeholder image for async load (…
kmichalikk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
246 changes: 246 additions & 0 deletions
246
apps/src/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react'; | ||
| import { createScenario } from '@apps/tests/shared/helpers'; | ||
| import { | ||
| StackContainer, | ||
| useStackNavigationContext, | ||
| } from '@apps/shared/gamma/containers/stack'; | ||
| import { StackHeaderConfigProps } from 'react-native-screens/components/gamma/stack/header'; | ||
| import { Button, ScrollView, Text, View, StyleSheet } from 'react-native'; | ||
| import { scenarioDescription } from './scenario-description'; | ||
| import { ToastProvider, useToast } from '@apps/shared'; | ||
| import { Colors } from '@apps/shared/styling'; | ||
| import { type PlatformIconIOS } from 'react-native-screens'; | ||
|
|
||
| type IconVariant = 'sfSymbol' | 'xcasset' | 'imageSource' | 'templateSource'; | ||
|
|
||
| const ICON_VARIANTS: IconVariant[] = [ | ||
| 'sfSymbol', | ||
| 'xcasset', | ||
| 'imageSource', | ||
| 'templateSource', | ||
| ]; | ||
|
|
||
| function iconForVariant(variant: IconVariant): PlatformIconIOS { | ||
| switch (variant) { | ||
| case 'sfSymbol': | ||
| return { type: 'sfSymbol', name: 'star.fill' }; | ||
| case 'xcasset': | ||
| return { type: 'xcasset', name: 'custom-icon-fill' }; | ||
| case 'imageSource': | ||
| return { | ||
| type: 'imageSource', | ||
| imageSource: require('@assets/search_black.png'), | ||
| }; | ||
| case 'templateSource': | ||
| return { | ||
| type: 'templateSource', | ||
| templateSource: require('@assets/variableIcons/icon.png'), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| function nextVariant(current: IconVariant): IconVariant { | ||
| const idx = ICON_VARIANTS.indexOf(current); | ||
| return ICON_VARIANTS[(idx + 1) % ICON_VARIANTS.length]!; | ||
| } | ||
|
|
||
| function buildHeaderConfig( | ||
| itemIconVariant: IconVariant, | ||
| menuIconVariant: IconVariant, | ||
| cycleMenuIcons: () => void, | ||
| showToast: (text: string) => void, | ||
| ): StackHeaderConfigProps { | ||
| const itemIcon = iconForVariant(itemIconVariant); | ||
| const menuIcon = iconForVariant(menuIconVariant); | ||
|
|
||
| return { | ||
| title: 'Header Icons', | ||
| ios: { | ||
| trailingItems: [ | ||
| { | ||
| type: 'item', | ||
| id: 'icon-item', | ||
| title: 'Actions', | ||
| icon: itemIcon, | ||
| onPress: () => showToast('Item pressed'), | ||
| menu: { | ||
| type: 'menu', | ||
| id: 'main-menu', | ||
| onSelectionChange: selection => | ||
| showToast('Selected: ' + selection.join(', ')), | ||
| children: [ | ||
| { | ||
| id: 'cycle-action', | ||
| type: 'menuItem', | ||
| itemType: 'action', | ||
| title: `Cycle icons (${menuIconVariant})`, | ||
| keepsMenuPresented: true, | ||
| onPress: cycleMenuIcons, | ||
| }, | ||
| { | ||
| id: 'toggle-1', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Toggle 1', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| { | ||
| id: 'toggle-2', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Toggle 2', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| { | ||
| id: 'toggle-3', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Toggle 3', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| { | ||
| id: 'submenu', | ||
| type: 'menu', | ||
| title: 'Submenu', | ||
| icon: menuIcon, | ||
| children: [ | ||
| { | ||
| id: 'sub-toggle-1', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Sub Toggle 1', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| { | ||
| id: 'sub-toggle-2', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Sub Toggle 2', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| { | ||
| id: 'sub-toggle-3', | ||
| type: 'menuItem', | ||
| itemType: 'toggle', | ||
| title: 'Sub Toggle 3', | ||
| icon: menuIcon, | ||
| keepsMenuPresented: true, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function ConfigScreen() { | ||
| const navigation = useStackNavigationContext(); | ||
| const toast = useToast(); | ||
|
|
||
| const [itemIconVariant, setItemIconVariant] = | ||
| useState<IconVariant>('sfSymbol'); | ||
| const [menuIconVariant, setMenuIconVariant] = | ||
| useState<IconVariant>('sfSymbol'); | ||
|
|
||
| const showToast = useCallback( | ||
| (text: string) => { | ||
| toast.push({ backgroundColor: Colors.GreenDark120, message: text }); | ||
| }, | ||
| [toast], | ||
| ); | ||
|
|
||
| const cycleMenuIcons = useCallback(() => { | ||
| setMenuIconVariant(v => nextVariant(v)); | ||
| }, []); | ||
|
|
||
| const { setRouteOptions, routeKey } = navigation; | ||
| const headerConfig = useMemo( | ||
| () => | ||
| buildHeaderConfig( | ||
| itemIconVariant, | ||
| menuIconVariant, | ||
| cycleMenuIcons, | ||
| showToast, | ||
| ), | ||
| [itemIconVariant, menuIconVariant, cycleMenuIcons, showToast], | ||
| ); | ||
|
|
||
| useLayoutEffect(() => { | ||
| setRouteOptions(routeKey, { | ||
| headerConfig, | ||
| }); | ||
| }, [headerConfig, setRouteOptions, routeKey]); | ||
|
|
||
| return ( | ||
| <ScrollView | ||
| contentInsetAdjustmentBehavior="automatic" | ||
| style={styles.container}> | ||
| <View style={styles.section}> | ||
| <Text style={styles.label}>Bar Button Item Icon</Text> | ||
| <Text style={styles.current}>{itemIconVariant}</Text> | ||
| <Button | ||
| title="Cycle item icon" | ||
| onPress={() => setItemIconVariant(nextVariant)} | ||
| /> | ||
| </View> | ||
| <View style={styles.section}> | ||
| <Text style={styles.label}>Menu Toggles + Submenu Icon</Text> | ||
| <Text style={styles.current}>{menuIconVariant}</Text> | ||
| <Button title="Cycle menu icons" onPress={cycleMenuIcons} /> | ||
| </View> | ||
| <View style={styles.section}> | ||
| <Button | ||
| title="Push another screen" | ||
| onPress={() => navigation.push('Home')} | ||
| /> | ||
| </View> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| function TestStackHeaderIconIOS() { | ||
| return ( | ||
| <ToastProvider> | ||
| <StackContainer | ||
| routeConfigs={[ | ||
| { | ||
| name: 'Home', | ||
| Component: ConfigScreen, | ||
| options: {}, | ||
| }, | ||
| ]} | ||
| /> | ||
| </ToastProvider> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| }, | ||
| section: { | ||
| padding: 16, | ||
| borderBottomWidth: StyleSheet.hairlineWidth, | ||
| borderBottomColor: '#ccc', | ||
| }, | ||
| label: { | ||
| fontSize: 14, | ||
| fontWeight: '600', | ||
| marginBottom: 4, | ||
| }, | ||
| current: { | ||
| fontSize: 16, | ||
| marginBottom: 8, | ||
| color: Colors.GreenDark120, | ||
| }, | ||
| }); | ||
|
|
||
| export default createScenario(TestStackHeaderIconIOS, scenarioDescription); |
11 changes: 11 additions & 0 deletions
11
...rc/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/scenario-description.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { ScenarioDescription } from '@apps/tests/shared/helpers'; | ||
|
|
||
| export const scenarioDescription: ScenarioDescription = { | ||
| name: 'Stack Header Icon (iOS)', | ||
| key: 'test-stack-header-icon-ios', | ||
| details: | ||
| 'Tests header item icons: SF Symbol, xcasset, imageSource, templateSource on bar button items and menu items, with runtime updates.', | ||
| platforms: ['ios'], | ||
| e2eCoverage: 'tbd', | ||
| smokeTest: false, | ||
| }; |
39 changes: 39 additions & 0 deletions
39
.../src/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/scenario.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Test Scenario: Stack Header Icons (iOS) | ||
|
|
||
| ## Details | ||
|
|
||
| **Description:** This test focuses on handling icons and images in header with runtime updates. | ||
|
|
||
| **OS test creation version:** iOS 26.4, iPadOS 26.4 | ||
|
|
||
| ## E2E test | ||
|
|
||
| TBD | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - iOS / iPadOS simulator | ||
|
|
||
| ## Note (Optional) | ||
|
|
||
| - Crossfade doesn't work when changing between async images. | ||
| - Changing items in menu while it is presented (default for this test) results in visible layout shifts. | ||
| This is native behavior, we can't do much here | ||
|
|
||
| ## Steps on iPhone | ||
|
|
||
| 1. Inspect the header right item | ||
| - [ ] A "star" sfSymbol is visible | ||
| 2. Repeatedly click on "Cycle item icon" | ||
| - [ ] First, "star" changes to filled "3" | ||
| - [ ] Second, filled "3" changes to search icon | ||
| - [ ] Third, search icon changes to "3" | ||
| - [ ] Lastly, "3" changes again to "star" | ||
| 3. Inspect the menu | ||
| - [ ] Long press on the header item shows the menu | ||
| - [ ] both menu items and submenu items have "star" visible on the left side | ||
| 4. Repeatedly click on "Cycle icons" action inside the menu | ||
| - [ ] First, "star" changes to filled "3" | ||
| - [ ] Second, filled "3" changes to search icon | ||
| - [ ] Third, search icon changes to "3" | ||
| - [ ] Lastly, "3" changes again to "star" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we add support for tinting, we should update the test to check that
templateSourceis tinted correctly (how about nested menus here?) andimageSourceis not.