From b83e00934e794a936c9d3d23d7f94bbe89cedcd5 Mon Sep 17 00:00:00 2001 From: Ely Lucas Date: Mon, 1 Mar 2021 09:34:13 -0700 Subject: [PATCH] feat(react): add react hooks to control overlay components (#22484) --- core/src/components/action-sheet/readme.md | 53 +++++++++ .../components/action-sheet/usage/react.md | 53 +++++++++ core/src/components/alert/readme.md | 42 +++++++ core/src/components/alert/usage/react.md | 42 +++++++ core/src/components/loading/readme.md | 37 ++++++ core/src/components/loading/usage/react.md | 37 ++++++ core/src/components/modal/readme.md | 64 ++++++++++ core/src/components/modal/usage/react.md | 64 ++++++++++ core/src/components/picker/readme.md | 89 ++++++++++++++ core/src/components/picker/usage/react.md | 82 +++++++++++++ core/src/components/popover/readme.md | 53 +++++++++ core/src/components/popover/usage/react.md | 53 +++++++++ core/src/components/toast/readme.md | 42 +++++++ core/src/components/toast/usage/react.md | 42 +++++++ packages/react/src/components/index.ts | 9 ++ .../react/src/hooks/HookOverlayOptions.ts | 8 ++ packages/react/src/hooks/useController.ts | 83 +++++++++++++ packages/react/src/hooks/useIonActionSheet.ts | 53 +++++++++ packages/react/src/hooks/useIonAlert.ts | 53 +++++++++ packages/react/src/hooks/useIonLoading.tsx | 60 ++++++++++ packages/react/src/hooks/useIonModal.ts | 36 ++++++ packages/react/src/hooks/useIonPicker.tsx | 58 +++++++++ packages/react/src/hooks/useIonPopover.ts | 36 ++++++ packages/react/src/hooks/useIonToast.ts | 53 +++++++++ packages/react/src/hooks/useOverlay.ts | 111 ++++++++++++++++++ 25 files changed, 1313 insertions(+) create mode 100644 core/src/components/picker/usage/react.md create mode 100644 packages/react/src/hooks/HookOverlayOptions.ts create mode 100644 packages/react/src/hooks/useController.ts create mode 100644 packages/react/src/hooks/useIonActionSheet.ts create mode 100644 packages/react/src/hooks/useIonAlert.ts create mode 100644 packages/react/src/hooks/useIonLoading.tsx create mode 100644 packages/react/src/hooks/useIonModal.ts create mode 100644 packages/react/src/hooks/useIonPicker.tsx create mode 100644 packages/react/src/hooks/useIonPopover.ts create mode 100644 packages/react/src/hooks/useIonToast.ts create mode 100644 packages/react/src/hooks/useOverlay.ts diff --git a/core/src/components/action-sheet/readme.md b/core/src/components/action-sheet/readme.md index 83690632c5..d43895b277 100644 --- a/core/src/components/action-sheet/readme.md +++ b/core/src/components/action-sheet/readme.md @@ -155,6 +155,59 @@ async function presentActionSheet() { ### React ```tsx +/* Using with useIonActionSheet Hook */ + +import React from 'react'; +import { + IonButton, + IonContent, + IonPage, + useIonActionSheet, +} from '@ionic/react'; + +const ActionSheetExample: React.FC = () => { + const [present, dismiss] = useIonActionSheet(); + + return ( + + + + present({ + buttons: [{ text: 'Ok' }, { text: 'Cancel' }], + header: 'Action Sheet' + }) + } + > + Show ActionSheet + + + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet') + } + > + Show ActionSheet using params + + { + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet'); + setTimeout(dismiss, 3000); + }} + > + Show ActionSheet, hide after 3 seconds + + + + ); +}; +``` + +```tsx +/* Using with IonActionSheet Component */ + import React, { useState } from 'react'; import { IonActionSheet, IonContent, IonButton } from '@ionic/react'; import { trash, share, caretForwardCircle, heart, close } from 'ionicons/icons'; diff --git a/core/src/components/action-sheet/usage/react.md b/core/src/components/action-sheet/usage/react.md index ed9cc8505c..7259fed327 100644 --- a/core/src/components/action-sheet/usage/react.md +++ b/core/src/components/action-sheet/usage/react.md @@ -1,4 +1,57 @@ ```tsx +/* Using with useIonActionSheet Hook */ + +import React from 'react'; +import { + IonButton, + IonContent, + IonPage, + useIonActionSheet, +} from '@ionic/react'; + +const ActionSheetExample: React.FC = () => { + const [present, dismiss] = useIonActionSheet(); + + return ( + + + + present({ + buttons: [{ text: 'Ok' }, { text: 'Cancel' }], + header: 'Action Sheet' + }) + } + > + Show ActionSheet + + + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet') + } + > + Show ActionSheet using params + + { + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet'); + setTimeout(dismiss, 3000); + }} + > + Show ActionSheet, hide after 3 seconds + + + + ); +}; +``` + +```tsx +/* Using with IonActionSheet Component */ + import React, { useState } from 'react'; import { IonActionSheet, IonContent, IonButton } from '@ionic/react'; import { trash, share, caretForwardCircle, heart, close } from 'ionicons/icons'; diff --git a/core/src/components/alert/readme.md b/core/src/components/alert/readme.md index ab7fa95b9b..28f0cd6089 100644 --- a/core/src/components/alert/readme.md +++ b/core/src/components/alert/readme.md @@ -588,6 +588,48 @@ function presentAlertCheckbox() { ### React ```tsx +/* Using with useIonAlert Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonAlert } from '@ionic/react'; + +const AlertExample: React.FC = () => { + const [present] = useIonAlert(); + return ( + + + + present({ + cssClass: 'my-css', + header: 'Alert', + message: 'alert from hook', + buttons: [ + 'Cancel', + { text: 'Ok', handler: (d) => console.log('ok pressed') }, + ], + onDidDismiss: (e) => console.log('did dismiss'), + }) + } + > + Show Alert + + present('hello with params', [{ text: 'Ok' }])} + > + Show Alert using params + + + + ); +}; +``` + +```tsx +/* Using with IonAlert Component */ + import React, { useState } from 'react'; import { IonAlert, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/alert/usage/react.md b/core/src/components/alert/usage/react.md index 4776e11bb3..17a365d434 100644 --- a/core/src/components/alert/usage/react.md +++ b/core/src/components/alert/usage/react.md @@ -1,4 +1,46 @@ ```tsx +/* Using with useIonAlert Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonAlert } from '@ionic/react'; + +const AlertExample: React.FC = () => { + const [present] = useIonAlert(); + return ( + + + + present({ + cssClass: 'my-css', + header: 'Alert', + message: 'alert from hook', + buttons: [ + 'Cancel', + { text: 'Ok', handler: (d) => console.log('ok pressed') }, + ], + onDidDismiss: (e) => console.log('did dismiss'), + }) + } + > + Show Alert + + present('hello with params', [{ text: 'Ok' }])} + > + Show Alert using params + + + + ); +}; +``` + +```tsx +/* Using with IonAlert Component */ + import React, { useState } from 'react'; import { IonAlert, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/loading/readme.md b/core/src/components/loading/readme.md index 84f29b9a4d..0f2f7879c1 100644 --- a/core/src/components/loading/readme.md +++ b/core/src/components/loading/readme.md @@ -131,6 +131,43 @@ async function presentLoadingWithOptions() { ### React ```tsx +/* Using with useIonLoading Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonLoading } from '@ionic/react'; + +interface LoadingProps {} + +const LoadingExample: React.FC = () => { + const [present] = useIonLoading(); + return ( + + + + present({ + duration: 3000, + }) + } + > + Show Loading + + present('Loading', 2000, 'dots')} + > + Show Loading using params + + + + ); +}; +``` + +```tsx +/* Using with IonLoading Component */ + import React, { useState } from 'react'; import { IonLoading, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/loading/usage/react.md b/core/src/components/loading/usage/react.md index 102b5b730d..b99e0d2951 100644 --- a/core/src/components/loading/usage/react.md +++ b/core/src/components/loading/usage/react.md @@ -1,4 +1,41 @@ ```tsx +/* Using with useIonLoading Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonLoading } from '@ionic/react'; + +interface LoadingProps {} + +const LoadingExample: React.FC = () => { + const [present] = useIonLoading(); + return ( + + + + present({ + duration: 3000, + }) + } + > + Show Loading + + present('Loading', 2000, 'dots')} + > + Show Loading using params + + + + ); +}; +``` + +```tsx +/* Using with IonLoading Component */ + import React, { useState } from 'react'; import { IonLoading, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/modal/readme.md b/core/src/components/modal/readme.md index 9961776372..af3bee1008 100644 --- a/core/src/components/modal/readme.md +++ b/core/src/components/modal/readme.md @@ -332,6 +332,70 @@ modalElement.presentingElement = await modalController.getTop(); // Get the top- ### React ```tsx +/* Using with useIonModal Hook */ + +import React, { useState } from 'react'; +import { IonButton, IonContent, IonPage, useIonModal } from '@ionic/react'; + +const Body: React.FC<{ + count: number; + onDismiss: () => void; + onIncrement: () => void; +}> = ({ count, onDismiss, onIncrement }) => ( +
+ count: {count} + onIncrement()}> + Increment Count + + onDismiss()}> + Close + +
+); + +const ModalExample: React.FC = () => { + const [count, setCount] = useState(0); + + const handleIncrement = () => { + setCount(count + 1); + }; + + const handleDismiss = () => { + dismiss(); + }; + + /** + * First parameter is the component to show, second is the props to pass + */ + const [present, dismiss] = useIonModal(Body, { + count, + onDismiss: handleDismiss, + onIncrement: handleIncrement, + }); + + return ( + + + { + present({ + cssClass: 'my-class', + }); + }} + > + Show Modal + +
Count: {count}
+
+
+ ); +}; +``` + +```tsx +/* Using with IonModal Component */ + import React, { useState } from 'react'; import { IonModal, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/modal/usage/react.md b/core/src/components/modal/usage/react.md index 71926dc662..5dc0d651db 100644 --- a/core/src/components/modal/usage/react.md +++ b/core/src/components/modal/usage/react.md @@ -1,4 +1,68 @@ ```tsx +/* Using with useIonModal Hook */ + +import React, { useState } from 'react'; +import { IonButton, IonContent, IonPage, useIonModal } from '@ionic/react'; + +const Body: React.FC<{ + count: number; + onDismiss: () => void; + onIncrement: () => void; +}> = ({ count, onDismiss, onIncrement }) => ( +
+ count: {count} + onIncrement()}> + Increment Count + + onDismiss()}> + Close + +
+); + +const ModalExample: React.FC = () => { + const [count, setCount] = useState(0); + + const handleIncrement = () => { + setCount(count + 1); + }; + + const handleDismiss = () => { + dismiss(); + }; + + /** + * First parameter is the component to show, second is the props to pass + */ + const [present, dismiss] = useIonModal(Body, { + count, + onDismiss: handleDismiss, + onIncrement: handleIncrement, + }); + + return ( + + + { + present({ + cssClass: 'my-class', + }); + }} + > + Show Modal + +
Count: {count}
+
+
+ ); +}; +``` + +```tsx +/* Using with IonModal Component */ + import React, { useState } from 'react'; import { IonModal, IonButton, IonContent } from '@ionic/react'; diff --git a/core/src/components/picker/readme.md b/core/src/components/picker/readme.md index 0e7513a954..bcae3a1a86 100644 --- a/core/src/components/picker/readme.md +++ b/core/src/components/picker/readme.md @@ -7,6 +7,95 @@ A Picker is a dialog that displays a row of buttons and columns underneath. It a +## Usage + +### React + +```tsx +/* Using with useIonPicker Hook */ + +import React, { useState } from 'react'; +import { IonButton, IonContent, IonPage, useIonPicker } from '@ionic/react'; + +const PickerExample: React.FC = () => { + const [present] = useIonPicker(); + const [value, setValue] = useState(''); + return ( + + + + present({ + buttons: [ + { + text: 'Confirm', + handler: (selected) => { + setValue(selected.animal.value) + }, + }, + ], + columns: [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + ], + }) + } + > + Show Picker + + + present( + [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + { + name: 'vehicle', + options: [ + { text: 'Car', value: 'car' }, + { text: 'Truck', value: 'truck' }, + { text: 'Bike', value: 'bike' }, + ], + }, + ], + [ + { + text: 'Confirm', + handler: (selected) => { + setValue(`${selected.animal.value}, ${selected.vehicle.value}`) + }, + }, + ] + ) + } + > + Show Picker using params + + {value && ( +
Selected Value: {value}
+ )} +
+
+ ); +}; +``` + + + ## Properties | Property | Attribute | Description | Type | Default | diff --git a/core/src/components/picker/usage/react.md b/core/src/components/picker/usage/react.md new file mode 100644 index 0000000000..1d016decba --- /dev/null +++ b/core/src/components/picker/usage/react.md @@ -0,0 +1,82 @@ +```tsx +/* Using with useIonPicker Hook */ + +import React, { useState } from 'react'; +import { IonButton, IonContent, IonPage, useIonPicker } from '@ionic/react'; + +const PickerExample: React.FC = () => { + const [present] = useIonPicker(); + const [value, setValue] = useState(''); + return ( + + + + present({ + buttons: [ + { + text: 'Confirm', + handler: (selected) => { + setValue(selected.animal.value) + }, + }, + ], + columns: [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + ], + }) + } + > + Show Picker + + + present( + [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + { + name: 'vehicle', + options: [ + { text: 'Car', value: 'car' }, + { text: 'Truck', value: 'truck' }, + { text: 'Bike', value: 'bike' }, + ], + }, + ], + [ + { + text: 'Confirm', + handler: (selected) => { + setValue(`${selected.animal.value}, ${selected.vehicle.value}`) + }, + }, + ] + ) + } + > + Show Picker using params + + {value && ( +
Selected Value: {value}
+ )} +
+
+ ); +}; +``` \ No newline at end of file diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index b8fbd9b1b1..b62156f9db 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -114,6 +114,59 @@ function presentPopover(ev) { ### React ```tsx +/* Using with useIonPopover Hook */ + +import React from 'react'; +import { + IonButton, + IonContent, + IonItem, + IonList, + IonListHeader, + IonPage, + useIonPopover, +} from '@ionic/react'; + +const PopoverList: React.FC<{ + onHide: () => void; +}> = ({ onHide }) => ( + + Ionic + Learn Ionic + Documentation + Showcase + GitHub Repo + + Close + + +); + +const PopoverExample: React.FC = () => { + const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() }); + + return ( + + + + present({ + event: e.nativeEvent, + }) + } + > + Show Popover + + + + ); +}; +``` + +```tsx +/* Using with IonPopover Component */ + import React, { useState } from 'react'; import { IonPopover, IonButton } from '@ionic/react'; diff --git a/core/src/components/popover/usage/react.md b/core/src/components/popover/usage/react.md index f55b109f52..e0d444e5dd 100644 --- a/core/src/components/popover/usage/react.md +++ b/core/src/components/popover/usage/react.md @@ -1,4 +1,57 @@ ```tsx +/* Using with useIonPopover Hook */ + +import React from 'react'; +import { + IonButton, + IonContent, + IonItem, + IonList, + IonListHeader, + IonPage, + useIonPopover, +} from '@ionic/react'; + +const PopoverList: React.FC<{ + onHide: () => void; +}> = ({ onHide }) => ( + + Ionic + Learn Ionic + Documentation + Showcase + GitHub Repo + + Close + + +); + +const PopoverExample: React.FC = () => { + const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() }); + + return ( + + + + present({ + event: e.nativeEvent, + }) + } + > + Show Popover + + + + ); +}; +``` + +```tsx +/* Using with IonPopover Component */ + import React, { useState } from 'react'; import { IonPopover, IonButton } from '@ionic/react'; diff --git a/core/src/components/toast/readme.md b/core/src/components/toast/readme.md index f29b1c03d8..9ca8a37938 100644 --- a/core/src/components/toast/readme.md +++ b/core/src/components/toast/readme.md @@ -111,6 +111,48 @@ async function presentToastWithOptions() { ### React ```tsx +/* Using the useIonToast Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonToast } from '@ionic/react'; + +const ToastExample: React.FC = () => { + const [present, dismiss] = useIonToast(); + + return ( + + + + present({ + buttons: [{ text: 'hide', handler: () => dismiss() }], + message: 'toast from hook, click hide to dismiss', + onDidDismiss: () => console.log('dismissed'), + onWillDismiss: () => console.log('will dismiss'), + }) + } + > + Show Toast + + present('hello from hook', 3000)} + > + Show Toast using params, closes in 3 secs + + + Hide Toast + + + + ); +}; +``` + +```tsx +/* Using the IonToast Component */ + import React, { useState } from 'react'; import { IonToast, IonContent, IonButton } from '@ionic/react'; diff --git a/core/src/components/toast/usage/react.md b/core/src/components/toast/usage/react.md index 5aad0cf5df..82367cc525 100644 --- a/core/src/components/toast/usage/react.md +++ b/core/src/components/toast/usage/react.md @@ -1,4 +1,46 @@ ```tsx +/* Using the useIonToast Hook */ + +import React from 'react'; +import { IonButton, IonContent, IonPage, useIonToast } from '@ionic/react'; + +const ToastExample: React.FC = () => { + const [present, dismiss] = useIonToast(); + + return ( + + + + present({ + buttons: [{ text: 'hide', handler: () => dismiss() }], + message: 'toast from hook, click hide to dismiss', + onDidDismiss: () => console.log('dismissed'), + onWillDismiss: () => console.log('will dismiss'), + }) + } + > + Show Toast + + present('hello from hook', 3000)} + > + Show Toast using params, closes in 3 secs + + + Hide Toast + + + + ); +}; +``` + +```tsx +/* Using the IonToast Component */ + import React, { useState } from 'react'; import { IonToast, IonContent, IonButton } from '@ionic/react'; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 39acf1346c..0395ab811a 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -69,6 +69,15 @@ export * from './hrefprops'; // Ionic Animations export { CreateAnimation } from './CreateAnimation'; +// Hooks +export { useIonActionSheet, UseIonActionSheetResult } from '../hooks/useIonActionSheet'; +export { useIonAlert, UseIonAlertResult } from '../hooks/useIonAlert'; +export { useIonToast, UseIonToastResult } from '../hooks/useIonToast'; +export { useIonModal, UseIonModalResult } from '../hooks/useIonModal'; +export { useIonPopover, UseIonPopoverResult } from '../hooks/useIonPopover'; +export { useIonPicker, UseIonPickerResult } from '../hooks/useIonPicker'; +export { useIonLoading, UseIonLoadingResult } from '../hooks/useIonLoading'; + // Icons that are used by internal components addIcons({ 'arrow-back-sharp': arrowBackSharp, diff --git a/packages/react/src/hooks/HookOverlayOptions.ts b/packages/react/src/hooks/HookOverlayOptions.ts new file mode 100644 index 0000000000..431652758c --- /dev/null +++ b/packages/react/src/hooks/HookOverlayOptions.ts @@ -0,0 +1,8 @@ +import { OverlayEventDetail } from '@ionic/core'; + +export interface HookOverlayOptions { + onDidDismiss?: (event: CustomEvent) => void; + onDidPresent?: (event: CustomEvent) => void; + onWillDismiss?: (event: CustomEvent) => void; + onWillPresent?: (event: CustomEvent) => void; +} diff --git a/packages/react/src/hooks/useController.ts b/packages/react/src/hooks/useController.ts new file mode 100644 index 0000000000..fa596f3704 --- /dev/null +++ b/packages/react/src/hooks/useController.ts @@ -0,0 +1,83 @@ +import { OverlayEventDetail } from '@ionic/core'; +import { useMemo, useRef } from 'react'; + +import { attachProps } from '../components/utils'; + +import { HookOverlayOptions } from './HookOverlayOptions'; + +interface OverlayBase extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export function useController< + OptionsType, + OverlayType extends OverlayBase +>( + displayName: string, + controller: { create: (options: OptionsType) => Promise } +) { + const overlayRef = useRef(); + const didDismissEventName = useMemo( + () => `on${displayName}DidDismiss`, + [displayName] + ); + const didPresentEventName = useMemo( + () => `on${displayName}DidPresent`, + [displayName] + ); + const willDismissEventName = useMemo( + () => `on${displayName}WillDismiss`, + [displayName] + ); + const willPresentEventName = useMemo( + () => `on${displayName}WillPresent`, + [displayName] + ); + + const present = async (options: OptionsType & HookOverlayOptions) => { + if (overlayRef.current) { + return; + } + const { + onDidDismiss, + onWillDismiss, + onDidPresent, + onWillPresent, + ...rest + } = options; + + const handleDismiss = (event: CustomEvent>) => { + if (onDidDismiss) { + onDidDismiss(event); + } + overlayRef.current = undefined; + } + + overlayRef.current = await controller.create({ + ...(rest as any), + }); + + attachProps(overlayRef.current, { + [didDismissEventName]: handleDismiss, + [didPresentEventName]: (e: CustomEvent) => + onDidPresent && onDidPresent(e), + [willDismissEventName]: (e: CustomEvent) => + onWillDismiss && onWillDismiss(e), + [willPresentEventName]: (e: CustomEvent) => + onWillPresent && onWillPresent(e), + }); + + overlayRef.current.present(); + }; + + const dismiss = async () => { + overlayRef.current && await overlayRef.current.dismiss(); + overlayRef.current = undefined; + }; + + return { + present, + dismiss, + }; +} diff --git a/packages/react/src/hooks/useIonActionSheet.ts b/packages/react/src/hooks/useIonActionSheet.ts new file mode 100644 index 0000000000..cbd045f4ef --- /dev/null +++ b/packages/react/src/hooks/useIonActionSheet.ts @@ -0,0 +1,53 @@ +import { ActionSheetButton, ActionSheetOptions, actionSheetController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { useController } from './useController'; + +/** + * A hook for presenting/dismissing an IonActionSheet component + * @returns Returns the present and dismiss methods in an array + */ +export function useIonActionSheet(): UseIonActionSheetResult { + const controller = useController( + 'IonActionSheet', + actionSheetController + ); + + function present(buttons: ActionSheetButton[], header?: string): void; + function present(options: ActionSheetOptions & HookOverlayOptions): void; + function present(buttonsOrOptions: ActionSheetButton[] | ActionSheetOptions & HookOverlayOptions, header?: string) { + if (Array.isArray(buttonsOrOptions)) { + controller.present({ + buttons: buttonsOrOptions, + header + }); + } else { + controller.present(buttonsOrOptions); + } + } + + return [ + present, + controller.dismiss + ]; +} + +export type UseIonActionSheetResult = [ + { + /** + * Presents the action sheet + * @param buttons An array of buttons for the action sheet + * @param header Optional - Title for the action sheet + */ + (buttons: ActionSheetButton[], header?: string | undefined): void; + /** + * Presents the action sheet + * @param options The options to pass to the IonActionSheet + */ + (options: ActionSheetOptions & HookOverlayOptions): void; + }, + /** + * Dismisses the action sheet + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonAlert.ts b/packages/react/src/hooks/useIonAlert.ts new file mode 100644 index 0000000000..3f1f7cf7fc --- /dev/null +++ b/packages/react/src/hooks/useIonAlert.ts @@ -0,0 +1,53 @@ +import { AlertButton, AlertOptions, alertController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { useController } from './useController'; + +/** + * A hook for presenting/dismissing an IonAlert component + * @returns Returns the present and dismiss methods in an array + */ +export function useIonAlert(): UseIonAlertResult { + const controller = useController( + 'IonAlert', + alertController + ); + + function present(message: string, buttons?: AlertButton[]): void; + function present(options: AlertOptions & HookOverlayOptions): void; + function present(messageOrOptions: string | AlertOptions & HookOverlayOptions, buttons?: AlertButton[]) { + if (typeof messageOrOptions === 'string') { + controller.present({ + message: messageOrOptions, + buttons: buttons ?? [{ text: 'Ok' }] + }); + } else { + controller.present(messageOrOptions); + } + }; + + return [ + present, + controller.dismiss + ]; +} + +export type UseIonAlertResult = [ + { + /** + * Presents the alert + * @param message The main message to be displayed in the alert + * @param buttons Optional - Array of buttons to be added to the alert + */ + (message: string, buttons?: AlertButton[]): void; + /** + * Presents the alert + * @param options The options to pass to the IonAlert + */ + (options: AlertOptions & HookOverlayOptions): void; + }, + /** + * Dismisses the alert + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonLoading.tsx b/packages/react/src/hooks/useIonLoading.tsx new file mode 100644 index 0000000000..779a413c18 --- /dev/null +++ b/packages/react/src/hooks/useIonLoading.tsx @@ -0,0 +1,60 @@ +import { LoadingOptions, SpinnerTypes, loadingController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { useController } from './useController'; + +/** + * A hook for presenting/dismissing an IonLoading component + * @returns Returns the present and dismiss methods in an array + */ +export function useIonLoading(): UseIonLoadingResult { + const controller = useController( + 'IonLoading', + loadingController + ); + + function present( + message?: string, + duration?: number, + spinner?: SpinnerTypes + ): void; + function present(options: LoadingOptions & HookOverlayOptions): void; + function present( + messageOrOptions: string | (LoadingOptions & HookOverlayOptions) = '', + duration?: number, + spinner?: SpinnerTypes + ) { + if (typeof messageOrOptions === 'string') { + controller.present({ + message: messageOrOptions, + duration, + spinner: spinner ?? 'lines', + }); + } else { + controller.present(messageOrOptions); + } + } + + return [present, controller.dismiss]; +} + +export type UseIonLoadingResult = [ + { + /** + * Presents the loading indicator + * @param message Optional - Text content to display in the loading indicator, defaults to blank string + * @param duration Optional - Number of milliseconds to wait before dismissing the loading indicator + * @param spinner Optional - The name of the spinner to display, defaults to "lines" + */ + (message?: string, duration?: number, spinner?: SpinnerTypes): void; + /** + * Presents the loading indicator + * @param options The options to pass to the IonLoading + */ + (options: LoadingOptions & HookOverlayOptions): void; + }, + /** + * Dismisses the loading indicator + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonModal.ts b/packages/react/src/hooks/useIonModal.ts new file mode 100644 index 0000000000..4a84969aca --- /dev/null +++ b/packages/react/src/hooks/useIonModal.ts @@ -0,0 +1,36 @@ +import { ModalOptions, modalController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { ReactComponentOrElement, useOverlay } from './useOverlay'; + +/** + * A hook for presenting/dismissing an IonModal component + * @param component The component that the modal will show. Can be a React Component, a functional component, or a JSX Element + * @param componentProps The props that will be passed to the component, if required + * @returns Returns the present and dismiss methods in an array + */ +export function useIonModal(component: ReactComponentOrElement, componentProps?: any): UseIonModalResult { + const controller = useOverlay( + 'IonModal', + modalController, + component, + componentProps + ); + + function present(options: Omit & HookOverlayOptions = {}) { + controller.present(options as any); + }; + + return [ + present, + controller.dismiss + ]; +} + +export type UseIonModalResult = [ + (options?: Omit & HookOverlayOptions) => void, + /** + * Dismisses the modal + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonPicker.tsx b/packages/react/src/hooks/useIonPicker.tsx new file mode 100644 index 0000000000..36473862aa --- /dev/null +++ b/packages/react/src/hooks/useIonPicker.tsx @@ -0,0 +1,58 @@ +import { + PickerButton, + PickerColumn, + PickerOptions, + pickerController, +} from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { useController } from './useController'; + +/** + * A hook for presenting/dismissing an IonPicker component + * @returns Returns the present and dismiss methods in an array + */ +export function useIonPicker(): UseIonPickerResult { + const controller = useController( + 'IonPicker', + pickerController + ); + + function present(columns: PickerColumn[], buttons?: PickerButton[]): void; + function present(options: PickerOptions & HookOverlayOptions): void; + function present( + columnsOrOptions: PickerColumn[] | (PickerOptions & HookOverlayOptions), + buttons?: PickerButton[] + ) { + if (Array.isArray(columnsOrOptions)) { + controller.present({ + columns: columnsOrOptions, + buttons: buttons ?? [{ text: 'Ok' }], + }); + } else { + controller.present(columnsOrOptions); + } + } + + return [present, controller.dismiss]; +} + +export type UseIonPickerResult = [ + { + /** + * Presents the picker + * @param columns Array of columns to be displayed in the picker. + * @param buttons Optional - Array of buttons to be displayed at the top of the picker. + */ + (columns: PickerColumn[], buttons?: PickerButton[]): void; + /** + * Presents the picker + * @param options The options to pass to the IonPicker + */ + (options: PickerOptions & HookOverlayOptions): void; + }, + /** + * Dismisses the picker + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonPopover.ts b/packages/react/src/hooks/useIonPopover.ts new file mode 100644 index 0000000000..42ecf6600c --- /dev/null +++ b/packages/react/src/hooks/useIonPopover.ts @@ -0,0 +1,36 @@ +import { PopoverOptions, popoverController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { ReactComponentOrElement, useOverlay } from './useOverlay'; + +/** + * A hook for presenting/dismissing an IonPicker component + * @param component The component that the popover will show. Can be a React Component, a functional component, or a JSX Element + * @param componentProps The props that will be passed to the component, if required + * @returns Returns the present and dismiss methods in an array + */ +export function useIonPopover(component: ReactComponentOrElement, componentProps?: any): UseIonPopoverResult { + const controller = useOverlay( + 'IonPopover', + popoverController, + component, + componentProps + ); + + function present(options: Omit & HookOverlayOptions = {}) { + controller.present(options as any); + }; + + return [ + present, + controller.dismiss + ]; +} + +export type UseIonPopoverResult = [ + (options?: Omit & HookOverlayOptions) => void, + /** + * Dismisses the popover + */ + () => void +]; diff --git a/packages/react/src/hooks/useIonToast.ts b/packages/react/src/hooks/useIonToast.ts new file mode 100644 index 0000000000..258d5b2bc8 --- /dev/null +++ b/packages/react/src/hooks/useIonToast.ts @@ -0,0 +1,53 @@ +import { ToastOptions, toastController } from '@ionic/core'; + +import { HookOverlayOptions } from './HookOverlayOptions'; +import { useController } from './useController'; + +/** + * A hook for presenting/dismissing an IonToast component + * @returns Returns the present and dismiss methods in an array + */ +export function useIonToast(): UseIonToastResult { + const controller = useController( + 'IonToast', + toastController + ); + + function present(message: string, duration?: number): void; + function present(options: ToastOptions & HookOverlayOptions): void; + function present(messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) { + if (typeof messageOrOptions === 'string') { + controller.present({ + message: messageOrOptions, + duration + }); + } else { + controller.present(messageOrOptions); + } + }; + + return [ + present, + controller.dismiss + ]; +} + +export type UseIonToastResult = [ + { + /** + * Presents the toast + * @param message Message to be shown in the toast. + * @param duration Optional - How many milliseconds to wait before hiding the toast. By default, it will show until dismissToast() is called. + */ + (message: string, duration?: number): void; + /** + * Presents the Toast + * @param options The options to pass to the IonToast. + */ + (options: ToastOptions & HookOverlayOptions): void; + }, + /** + * Dismisses the toast + */ + () => void +]; diff --git a/packages/react/src/hooks/useOverlay.ts b/packages/react/src/hooks/useOverlay.ts new file mode 100644 index 0000000000..6365bc4829 --- /dev/null +++ b/packages/react/src/hooks/useOverlay.ts @@ -0,0 +1,111 @@ +import { OverlayEventDetail } from '@ionic/core'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; + +import { attachProps } from '../components/utils'; + +import { HookOverlayOptions } from './HookOverlayOptions'; + +export type ReactComponentOrElement = React.ComponentClass | React.FC | JSX.Element; + +interface OverlayBase extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export function useOverlay< + OptionsType, + OverlayType extends OverlayBase +>( + displayName: string, + controller: { create: (options: OptionsType) => Promise; }, + component: ReactComponentOrElement, + componentProps?: any +) { + const overlayRef = useRef(); + const containerElRef = useRef(); + const didDismissEventName = useMemo( + () => `on${displayName}DidDismiss`, + [displayName] + ); + const didPresentEventName = useMemo( + () => `on${displayName}DidPresent`, + [displayName] + ); + const willDismissEventName = useMemo( + () => `on${displayName}WillDismiss`, + [displayName] + ); + const willPresentEventName = useMemo( + () => `on${displayName}WillPresent`, + [displayName] + ); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (isOpen && component && containerElRef.current) { + if (React.isValidElement(component)) { + ReactDOM.render(component, containerElRef.current); + } else { + ReactDOM.render(React.createElement(component as React.ComponentClass, componentProps), containerElRef.current); + } + } + }, [component, containerElRef.current, isOpen, componentProps]); + + const present = async (options: OptionsType & HookOverlayOptions) => { + if (overlayRef.current) { + return; + } + + const { + onDidDismiss, + onWillDismiss, + onDidPresent, + onWillPresent, + ...rest + } = options; + + if (typeof document !== 'undefined') { + containerElRef.current = document.createElement('div'); + } + + overlayRef.current = await controller.create({ + ...(rest as any), + component: containerElRef.current + }); + + attachProps(overlayRef.current, { + [didDismissEventName]: handleDismiss, + [didPresentEventName]: (e: CustomEvent) => + onDidPresent && onDidPresent(e), + [willDismissEventName]: (e: CustomEvent) => + onWillDismiss && onWillDismiss(e), + [willPresentEventName]: (e: CustomEvent) => + onWillPresent && onWillPresent(e), + }); + + overlayRef.current.present(); + + setIsOpen(true); + + function handleDismiss(event: CustomEvent>) { + if (onDidDismiss) { + onDidDismiss(event); + } + overlayRef.current = undefined; + containerElRef.current = undefined; + setIsOpen(false); + } + }; + + const dismiss = async () => { + overlayRef.current && await overlayRef.current.dismiss(); + overlayRef.current = undefined; + containerElRef.current = undefined; + }; + + return { + present, + dismiss, + }; +}