diff --git a/demo/src/screens/MenuStructure.js b/demo/src/screens/MenuStructure.js
index c755c44436..da530ea008 100644
--- a/demo/src/screens/MenuStructure.js
+++ b/demo/src/screens/MenuStructure.js
@@ -84,6 +84,11 @@ export const navigationData = {
title: 'Overlays',
screens: [
{title: 'Action Sheet', tags: 'action sheet cross-platform', screen: 'unicorn.components.ActionSheetScreen'},
+ {
+ title: 'Action Sheet (Incubator)',
+ tags: 'action sheet',
+ screen: 'unicorn.components.IncubatorActionSheetScreen'
+ },
{title: 'Dialog', tags: 'dialog modal popup alert', screen: 'unicorn.components.DialogScreen'},
{title: 'Feature Highlight', tags: 'feature overlay', screen: 'unicorn.components.FeatureHighlightScreen'},
{title: 'Floating Button', tags: 'floating button', screen: 'unicorn.components.FloatingButtonScreen'},
@@ -99,7 +104,11 @@ export const navigationData = {
{title: 'Conversation List', tags: 'list conversation', screen: 'unicorn.lists.ConversationListScreen'},
{title: 'Drawer', tags: 'drawer', screen: 'unicorn.components.DrawerScreen'},
{title: 'SortableList', tags: 'sortable list drag', screen: 'unicorn.components.SortableListScreen'},
- {title: 'HorizontalSortableList', tags: 'sortable horizontal list drag', screen: 'unicorn.components.HorizontalSortableListScreen'},
+ {
+ title: 'HorizontalSortableList',
+ tags: 'sortable horizontal list drag',
+ screen: 'unicorn.components.HorizontalSortableListScreen'
+ },
{title: 'GridList', tags: 'grid list', screen: 'unicorn.components.GridListScreen'},
{title: 'SortableGridList', tags: 'sort grid list drag', screen: 'unicorn.components.SortableGridListScreen'}
]
@@ -120,7 +129,11 @@ export const navigationData = {
{title: 'Modal', tags: 'modal topbar screen', screen: 'unicorn.screens.ModalScreen'},
{title: 'StateScreen', tags: 'empty state screen', screen: 'unicorn.screens.EmptyStateScreen'},
{title: 'TabController', tags: 'tabbar controller native', screen: 'unicorn.components.TabControllerScreen'},
- {title: 'TabControllerWithStickyHeader', tags: 'tabbar controller native sticky header', screen: 'unicorn.components.TabControllerWithStickyHeaderScreen'},
+ {
+ title: 'TabControllerWithStickyHeader',
+ tags: 'tabbar controller native sticky header',
+ screen: 'unicorn.components.TabControllerWithStickyHeaderScreen'
+ },
{title: 'Timeline', tags: 'timeline', screen: 'unicorn.components.TimelineScreen'},
{
title: 'withScrollEnabler',
diff --git a/demo/src/screens/incubatorScreens/ActionSheetItems.tsx b/demo/src/screens/incubatorScreens/ActionSheetItems.tsx
new file mode 100644
index 0000000000..c0260a0aa1
--- /dev/null
+++ b/demo/src/screens/incubatorScreens/ActionSheetItems.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import {Alert, StyleSheet} from 'react-native';
+import {Assets, BorderRadiuses, Colors, Image, ImageProps, Spacings, View} from 'react-native-ui-lib';
+
+export enum TEXT_LENGTH {
+ NO_TEXT = 'No text',
+ SHORT = 'Short',
+ LONG = 'Long'
+}
+
+export enum CUSTOM_TITLE_COMPONENT {
+ NONE = 'None',
+ AVATAR = 'Avatar',
+ ICON = 'Icon',
+ THUMBNAIL = 'Thumbnail'
+}
+
+export enum OPTIONS_TYPE {
+ NONE = 'None',
+ REGULAR = 'Regular',
+ WITH_ICONS = 'With icons',
+ SECTION_HEADERS = 'Section headers',
+ GRID_VIEW = 'Grid view',
+ GRID_VIEW_LONG = 'Grid view long'
+}
+
+export type State = {
+ titleLength: TEXT_LENGTH;
+ titleIsProminent: boolean;
+ titleIsClickable: boolean;
+ subtitleLength: TEXT_LENGTH;
+ showFooter: boolean;
+ optionsType: OPTIONS_TYPE;
+ visible: boolean;
+};
+
+export const ICONS = [
+ Assets.icons.demo.settings,
+ Assets.icons.demo.refresh,
+ Assets.icons.check,
+ Assets.icons.x,
+ Assets.icons.plusSmall,
+ Assets.icons.demo.camera
+];
+
+const GRID_ITEM_CIRCLE_SIZE = 52;
+
+const pickOption = (option: string) => {
+ Alert.alert(`picked: ${option}`);
+};
+
+const renderCustomItem = (imageProps: ImageProps) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const listItems = [
+ {title: 'Open Settings', onPress: () => pickOption('Open Settings')},
+ {title: 'View Notifications', onPress: () => pickOption('View Notifications')},
+ {title: 'Update Profile', onPress: () => pickOption('Update Profile')},
+ {title: 'Log Out', onPress: () => pickOption('Log Out')},
+ {title: 'Share Post', onPress: () => pickOption('Share Post')},
+ {title: 'Send Message', onPress: () => pickOption('Send Message')},
+ {title: 'Take Photo', onPress: () => pickOption('Take Photo')},
+ {title: 'Record Video', onPress: () => pickOption('Record Video')},
+ {title: 'Add to Favorites', onPress: () => pickOption('Add to Favorites')},
+ {title: 'Search', onPress: () => pickOption('Search')},
+ {title: 'Refresh Feed', onPress: () => pickOption('Refresh Feed')},
+ {title: 'Edit Post', onPress: () => pickOption('Edit Post')},
+ {title: 'Report Issue', onPress: () => pickOption('Report Issue')},
+ {title: 'Contact Support', onPress: () => pickOption('Contact Support')},
+ {title: 'View Profile', onPress: () => pickOption('View Profile')},
+ {title: 'Cancel', onPress: () => pickOption('Cancel')}
+];
+
+export const gridItems = [
+ {
+ title: 'Open Settings',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.settings}),
+ onPress: () => pickOption('Open Settings')
+ },
+ {
+ title: 'View Notifications',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.refresh}),
+ onPress: () => pickOption('View Notifications')
+ },
+ {
+ title: 'Update Profile',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.check}),
+ onPress: () => pickOption('Update Profile'),
+ avoidDismiss: true
+ },
+ {
+ title: 'Log Out',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.x}),
+ onPress: () => pickOption('Log Out')
+ },
+ {
+ title: 'Share Post',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.plusSmall}),
+ onPress: () => pickOption('Share Post')
+ },
+ {
+ title: 'Take Photo',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.camera}),
+ onPress: () => pickOption('Take Photo')
+ },
+ {
+ title: 'Record Video',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.camera}),
+ onPress: () => pickOption('Record Video')
+ },
+ {
+ title: 'Send Message',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.settings}),
+ onPress: () => pickOption('Send Message')
+ },
+ {
+ title: 'Create Event',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.x}),
+ onPress: () => pickOption('Create Event')
+ },
+ {
+ title: 'Browse Contacts',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.check}),
+ onPress: () => pickOption('Browse Contacts')
+ },
+ {
+ title: 'Check Updates',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.plusSmall}),
+ onPress: () => pickOption('Check Updates')
+ },
+ {
+ title: 'Provide Feedback',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.refresh}),
+ onPress: () => pickOption('Provide Feedback')
+ },
+ {
+ title: 'View Gallery',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.camera}),
+ onPress: () => pickOption('View Gallery')
+ },
+ {
+ title: 'Access Help',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.check}),
+ onPress: () => pickOption('Access Help')
+ },
+ {
+ title: 'Explore Settings',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.x}),
+ onPress: () => pickOption('Explore Settings')
+ },
+ {
+ title: 'Manage Subscriptions',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.plusSmall}),
+ onPress: () => pickOption('Manage Subscriptions')
+ },
+ {
+ title: 'Change Password',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.settings}),
+ onPress: () => pickOption('Change Password')
+ },
+ {
+ title: 'View Terms of Service',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.refresh}),
+ onPress: () => pickOption('View Terms of Service')
+ },
+ {
+ title: 'Contact Support',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.x}),
+ onPress: () => pickOption('Contact Support')
+ },
+ {
+ title: 'Manage Privacy Settings',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.check}),
+ onPress: () => pickOption('Manage Privacy Settings')
+ },
+ {
+ title: 'Send Feedback',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.plusSmall}),
+ onPress: () => pickOption('Send Feedback')
+ },
+ {
+ title: 'View FAQ',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.camera}),
+ onPress: () => pickOption('View FAQ')
+ },
+ {
+ title: 'Reset App Preferences',
+ renderCustomItem: () => renderCustomItem({source: Assets.icons.demo.refresh}),
+ onPress: () => pickOption('Reset App Preferences')
+ }
+];
+
+const styles = StyleSheet.create({
+ gridItemCircle: {
+ width: GRID_ITEM_CIRCLE_SIZE,
+ height: GRID_ITEM_CIRCLE_SIZE,
+ borderWidth: 1,
+ borderRadius: BorderRadiuses.br100,
+ borderColor: Colors.$outlineDisabled
+ },
+
+ containerStyle: {marginBottom: Spacings.s2, marginHorizontal: Spacings.s2, alignContent: 'center'}
+});
diff --git a/demo/src/screens/incubatorScreens/IncubatorActionSheetScreen.tsx b/demo/src/screens/incubatorScreens/IncubatorActionSheetScreen.tsx
new file mode 100644
index 0000000000..0ffd0aeb1e
--- /dev/null
+++ b/demo/src/screens/incubatorScreens/IncubatorActionSheetScreen.tsx
@@ -0,0 +1,260 @@
+import _ from 'lodash';
+import React, {useState} from 'react';
+import {Alert, ScrollView, StyleSheet} from 'react-native';
+import {View, Button, Incubator, Text, Switch, RadioButton, RadioGroup, Typography, Colors} from 'react-native-ui-lib';
+import {listItems, gridItems, TEXT_LENGTH, OPTIONS_TYPE, State, ICONS} from './ActionSheetItems';
+
+function IncubatorActionSheetScreen() {
+ const [actionSheetOptions, setActionSheetOptions] = useState({
+ titleLength: TEXT_LENGTH.NO_TEXT,
+ titleIsProminent: false,
+ titleIsClickable: false,
+ subtitleLength: TEXT_LENGTH.NO_TEXT,
+ showFooter: false,
+ optionsType: OPTIONS_TYPE.NONE,
+ visible: false
+ });
+
+ const updateState = (newValues: Partial) => {
+ setActionSheetOptions(prevOptions => ({
+ ...prevOptions,
+ ...newValues
+ }));
+ };
+
+ const setTitleLength = (titleLength: TEXT_LENGTH) => {
+ if (titleLength !== actionSheetOptions.titleLength) {
+ updateState({titleLength});
+ }
+ };
+
+ const toggleProminent = () => {
+ updateState({titleIsProminent: !actionSheetOptions.titleIsProminent});
+ };
+
+ const toggleClickable = () => {
+ updateState({titleIsClickable: !actionSheetOptions.titleIsClickable});
+ };
+
+ const setSubtitleLength = (subtitleLength: TEXT_LENGTH) => {
+ if (subtitleLength !== actionSheetOptions.subtitleLength) {
+ updateState({subtitleLength});
+ }
+ };
+
+ const toggleFooter = () => {
+ updateState({showFooter: !actionSheetOptions.showFooter});
+ };
+
+ const setOptionsType = (optionsType: OPTIONS_TYPE) => {
+ if (optionsType !== actionSheetOptions.optionsType) {
+ updateState({optionsType});
+ }
+ };
+
+ const showActionSheet = () => {
+ updateState({visible: true});
+ };
+
+ const setVisible = (visible: boolean) => {
+ updateState({visible});
+ };
+
+ const getTitle = () => {
+ const {titleLength} = actionSheetOptions;
+ switch (titleLength) {
+ case TEXT_LENGTH.NO_TEXT:
+ return undefined;
+ case TEXT_LENGTH.SHORT:
+ default:
+ return 'Title';
+ case TEXT_LENGTH.LONG:
+ return 'This is a very long title, perhaps too long';
+ }
+ };
+
+ const clicked = (text: string) => {
+ Alert.alert(text);
+ };
+
+ const getSubtitle = () => {
+ const {subtitleLength} = actionSheetOptions;
+ switch (subtitleLength) {
+ case TEXT_LENGTH.NO_TEXT:
+ default:
+ return undefined;
+ case TEXT_LENGTH.SHORT:
+ return 'Subtitle';
+ case TEXT_LENGTH.LONG:
+ return 'This is a very long subtitle that hopefully tests two lines';
+ }
+ };
+
+ const getHeaderProps = () => {
+ const {titleIsProminent, titleIsClickable} = actionSheetOptions;
+ const onPress = titleIsClickable ? clicked('Header clicked') : undefined;
+ const titleStyle = titleIsProminent ? {...Typography.text70BO} : undefined;
+
+ return {
+ title: getTitle(),
+ titleProps: {accessibilityLabel: 'Custom accessibility label for ActionSheet header'},
+ subtitle: getSubtitle(),
+ titleStyle,
+ onPress
+ };
+ };
+
+ const getGridOptions = () => {
+ const {optionsType} = actionSheetOptions;
+ if (optionsType === OPTIONS_TYPE.GRID_VIEW || optionsType === OPTIONS_TYPE.GRID_VIEW_LONG) {
+ return {
+ numColumns: 3
+ };
+ }
+ };
+
+ const getList = () => {
+ const {optionsType} = actionSheetOptions;
+ switch (optionsType) {
+ case 'None':
+ return [];
+ case 'Regular':
+ return listItems;
+ case 'With icons':
+ return listItems.map((item, index) => ({
+ ...item,
+ icon: {
+ source: ICONS[index % ICONS.length],
+ tintColor: index % ICONS.length === 2 && 'red',
+ style: {marginRight: 10}
+ }
+ }));
+ case 'Grid view':
+ return gridItems.slice(0, 6).map(item => ({
+ ...item,
+ containerStyle: styles.gridItemsContainer
+ }));
+ case 'Grid view long':
+ return gridItems.map(item => ({
+ ...item,
+ containerStyle: styles.gridItemsContainer
+ }));
+ case 'Section headers':
+ return listItems.map((item, index) => ({
+ ...item,
+ isSectionHeader: index % 3 === 0,
+ titleStyle: index % 3 === 0 && {...Typography.text65},
+ sectionHeaderStyle: styles.sectionHeaders
+ }));
+ default:
+ return [];
+ }
+ };
+
+ const renderRadioButton = (key: string, value: string, hasLeftMargin: boolean) => {
+ return ;
+ };
+
+ const renderRadioGroup = (title: string,
+ data: {[s: number]: string},
+ initialValue?: string,
+ onValueChange?: (data: any) => void) => {
+ const radioButtons: React.ReactElement[] = [];
+ Object.entries(data).forEach(([key, value], index) => {
+ radioButtons.push(renderRadioButton(`${title}_${key}`, value, index !== _.size(data) - 1));
+ });
+
+ return (
+
+ {title}:
+
+ {radioButtons}
+
+
+ );
+ };
+
+ const renderTitleSwitches = () => {
+ const {titleIsProminent, titleIsClickable} = actionSheetOptions;
+
+ return (
+
+ Prominent style:
+
+ Clickable:
+
+
+ );
+ };
+
+ const renderActionSheet = () => {
+ const {visible, showFooter} = actionSheetOptions;
+ const list = getList();
+ const headerProps = getHeaderProps();
+ const gridOptions = getGridOptions();
+ const footerCustomElement = showFooter ? (
+
+
+ Footer
+
+
+ ) : undefined;
+
+ return (
+ {
+ console.log(`props onDismiss called!`);
+ setVisible(false);
+ }}
+ dialogProps={{
+ bottom: true,
+ centerH: true,
+ width: '95%',
+ height: _.isEmpty(list) && !gridOptions ? 150 : undefined,
+ //@ts-ignore
+ headerProps
+ }}
+ gridOptions={gridOptions}
+ footerCustomElement={footerCustomElement}
+ />
+ );
+ };
+
+ return (
+
+
+ Action Sheet
+
+
+
+ {renderRadioGroup('Title', TEXT_LENGTH, actionSheetOptions.titleLength, setTitleLength)}
+ {renderTitleSwitches()}
+ {renderRadioGroup('Subtitle', TEXT_LENGTH, actionSheetOptions.subtitleLength, setSubtitleLength)}
+ {renderRadioGroup('Options', OPTIONS_TYPE, actionSheetOptions.optionsType, setOptionsType)}
+
+ Add footer:
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ dismissButton: {
+ justifyContent: 'center'
+ },
+ scrollViewContainer: {
+ paddingBottom: 12
+ },
+ gridItemsContainer: {margin: 5, justifyContent: 'center', alignItems: 'center'},
+ sectionHeaders: {backgroundColor: Colors.grey60, padding: 10}
+});
+
+export default IncubatorActionSheetScreen;
diff --git a/demo/src/screens/incubatorScreens/index.js b/demo/src/screens/incubatorScreens/index.js
index 247080359c..0546b53f8e 100644
--- a/demo/src/screens/incubatorScreens/index.js
+++ b/demo/src/screens/incubatorScreens/index.js
@@ -6,4 +6,5 @@ export function registerScreens(registrar) {
registrar('unicorn.components.IncubatorToastScreen', () => require('./IncubatorToastScreen').default);
registrar('unicorn.incubator.PanViewScreen', () => require('./PanViewScreen').default);
registrar('unicorn.components.IncubatorSliderScreen', () => require('./IncubatorSliderScreen').default);
+ registrar('unicorn.components.IncubatorActionSheetScreen', () => require('./IncubatorActionSheetScreen').default);
}
diff --git a/src/components/actionSheet/actionSheet.api.json b/src/components/actionSheet/actionSheet.api.json
index 1d740d38e6..7116d2a8ba 100644
--- a/src/components/actionSheet/actionSheet.api.json
+++ b/src/components/actionSheet/actionSheet.api.json
@@ -16,7 +16,7 @@
{
"name": "cancelButtonIndex",
"type": "number",
- "description": "Index of the option represents the cancel action (to be displayed as the separated bottom bold button)"
+ "description": "Index of the option represents the cancel action (to be displayed as the separated bottom bold button). Relevant for Native IOS ActionSheet only."
},
{
"name": "destructiveButtonIndex",
diff --git a/src/components/actionSheet/index.tsx b/src/components/actionSheet/index.tsx
index 155a132f90..f6516c1fce 100644
--- a/src/components/actionSheet/index.tsx
+++ b/src/components/actionSheet/index.tsx
@@ -36,6 +36,7 @@ type ActionSheetProps = {
message?: string;
/**
* Index of the option represents the cancel action (to be displayed as the separated bottom bold button)
+ * Relevant for Native IOS ActionSheet only
*/
cancelButtonIndex?: number;
/**
diff --git a/src/components/fadedScrollView/index.tsx b/src/components/fadedScrollView/index.tsx
index a9472bc573..fc305bfff0 100644
--- a/src/components/fadedScrollView/index.tsx
+++ b/src/components/fadedScrollView/index.tsx
@@ -6,7 +6,7 @@ import {
NativeScrollEvent,
LayoutChangeEvent
} from 'react-native';
-import {ScrollView as GestureScrollView, GestureHandlerRootView} from 'react-native-gesture-handler';
+import {ScrollView as GestureScrollView} from 'react-native-gesture-handler';
import Fader, {FaderProps} from '../fader';
import useScrollEnabler from '../../hooks/useScrollEnabler';
import useScrollReached from '../../hooks/useScrollReached';
@@ -107,7 +107,7 @@ const FadedScrollView = (props: Props) => {
if (children) {
return (
-
+ <>
{
position={horizontal ? Fader.position.END : Fader.position.BOTTOM}
{...endFaderProps}
/>
-
+ >
);
}
diff --git a/src/incubator/ActionSheet/GridOptions.tsx b/src/incubator/ActionSheet/GridOptions.tsx
new file mode 100644
index 0000000000..a609ed65bc
--- /dev/null
+++ b/src/incubator/ActionSheet/GridOptions.tsx
@@ -0,0 +1,37 @@
+import _ from 'lodash';
+import React, {useMemo} from 'react';
+import GridView from '../../components/gridView';
+import {ActionSheetGridList, ActionSheetGridItemProps} from './types';
+
+const GRID_COLUMN_NUMBER = 3;
+
+const defaultProps = {
+ gridNumColumns: GRID_COLUMN_NUMBER
+};
+
+const GridOptions = React.memo((props: ActionSheetGridList) => {
+ const {options, onItemPress} = props;
+
+ const gridItems = useMemo(() => {
+ return _.map(options as ActionSheetGridItemProps[], (option, index) => {
+ const customValue = {selectedIndex: index, selectedOption: option};
+ return {
+ ...option,
+ onPress: () => {
+ option.onPress?.(option, index);
+ onItemPress?.(customValue);
+ }
+ };
+ });
+ }, [options, onItemPress]);
+
+ const gridView = useMemo(() => {
+ return ;
+ }, [gridItems]);
+
+ return gridView;
+});
+
+// @ts-ignore
+GridOptions.defaultProps = defaultProps;
+export default GridOptions;
diff --git a/src/incubator/ActionSheet/actionSheet.api.json b/src/incubator/ActionSheet/actionSheet.api.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/incubator/ActionSheet/index.tsx b/src/incubator/ActionSheet/index.tsx
new file mode 100644
index 0000000000..10c21549b9
--- /dev/null
+++ b/src/incubator/ActionSheet/index.tsx
@@ -0,0 +1,182 @@
+import _ from 'lodash';
+import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react';
+import {View, Text, ListItem, Icon} from '../../index';
+import FadedScrollView from '../../components/fadedScrollView';
+import {Dialog} from '../../incubator';
+import {withScrollReached, withScrollEnabler} from '../../commons/new';
+import {Colors} from '../../style';
+import GridOptions from './GridOptions';
+import {
+ ActionSheetProps,
+ ActionSheetOptionProps,
+ ActionSheetGridItemProps,
+ ActionSheetDismissReason,
+ ScrollReachedProps
+} from './types';
+
+export {ActionSheetProps, ActionSheetOptionProps, ActionSheetGridItemProps, ActionSheetDismissReason};
+
+type SelectedOption = ActionSheetOptionProps | ActionSheetGridItemProps;
+type Selection = {
+ selectedOption: SelectedOption;
+ selectedIndex: number;
+};
+
+const ActionSheet = (props: ActionSheetProps) => {
+ const {
+ dialogProps,
+ footerCustomElement,
+ options,
+ gridOptions,
+ onDismiss,
+ testID,
+ visible: propsVisibility,
+ ...others
+ } = props;
+
+ const [visible, setVisible] = useState(undefined);
+ const selection = useRef();
+
+ useEffect(() => {
+ if (propsVisibility && visible === undefined) {
+ setVisible(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [propsVisibility]);
+
+ const invokeUserOnPress = useCallback(() => {
+ selection.current?.selectedOption.onPress?.(selection.current?.selectedOption, selection.current?.selectedIndex);
+ }, []);
+
+ const resetSelected = useCallback(() => {
+ selection.current = undefined;
+ }, []);
+
+ const onItemPress = useCallback(({customValue}: {customValue: Selection}) => {
+ selection.current = customValue;
+ if (selection.current?.selectedOption.avoidDismiss) {
+ // do not dismiss (but notify the user the option was pressed)
+ invokeUserOnPress();
+ resetSelected();
+ return;
+ }
+ setVisible(false);
+ },
+ [invokeUserOnPress, resetSelected, setVisible]);
+
+ const invokeAfterActionSelected = useCallback(() => {
+ invokeUserOnPress();
+ onDismiss?.(ActionSheet.dismissReason.ACTION);
+ resetSelected();
+ setVisible(undefined);
+ }, [invokeUserOnPress, onDismiss, resetSelected, setVisible]);
+
+ const _onDismiss = useCallback(() => {
+ if (!_.isUndefined(selection.current)) {
+ invokeAfterActionSelected();
+ } else {
+ onDismiss?.(ActionSheet.dismissReason.CANCELED);
+ resetSelected();
+ setVisible(undefined);
+ }
+ }, [invokeAfterActionSelected, onDismiss, resetSelected, setVisible]);
+
+ const _modalProps = useMemo(() => {
+ const {modalProps} = dialogProps || {};
+ return {accessibilityLabel: 'Close action menu', ...modalProps};
+ }, [dialogProps]);
+
+ const renderIcon = (icon: ActionSheetOptionProps['icon']) => {
+ return ;
+ };
+
+ const renderSectionHeader = (option: ActionSheetOptionProps, index: number) => {
+ const {key, title, titleStyle, sectionHeaderStyle, testID} = option;
+ return (
+
+
+ {title}
+
+
+ );
+ };
+
+ const renderActionItem = (selectedOption: ActionSheetOptionProps, selectedIndex: number) => {
+ return (
+ onItemPress({customValue: {selectedOption, selectedIndex}})}
+ activeBackgroundColor={Colors.$backgroundNeutralLight}
+ >
+
+ {renderIcon(selectedOption.icon)}
+
+ {selectedOption.title}
+
+
+
+ );
+ };
+
+ const renderList = () => {
+ const {renderAction} = props;
+ return (
+
+ {_.map(options as ActionSheetOptionProps, (option: ActionSheetOptionProps, index: number) => {
+ const {isSectionHeader} = option;
+ if (isSectionHeader) {
+ return renderSectionHeader(option, index);
+ } else {
+ return renderAction ? renderAction(option, index) : renderActionItem(option, index);
+ }
+ })}
+
+ );
+ };
+
+ const _gridOptions = useMemo(() => {
+ if (gridOptions) {
+ return {
+ ...gridOptions,
+ options,
+ onItemPress
+ };
+ }
+ }, [gridOptions, options, onItemPress]);
+
+ const content = () => {
+ return (
+
+ {_gridOptions ? : renderList()}
+ {footerCustomElement}
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+ActionSheet.displayName = 'ActionSheet';
+ActionSheet.dismissReason = ActionSheetDismissReason;
+
+export default withScrollReached(withScrollEnabler(ActionSheet));
diff --git a/src/incubator/ActionSheet/types.ts b/src/incubator/ActionSheet/types.ts
new file mode 100644
index 0000000000..e6bf166968
--- /dev/null
+++ b/src/incubator/ActionSheet/types.ts
@@ -0,0 +1,121 @@
+import {StyleProp, TextStyle, ViewStyle} from 'react-native';
+import {DialogProps} from '../dialog';
+import {GridListItemProps} from '../../components/gridListItem';
+import {GridViewProps} from '../../components/gridView';
+import {IconProps} from '../../components/icon';
+import {WithScrollReachedProps, WithScrollEnablerProps} from '../../commons/new';
+
+export enum ActionSheetDismissReason {
+ ACTION = 'action',
+ CANCELED = 'canceled'
+}
+
+type BaseActionSheetOptionProps = {
+ /**
+ * Action handler for the GridItem
+ */
+ onPress?: (selectedOptionProps: ActionSheetGridItemProps, selectedOptionIndex: number) => void;
+ /**
+ * Send true to avoid dismissing the ActionSheet on press
+ */
+ avoidDismiss?: boolean;
+};
+
+export type ActionSheetOptionProps = BaseActionSheetOptionProps & {
+ /**
+ * Action title
+ */
+ title?: string;
+ /**
+ * title custom style
+ */
+ titleStyle?: StyleProp;
+ /**
+ * Action subtitle
+ */
+ subtitle?: string;
+ /**
+ * subtitle custom style
+ */
+ subtitleStyle?: StyleProp;
+ /**
+ * Icon to display for the action, render before the title
+ */
+ icon?: IconProps;
+ /**
+ * Custom element to be rendered on the Action left side
+ */
+ leftCustomElement?: React.ReactElement;
+ /**
+ * Custom element to be rendered on the Action right side
+ */
+ rightCustomElement?: React.ReactElement;
+ /**
+ * Is this option a section header
+ */
+ isSectionHeader?: boolean;
+ /**
+ * Section header style
+ */
+ sectionHeaderStyle?: StyleProp;
+ /**
+ * Testing identifier
+ */
+ testID?: string;
+ key?: string | number;
+};
+
+export type ActionSheetGridItemProps = Omit & BaseActionSheetOptionProps;
+
+export type ActionSheetGridProps = Omit & {
+ onItemPress?: (props: any) => void;
+};
+
+export type ActionSheetGridList = ActionSheetGridProps & Pick;
+
+export interface ActionSheetProps extends Pick {
+ dialogProps?: Omit;
+ /**
+ * The callback that is called when the ActionSheet is dismissed
+ */
+ onDismiss?: (reason: ActionSheetDismissReason) => void;
+ /**
+ * List of options for the action sheet
+ */
+ options?: ActionSheetOptionProps[] | ActionSheetGridItemProps[];
+ /**
+ * Grid options for the action sheet
+ */
+ gridOptions?: ActionSheetGridProps;
+ /**
+ * The options' container style
+ */
+ optionsContainerStyle?: StyleProp;
+ /**
+ * Index of the option represents the destructive action (will display red text. Usually used for 'delete' or
+ * 'abort' actions)
+ * Note: this is now used in both platforms, not only for native iOS
+ */
+ destructiveButtonIndex?: number;
+ /**
+ * Index of the option represents the prominent action
+ * (will display text with theme color. Used for a prominent action)
+ */
+ prominentButtonIndex?: number;
+ /**
+ * The props of footer checkbox
+ */
+ footerCustomElement?: React.ReactElement;
+ /**
+ * Render custom action
+ * Note: you will need to call onOptionPress so the option's onPress will be called
+ */
+ renderAction?: (option: ActionSheetOptionProps, index: number) => JSX.Element;
+ /**
+ * Component Test Id
+ */
+ testID?: string;
+}
+
+export interface ScrollReachedProps extends ActionSheetProps, WithScrollReachedProps {}
+export interface ScrollEnabledProps extends ScrollReachedProps, WithScrollEnablerProps {}
diff --git a/src/incubator/index.ts b/src/incubator/index.ts
index b864f24132..aebc5f4497 100644
--- a/src/incubator/index.ts
+++ b/src/incubator/index.ts
@@ -15,6 +15,24 @@ export {default as TouchableOpacity, TouchableOpacityProps} from './TouchableOpa
export {default as PanView, PanViewProps, PanViewDirections, PanViewDismissThreshold} from './panView';
export {default as Slider, SliderRef} from './slider';
export {default as Dialog, DialogProps, DialogHeaderProps, DialogStatics, DialogImperativeMethods} from './dialog';
+export {
+ default as ActionSheet,
+ ActionSheetProps,
+ ActionSheetOptionProps,
+ ActionSheetGridItemProps,
+ ActionSheetDismissReason
+} from './ActionSheet';
// TODO: delete exports after fully removing from private
-export {default as ChipsInput, ChipsInputProps, ChipsInputChangeReason, ChipsInputChipProps} from '../components/chipsInput';
-export {default as WheelPicker, WheelPickerProps, WheelPickerItemProps, WheelPickerAlign, WheelPickerItemValue} from '../components/WheelPicker';
+export {
+ default as ChipsInput,
+ ChipsInputProps,
+ ChipsInputChangeReason,
+ ChipsInputChipProps
+} from '../components/chipsInput';
+export {
+ default as WheelPicker,
+ WheelPickerProps,
+ WheelPickerItemProps,
+ WheelPickerAlign,
+ WheelPickerItemValue
+} from '../components/WheelPicker';