-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1106 from cozy/page-layout
feat: Page layout
- Loading branch information
Showing
7 changed files
with
257 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
The Page components enables to make layout that react well to keyboard appearing/disappearing. | ||
|
||
In the example below, the Button will appear at the bottom of the screen even if the PageContent | ||
does not takes all the space (the content grows to fill all the page). When the keyboard is | ||
shown, the Page real estate shrink and the Button will try to appear above the keyboard if it | ||
has enough space. | ||
|
||
```jsx static | ||
<PageLayout> | ||
<PageContent>Hello world !</PageContent> | ||
<PageFooter><Button>Click me !</Button></PageFooter> | ||
</PageLayout> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/** | ||
* Layout components that know how to handle the keyboard. | ||
* | ||
* Work when the webview does not resize when the keyboard appears. | ||
* | ||
* https://github.com/ionic-team/cordova-plugin-ionic-keyboard | ||
* <preference name="KeyboardResize" value="false" /> | ||
*/ | ||
|
||
import React from 'react' | ||
import { useKeyboardInfo } from './keyboard' | ||
import styles from './styles.styl' | ||
import withBreakpoints from '../helpers/withBreakpoints' | ||
|
||
const TOP_BAR_HEIGHT = 48 | ||
const BOTTOM_BAR_HEIGHT = 48 | ||
|
||
/** | ||
* Returns the min-height CSS property for the Page | ||
* | ||
* The goal is for the Page to fill all the screen that is visible to the user. | ||
* Since the keyboard appearing does not have any effect on the layout of the | ||
* page (), we have to remove its height from the available space when it appears. | ||
* | ||
* Also handles fixed bars space: | ||
* | ||
* - topBar height is removed from the real estate available. | ||
* - bottomBar height is removed from the real estate available, unless the | ||
* keyboard is visible : since the bottom bar is expected to be hidden | ||
* when an input is focused (to prevent iOS flickers), it does not take space | ||
* when the keyboard is visible. | ||
*/ | ||
const contentHeight = ({ | ||
topBarHeight = TOP_BAR_HEIGHT, | ||
bottomBarHeight = BOTTOM_BAR_HEIGHT, | ||
extraHeight = 0, | ||
keyboardState, | ||
keyboardHeight | ||
} = {}) => { | ||
const removedSpace = | ||
topBarHeight + | ||
(keyboardState === 'showing' ? 0 : bottomBarHeight) + | ||
// Surprisingly, vh changes when keyboard appears and 1 keyboard is added | ||
// to the vh, this is why instead of adding 1 keyboardHeight, we add 2. | ||
(keyboardState === 'showing' ? 2 * keyboardHeight : 0) + | ||
extraHeight | ||
return `calc(100vh ${removedSpace > 0 ? '-' : '+'} ${Math.abs( | ||
removedSpace | ||
)}px)` | ||
} | ||
|
||
/** | ||
* Empty container, that has the same height as the keyboard when it is opened, | ||
* used as a "cushion" upon which the PageFooter rests when the keyboard is opened. | ||
*/ | ||
const KeyboardSpace = () => { | ||
const { keyboardState, keyboardHeight } = useKeyboardInfo() | ||
return ( | ||
<div | ||
style={{ | ||
height: keyboardState === 'showing' ? keyboardHeight : 0 | ||
}} | ||
> | ||
{' '} | ||
</div> | ||
) | ||
} | ||
|
||
export const MobilePageLayout = ({ children, extraHeight = 0 }) => { | ||
const { keyboardState, keyboardHeight } = useKeyboardInfo() | ||
const minHeight = contentHeight({ | ||
keyboardState, | ||
keyboardHeight, | ||
extraHeight | ||
}) | ||
return ( | ||
<> | ||
<div | ||
className={styles.PageLayout} | ||
style={{ | ||
minHeight | ||
}} | ||
> | ||
{children} | ||
</div> | ||
<KeyboardSpace /> | ||
</> | ||
) | ||
} | ||
|
||
/** | ||
* - On mobile, wraps into MobilePageLayout. | ||
* - On desktop, wraps into a simple div. | ||
*/ | ||
export const PageLayout = React.memo( | ||
withBreakpoints()(({ breakpoints: { isMobile }, ...props }) => { | ||
return isMobile ? ( | ||
<MobilePageLayout {...props} /> | ||
) : ( | ||
<div>{props.children}</div> | ||
) | ||
}) | ||
) | ||
|
||
export const PageContent = React.memo(({ children }) => ( | ||
<div className={styles.PageContent}>{children}</div> | ||
)) | ||
|
||
export const PageFooter = ({ children }) => ( | ||
<div className={styles.PageFooter}>{children}</div> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/** | ||
* Hooks to access keyboard properties. | ||
* | ||
* Works with events sent by cordova-plugin-ionic-keyboard. | ||
* | ||
* https://github.com/ionic-team/cordova-plugin-ionic-keyboard | ||
*/ | ||
|
||
import { useEffect, useState } from 'react' | ||
|
||
/** | ||
* Helper hook that makes writing hooks for keyboard easier. | ||
* | ||
* With this hook, you can provide an event handler for a particular | ||
* keyboard event and its lifecycle will be handled for you (attach events | ||
* on component mount, removes them on component unmount). | ||
*/ | ||
const useKeyboard = ({ | ||
onKeyboardWillShow, | ||
onKeyboardWillHide, | ||
onKeyboardHeightWillChange, | ||
onKeyboardDidHide, | ||
onKeyboardDidShow | ||
}) => { | ||
useEffect(() => { | ||
if (!window.Keyboard) { | ||
console.warn( | ||
'window.Keyboard is falsy. The `useKeyboard` hook cannot listen to keyboard events if the `cordova-plugin-ionic-keyboard` plugin is not installed.' | ||
) | ||
} | ||
onKeyboardWillShow && | ||
window.addEventListener('keyboardWillShow', onKeyboardWillShow) | ||
onKeyboardWillHide && | ||
window.addEventListener('keyboardWillHide', onKeyboardWillHide) | ||
onKeyboardHeightWillChange && | ||
window.addEventListener( | ||
'keyboardHeightWillChange', | ||
onKeyboardHeightWillChange | ||
) | ||
onKeyboardDidHide && | ||
window.addEventListener('keyboardDidHide', onKeyboardDidHide) | ||
onKeyboardDidShow && | ||
window.addEventListener('keyboardDidShow', onKeyboardDidShow) | ||
return () => { | ||
onKeyboardWillShow && | ||
window.removeEventListener('keyboardWillShow', onKeyboardWillShow) | ||
onKeyboardWillHide && | ||
window.removeEventListener('keyboardWillHide', onKeyboardWillHide) | ||
onKeyboardHeightWillChange && | ||
window.removeEventListener( | ||
'keyboardHeightWillChange', | ||
onKeyboardHeightWillChange | ||
) | ||
onKeyboardDidHide && | ||
window.removeEventListener('keyboardDidHide', onKeyboardDidHide) | ||
onKeyboardDidShow && | ||
window.removeEventListener('keyboardDidShow', onKeyboardDidShow) | ||
} | ||
}, [ | ||
onKeyboardWillShow, | ||
onKeyboardWillHide, | ||
onKeyboardHeightWillChange, | ||
onKeyboardDidHide, | ||
onKeyboardDidShow | ||
]) | ||
} | ||
|
||
/** | ||
* Provides keyboardHeight and keyboardState | ||
* | ||
* keyboardState can be | ||
* | ||
* - will-hide | ||
* - will-show | ||
* - hidden | ||
* - showing | ||
* | ||
* More information on the keyboard plugin page: | ||
* https://github.com/ionic-team/cordova-plugin-ionic-keyboard | ||
*/ | ||
const useKeyboardInfo = () => { | ||
const [keyboardHeight, setKeyboardHeight] = useState(0) | ||
const [keyboardState, setKeyboardState] = useState('hidden') | ||
useKeyboard({ | ||
onKeyboardWillHide: () => { | ||
setKeyboardState('will-hide') | ||
}, | ||
onKeyboardWillShow: () => { | ||
setKeyboardState('will-show') | ||
}, | ||
onKeyboardDidHide: () => { | ||
setKeyboardState('hidden') | ||
}, | ||
onKeyboardDidShow: ev => { | ||
if (ev.keyboardHeight) { | ||
setKeyboardHeight(ev.keyboardHeight) | ||
} | ||
setKeyboardState('showing') | ||
} | ||
}) | ||
return { keyboardHeight, keyboardState } | ||
} | ||
|
||
export { useKeyboard, useKeyboardInfo } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.PageFooter | ||
flex-grow 0 | ||
|
||
.PageContent | ||
flex-grow 1 | ||
|
||
.PageLayout | ||
display flex | ||
flex-direction column |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11767,15 +11767,15 @@ [email protected]: | |
node-dir "^0.1.10" | ||
recast "^0.13.0" | ||
|
||
react-dom@16.6.3: | ||
version "16.6.3" | ||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0" | ||
integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ== | ||
react-dom@16.8.6: | ||
version "16.8.6" | ||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" | ||
integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== | ||
dependencies: | ||
loose-envify "^1.1.0" | ||
object-assign "^4.1.1" | ||
prop-types "^15.6.2" | ||
scheduler "^0.11.2" | ||
scheduler "^0.13.6" | ||
|
||
react-error-overlay@^4.0.1: | ||
version "4.0.1" | ||
|
@@ -12000,16 +12000,6 @@ react-transition-group@^2.2.1: | |
prop-types "^15.6.2" | ||
react-lifecycles-compat "^3.0.4" | ||
|
||
[email protected]: | ||
version "16.6.3" | ||
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c" | ||
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw== | ||
dependencies: | ||
loose-envify "^1.1.0" | ||
object-assign "^4.1.1" | ||
prop-types "^15.6.2" | ||
scheduler "^0.11.2" | ||
|
||
[email protected]: | ||
version "16.7.0" | ||
resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" | ||
|
@@ -12020,6 +12010,16 @@ [email protected]: | |
prop-types "^15.6.2" | ||
scheduler "^0.12.0" | ||
|
||
[email protected]: | ||
version "16.8.6" | ||
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" | ||
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== | ||
dependencies: | ||
loose-envify "^1.1.0" | ||
object-assign "^4.1.1" | ||
prop-types "^15.6.2" | ||
scheduler "^0.13.6" | ||
|
||
read-cache@^1.0.0: | ||
version "1.0.0" | ||
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" | ||
|