diff --git a/package.json b/package.json index 7e89c901bb..b328a9388c 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ "preact-portal": "1.1.3", "pretty": "2.0.0", "prop-types": "15.7.2", - "react": "16.6.3", - "react-dom": "16.6.3", + "react": "16.8.6", + "react-dom": "16.8.6", "react-redux": "5.1.1", "react-styleguidist": "7.3.11", "react-test-renderer": "16.6.3", @@ -134,8 +134,8 @@ "piwik-react-router": "^0.8.2", "preact": "^8.3.1", "preact-portal": "^1.1.3", - "react": "^16.6.3", - "react-dom": "^16.5.2" + "react": "^16.8.6", + "react-dom": "^16.8.6" }, "eslintConfig": { "extends": [ diff --git a/react/Page/Readme.md b/react/Page/Readme.md new file mode 100644 index 0000000000..7ab4142a25 --- /dev/null +++ b/react/Page/Readme.md @@ -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 + + Hello world ! + + +``` diff --git a/react/Page/index.jsx b/react/Page/index.jsx new file mode 100644 index 0000000000..fade038113 --- /dev/null +++ b/react/Page/index.jsx @@ -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 + * + */ + +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 ( +
+ {' '} +
+ ) +} + +export const MobilePageLayout = ({ children, extraHeight = 0 }) => { + const { keyboardState, keyboardHeight } = useKeyboardInfo() + const minHeight = contentHeight({ + keyboardState, + keyboardHeight, + extraHeight + }) + return ( + <> +
+ {children} +
+ + + ) +} + +/** + * - On mobile, wraps into MobilePageLayout. + * - On desktop, wraps into a simple div. + */ +export const PageLayout = React.memo( + withBreakpoints()(({ breakpoints: { isMobile }, ...props }) => { + return isMobile ? ( + + ) : ( +
{props.children}
+ ) + }) +) + +export const PageContent = React.memo(({ children }) => ( +
{children}
+)) + +export const PageFooter = ({ children }) => ( +
{children}
+) diff --git a/react/Page/keyboard.js b/react/Page/keyboard.js new file mode 100644 index 0000000000..370a1d6120 --- /dev/null +++ b/react/Page/keyboard.js @@ -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 } diff --git a/react/Page/styles.styl b/react/Page/styles.styl new file mode 100644 index 0000000000..e5c5907711 --- /dev/null +++ b/react/Page/styles.styl @@ -0,0 +1,9 @@ +.PageFooter + flex-grow 0 + +.PageContent + flex-grow 1 + +.PageLayout + display flex + flex-direction column diff --git a/react/index.js b/react/index.js index 63474ca784..1dc47c98e5 100644 --- a/react/index.js +++ b/react/index.js @@ -77,3 +77,4 @@ export { default as Card } from './Card' export { default as InlineCard } from './InlineCard' export { default as PercentageLine } from './PercentageLine' export { default as InputGroup } from './InputGroup' +export { PageFooter, PageContent, PageLayout } from './Page' diff --git a/yarn.lock b/yarn.lock index febd018229..bb6fc296aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11767,15 +11767,15 @@ react-docgen@3.0.0-beta12: 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" -react@16.6.3: - 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" - react@16.7.0: version "16.7.0" resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381" @@ -12020,6 +12010,16 @@ react@16.7.0: prop-types "^15.6.2" scheduler "^0.12.0" +react@16.8.6: + 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"