Skip to content

Commit 3eca399

Browse files
committed
fix: modal useHistory polished
1 parent 4bf5c88 commit 3eca399

6 files changed

Lines changed: 156 additions & 56 deletions

File tree

src/components/Inputs/PhoneNumberInput.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface PhoneNumberInputProps
2424
onChangeCountryCode?: (countryCode: string) => void;
2525
phoneNumber?: string;
2626
onChangePhoneNumber?: (phoneNumber: string) => void;
27+
/** Prop to be passed to modal */
28+
useHistory?: boolean;
2729
theme: Theme;
2830
/** Label displayed when showing country selection */
2931
header?: React.ReactElement<any>;
@@ -50,6 +52,7 @@ const PhoneNumberInputBase = (props: PhoneNumberInputProps) => {
5052
header,
5153
theme,
5254
getStyles,
55+
useHistory = false,
5356
...textInputProps
5457
} = props;
5558

@@ -84,7 +87,11 @@ const PhoneNumberInputBase = (props: PhoneNumberInputProps) => {
8487
}
8588
title={`+${countryList[countryCode].phone}`}
8689
/>
87-
<Modal visible={on}>
90+
<Modal
91+
visible={on}
92+
useHistory={useHistory}
93+
onRequestClose={() => set(false)}
94+
>
8895
<ModalContent onClose={() => set(false)}>
8996
<FlatList
9097
ListHeaderComponent={header}

src/components/Modal/HistoryModal.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import ModalBase, { ModalBaseProps } from './ModalBase';
55
export interface HistoryModalProps extends ModalBaseProps {
66
/** (Web) In order to mimic similar behavior to Android's back button to close the modal on mobile web, HistoryModal will use hash or query string to determine whether it is opened or not. @default false */
77
useHistory?: boolean;
8-
/** (Web) Only works when `useHistory` is true. Hash string to append to location */
9-
hash?: string | null;
10-
/** (Web) Only works when `useHistory` is true. Same as hash, but using query string. Query string will take precedence over hash @default null */
118
}
129

1310
export default ModalBase;

src/components/Modal/HistoryModal.web.tsx

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,133 @@ import * as React from 'react';
33
import { HistoryModalProps } from './HistoryModal';
44
import ModalBase from './ModalBase';
55

