Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5009 mobile modal improvements #6065

Draft
wants to merge 21 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/packages/common/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export * from './useQueryParams';
export * from './useUrlQuery';
export * from './useToggle';
export * from './useWindowDimensions';
export * from './useKeyboardHeightAdjustment';
export * from './useKeyboard';
export * from './useNativeClient';
export * from './useWebClient';
export * from './useInterval';
Expand Down
33 changes: 28 additions & 5 deletions client/packages/common/src/hooks/useDialog/useDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent, { DialogContentProps } from '@mui/material/DialogContent';
import { TransitionProps } from '@mui/material/transitions';
import { Slide } from '../../ui/animations';
import { BasicModal, ModalTitle } from '@common/components';
import { useIntlUtils } from '@common/intl';
import { SxProps, Theme } from '@mui/material';
import { BasicModal, IconButton, ModalTitle } from '@common/components';
import { useIntlUtils, useTranslation } from '@common/intl';
import { SxProps, Theme, useMediaQuery } from '@mui/material';
import { useKeyboardContext } from '../useKeyboard';
import { CloseIcon } from '@common/icons';

type OkClickEvent = React.MouseEvent<HTMLButtonElement, MouseEvent>;

Expand Down Expand Up @@ -53,6 +55,7 @@ export interface DialogProps {
animationTimeout?: number;
disableBackdrop?: boolean;
disableEscapeKey?: boolean;
disableMobileFullScreen?: boolean;
}

