Rework dialog and add native FocusScope
This commit is contained in:
+13
-10
@@ -72,6 +72,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
|||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
|
import {Provider as BlockingAnnouncementsPortalProvider} from '#/components/dialogs/BlockingAnnouncements'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
@@ -220,16 +221,18 @@ function App() {
|
|||||||
<ModalStateProvider>
|
<ModalStateProvider>
|
||||||
<DialogStateProvider>
|
<DialogStateProvider>
|
||||||
<LightboxStateProvider>
|
<LightboxStateProvider>
|
||||||
<PortalProvider>
|
<BlockingAnnouncementsPortalProvider>
|
||||||
<BottomSheetProvider>
|
<PortalProvider>
|
||||||
<StarterPackProvider>
|
<BottomSheetProvider>
|
||||||
<SafeAreaProvider
|
<StarterPackProvider>
|
||||||
initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider
|
||||||
<InnerApp />
|
initialMetrics={initialWindowMetrics}>
|
||||||
</SafeAreaProvider>
|
<InnerApp />
|
||||||
</StarterPackProvider>
|
</SafeAreaProvider>
|
||||||
</BottomSheetProvider>
|
</StarterPackProvider>
|
||||||
</PortalProvider>
|
</BottomSheetProvider>
|
||||||
|
</PortalProvider>
|
||||||
|
</BlockingAnnouncementsPortalProvider>
|
||||||
</LightboxStateProvider>
|
</LightboxStateProvider>
|
||||||
</DialogStateProvider>
|
</DialogStateProvider>
|
||||||
</ModalStateProvider>
|
</ModalStateProvider>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
|
|||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
|
import {Provider as BlockingAnnouncementsPortalProvider} from '#/components/dialogs/BlockingAnnouncements'
|
||||||
import {ToastContainer} from '#/components/Toast'
|
import {ToastContainer} from '#/components/Toast'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||||
@@ -196,11 +197,13 @@ function App() {
|
|||||||
<ModalStateProvider>
|
<ModalStateProvider>
|
||||||
<DialogStateProvider>
|
<DialogStateProvider>
|
||||||
<LightboxStateProvider>
|
<LightboxStateProvider>
|
||||||
|
<BlockingAnnouncementsPortalProvider>
|
||||||
<PortalProvider>
|
<PortalProvider>
|
||||||
<StarterPackProvider>
|
<StarterPackProvider>
|
||||||
<InnerApp />
|
<InnerApp />
|
||||||
</StarterPackProvider>
|
</StarterPackProvider>
|
||||||
</PortalProvider>
|
</PortalProvider>
|
||||||
|
</BlockingAnnouncementsPortalProvider>
|
||||||
</LightboxStateProvider>
|
</LightboxStateProvider>
|
||||||
</DialogStateProvider>
|
</DialogStateProvider>
|
||||||
</ModalStateProvider>
|
</ModalStateProvider>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
Children,
|
||||||
|
cloneElement,
|
||||||
|
ReactNode,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
isValidElement,
|
||||||
|
FunctionComponentElement,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
AccessibilityInfo,
|
||||||
|
Pressable,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
findNodeHandle,
|
||||||
|
} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useA11y} from '#/state/a11y'
|
||||||
|
|
||||||
|
export function FocusScope({children}: {children: ReactNode}) {
|
||||||
|
const {screenReaderEnabled} = useA11y()
|
||||||
|
|
||||||
|
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||||
|
}
|
||||||
|
|
||||||
|
function FocusTrap({children}: {children: ReactNode}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const child = useRef<View>(null)
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
return Children.toArray(children).map((node, i) => {
|
||||||
|
if (i === 0 && isValidElement(node)) {
|
||||||
|
return cloneElement(node as FunctionComponentElement<any>, {
|
||||||
|
ref: child,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
})
|
||||||
|
}, [children])
|
||||||
|
|
||||||
|
const focus = useCallback((ref: View | null) => {
|
||||||
|
if (!ref) return
|
||||||
|
const node = findNodeHandle(ref)
|
||||||
|
if (node) {
|
||||||
|
AccessibilityInfo.setAccessibilityFocus(node)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
focus(child.current)
|
||||||
|
}, 1e3)
|
||||||
|
}, [focus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable
|
||||||
|
accessible
|
||||||
|
accessibilityLabel={_(
|
||||||
|
msg`You've reached the start of the active content. Please go back, or activate to focus the first item.`,
|
||||||
|
)}
|
||||||
|
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||||
|
onAccessibilityAction={event => {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focus(child.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Noop />
|
||||||
|
</Pressable>
|
||||||
|
{content}
|
||||||
|
<Pressable
|
||||||
|
accessibilityLabel={_(
|
||||||
|
msg`You've reached the end of the active content. Please go back, or activate to go back to the beginning.`,
|
||||||
|
)}
|
||||||
|
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||||
|
onAccessibilityAction={event => {
|
||||||
|
switch (event.nativeEvent.actionName) {
|
||||||
|
case 'activate': {
|
||||||
|
focus(child.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Noop />
|
||||||
|
</Pressable>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Noop() {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
accessible={false}
|
||||||
|
style={{
|
||||||
|
height: 1,
|
||||||
|
opacity: 0,
|
||||||
|
}}>
|
||||||
|
{' '}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import {View, ScrollView, useWindowDimensions} from 'react-native'
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {FocusScope} from '#/components/FocusScope'
|
||||||
|
import {isNative, isAndroid} from '#/platform/detection'
|
||||||
import {atoms as a, useTheme, useBreakpoints, web, flatten} from '#/alf'
|
import {atoms as a, useTheme, useBreakpoints, web, flatten} from '#/alf'
|
||||||
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
@@ -49,7 +50,7 @@ export function AnnouncementDialog({
|
|||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View style={[a.fixed, a.inset_0, {zIndex: 9999}]}>
|
||||||
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
|
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[
|
colors={[
|
||||||
@@ -94,8 +95,6 @@ export function AnnouncementDialog({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
]}>
|
]}>
|
||||||
{/* <FocusScope.FocusScope loop asChild trapped> */}
|
|
||||||
|
|
||||||
{!gtPhone && (
|
{!gtPhone && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -117,31 +116,33 @@ export function AnnouncementDialog({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View
|
<FocusScope>
|
||||||
role="dialog"
|
<View
|
||||||
aria-role="dialog"
|
accessible={isAndroid}
|
||||||
aria-label={label}
|
role="dialog"
|
||||||
style={flatten([
|
aria-role="dialog"
|
||||||
a.relative,
|
aria-label={label}
|
||||||
a.w_full,
|
style={flatten([
|
||||||
a.p_2xl,
|
a.relative,
|
||||||
t.atoms.bg,
|
a.w_full,
|
||||||
!reduceMotionEnabled && a.zoom_fade_in,
|
a.p_2xl,
|
||||||
gtPhone && [
|
t.atoms.bg,
|
||||||
a.rounded_md,
|
!reduceMotionEnabled && a.zoom_fade_in,
|
||||||
a.border,
|
gtPhone && [
|
||||||
t.atoms.shadow_lg,
|
a.rounded_md,
|
||||||
t.atoms.border_contrast_low,
|
a.border,
|
||||||
web({
|
t.atoms.shadow_lg,
|
||||||
maxWidth: 420,
|
t.atoms.border_contrast_low,
|
||||||
}),
|
web({
|
||||||
],
|
maxWidth: 420,
|
||||||
])}>
|
}),
|
||||||
{children}
|
],
|
||||||
</View>
|
])}>
|
||||||
{/* </FocusScope.FocusScope> */}
|
{children}
|
||||||
|
</View>
|
||||||
|
</FocusScope>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
} from '#/components/dialogs/BlockingAnnouncements/common'
|
} from '#/components/dialogs/BlockingAnnouncements/common'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Span, Text} from '#/components/Typography'
|
import {Span, Text} from '#/components/Typography'
|
||||||
import {AnnouncementDialog, useAnnouncementDialogContext} from '#/components/dialogs/BlockingAnnouncements/AnnouncementDialog'
|
import {
|
||||||
|
AnnouncementDialog,
|
||||||
|
useAnnouncementDialogContext,
|
||||||
|
} from '#/components/dialogs/BlockingAnnouncements/AnnouncementDialog'
|
||||||
|
|
||||||
export function useLocalState() {
|
export function useLocalState() {
|
||||||
return useAnnouncementState({
|
return useAnnouncementState({
|
||||||
@@ -111,6 +114,8 @@ export function Announcement() {
|
|||||||
<View style={[a.w_full, a.gap_md]}>
|
<View style={[a.w_full, a.gap_md]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Continue`)}
|
label={_(msg`Continue`)}
|
||||||
|
accessibilityHint={_(msg`Tap to acknowledge that you understand and
|
||||||
|
agree to these updates and continue using Bluesky`)}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onPress={handleClose}>
|
onPress={handleClose}>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import {AnnouncementDialogOuter} from '#/components/dialogs/BlockingAnnouncements/AnnouncementDialog'
|
import {AnnouncementDialogOuter} from '#/components/dialogs/BlockingAnnouncements/AnnouncementDialog'
|
||||||
import * as PolicyUpdate20250801 from '#/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801'
|
import * as PolicyUpdate20250801 from '#/components/dialogs/BlockingAnnouncements/PolicyUpdate20250801'
|
||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
|
|
||||||
|
const portalGroup = createPortalGroup()
|
||||||
|
|
||||||
|
export const Provider = portalGroup.Provider
|
||||||
|
export const Outlet = portalGroup.Outlet
|
||||||
|
export const Portal = portalGroup.Portal
|
||||||
|
|
||||||
export function BlockingAnnouncements() {
|
export function BlockingAnnouncements() {
|
||||||
const policyUpdate20250801 = PolicyUpdate20250801.useLocalState()
|
const policyUpdate20250801 = PolicyUpdate20250801.useLocalState()
|
||||||
@@ -12,8 +19,10 @@ export function BlockingAnnouncements() {
|
|||||||
if (policyUpdate20250801.completed) return null
|
if (policyUpdate20250801.completed) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnnouncementDialogOuter>
|
<Portal>
|
||||||
<PolicyUpdate20250801.Announcement />
|
<AnnouncementDialogOuter>
|
||||||
</AnnouncementDialogOuter>
|
<PolicyUpdate20250801.Announcement />
|
||||||
|
</AnnouncementDialogOuter>
|
||||||
|
</Portal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {atoms as a, useLayoutBreakpoints} from '#/alf'
|
|||||||
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
|
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
|
||||||
import {DesktopLeftNav} from './desktop/LeftNav'
|
import {DesktopLeftNav} from './desktop/LeftNav'
|
||||||
import {DesktopRightNav} from './desktop/RightNav'
|
import {DesktopRightNav} from './desktop/RightNav'
|
||||||
|
import {BlockingAnnouncements} from '#/components/dialogs/BlockingAnnouncements'
|
||||||
|
|
||||||
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
|
||||||
requireAuth?: boolean
|
requireAuth?: boolean
|
||||||
@@ -167,6 +168,7 @@ function NativeStackNavigator({
|
|||||||
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
{!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<BlockingAnnouncements />
|
||||||
</NavigationContent>
|
</NavigationContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
|||||||
import {atoms as a, select, useTheme} from '#/alf'
|
import {atoms as a, select, useTheme} from '#/alf'
|
||||||
import {setSystemUITheme} from '#/alf/util/systemUI'
|
import {setSystemUITheme} from '#/alf/util/systemUI'
|
||||||
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
||||||
import {BlockingAnnouncements} from '#/components/dialogs/BlockingAnnouncements'
|
|
||||||
import {EmailDialog} from '#/components/dialogs/EmailDialog'
|
import {EmailDialog} from '#/components/dialogs/EmailDialog'
|
||||||
import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
|
import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent'
|
||||||
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
|
import {Outlet as BlockingAccouncementsPortalOutlet} from '#/components/dialogs/BlockingAnnouncements'
|
||||||
import {RoutesContainer, TabsNavigator} from '#/Navigation'
|
import {RoutesContainer, TabsNavigator} from '#/Navigation'
|
||||||
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
||||||
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
||||||
@@ -163,7 +163,9 @@ function ShellInner() {
|
|||||||
<Lightbox />
|
<Lightbox />
|
||||||
<PortalOutlet />
|
<PortalOutlet />
|
||||||
<BottomSheetOutlet />
|
<BottomSheetOutlet />
|
||||||
<BlockingAnnouncements />
|
|
||||||
|
{/* MUST BE LAST */}
|
||||||
|
<BlockingAccouncementsPortalOutlet />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal'
|
|||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {atoms as a, select, useTheme} from '#/alf'
|
import {atoms as a, select, useTheme} from '#/alf'
|
||||||
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog'
|
||||||
import {BlockingAnnouncements} from '#/components/dialogs/BlockingAnnouncements'
|
import {Outlet as BlockingAccouncementsPortalOutlet} from '#/components/dialogs/BlockingAnnouncements'
|
||||||
import {EmailDialog} from '#/components/dialogs/EmailDialog'
|
import {EmailDialog} from '#/components/dialogs/EmailDialog'
|
||||||
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
@@ -115,7 +115,8 @@ function ShellInner() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<BlockingAnnouncements />
|
{/* MUST BE LAST */}
|
||||||
|
<BlockingAccouncementsPortalOutlet />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user