6-
// TODO: needs more work
6+
let modalId = 0;
7+
8+
/**
9+
* There are 3 ways a user may close the modal on the browser
10+
* 1. Back button press
11+
* 2. Escape key
12+
* 3. Manually closed from within the modal (e.g. Close button)
13+
* Each of them should properly restore the page user was at
14+
*/
715
class HistoryModal extends React.PureComponent<HistoryModalProps> {
8-
public static defaultProps = {
9-
hash: '#modal-open',
10-
};
11-
12-
public initialHref: string | null = null;
13-
14-
public componentDidMount = () => {
15-
this.initialHref = window.location.href;
16-
17-
/**
18-
* For back button, if use clicks back button it should be interpreted as a close request.
19-
*/
20-
window.addEventListener('hashchange', this.handleHashChange, false);
21-
};
16+
public modalId = ++modalId;
2217

23-
public componentDidUpdate = (prevProps: HistoryModalProps) => {
18+
public componentDidUpdate = (previousProps: HistoryModalProps) => {
2419
const { visible } = this.props;
2520

26-
if (visible && visible !== prevProps.visible && !this.isHistoryActive()) {
27-
this.pushHistory();
21+
if (previousProps.visible !== visible) {
22+
if (visible) {
23+
this.updateBrowserHistory();
24+
} else {
25+
this.handleManuallyClosed();
26+
}
2827
}
2928
};
3029

31-
public componentWillUnmount() {
32-
window.removeEventListener('hashchange', this.handleHashChange, false);
33-
}
30+
public componentWillUnmount = () => {
31+
this.clearBrowserState();
32+
};
3433

35-
public handleHashChange = () => {
36-
const { onRequestClose } = this.props;
34+
public updateBrowserHistory = () => {
35+
// Ensure history state does not already contain our modal name to avoid double-pushing
36+
if (
37+
!history.state ||
38+
!history.state.modal ||
39+
!history.state.modal[this.modalId]
40+
) {
41+
// Push to history so that the page is not lost on browser back button press
42+
history.pushState(
43+
{
44+
...history.state,
45+
modal: {
46+
...(history.state && history.state.modal),
47+
[this.modalId]: true,
48+
},
49+
},
50+
'',
51+
);
52+
}
3753

38-
if (onRequestClose) {
54+
// Listen for back button presses
55+
window.addEventListener('popstate', this.handlePopstate, false);
56+
};
57+
58+
/** Back button press */
59+
public handlePopstate = (event: PopStateEvent) => {
60+
const { visible, onRequestClose } = this.props;
61+
// Close the modal if the history state no longer contains our modal name
62+
if (
63+
(!event.state ||
64+
!event.state.modal ||
65+
!event.state.modal[this.modalId]) &&
66+
onRequestClose
67+
) {
68+
this.clearBrowserState();
3969
onRequestClose();
4070
}
71+
72+
// When the browser back button is pressed and uppy is now the latest entry in the history but the modal is closed, fix the history by removing the uppy history entry
73+
// This occurs when another entry is added into the history state while the modal is open, and then the modal gets manually closed
74+
// Solves PR #575 (https://github.com/transloadit/uppy/pull/575)
75+
if (
76+
!visible &&
77+
event.state &&
78+
event.state.modal &&
79+
event.state.modal[this.modalId]
80+
) {
81+
history.go(-1);
82+
}
4183
};
4284

43-
/**
44-
* Relay the callback and also clean up the hash state
45-
*/
46-
public handleRequestClose = () => {
85+
/** Escape key */
86+
public handleEscapeKey = () => {
4787
const { onRequestClose } = this.props;
4888

49-
if (onRequestClose) {
89+
if (
90+
(history.state ||
91+
history.state.modal ||
92+
history.state.modal[this.modalId]) &&
93+
onRequestClose
94+
) {
95+
/** Clear state, so that when a falsy `visible` prop gets passed, it can check whether the state still exists in `handleManuallyClosed` */
96+
this.clearBrowserState();
5097
onRequestClose();
98+
history.go(-1);
5199
}
52-
53-
history.replaceState(null, '', this.initialHref);
54100
};
55101

56-
public pushHistory = () => {
57-
const { hash } = this.props;
58-
59-
if (hash) {
60-
history.pushState(null, '', this.initialHref + hash);
102+
/** Manual */
103+
public handleManuallyClosed = () => {
104+
if (
105+
history.state &&
106+
history.state.modal &&
107+
history.state.modal[this.modalId]
108+
) {
109+
this.clearBrowserState();
110+
history.go(-1);
61111
}
62112
};
63113

64-
public isHistoryActive = () => {
65-
const { hash } = this.props;
114+
public clearBrowserState = () => {
115+
history.replaceState(
116+
{
117+
...history.state,
118+
modal: {
119+
...(history.state && history.state.modal),
120+
[this.modalId]: false,
121+
},
122+
},
123+
'',
124+
);
66125

67-
return hash === window.location.hash;
126+
window.removeEventListener('popstate', this.handlePopstate, false);
68127
};
69128

70129
public render() {
71-
const { hash, ...modalProps } = this.props;
130+
const { ...modalProps } = this.props;
72131

73-
return (
74-
<ModalBase {...modalProps} onRequestClose={this.handleRequestClose} />
75-
);
132+
return <ModalBase {...modalProps} onRequestClose={this.handleEscapeKey} />;
76133
}
77134
}
78135

src/components/Modal/Modal.mdx

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,59 @@ import { Text } from '../Typography';
8383

8484
## Modal using browser history
8585

86-
Same as `Modal` below, with the difference that this uses `useHistory` prop that uses `hash` or `query-string` to determine visibility of the modal. The goal is to mimic Android's back button behavior on web which closes modals. On RN, this will have no effect and will use a regular `Modal`
86+
By using `useHistory` prop it will mimic Android's back button behavior on web which closes modals. On RN, this will have no effect and will use a regular `Modal`. Works in nested modals as well.
87+
88+
Note: Uses history `state` to manipulate modal state
8789

8890
<Playground>
8991
<Toggle initial={false}>
90-
{({ on, toggle, set }) => (
92+
{({ on: outerOn, set: outerSet }) => (
9193
<Box>
92-
<Modal useHistory visible={on} onRequestClose={() => set(false)}>
94+
<Modal
95+
useHistory
96+
visible={outerOn}
97+
onRequestClose={() => outerSet(false)}
98+
>
9399
<Box height={1800}>
94-
<Text>Put any content in the modal</Text>
95-
<Button onPress={() => set(false)} title="Close button one" />
96-
<Button onPress={() => set(false)} title="Close button two" />
100+
<Text>Outer modal</Text>
101+
<Button
102+
onPress={() => outerSet(false)}
103+
title="Close outer modal button one"
104+
/>
105+
<Button
106+
onPress={() => outerSet(false)}
107+
title="Close outer modal button two"
108+
/>
109+
<Toggle initial={false}>
110+
{({ on: innerOn, set: innerSet }) => (
111+
<Box>
112+
<Modal
113+
visible={innerOn}
114+
useHistory
115+
onRequestClose={() => innerSet(false)}
116+
>
117+
<Box height={1800}>
118+
<Text>Inner modal</Text>
119+
<Button
120+
onPress={() => innerSet(false)}
121+
title="Close inner modal button one"
122+
/>
123+
<Button
124+
onPress={() => innerSet(false)}
125+
title="Close inner modal button two"
126+
/>
127+
</Box>
128+
</Modal>
129+
<Button
130+
onPress={() => innerSet(true)}
131+
title="Open inner modal"
132+
/>
133+
</Box>
134+
)}
135+
</Toggle>
97136
</Box>
98137
</Modal>
99-
<Button onPress={() => set(true)} title="Open history modal" />
138+
<Button onPress={() => outerSet(true)} title="Open outer modal" />
100139
</Box>
101140
)}
102141
</Toggle>

src/components/Modal/ModalContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const ModalContentBase = (props: ModalContentProps) => {
2525
}}
2626
>
2727
<TouchableOpacity
28-
style={{ width: 56 }}
28+
style={{ width: 56, height: 60, justifyContent: 'center' }}
2929
onPress={event => {
3030
event.preventDefault();
3131
if (onClose) onClose();

tests/__snapshots__/snapshot.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21109,7 +21109,7 @@ exports[`Modal_2 1`] = `
2110921109
}
2111021110
}
2111121111
>
21112-
Open history modal
21112+
Open outer modal
2111321113
</div>
2111421114
</div>
2111521115
</div>

0 commit comments

Comments
 (0)