Skip to content

Commit

Permalink
fix(components): style position for <PortalsContainer> layout (#2541)
Browse files Browse the repository at this point in the history
* fix(components): style position for <PortalsContainer> layout

* refactor(playground): clean up and improve layout
  • Loading branch information
emmenko authored Apr 8, 2022
1 parent 602822c commit 3853d77
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 171 deletions.
12 changes: 12 additions & 0 deletions .changeset/orange-dolphins-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@commercetools-frontend/application-components': patch
'@commercetools-frontend/application-shell': patch
---

Fix layout issue with modal components when the underlying page has a scrolling position, causing the modal container position to "scroll" with the page position.

The expected behavior is for the modal page to always be correctly positioned and visible, regardless of the scrolling position of the underlying page.

To fix that, the `<PortalsContainer>` now uses `position: fixed` when a modal container opens.

The component now accepts some props to allow consumers to adjust the layout accordingly. However, for Custom Applications everything is pre-configured, so there is no action required.
Original file line number Diff line number Diff line change
@@ -1,18 +1,64 @@
import { css } from '@emotion/react';
import { css, Global } from '@emotion/react';
import { PORTALS_CONTAINER_ID } from '@commercetools-frontend/constants';

type TPortalsContainerProps = {
/**
* The offset value for positioning the container from the top, when opened.
* Usually this is corresponds to the height of the header section.
*/
offsetTop: string;
/**
* The CSS selector to apply the `overflow: hidden` style to (preventing scrolling)
* when a modal container is open.
*/
containerSelectorToPreventScrollingOnOpen: string;
/**
* The `z-index` value to apply to the portal container. Default to `10000`.
*/
zIndex: number;
};
const defaultProps: Pick<
TPortalsContainerProps,
'offsetTop' | 'containerSelectorToPreventScrollingOnOpen' | 'zIndex'
> = {
offsetTop: '0',
containerSelectorToPreventScrollingOnOpen: 'main',
zIndex: 10000,
};

// All modal components expect to be rendered inside this container.
const PortalsContainer = () => (
<div
id={PORTALS_CONTAINER_ID}
// The container needs a height in order to be tabbable: https://reactjs/react-modal#774
css={css`
display: flex;
height: 1px;
margin-top: -1px;
`}
/>
const PortalsContainer = (props: TPortalsContainerProps) => (
<>
<Global
// Apply some global styles, based on the `.ReactModal__Body--open` class.
styles={css`
.ReactModal__Body--open
${props.containerSelectorToPreventScrollingOnOpen} {
overflow: hidden;
}
.ReactModal__Body--open #${PORTALS_CONTAINER_ID} {
position: fixed;
height: calc(100% - ${props.offsetTop});
width: 100%;
top: ${props.offsetTop};
bottom: 0;
z-index: ${props.zIndex};
}
`}
/>
<div
id={PORTALS_CONTAINER_ID}
// The container needs a height in order to be tabbable: https://reactjs/react-modal#774
css={css`
display: flex;
height: 1px;
margin-top: -1px;
`}
/>
</>
);
PortalsContainer.displayName = 'PortalsContainer';
PortalsContainer.defaultProps = defaultProps;

export default PortalsContainer;
7 changes: 4 additions & 3 deletions packages/application-shell/src/components/app-bar/app-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { css } from '@emotion/react';
import Spacings from '@commercetools-uikit/spacings';
import { customProperties } from '@commercetools-uikit/design-system';
import LogoSVG from '@commercetools-frontend/assets/images/logo.svg';
import { CONTAINERS } from '../../constants';
import { CONTAINERS, HEIGHTS } from '../../constants';
import { getPreviousProjectKey } from '../../utils';
import UserSettingsMenu from '../user-settings-menu';
import ProjectSwitcher from '../project-switcher';
Expand All @@ -26,8 +26,9 @@ const AppBar = (props: Props) => {
return (
<div
css={css`
background-color: ${customProperties.colorSurface};
box-shadow: ${customProperties.shadow1};
min-height: 43px;
min-height: ${HEIGHTS.header};
position: relative;
width: 100%;
z-index: 20000;
Expand Down Expand Up @@ -103,7 +104,7 @@ const AppBar = (props: Props) => {
<div
css={css`
border-left: 1px ${customProperties.colorNeutral90} solid;
height: 43px;
height: ${HEIGHTS.header};
`}
/>
{props.user ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { AsyncLocaleData } from '@commercetools-frontend/i18n';
import version from '../../version';
import internalReduxStore from '../../configure-store';
import { selectProjectKeyFromUrl, getPreviousProjectKey } from '../../utils';
import { HEIGHTS } from '../../constants';
import ProjectDataLocale from '../project-data-locale';
import ApplicationShellProvider from '../application-shell-provider';
import { getBrowserLocale } from '../application-shell-provider/utils';
Expand Down Expand Up @@ -239,7 +240,7 @@ export const RestrictedApplication = <
css={css`
height: 100vh;
display: grid;
grid-template-rows: auto 43px 1fr;
grid-template-rows: auto ${HEIGHTS.header} 1fr;
grid-template-columns: auto 1fr;
`}
>
Expand Down Expand Up @@ -402,7 +403,7 @@ export const RestrictedApplication = <
}
`}
>
<PortalsContainer />
<PortalsContainer offsetTop={HEIGHTS.header} />
<Switch>
<Redirect
from="/profile"
Expand Down Expand Up @@ -508,10 +509,6 @@ const ApplicationShell = <AdditionalEnvironmentProperties extends {}>(
#app {
height: 100%;
}
.ReactModal__Body--open main {
/* When a modal is open, we should prevent the content to be scrollable */
overflow: hidden;
}
`}
/>
<ApplicationShellProvider<AdditionalEnvironmentProperties>
Expand Down
4 changes: 4 additions & 0 deletions packages/application-shell/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const HEIGHTS = {
header: '43px',
} as const;

export const SUPPORTED_HEADERS = {
ACCEPT_VERSION: 'Accept-version',
AUTHORIZATION: 'Authorization',
Expand Down
59 changes: 4 additions & 55 deletions playground/src/components/entry-point/entry-point.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,25 @@
import { lazy } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import {
ApplicationShell,
setupGlobalErrorListener,
RouteCatchAll,
} from '@commercetools-frontend/application-shell';
import { Sdk } from '@commercetools-frontend/sdk';
import * as globalActions from '@commercetools-frontend/actions-global';
import Text from '@commercetools-uikit/text';
import loadMessages from '../../messages';
import { entryPointUriPath } from '../../constants';

// Here we split up the main (app) bundle with the actual application business logic.
// Splitting by route is usually recommended and you can potentially have a splitting
// point for each route. More info at https://reactjs.org/docs/code-splitting.html
const AsyncStateMachines = lazy(
const AsyncPlaygroundRoutes = lazy(
() => import('../../routes' /* webpackChunkName: "app-kit-playground" */)
);

export const ApplicationStateMachines = () => (
<Switch>
{
/* The /account route is only useful for the playground app for testing purposes.
If you build an application for production, this route should be removed. */
process.env.NODE_ENV === 'production' ? null : (
<Route
path="/account"
render={() => (
<Text.Body>
{
'This is a placeholder page for the /account routes and is only useful for testing purposes. Do not use this in production.'
}
</Text.Body>
)}
/>
)
}
{
/* For development, it's useful to redirect to the actual
application routes when you open the browser at http://localhost:3001 */
process.env.NODE_ENV === 'production' ? null : (
<Redirect
exact={true}
from="/:projectKey"
to={`/:projectKey/${entryPointUriPath}`}
/>
)
}
<Route
path={`/:projectKey/${entryPointUriPath}`}
component={AsyncStateMachines}
/>
{/* Catch-all route */}
<RouteCatchAll />
</Switch>
);
ApplicationStateMachines.displayName = 'ApplicationStateMachines';

// Ensure to setup the global error listener before any React component renders
// in order to catch possible errors on rendering/mounting.
setupGlobalErrorListener();

const EntryPoint = () => (
<ApplicationShell
environment={window.app}
onRegisterErrorListeners={({ dispatch }) => {
Sdk.Get.errorHandler = (error) =>
globalActions.handleActionError(error, 'sdk')(dispatch);
}}
applicationMessages={loadMessages}
render={() => <ApplicationStateMachines />}
/>
<ApplicationShell environment={window.app} applicationMessages={loadMessages}>
<AsyncPlaygroundRoutes />
</ApplicationShell>
);
EntryPoint.displayName = 'EntryPoint';

Expand Down
1 change: 0 additions & 1 deletion playground/src/components/entry-point/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default } from './entry-point';
export { ApplicationStateMachines } from './entry-point';
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors';
import { InfoModalPage } from '@commercetools-frontend/application-components';
import {
GtmContext,
useMcQuery,
} from '@commercetools-frontend/application-shell';
import { ListIcon, CheckBoldIcon } from '@commercetools-uikit/icons';
import { CheckBoldIcon } from '@commercetools-uikit/icons';
import LoadingSpinner from '@commercetools-uikit/loading-spinner';
import Spacings from '@commercetools-uikit/spacings';
import Text from '@commercetools-uikit/text';
import Grid from '@commercetools-uikit/grid';
import Constraints from '@commercetools-uikit/constraints';
import FlatButton from '@commercetools-uikit/flat-button';
import {
formatLocalizedString,
transformLocalizedFieldToLocalizedString,
Expand All @@ -37,14 +37,15 @@ const getStateName = (state, dataLocale, projectLanguages) =>
) || 'n/a';

const StateMachinesDetails = (props) => {
const params = useParams();
const { dataLocale, projectLanguages } = useApplicationContext((context) => ({
dataLocale: context.dataLocale,
projectLanguages: context.project.languages,
}));

const { data, error, loading } = useMcQuery(FetchStateQuery, {
variables: {
id: props.id,
id: params.id,
},
context: {
target: GRAPHQL_TARGETS.COMMERCETOOLS_PLATFORM,
Expand All @@ -57,56 +58,46 @@ const StateMachinesDetails = (props) => {
track('rendered', 'State machine details');
}, [track]);

if (error) {
return (
<ContentNotification type="error">
<Text.Body>{getErrorMessage(error)}</Text.Body>
</ContentNotification>
);
}

return (
<Spacings.Inset scale="m">
<InfoModalPage
isOpen
title={
data ? getStateName(data.state, dataLocale, projectLanguages) : 'asd'
}
onClose={props.goToStateMachinesList}
>
<Spacings.Stack scale="l">
{loading && <LoadingSpinner />}
{error && (
<ContentNotification type="error">
<Text.Body>{getErrorMessage(error)}</Text.Body>
</ContentNotification>
)}
{data && (
<>
<FlatButton
as={Link}
icon={<ListIcon />}
label="Back to list"
to={props.backToListPath}
/>
<Spacings.Stack scale="xs">
<Text.Headline as="h2">
{getStateName(data?.state, dataLocale, projectLanguages)}
</Text.Headline>
<Text.Detail>{data.state.key}</Text.Detail>
</Spacings.Stack>
<Constraints.Horizontal max={7}>
<Grid
gridGap="16px"
gridAutoColumns="1fr"
gridTemplateColumns="repeat(2, 1fr)"
>
<Text.Body>{'Type'}</Text.Body>
<Text.Body>{data.state.type}</Text.Body>
<Text.Body>{'Built In'}</Text.Body>
<span>{data.state.builtIn ? <CheckBoldIcon /> : ''}</span>
<Text.Body>{'Initial'}</Text.Body>
<span>{data.state.initial ? <CheckBoldIcon /> : ''}</span>
</Grid>
</Constraints.Horizontal>
</>
<Constraints.Horizontal max={7}>
<Grid
gridGap="16px"
gridAutoColumns="1fr"
gridTemplateColumns="repeat(2, 1fr)"
>
<Text.Body>{'Key'}</Text.Body>
<Text.Body>{data.state.key}</Text.Body>
<Text.Body>{'Type'}</Text.Body>
<Text.Body>{data.state.type}</Text.Body>
<Text.Body>{'Built In'}</Text.Body>
<span>{data.state.builtIn ? <CheckBoldIcon /> : ''}</span>
<Text.Body>{'Initial'}</Text.Body>
<span>{data.state.initial ? <CheckBoldIcon /> : ''}</span>
</Grid>
</Constraints.Horizontal>
)}
</Spacings.Stack>
</Spacings.Inset>
</InfoModalPage>
);
};
StateMachinesDetails.displayName = 'StateMachinesDetails';
StateMachinesDetails.propTypes = {
id: PropTypes.string.isRequired,
backToListPath: PropTypes.string.isRequired,
goToStateMachinesList: PropTypes.func.isRequired,
};

export default StateMachinesDetails;
Loading

1 comment on commit 3853d77

@vercel
Copy link

@vercel vercel bot commented on 3853d77 Apr 8, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.