interface DialogState {
Expand Down Expand Up @@ -98,6 +101,7 @@ const useSlideAnimation = (isRtl: boolean, timeout: number) => {
* @property {number} [animationTimeout=500] the timeout for the slide animation
* @property {boolean} [disableBackdrop=false] (optional) disable clicking the backdrop to close the modal
* @property {boolean} [disableEscape=false] (optional) disable pressing of the escape key to close the modal
* @property {boolean} [disableMobileFullScreen=false] (optional) disable modal entering fullscreen mode on smaller screens
* @property {boolean} isOpen (optional) is the modal open
* @property {function} onClose (optional) method to run on closing the modal
* @return {DialogState} the dialog state. Properties are:
Expand All @@ -113,6 +117,7 @@ export const useDialog = (dialogProps?: DialogProps): DialogState => {
animationTimeout = 500,
disableBackdrop = true,
disableEscapeKey = false,
disableMobileFullScreen = false,
} = dialogProps ?? {};
const [open, setOpen] = React.useState(false);
const showDialog = useCallback(() => setOpen(true), []);
Expand Down Expand Up @@ -161,6 +166,9 @@ export const useDialog = (dialogProps?: DialogProps): DialogState => {
isRtl,
animationTimeout
);
const { isOpen: keyboardIsOpen } = useKeyboardContext();
const isSmallerScreen = useMediaQuery('(max-height: 850px)');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Felt like a reasonable height to be okay with sticking with modals as they are, most tablets around 750-800 high... happy to play around with this

const t = useTranslation();

const defaultPreventedOnClick =
(onClick: (e?: OkClickEvent) => Promise<boolean>) =>
Expand Down Expand Up @@ -215,6 +223,8 @@ export const useDialog = (dialogProps?: DialogProps): DialogState => {
width: width ? Math.min(window.innerWidth - 50, width) : undefined,
};

const fullScreen = isSmallerScreen && !disableMobileFullScreen;

return (
<BasicModal
open={open}
Expand All @@ -224,7 +234,20 @@ export const useDialog = (dialogProps?: DialogProps): DialogState => {
sx={sx}
TransitionComponent={Transition}
disableEscapeKeyDown={false}
fullScreen={fullScreen}
>
{fullScreen && (
<IconButton
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some modals used clicking on the backdrop to close (e.g. internal order create) so added close button

icon={<CloseIcon />}
color="primary"
onClick={() => {
onClose && onClose();
hideDialog();
}}
sx={{ position: 'absolute', right: 0, top: 0, padding: 2 }}
label={t('button.close')}
/>
)}
{title ? <ModalTitle title={title} /> : null}
<form
style={{
Expand All @@ -250,8 +273,8 @@ export const useDialog = (dialogProps?: DialogProps): DialogState => {
<DialogActions
sx={{
justifyContent: 'center',
marginBottom: '30px',
marginTop: '30px',
marginBottom: keyboardIsOpen ? 0 : '30px',
marginTop: keyboardIsOpen ? 0 : '30px',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes the margins around the buttons smaller while using the keyboard (because they take up a lot of space)

}}
>
{cancelButton}
Expand Down
64 changes: 64 additions & 0 deletions client/packages/common/src/hooks/useKeyboard/Keyboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useContext, useEffect, useState } from 'react';
import { Keyboard } from '@capacitor/keyboard';
import { Capacitor } from '@capacitor/core';
import { createRegisteredContext } from 'react-singleton-context';

interface KeyboardControl {
isOpen: boolean;
isEnabled: boolean;
}

const defaultKeyboardControl: KeyboardControl = {
isOpen: false,
isEnabled: false,
};

const KeyboardContext = createRegisteredContext<KeyboardControl>(
'keyboard-context',
defaultKeyboardControl
);

const { Provider } = KeyboardContext;

export const KeyboardProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [isOpen, setOpen] = useState(false);
const isEnabled = Capacitor.isPluginAvailable('Keyboard');

useEffect(() => {
(async () => {
if (!isEnabled) return;

const showListener = await Keyboard.addListener('keyboardDidShow', () =>
setOpen(true)
);
const hideListener = await Keyboard.addListener('keyboardDidHide', () =>
setOpen(false)
);

return () => {
showListener.remove();
hideListener.remove();
};
})();
}, []);

return (
<Provider
value={{
isOpen,
isEnabled,
}}
>
{children}
</Provider>
);
};

export const useKeyboardContext = () => {
const keyboardControl = useContext(KeyboardContext);
return keyboardControl;
};
1 change: 1 addition & 0 deletions client/packages/common/src/hooks/useKeyboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Keyboard';

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './types';
import { BasicTextInput } from '../TextInput';
import { StyledPopper } from './components';
import { useKeyboardContext, useToggle } from '@common/hooks';

export interface AutocompleteProps<T>
extends Omit<
Expand Down Expand Up @@ -73,9 +74,15 @@ export function Autocomplete<T>({
getOptionLabel,
popperMinWidth,
inputProps,
open,
onOpen,
onClose,
...restOfAutocompleteProps
}: PropsWithChildren<AutocompleteProps<T>>): JSX.Element {
const filter = filterOptions ?? createFilterOptions(filterOptionConfig);
const keyboard = useKeyboardContext();
// manage popper display when android keyboard appears
const keyboardSelectControl = useToggle();

const defaultRenderInput = (props: AutocompleteRenderInputParams) => (
<BasicTextInput
Expand Down Expand Up @@ -104,6 +111,18 @@ export function Autocomplete<T>({
/>
);

const getIsOpen = () => {
// Use passed in or default if not using android keyboard
if (!keyboard.isEnabled) return open;

// Keep popper closed until keyboard is open
// keyboard moves popper up & out of correct position
if (!keyboard.isOpen) return false;

// Use externally controlled `open` state if provided
return open ?? keyboardSelectControl.isOn;
};

return (
<MuiAutocomplete
{...restOfAutocompleteProps}
Expand All @@ -126,6 +145,15 @@ export function Autocomplete<T>({
onChange={onChange}
getOptionLabel={getOptionLabel || defaultGetOptionLabel}
PopperComponent={popperMinWidth ? CustomPopper : StyledPopper}
open={getIsOpen()}
onOpen={e => {
keyboard.isEnabled && keyboardSelectControl.toggleOn();
onOpen?.(e);
}}
onClose={(...args) => {
keyboard.isEnabled && keyboardSelectControl.toggleOff();
onClose?.(...args);
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
Box,
} from '@mui/material';
import { BasicTextInput } from '../TextInput';
import { useDebounceCallback } from '@common/hooks';
import {
useDebounceCallback,
useKeyboardContext,
useToggle,
} from '@common/hooks';
import type { AutocompleteProps } from './Autocomplete';
import { StyledPopper } from './components';
import { ArrayUtils } from '@common/utils';
Expand Down Expand Up @@ -65,11 +69,16 @@ export function AutocompleteWithPagination<T extends RecordWithId>({
paginationDebounce,
onPageChange,
mapOptions,
open,
onOpen,
onClose,
...restOfAutocompleteProps
}: PropsWithChildren<AutocompleteWithPaginationProps<T>>) {
const filter = filterOptions ?? createFilterOptions(filterOptionConfig);
const [isLoading, setIsLoading] = useState(true);
const lastOptions = useRef<T[]>([]);
const keyboard = useKeyboardContext();
const keyboardSelectControl = useToggle();

const options = useMemo(() => {
if (!pages) {
Expand Down Expand Up @@ -167,6 +176,18 @@ export function AutocompleteWithPagination<T extends RecordWithId>({
setTimeout(() => setIsLoading(false), LOADER_HIDE_TIMEOUT);
}, [options]);

const getIsOpen = () => {
// Use passed in or default if not using android keyboard
if (!keyboard.isEnabled) return open;

// Keep popper closed until keyboard is open
// keyboard moves popper up & out of correct position
if (!keyboard.isOpen) return false;

// Use externally controlled `open` state if provided
return open ?? keyboardSelectControl.isOn;
};

return (
<MuiAutocomplete
{...restOfAutocompleteProps}
Expand All @@ -190,6 +211,15 @@ export function AutocompleteWithPagination<T extends RecordWithId>({
getOptionLabel={getOptionLabel || defaultGetOptionLabel}
PopperComponent={popperMinWidth ? CustomPopper : StyledPopper}
ListboxProps={listboxProps}
open={getIsOpen()}
onOpen={e => {
keyboard.isEnabled && keyboardSelectControl.toggleOn();
onOpen?.(e);
}}
onClose={(...args) => {
keyboard.isEnabled && keyboardSelectControl.toggleOff();
onClose?.(...args);
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ export const BasicModal: FC<DialogProps> = ({
width = 500,
height = 400,
sx,
fullScreen,
...dialogProps
}) => {
const { isRtl } = useIntlUtils();
return (
<Dialog
fullScreen={fullScreen}
PaperProps={{
sx: {
borderRadius: '20px',
borderRadius: fullScreen ? undefined : '20px',
minHeight: `${height}px`,
minWidth: `${width}px`,
direction: isRtl ? 'rtl' : 'ltr',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export const InputModal = ({
onClose,
title,
}: InputModalProps) => {
const { Modal } = useDialog({ isOpen, onClose });
const { Modal } = useDialog({
isOpen,
onClose,
disableMobileFullScreen: true,
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input modal is small (used for like tax input) so don't need the full screen capability here

const [loading, setLoading] = useState(false);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, BoxProps, Portal } from '@mui/material';
import { styled } from '@mui/material/styles';
import React, { FC, ReactNode, useEffect, useRef } from 'react';
import { useHostContext } from '@common/hooks';
import { useHostContext, useKeyboardContext } from '@common/hooks';
import { useIsCentralServerApi } from '@openmsupply-client/common';

const Container = styled('div')(() => ({
Expand All @@ -17,6 +17,7 @@ const Container = styled('div')(() => ({
export const AppFooter: FC = () => {
const { setAppFooterRef, setAppSessionDetailsRef, fullScreen } =
useHostContext();
const { isOpen: keyboardOpen } = useKeyboardContext();
const appFooterRef = useRef(null);
const appSessionDetailsRef = useRef(null);
const isCentralServer = useIsCentralServerApi();
Expand All @@ -26,8 +27,10 @@ export const AppFooter: FC = () => {
setAppSessionDetailsRef(appSessionDetailsRef);
}, []);

const hideFooter = fullScreen || keyboardOpen;

return (
<Box sx={{ display: fullScreen ? 'none' : undefined }}>
<Box sx={{ display: hideFooter ? 'none' : undefined }}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When not in modal, i.e. typing in a table filter, hide the footers so can see more of the results

<Container ref={appFooterRef} style={{ flex: 0 }} />
<Container
ref={appSessionDetailsRef}
Expand Down
Loading
Loading