From 8d0201aef59398a9847c77c5151c0d8aaf0afd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Sat, 16 May 2026 03:28:15 +0530 Subject: [PATCH 1/3] [ui][compose] - Add Snackbar component (#45667) # Why Adds Compose [Snackbar](https://developer.android.com/develop/ui/compose/components/snackbar) # How - Added native compose view, examples, docs. - Uses `SnackbarHost() { data -> Snackbar() }` API which provides us customisability and imperative `showSnackbar` function. # Test Plan Added example screen and tested docs examples. https://github.com/user-attachments/assets/cdd78b7d-32d7-4bba-8e19-947b25cba646 # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- .../src/screens/UI/SnackbarScreen.android.tsx | 99 +++++++++++++ .../src/screens/UI/UIScreen.android.tsx | 8 + .../sdk/ui/jetpack-compose/snackbar.mdx | 139 ++++++++++++++++++ .../sdk/ui/jetpack-compose/snackbar.mdx | 139 ++++++++++++++++++ .../expo-ui/jetpack-compose/snackbar.json | 1 + .../expo-ui/jetpack-compose/snackbar.json | 1 + .../images/expo-ui/snackbar/android-dark.webp | Bin 0 -> 4734 bytes .../expo-ui/snackbar/android-light.webp | Bin 0 -> 4880 bytes packages/expo-ui/CHANGELOG.md | 2 + .../main/java/expo/modules/ui/ExpoUIModule.kt | 10 ++ .../main/java/expo/modules/ui/SnackbarView.kt | 126 ++++++++++++++++ .../build/jetpack-compose/Snackbar/index.d.ts | 94 ++++++++++++ .../jetpack-compose/Snackbar/index.d.ts.map | 1 + .../expo-ui/build/jetpack-compose/index.d.ts | 1 + .../build/jetpack-compose/index.d.ts.map | 2 +- .../src/jetpack-compose/Snackbar/index.tsx | 135 +++++++++++++++++ packages/expo-ui/src/jetpack-compose/index.ts | 1 + tools/src/commands/GenerateDocsAPIData.ts | 1 + 18 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 apps/native-component-list/src/screens/UI/SnackbarScreen.android.tsx create mode 100644 docs/pages/versions/unversioned/sdk/ui/jetpack-compose/snackbar.mdx create mode 100644 docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/snackbar.mdx create mode 100644 docs/public/static/data/unversioned/expo-ui/jetpack-compose/snackbar.json create mode 100644 docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/snackbar.json create mode 100644 docs/public/static/images/expo-ui/snackbar/android-dark.webp create mode 100644 docs/public/static/images/expo-ui/snackbar/android-light.webp create mode 100644 packages/expo-ui/android/src/main/java/expo/modules/ui/SnackbarView.kt create mode 100644 packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts create mode 100644 packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts.map create mode 100644 packages/expo-ui/src/jetpack-compose/Snackbar/index.tsx diff --git a/apps/native-component-list/src/screens/UI/SnackbarScreen.android.tsx b/apps/native-component-list/src/screens/UI/SnackbarScreen.android.tsx new file mode 100644 index 00000000000000..7b68771bb73035 --- /dev/null +++ b/apps/native-component-list/src/screens/UI/SnackbarScreen.android.tsx @@ -0,0 +1,99 @@ +import { + Box, + Button, + Column, + Host, + Snackbar, + SnackbarHost, + type SnackbarHostRef, + Text as ComposeText, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; +import * as React from 'react'; + +export default function SnackbarScreen() { + const defaultHostRef = React.useRef(null); + const styledHostRef = React.useRef(null); + const [lastResult, setLastResult] = React.useState(''); + + const showShort = async () => { + const result = await defaultHostRef.current?.showSnackbar({ + message: 'Item archived', + actionLabel: 'Undo', + duration: 'short', + }); + setLastResult(`short: ${result ?? 'no host'}`); + }; + + const showWithDismissAction = async () => { + const result = await defaultHostRef.current?.showSnackbar({ + message: 'New email received', + withDismissAction: true, + duration: 'long', + }); + setLastResult(`long+dismiss: ${result ?? 'no host'}`); + }; + + const showIndefinite = async () => { + const result = await defaultHostRef.current?.showSnackbar({ + message: 'Connection lost, tap to retry', + actionLabel: 'Retry', + duration: 'indefinite', + }); + setLastResult(`indefinite: ${result ?? 'no host'}`); + }; + + const showStyled = async () => { + const result = await styledHostRef.current?.showSnackbar({ + message: 'Saved with custom colors', + actionLabel: 'OK', + duration: 'short', + }); + setLastResult(`styled: ${result ?? 'no host'}`); + }; + + return ( + + + + Default styling + + + + + Custom styling + Pass a Snackbar child to SnackbarHost to override colors. + + + Last result: {lastResult || '-'} + + + + + + + + + + + + + ); +} + +SnackbarScreen.navigationOptions = { + title: 'Snackbar', +}; diff --git a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx index f238f34c7cf32d..d9ab385ca8bdab 100644 --- a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx +++ b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx @@ -370,6 +370,14 @@ export const UIScreens = [ return optionalRequire(() => require('./SurfaceScreen')); }, }, + { + name: 'Snackbar component', + route: 'ui/snackbar', + options: {}, + getComponent() { + return optionalRequire(() => require('./SnackbarScreen')); + }, + }, { name: 'Tooltip component', route: 'ui/tooltip', diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/snackbar.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/snackbar.mdx new file mode 100644 index 00000000000000..66f0d81630284f --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/snackbar.mdx @@ -0,0 +1,139 @@ +--- +title: Snackbar +description: A brief notification that appears at the bottom of the screen to provide feedback without interrupting the user. +sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' +packageName: '@expo/ui' +platforms: ['android', 'expo-go'] +--- + +import APISection from '~/components/plugins/APISection'; +import { APIInstallSection } from '~/components/plugins/InstallSection'; +import { ComponentDiagram } from '~/ui/components/Diagram'; + +Expo UI exposes two components that mirror Jetpack Compose's [Snackbar](https://developer.android.com/develop/ui/compose/components/snackbar) APIs: + +- [`SnackbarHost`](#snackbarhost) wraps Compose's [SnackbarHost](https://developer.android.com/reference/kotlin/androidx/compose/material3/SnackbarHost.composable) and [SnackbarHostState](https://developer.android.com/reference/kotlin/androidx/compose/material3/SnackbarHostState). Place it once in your layout, then call `showSnackbar` on the `ref`. Snackbars auto-dismiss based on `duration` and can also be dismissed via the action button or the optional close icon. +- [`Snackbar`](#snackbar) is a styling-only child of [`SnackbarHost`](#snackbarhost). Pass one to override colors or place the action on a new line. + + + +## Installation + + + +## Usage + +### Showing a snackbar + +Place a [`SnackbarHost`](#snackbarhost) once in your layout and call `showSnackbar` on its ref to display a message. The returned promise resolves with `'actionPerformed'` or `'dismissed'` once the snackbar goes away. + +```tsx SnackbarExample.tsx +import { useRef } from 'react'; +import { + Box, + Button, + Column, + Host, + SnackbarHost, + Text, + type SnackbarHostRef, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; + +export default function SnackbarExample() { + const hostRef = useRef(null); + + const onArchive = async () => { + const result = await hostRef.current?.showSnackbar({ + message: 'Item archived', + actionLabel: 'Undo', + duration: 'short', + }); + if (result === 'actionPerformed') { + // The user tapped Undo, restore the item. + } + }; + + return ( + + + + + + + + + + + + ); +} +``` + +### Custom styling + +Pass a [`Snackbar`](#snackbar) child to [`SnackbarHost`](#snackbarhost) to override colors or place the action on a new line. The [`Snackbar`](#snackbar) itself takes no content, the message and action come from each `showSnackbar` call. + +```tsx StyledSnackbar.tsx +import { useRef } from 'react'; +import { + Box, + Button, + Column, + Host, + Snackbar, + SnackbarHost, + Text, + type SnackbarHostRef, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; + +export default function StyledSnackbar() { + const hostRef = useRef(null); + + const onSave = () => { + hostRef.current?.showSnackbar({ + message: 'Saved', + actionLabel: 'Undo', + }); + }; + + return ( + + + + + + + + + + + + + + ); +} +``` + +## API + +```tsx +import { Snackbar, SnackbarHost } from '@expo/ui/jetpack-compose'; +``` + + diff --git a/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/snackbar.mdx b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/snackbar.mdx new file mode 100644 index 00000000000000..d22ed88790523a --- /dev/null +++ b/docs/pages/versions/v56.0.0/sdk/ui/jetpack-compose/snackbar.mdx @@ -0,0 +1,139 @@ +--- +title: Snackbar +description: A brief notification that appears at the bottom of the screen to provide feedback without interrupting the user. +sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-56/packages/expo-ui' +packageName: '@expo/ui' +platforms: ['android', 'expo-go'] +--- + +import APISection from '~/components/plugins/APISection'; +import { APIInstallSection } from '~/components/plugins/InstallSection'; +import { ComponentDiagram } from '~/ui/components/Diagram'; + +Expo UI exposes two components that mirror Jetpack Compose's [Snackbar](https://developer.android.com/develop/ui/compose/components/snackbar) APIs: + +- [`SnackbarHost`](#snackbarhost) wraps Compose's [SnackbarHost](https://developer.android.com/reference/kotlin/androidx/compose/material3/SnackbarHost.composable) and [SnackbarHostState](https://developer.android.com/reference/kotlin/androidx/compose/material3/SnackbarHostState). Place it once in your layout, then call `showSnackbar` on the `ref`. Snackbars auto-dismiss based on `duration` and can also be dismissed via the action button or the optional close icon. +- [`Snackbar`](#snackbar) is a styling-only child of [`SnackbarHost`](#snackbarhost). Pass one to override colors or place the action on a new line. Maps to the `snackbar` lambda parameter of `SnackbarHost(hostState) { data -> Snackbar(data, ...) }` in Compose. + + + +## Installation + + + +## Usage + +### Showing a snackbar + +Place a [`SnackbarHost`](#snackbarhost) once in your layout and call `showSnackbar` on its ref to display a message. The returned promise resolves with `'actionPerformed'` or `'dismissed'` once the snackbar goes away. + +```tsx SnackbarExample.tsx +import { useRef } from 'react'; +import { + Box, + Button, + Column, + Host, + SnackbarHost, + Text, + type SnackbarHostRef, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; + +export default function SnackbarExample() { + const hostRef = useRef(null); + + const onArchive = async () => { + const result = await hostRef.current?.showSnackbar({ + message: 'Item archived', + actionLabel: 'Undo', + duration: 'short', + }); + if (result === 'actionPerformed') { + // The user tapped Undo, restore the item. + } + }; + + return ( + + + + + + + + + + + + ); +} +``` + +### Custom styling + +Pass a [`Snackbar`](#snackbar) child to [`SnackbarHost`](#snackbarhost) to override colors or place the action on a new line. The [`Snackbar`](#snackbar) itself takes no content, the message and action come from each `showSnackbar` call. + +```tsx StyledSnackbar.tsx +import { useRef } from 'react'; +import { + Box, + Button, + Column, + Host, + Snackbar, + SnackbarHost, + Text, + type SnackbarHostRef, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; + +export default function StyledSnackbar() { + const hostRef = useRef(null); + + const onSave = () => { + hostRef.current?.showSnackbar({ + message: 'Saved', + actionLabel: 'Undo', + }); + }; + + return ( + + + + + + + + + + + + + + ); +} +``` + +## API + +```tsx +import { Snackbar, SnackbarHost } from '@expo/ui/jetpack-compose'; +``` + + diff --git a/docs/public/static/data/unversioned/expo-ui/jetpack-compose/snackbar.json b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/snackbar.json new file mode 100644 index 00000000000000..b24d3a1834d7ae --- /dev/null +++ b/docs/public/static/data/unversioned/expo-ui/jetpack-compose/snackbar.json @@ -0,0 +1 @@ +{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/snackbar","variant":"project","kind":1,"children":[{"name":"SnackbarDuration","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"How long the snackbar is shown. Mirrors Compose's "},{"kind":"code","text":"`SnackbarDuration`"},{"kind":"text","text":" enum."}]},"type":{"type":"union","types":[{"type":"literal","value":"short"},{"type":"literal","value":"long"},{"type":"literal","value":"indefinite"}]}},{"name":"SnackbarHostProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Optional "},{"kind":"code","text":"`Snackbar`"},{"kind":"text","text":" child supplying styling for shown snackbars. Mirrors\nCompose's "},{"kind":"code","text":"`SnackbarHost(hostState) { data -> Snackbar(data, ...) }`"},{"kind":"text","text":" lambda."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"React.ReactNode","package":"@types/react"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Ref exposing the imperative "},{"kind":"code","text":"`showSnackbar`"},{"kind":"text","text":" method."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"SnackbarHostRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}}]},{"name":"SnackbarHostRef","variant":"declaration","kind":2097152,"children":[{"name":"showSnackbar","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Shows a snackbar and resolves with "},{"kind":"code","text":"`'actionPerformed'`"},{"kind":"text","text":" when the user taps\nthe action, or "},{"kind":"code","text":"`'dismissed'`"},{"kind":"text","text":" when it times out or the dismiss-action\nbutton is tapped. Subsequent calls queue and show after the current\nsnackbar is dismissed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarShowOptions","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"SnackbarResult","package":"@expo/ui"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"SnackbarProps","variant":"declaration","kind":2097152,"children":[{"name":"actionContentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The content color used for the action button."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"actionOnNewLine","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the action should be placed on a new line below the message.\nUseful for long action labels."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the snackbar container."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred content color used for the message text."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"dismissActionContentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The content color used for the dismiss-action icon button."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}}]},{"name":"SnackbarResult","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Reason a snackbar invocation resolved. Mirrors Compose's "},{"kind":"code","text":"`SnackbarResult`"},{"kind":"text","text":" enum."}]},"type":{"type":"union","types":[{"type":"literal","value":"actionPerformed"},{"type":"literal","value":"dismissed"}]}},{"name":"SnackbarShowOptions","variant":"declaration","kind":2097152,"children":[{"name":"actionLabel","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Label for the optional action button. When omitted, no action button is shown."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"duration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"How long to show the snackbar. Defaults to "},{"kind":"code","text":"`'short'`"},{"kind":"text","text":" when an "},{"kind":"code","text":"`actionLabel`"},{"kind":"text","text":"\nis not provided, and "},{"kind":"code","text":"`'indefinite'`"},{"kind":"text","text":" when it is, matching Compose."}]},"type":{"type":"reference","name":"SnackbarDuration","package":"@expo/ui"}},{"name":"message","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The message body of the snackbar."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"withDismissAction","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show a trailing close (X) icon button to dismiss the snackbar."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"Snackbar","variant":"declaration","kind":64,"signatures":[{"name":"Snackbar","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Styling configuration for the snackbar shown by "},{"kind":"code","text":"`SnackbarHost`"},{"kind":"text","text":". Pass as a\nchild to override colors or place the action on a new line."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]},{"name":"SnackbarHost","variant":"declaration","kind":64,"signatures":[{"name":"SnackbarHost","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A Material 3 [SnackbarHost](https://developer.android.com/develop/ui/compose/components/snackbar)\nthat displays snackbars triggered via its ref's "},{"kind":"code","text":"`showSnackbar`"},{"kind":"text","text":" method."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarHostProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"} \ No newline at end of file diff --git a/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/snackbar.json b/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/snackbar.json new file mode 100644 index 00000000000000..b24d3a1834d7ae --- /dev/null +++ b/docs/public/static/data/v56.0.0/expo-ui/jetpack-compose/snackbar.json @@ -0,0 +1 @@ +{"schemaVersion":"2.0","name":"expo-ui/jetpack-compose/snackbar","variant":"project","kind":1,"children":[{"name":"SnackbarDuration","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"How long the snackbar is shown. Mirrors Compose's "},{"kind":"code","text":"`SnackbarDuration`"},{"kind":"text","text":" enum."}]},"type":{"type":"union","types":[{"type":"literal","value":"short"},{"type":"literal","value":"long"},{"type":"literal","value":"indefinite"}]}},{"name":"SnackbarHostProps","variant":"declaration","kind":2097152,"children":[{"name":"children","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Optional "},{"kind":"code","text":"`Snackbar`"},{"kind":"text","text":" child supplying styling for shown snackbars. Mirrors\nCompose's "},{"kind":"code","text":"`SnackbarHost(hostState) { data -> Snackbar(data, ...) }`"},{"kind":"text","text":" lambda."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.ReactNode"},"name":"React.ReactNode","package":"@types/react"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}},{"name":"ref","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Ref exposing the imperative "},{"kind":"code","text":"`showSnackbar`"},{"kind":"text","text":" method."}]},"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"index.d.ts","qualifiedName":"React.Ref"},"typeArguments":[{"type":"reference","name":"SnackbarHostRef","package":"@expo/ui"}],"name":"Ref","package":"@types/react","qualifiedName":"React.Ref"}}]},{"name":"SnackbarHostRef","variant":"declaration","kind":2097152,"children":[{"name":"showSnackbar","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"Shows a snackbar and resolves with "},{"kind":"code","text":"`'actionPerformed'`"},{"kind":"text","text":" when the user taps\nthe action, or "},{"kind":"code","text":"`'dismissed'`"},{"kind":"text","text":" when it times out or the dismiss-action\nbutton is tapped. Subsequent calls queue and show after the current\nsnackbar is dismissed."}]},"type":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"signatures":[{"name":"__type","variant":"signature","kind":4096,"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarShowOptions","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"SnackbarResult","package":"@expo/ui"}],"name":"Promise","package":"typescript"}}]}}}]},{"name":"SnackbarProps","variant":"declaration","kind":2097152,"children":[{"name":"actionContentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The content color used for the action button."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"actionOnNewLine","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether the action should be placed on a new line below the message.\nUseful for long action labels."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}},{"name":"containerColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The background color of the snackbar container."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"contentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The preferred content color used for the message text."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"dismissActionContentColor","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The content color used for the dismiss-action icon button."}]},"type":{"type":"reference","target":{"packageName":"react-native","packagePath":"Libraries/StyleSheet/StyleSheet.d.ts","qualifiedName":"ColorValue"},"name":"ColorValue","package":"react-native"}},{"name":"modifiers","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Modifiers for the component."}]},"type":{"type":"array","elementType":{"type":"reference","target":{"packageName":"@expo/ui","packagePath":"src/types.ts","qualifiedName":"ModifierConfig"},"name":"ModifierConfig","package":"@expo/ui"}}}]},{"name":"SnackbarResult","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Reason a snackbar invocation resolved. Mirrors Compose's "},{"kind":"code","text":"`SnackbarResult`"},{"kind":"text","text":" enum."}]},"type":{"type":"union","types":[{"type":"literal","value":"actionPerformed"},{"type":"literal","value":"dismissed"}]}},{"name":"SnackbarShowOptions","variant":"declaration","kind":2097152,"children":[{"name":"actionLabel","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Label for the optional action button. When omitted, no action button is shown."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"duration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"How long to show the snackbar. Defaults to "},{"kind":"code","text":"`'short'`"},{"kind":"text","text":" when an "},{"kind":"code","text":"`actionLabel`"},{"kind":"text","text":"\nis not provided, and "},{"kind":"code","text":"`'indefinite'`"},{"kind":"text","text":" when it is, matching Compose."}]},"type":{"type":"reference","name":"SnackbarDuration","package":"@expo/ui"}},{"name":"message","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The message body of the snackbar."}]},"type":{"type":"intrinsic","name":"string"}},{"name":"withDismissAction","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Whether to show a trailing close (X) icon button to dismiss the snackbar."}],"blockTags":[{"tag":"@default","content":[{"kind":"text","text":"false"}]}]},"type":{"type":"intrinsic","name":"boolean"}}]},{"name":"Snackbar","variant":"declaration","kind":64,"signatures":[{"name":"Snackbar","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Styling configuration for the snackbar shown by "},{"kind":"code","text":"`SnackbarHost`"},{"kind":"text","text":". Pass as a\nchild to override colors or place the action on a new line."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]},{"name":"SnackbarHost","variant":"declaration","kind":64,"signatures":[{"name":"SnackbarHost","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"A Material 3 [SnackbarHost](https://developer.android.com/develop/ui/compose/components/snackbar)\nthat displays snackbars triggered via its ref's "},{"kind":"code","text":"`showSnackbar`"},{"kind":"text","text":" method."}]},"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","name":"SnackbarHostProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]}],"packageName":"@expo/ui"} \ No newline at end of file diff --git a/docs/public/static/images/expo-ui/snackbar/android-dark.webp b/docs/public/static/images/expo-ui/snackbar/android-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..0e9c745aaacf0a6758a47100daea195bf4025946 GIT binary patch literal 4734 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 HKTZDtASnP5 literal 0 HcmV?d00001 diff --git a/docs/public/static/images/expo-ui/snackbar/android-light.webp b/docs/public/static/images/expo-ui/snackbar/android-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..3e80d6563f8c91497611deae33035bf7f442a3b7 GIT binary patch literal 4880 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"SnackbarHostView") { + val showSnackbar by AsyncFunction() + + Content { props -> + SnackbarHostContent(props, showSnackbar) + } + } + ExpoUIView("TooltipBoxView") { val show by AsyncFunction() val dismiss by AsyncFunction() diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/SnackbarView.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/SnackbarView.kt new file mode 100644 index 00000000000000..168b4ae9a6139e --- /dev/null +++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/SnackbarView.kt @@ -0,0 +1,126 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package expo.modules.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDefaults +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.records.Field +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.types.OptimizedRecord +import expo.modules.kotlin.views.AsyncFunctionHandle +import expo.modules.kotlin.views.ComposableScope +import expo.modules.kotlin.views.ComposeProps +import expo.modules.kotlin.views.ExpoComposeView +import expo.modules.kotlin.views.FunctionalComposableScope +import expo.modules.kotlin.views.OptimizedComposeProps +import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException + +// Holds styling props that `SnackbarHost` reads via `findChildOfType`. + +@OptimizedComposeProps +data class SnackbarViewProps( + val containerColor: MutableState = mutableStateOf(null), + val contentColor: MutableState = mutableStateOf(null), + val actionContentColor: MutableState = mutableStateOf(null), + val dismissActionContentColor: MutableState = mutableStateOf(null), + val actionOnNewLine: MutableState = mutableStateOf(false), + val modifiers: MutableState = mutableStateOf(emptyList()) +) : ComposeProps + +@SuppressLint("ViewConstructor") +class SnackbarView(context: Context, appContext: AppContext) : + ExpoComposeView(context, appContext) { + override val props = SnackbarViewProps() + + @Composable + override fun ComposableScope.Content() { + // Empty by design: `SnackbarHost` renders the snackbar using the props above. + } +} + +// --- SnackbarHostView --- + +@OptimizedRecord +data class SnackbarShowOptions( + @Field val message: String = "", + @Field val actionLabel: String? = null, + @Field val withDismissAction: Boolean = false, + @Field val duration: String? = null +) : Record + +@OptimizedComposeProps +data class SnackbarHostProps( + val modifiers: ModifierList = emptyList() +) : ComposeProps + +@Composable +fun FunctionalComposableScope.SnackbarHostContent( + props: SnackbarHostProps, + showSnackbar: AsyncFunctionHandle +) { + val hostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val snackbarConfig = findChildOfType(view) + + showSnackbar.handle { options -> + val duration = when (options.duration) { + "short" -> SnackbarDuration.Short + "long" -> SnackbarDuration.Long + "indefinite" -> SnackbarDuration.Indefinite + // M3 default: indefinite when there's an action label, else short. + else -> if (options.actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite + } + val result = try { + withContext(scope.coroutineContext) { + hostState.showSnackbar( + message = options.message, + actionLabel = options.actionLabel, + withDismissAction = options.withDismissAction, + duration = duration + ) + } + } catch (_: CancellationException) { + // The compose scope can be cancelled if the view is disposed before user dismisses the snackbar or performs the action. + // In that case, we treat it as if the snackbar was dismissed. + SnackbarResult.Dismissed + } + when (result) { + SnackbarResult.ActionPerformed -> "actionPerformed" + SnackbarResult.Dismissed -> "dismissed" + } + } + + SnackbarHost( + hostState = hostState, + modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher) + ) { data -> + Snackbar( + snackbarData = data, + modifier = snackbarConfig?.let { + ModifierRegistry.applyModifiers(it.props.modifiers.value, appContext, composableScope, globalEventDispatcher) + } ?: androidx.compose.ui.Modifier, + actionOnNewLine = snackbarConfig?.props?.actionOnNewLine?.value ?: false, + containerColor = snackbarConfig?.props?.containerColor?.value.composeOrNull ?: SnackbarDefaults.color, + contentColor = snackbarConfig?.props?.contentColor?.value.composeOrNull ?: SnackbarDefaults.contentColor, + actionContentColor = snackbarConfig?.props?.actionContentColor?.value.composeOrNull + ?: SnackbarDefaults.actionContentColor, + dismissActionContentColor = snackbarConfig?.props?.dismissActionContentColor?.value.composeOrNull + ?: SnackbarDefaults.dismissActionContentColor + ) + } +} diff --git a/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts b/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts new file mode 100644 index 00000000000000..63f4fe4916e6f8 --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts @@ -0,0 +1,94 @@ +import { type Ref } from 'react'; +import { type ColorValue } from 'react-native'; +import { type ModifierConfig } from '../../types'; +export type SnackbarProps = { + /** + * The background color of the snackbar container. + */ + containerColor?: ColorValue; + /** + * The preferred content color used for the message text. + */ + contentColor?: ColorValue; + /** + * The content color used for the action button. + */ + actionContentColor?: ColorValue; + /** + * The content color used for the dismiss-action icon button. + */ + dismissActionContentColor?: ColorValue; + /** + * Whether the action should be placed on a new line below the message. + * Useful for long action labels. + * @default false + */ + actionOnNewLine?: boolean; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; +}; +/** + * Styling configuration for the snackbar shown by `SnackbarHost`. Pass as a + * child to override colors or place the action on a new line. + */ +export declare function Snackbar(props: SnackbarProps): import("react/jsx-runtime").JSX.Element; +/** + * How long the snackbar is shown. Mirrors Compose's `SnackbarDuration` enum. + */ +export type SnackbarDuration = 'short' | 'long' | 'indefinite'; +/** + * Reason a snackbar invocation resolved. Mirrors Compose's `SnackbarResult` enum. + */ +export type SnackbarResult = 'actionPerformed' | 'dismissed'; +export type SnackbarShowOptions = { + /** + * The message body of the snackbar. + */ + message: string; + /** + * Label for the optional action button. When omitted, no action button is shown. + */ + actionLabel?: string; + /** + * Whether to show a trailing close (X) icon button to dismiss the snackbar. + * @default false + */ + withDismissAction?: boolean; + /** + * How long to show the snackbar. Defaults to `'short'` when an `actionLabel` + * is not provided, and `'indefinite'` when it is, matching Compose. + */ + duration?: SnackbarDuration; +}; +export type SnackbarHostRef = { + /** + * Shows a snackbar and resolves with `'actionPerformed'` when the user taps + * the action, or `'dismissed'` when it times out or the dismiss-action + * button is tapped. Subsequent calls queue and show after the current + * snackbar is dismissed. + */ + showSnackbar: (options: SnackbarShowOptions) => Promise; +}; +export type SnackbarHostProps = { + /** + * Ref exposing the imperative `showSnackbar` method. + */ + ref?: Ref; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Optional `Snackbar` child supplying styling for shown snackbars. Mirrors + * Compose's `SnackbarHost(hostState) { data -> Snackbar(data, ...) }` lambda. + */ + children?: React.ReactNode; +}; +/** + * A Material 3 [SnackbarHost](https://developer.android.com/develop/ui/compose/components/snackbar) + * that displays snackbars triggered via its ref's `showSnackbar` method. + */ +export declare function SnackbarHost(props: SnackbarHostProps): import("react/jsx-runtime").JSX.Element; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts.map new file mode 100644 index 00000000000000..4986b962e1f68a --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/Snackbar/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/jetpack-compose/Snackbar/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;OAEG;IACH,kBAAkB,CAAC,EAAE,UAAU,CAAC;IAChC;;OAEG;IACH,yBAAyB,CAAC,EAAE,UAAU,CAAC;IACvC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAOF;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,2CAS5C;AAID;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;AAE/D;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;OAEG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;IAC3B;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAOF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,2CAUpD"} \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts b/packages/expo-ui/build/jetpack-compose/index.d.ts index fff5cc182e4300..a534b87bd38f1f 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts @@ -33,6 +33,7 @@ export * from './ModalBottomSheet'; export * from './Carousel'; export { HorizontalPager, type HorizontalPagerHandle, type HorizontalPagerProps, } from './HorizontalPager'; export * from './SearchBar'; +export * from './Snackbar'; export * from './DockedSearchBar'; export * from './HorizontalFloatingToolbar'; export * from './FloatingActionButton'; diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/index.d.ts.map index 46e047d6912424..be418e6406bf50 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts.map +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAE1B,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC;AAC3B,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAE1B,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file diff --git a/packages/expo-ui/src/jetpack-compose/Snackbar/index.tsx b/packages/expo-ui/src/jetpack-compose/Snackbar/index.tsx new file mode 100644 index 00000000000000..0cc5d7f105587b --- /dev/null +++ b/packages/expo-ui/src/jetpack-compose/Snackbar/index.tsx @@ -0,0 +1,135 @@ +import { requireNativeView } from 'expo'; +import { type Ref } from 'react'; +import { type ColorValue } from 'react-native'; + +import { type ModifierConfig } from '../../types'; +import { createViewModifierEventListener } from '../modifiers/utils'; + +export type SnackbarProps = { + /** + * The background color of the snackbar container. + */ + containerColor?: ColorValue; + /** + * The preferred content color used for the message text. + */ + contentColor?: ColorValue; + /** + * The content color used for the action button. + */ + actionContentColor?: ColorValue; + /** + * The content color used for the dismiss-action icon button. + */ + dismissActionContentColor?: ColorValue; + /** + * Whether the action should be placed on a new line below the message. + * Useful for long action labels. + * @default false + */ + actionOnNewLine?: boolean; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; +}; + +const SnackbarNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'SnackbarView' +); + +/** + * Styling configuration for the snackbar shown by `SnackbarHost`. Pass as a + * child to override colors or place the action on a new line. + */ +export function Snackbar(props: SnackbarProps) { + const { modifiers, ...rest } = props; + return ( + + ); +} + +// --- SnackbarHost --- + +/** + * How long the snackbar is shown. Mirrors Compose's `SnackbarDuration` enum. + */ +export type SnackbarDuration = 'short' | 'long' | 'indefinite'; + +/** + * Reason a snackbar invocation resolved. Mirrors Compose's `SnackbarResult` enum. + */ +export type SnackbarResult = 'actionPerformed' | 'dismissed'; + +export type SnackbarShowOptions = { + /** + * The message body of the snackbar. + */ + message: string; + /** + * Label for the optional action button. When omitted, no action button is shown. + */ + actionLabel?: string; + /** + * Whether to show a trailing close (X) icon button to dismiss the snackbar. + * @default false + */ + withDismissAction?: boolean; + /** + * How long to show the snackbar. Defaults to `'short'` when an `actionLabel` + * is not provided, and `'indefinite'` when it is, matching Compose. + */ + duration?: SnackbarDuration; +}; + +export type SnackbarHostRef = { + /** + * Shows a snackbar and resolves with `'actionPerformed'` when the user taps + * the action, or `'dismissed'` when it times out or the dismiss-action + * button is tapped. Subsequent calls queue and show after the current + * snackbar is dismissed. + */ + showSnackbar: (options: SnackbarShowOptions) => Promise; +}; + +export type SnackbarHostProps = { + /** + * Ref exposing the imperative `showSnackbar` method. + */ + ref?: Ref; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Optional `Snackbar` child supplying styling for shown snackbars. Mirrors + * Compose's `SnackbarHost(hostState) { data -> Snackbar(data, ...) }` lambda. + */ + children?: React.ReactNode; +}; + +const SnackbarHostNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'SnackbarHostView' +); + +/** + * A Material 3 [SnackbarHost](https://developer.android.com/develop/ui/compose/components/snackbar) + * that displays snackbars triggered via its ref's `showSnackbar` method. + */ +export function SnackbarHost(props: SnackbarHostProps) { + const { modifiers, children, ...rest } = props; + return ( + + {children} + + ); +} diff --git a/packages/expo-ui/src/jetpack-compose/index.ts b/packages/expo-ui/src/jetpack-compose/index.ts index 7ccd8838baf5f4..b6c207c84499fc 100644 --- a/packages/expo-ui/src/jetpack-compose/index.ts +++ b/packages/expo-ui/src/jetpack-compose/index.ts @@ -49,6 +49,7 @@ export { type HorizontalPagerProps, } from './HorizontalPager'; export * from './SearchBar'; +export * from './Snackbar'; export * from './DockedSearchBar'; export * from './HorizontalFloatingToolbar'; export * from './FloatingActionButton'; diff --git a/tools/src/commands/GenerateDocsAPIData.ts b/tools/src/commands/GenerateDocsAPIData.ts index c352ab4b66609d..8c395205411917 100644 --- a/tools/src/commands/GenerateDocsAPIData.ts +++ b/tools/src/commands/GenerateDocsAPIData.ts @@ -146,6 +146,7 @@ const uiPackagesMapping: Record = { 'expo-ui/jetpack-compose/spacer': ['jetpack-compose/Spacer/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/surface': ['jetpack-compose/Surface/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/checkbox': ['jetpack-compose/Checkbox/index.tsx', 'expo-ui'], + 'expo-ui/jetpack-compose/snackbar': ['jetpack-compose/Snackbar/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/switch': ['jetpack-compose/Switch/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/text': ['jetpack-compose/Text/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/textfield': ['jetpack-compose/TextField/index.tsx', 'expo-ui'], From 9db8d4affdffa03f0ee146ef46c80e551b683925 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Fri, 15 May 2026 15:47:02 -0700 Subject: [PATCH 2/3] [create-expo] Publish (and -app) new version with prompt --- packages/create-expo/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-expo/package.json b/packages/create-expo/package.json index c8e04afedcc8df..d8015e234d43bc 100644 --- a/packages/create-expo/package.json +++ b/packages/create-expo/package.json @@ -1,6 +1,6 @@ { "name": "create-expo", - "version": "3.8.0", + "version": "4.0.0", "bin": "./bin/create-expo.js", "main": "build", "description": "Create universal Expo apps", From 13b47116dea2937393f369d03e09a6a9bf9674cf Mon Sep 17 00:00:00 2001 From: Ray <153027766+ramonclaudio@users.noreply.github.com> Date: Fri, 15 May 2026 19:00:21 -0400 Subject: [PATCH 3/3] [ci] Skip secret-gated workflows on forks (#45859) --- .github/workflows/code-review.yml | 14 +++++++------ .github/workflows/commentator.yml | 1 + .github/workflows/development-client-e2e.yml | 1 + .../development-client-latest-e2e.yml | 3 ++- .github/workflows/docs-pr-destroy.yml | 5 +++-- .github/workflows/docs-pr.yml | 1 + .github/workflows/docs.yml | 1 + .github/workflows/issue-closed.yml | 2 +- .github/workflows/issue-opened.yml | 2 ++ .github/workflows/issue-triage.yml | 20 +++++++++---------- .github/workflows/pr-contributor-labeler.yml | 1 + .github/workflows/pr-labeler.yml | 2 +- .github/workflows/sync-template.yml | 1 + 13 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 91962d8b77d1bb..afe8ede76afbe3 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -14,13 +14,15 @@ concurrency: jobs: code_review: - # Auto-run only on same-repo PRs. Fork PRs don't get EXPO_BOT_GITHUB_TOKEN - # under `pull_request`, so the review would fail anyway — skip cleanly - # instead of leaving a red check. Maintainers can review a fork PR - # manually via workflow_dispatch. + # Auto-run only on same-repo PRs to expo/expo. Fork PRs don't get + # EXPO_BOT_GITHUB_TOKEN under `pull_request`, so the review would fail + # anyway — skip cleanly instead of leaving a red check. Forks dispatching + # this workflow manually would also fail for the same reason; gate on + # `github.repository` to short-circuit. if: >- - github.event_name == 'workflow_dispatch' || - github.event.pull_request.head.repo.full_name == github.repository + github.repository == 'expo/expo' && + (github.event_name == 'workflow_dispatch' || + github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-24.04 timeout-minutes: 30 steps: diff --git a/.github/workflows/commentator.yml b/.github/workflows/commentator.yml index 6408fd89612891..a8ba738ad1ef07 100644 --- a/.github/workflows/commentator.yml +++ b/.github/workflows/commentator.yml @@ -9,6 +9,7 @@ on: jobs: comment: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout diff --git a/.github/workflows/development-client-e2e.yml b/.github/workflows/development-client-e2e.yml index 10176e133ad4b3..199a8de25e3ab4 100644 --- a/.github/workflows/development-client-e2e.yml +++ b/.github/workflows/development-client-e2e.yml @@ -18,6 +18,7 @@ concurrency: jobs: detox_e2e: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 strategy: matrix: diff --git a/.github/workflows/development-client-latest-e2e.yml b/.github/workflows/development-client-latest-e2e.yml index fc18c3210c6f48..371b331d7fa420 100644 --- a/.github/workflows/development-client-latest-e2e.yml +++ b/.github/workflows/development-client-latest-e2e.yml @@ -11,6 +11,7 @@ concurrency: jobs: detox_latest_e2e: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 strategy: matrix: @@ -74,7 +75,7 @@ jobs: path: packages/expo-dev-client/artifacts - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() + if: failure() && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_dev_client }} channel: '#dev-clients' diff --git a/.github/workflows/docs-pr-destroy.yml b/.github/workflows/docs-pr-destroy.yml index 00055662700fd3..847756070d755c 100644 --- a/.github/workflows/docs-pr-destroy.yml +++ b/.github/workflows/docs-pr-destroy.yml @@ -14,8 +14,9 @@ concurrency: jobs: docs-pr-destroy: - if: (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) || - (github.event.action == 'unlabeled' && github.event.label.name == 'preview') + if: github.repository == 'expo/expo' && + ((github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) || + (github.event.action == 'unlabeled' && github.event.label.name == 'preview')) runs-on: ubuntu-24.04 steps: - name: 👀 Checkout diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 9a92cce9da7e1f..f057954530878e 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -22,6 +22,7 @@ concurrency: jobs: docs-pr: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bff884d51c9855..6632729113f95f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,7 @@ concurrency: jobs: docs: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout diff --git a/.github/workflows/issue-closed.yml b/.github/workflows/issue-closed.yml index 7f4ba4600da17b..47c6cd8dfffe1b 100644 --- a/.github/workflows/issue-closed.yml +++ b/.github/workflows/issue-closed.yml @@ -7,7 +7,7 @@ on: jobs: run-on-issue-accepted: runs-on: ubuntu-24.04 - if: contains(github.event.issue.labels.*.name, 'Issue accepted') + if: github.repository == 'expo/expo' && contains(github.event.issue.labels.*.name, 'Issue accepted') steps: - name: 👀 Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index 3ad996fde5526c..d18c993efbca4d 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -9,6 +9,7 @@ on: jobs: check-if-trusted: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - uses: ./.github/actions/slack-notify @@ -32,6 +33,7 @@ jobs: }) validate: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 10ee47e0ee4afb..dd40f002ea69aa 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -14,7 +14,7 @@ on: jobs: needs-repro: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -58,7 +58,7 @@ jobs: needs-info: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'incomplete issue: missing info') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'incomplete issue: missing info') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -93,7 +93,7 @@ jobs: # expotools with EXPO_BOT_GITHUB_TOKEN, LINEAR_API_KEY, and OPENAI_API_KEY # in env. Without the guard, applying an "issue accepted" label to a PR # would trigger the same flow on a pull_request_target event. - if: github.event_name == 'issues' && github.event.label.name == 'issue accepted' + if: github.repository == 'expo/expo' && github.event_name == 'issues' && github.event.label.name == 'issue accepted' steps: - name: Comment on issue uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 @@ -151,7 +151,7 @@ jobs: question: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'invalid issue: question') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'invalid issue: question') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -179,7 +179,7 @@ jobs: feature-request: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'invalid issue: feature request') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'invalid issue: feature request') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -202,7 +202,7 @@ jobs: third-party: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'invalid issue: third-party library') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'invalid issue: third-party library') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -229,7 +229,7 @@ jobs: }) react-native-core: runs-on: ubuntu-24.04 - if: "${{ contains(github.event.label.name, 'invalid issue: react-native-core') }}" + if: "${{ github.repository == 'expo/expo' && contains(github.event.label.name, 'invalid issue: react-native-core') }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -251,7 +251,7 @@ jobs: }) comments-on-closed: runs-on: ubuntu-24.04 - if: "${{ github.event.label.name == 'Comments on closed issue' }}" + if: "${{ github.repository == 'expo/expo' && github.event.label.name == 'Comments on closed issue' }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -273,7 +273,7 @@ jobs: }) eas-build-troubleshooting: runs-on: ubuntu-24.04 - if: "${{ github.event.label.name == 'invalid issue: EAS Build troubleshooting' }}" + if: "${{ github.repository == 'expo/expo' && github.event.label.name == 'invalid issue: EAS Build troubleshooting' }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: @@ -295,7 +295,7 @@ jobs: }) version-bump: runs-on: ubuntu-24.04 - if: "${{ github.event.label.name == 'Version bump PR' }}" + if: "${{ github.repository == 'expo/expo' && github.event.label.name == 'Version bump PR' }}" steps: - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: diff --git a/.github/workflows/pr-contributor-labeler.yml b/.github/workflows/pr-contributor-labeler.yml index 811bbab95df5b8..7c242358ec53b2 100644 --- a/.github/workflows/pr-contributor-labeler.yml +++ b/.github/workflows/pr-contributor-labeler.yml @@ -13,6 +13,7 @@ permissions: jobs: label: + if: github.repository == 'expo/expo' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 5ad053cf695932..e66dc3fded2cff 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -27,7 +27,7 @@ concurrency: jobs: test-suite-fingerprint: runs-on: ubuntu-24.04 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }} + if: ${{ github.repository == 'expo/expo' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push') }} # REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }} permissions: diff --git a/.github/workflows/sync-template.yml b/.github/workflows/sync-template.yml index 0a8998a5c35318..6f9b5bac963613 100644 --- a/.github/workflows/sync-template.yml +++ b/.github/workflows/sync-template.yml @@ -10,6 +10,7 @@ on: jobs: build: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout