From 12aa521ba7d8e934b403c8dfd857d0ba6a36205e Mon Sep 17 00:00:00 2001 From: Lucas Homer <33523751+lucas-homer@users.noreply.github.com> Date: Fri, 3 Sep 2021 15:39:02 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20SQFormScrollableCardsMen?= =?UTF-8?q?uWrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wrapper to map CardPopoverMenu items to instances of SQFormScrollableCard to put many forms "into" one card. ✅ Closes: #347 --- .../SQFormScrollableCardsMenuWrapper.js | 111 +++++++++++++ .../SQFormScrollableCardsMenuWrapper/index.js | 1 + src/index.js | 1 + ...QFormScrollableCardsMenuWrapper.stories.js | 153 ++++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 src/components/SQFormScrollableCardsMenuWrapper/SQFormScrollableCardsMenuWrapper.js create mode 100644 src/components/SQFormScrollableCardsMenuWrapper/index.js create mode 100644 stories/SQFormScrollableCardsMenuWrapper.stories.js diff --git a/src/components/SQFormScrollableCardsMenuWrapper/SQFormScrollableCardsMenuWrapper.js b/src/components/SQFormScrollableCardsMenuWrapper/SQFormScrollableCardsMenuWrapper.js new file mode 100644 index 00000000..848f40b2 --- /dev/null +++ b/src/components/SQFormScrollableCardsMenuWrapper/SQFormScrollableCardsMenuWrapper.js @@ -0,0 +1,111 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Card, CardHeader, makeStyles} from '@material-ui/core'; +import {CardPopoverMenu} from 'scplus-shared-components'; + +const useStyles = makeStyles(theme => { + return { + card: { + display: 'grid', + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto 1fr auto', + gridTemplateAreas: `'header' 'content' 'footer'`, + height: '100%' + }, + cardHeader: { + gridArea: 'header', + borderBottom: '1px solid rgba(0, 0, 0, 0.12)', + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, + /** + * Have to do this because CardPopoverMenu adds a few extra + * pixels from a rogue span, which screws up alignment compared + * to normal SQFormScrollableCard headers or its more generic + * counterpart in MIAV, AdminViewCard. + */ + height: '70px', + alignItems: 'center' + }, + action: { + alignSelf: 'unset', + marginTop: 'unset' + } + }; +}); + +function getSelectedComponent(selectedTab, children) { + if (Array.isArray(children)) { + const selectedFormComponent = children.find( + child => child.props.id === selectedTab.value + ); + return selectedFormComponent; + } + return children; +} + +export default function SQFormScrollableCardsMenuWrapper({ + title, + menuItems, + children +}) { + const classes = useStyles(); + + const [selectedTab, setSelectedTab] = React.useState(menuItems[0]); + + const handleChange = selectedMenuItemValue => { + const newSelection = menuItems.find( + item => item.value === selectedMenuItemValue + ); + + if (selectedTab.value !== newSelection.value) { + setSelectedTab(newSelection); + } + }; + + const SelectedForm = React.useMemo(() => { + const selected = getSelectedComponent(selectedTab, children); + return selected; + }, [selectedTab, children]); + + return ( + + + } + /> + {SelectedForm} + + ); +} + +SQFormScrollableCardsMenuWrapper.propTypes = { + /** At least one instance of SQFormScrollableCard where each has + * prop `isHeaderDisabled` === true AND each has a unique string + * `id` prop value that maps to a `value` property of one of the + * `menuItems` objects. + */ + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.arrayOf(PropTypes.element) + ]), + /** For each child instance of SQFormScrollableCard, we need a + * corresponding menuItem object with whatever the popover menu + * item label should be, and a `value` that matches the `id` prop + * value in its corresponding SQFormScrollableCard + * */ + menuItems: PropTypes.arrayOf( + PropTypes.objectOf({ + label: PropTypes.string, + value: PropTypes.string + }) + ).isRequired, + /** The Title for the Header component */ + title: PropTypes.string +}; diff --git a/src/components/SQFormScrollableCardsMenuWrapper/index.js b/src/components/SQFormScrollableCardsMenuWrapper/index.js new file mode 100644 index 00000000..b61e1e89 --- /dev/null +++ b/src/components/SQFormScrollableCardsMenuWrapper/index.js @@ -0,0 +1 @@ +export {default} from './SQFormScrollableCardsMenuWrapper'; diff --git a/src/index.js b/src/index.js index 4dc237d1..8253634f 100644 --- a/src/index.js +++ b/src/index.js @@ -36,5 +36,6 @@ export {default as SQFormMultiSelect} from './components/SQForm/SQFormMultiSelec export {default as SQFormMaskedTextField} from './components/SQForm/SQFormMaskedTextField'; export {default as SQFormHelperText} from './components/SQForm/SQFormHelperText'; export {default as SQFormScrollableCard} from './components/SQFormScrollableCard'; +export {default as SQFormScrollableCardsMenuWrapper} from './components/SQFormScrollableCardsMenuWrapper'; export {default as SQFormGuidedWorkflow} from './components/SQFormGuidedWorkflow'; export {default as SQFormMultiValue} from './components/SQForm/SQFormMultiValue'; diff --git a/stories/SQFormScrollableCardsMenuWrapper.stories.js b/stories/SQFormScrollableCardsMenuWrapper.stories.js new file mode 100644 index 00000000..671ec335 --- /dev/null +++ b/stories/SQFormScrollableCardsMenuWrapper.stories.js @@ -0,0 +1,153 @@ +import React from 'react'; +import * as yup from 'yup'; + +import { + SQFormScrollableCardsMenuWrapper, + SQFormScrollableCard, + SQFormTextField, + SQFormCheckbox +} from '../src'; +import {createDocsPage} from './utils/createDocsPage'; + +export default { + title: 'Forms/SQFormScrollableCardsMenuWrapper', + component: SQFormScrollableCardsMenuWrapper, + parameters: { + docs: {page: createDocsPage({showStories: false})} + } +}; + +export const sqFormScrollableCardsMenuWrapper = () => { + const menuItems = [ + { + label: 'Details', + value: 'details' + }, + { + label: 'Permissions', + value: 'permissions' + } + ]; + + return ( +
+ + + + +
+ ); +}; + +function ScrollableDetails() { + const initialValues = { + name: '' + }; + + const validationSchema = { + name: yup.string().required('Required') + }; + + const handleSubmit = (values, actions) => { + window.alert(JSON.stringify(values, null, 2)); + actions.setSubmitting(false); + actions.resetForm(); + }; + + /** + * TODO: This code can/should be removed once this issue is resolved - https://github.com/SelectQuoteLabs/SQForm/issues/436 + * Once this problem is resolved, we can remove this code block below and also remove the wrapper container around our SQFormScrollableCard + */ + const [calculatedHeight, setCalculatedHeight] = React.useState(0); + + React.useEffect(() => { + const currentElement = document.getElementById(`ResultContainer`); + const topOffset = currentElement?.getBoundingClientRect().top; + const offsetBasedHeight = `calc(100vh - ${topOffset}px - 24px)`; + const parentHeight = currentElement?.parentElement?.clientHeight; + const parentTopOffset = currentElement?.parentElement?.getBoundingClientRect() + .top; + const topDifferential = + topOffset && parentTopOffset ? topOffset - parentTopOffset : 0; + const maxOffsetBasedHeight = `calc(${parentHeight}px - ${topDifferential}px)`; + const calculatedContainerHeight = `min(${offsetBasedHeight}, ${maxOffsetBasedHeight})`; + + setCalculatedHeight(calculatedContainerHeight); + }, []); + + return ( + + + + ); +} + +function ScrollablePermissions() { + const initialValues = { + isAdmin: false + }; + + const validationSchema = { + isAdmin: yup.boolean() + }; + + const handleSubmit = (values, actions) => { + window.alert(JSON.stringify(values, null, 2)); + actions.setSubmitting(false); + actions.resetForm(); + }; + /** + * TODO: This code can/should be removed once this issue is resolved - https://github.com/SelectQuoteLabs/SQForm/issues/436 + * Once this problem is resolved, we can remove this code block below and also remove the wrapper container around our SQFormScrollableCard + */ + const [calculatedHeight, setCalculatedHeight] = React.useState(0); + + React.useEffect(() => { + const currentElement = document.getElementById(`ResultContainer`); + const topOffset = currentElement?.getBoundingClientRect().top; + const offsetBasedHeight = `calc(100vh - ${topOffset}px - 24px)`; + const parentHeight = currentElement?.parentElement?.clientHeight; + const parentTopOffset = currentElement?.parentElement?.getBoundingClientRect() + .top; + const topDifferential = + topOffset && parentTopOffset ? topOffset - parentTopOffset : 0; + const maxOffsetBasedHeight = `calc(${parentHeight}px - ${topDifferential}px)`; + const calculatedContainerHeight = `min(${offsetBasedHeight}, ${maxOffsetBasedHeight})`; + + setCalculatedHeight(calculatedContainerHeight); + }, []); + + return ( + + + + ); +}