Skip to content

Commit

Permalink
WEB-2113 fix portal, show dividers while scrolling content
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro Ladaria committed Dec 19, 2024
1 parent 5cc29b3 commit fda0071
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 32 deletions.
4 changes: 3 additions & 1 deletion src/__stories__/drawer-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const Default = ({
<>
<Stack space={16}>
<ButtonPrimary onPress={() => setIsOpen(true)}>Open Drawer</ButtonPrimary>
<Text3 regular>Result: {result}</Text3>
<Text3 regular>
Result: <span data-testid="result">{result}</span>
</Text3>
</Stack>
{isOpen && (
<Drawer
Expand Down
11 changes: 10 additions & 1 deletion src/drawer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const drawer = style([
flexGrow: 1,
}),
{
border: '1px dotted red',
'@media': {
[mq.tabletOrSmaller]: {
paddingBottom: 16 - 8,
Expand All @@ -47,6 +46,16 @@ export const drawer = style([
},
]);

export const titleContainer = style([
sprinkles({
flexShrink: 0,
flexGrow: 0,
}),
{
marginBottom: 16,
},
]);

export const scrollableSection = style([
sprinkles({
flexGrow: 1,
Expand Down
88 changes: 74 additions & 14 deletions src/drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import Box from './box';
import * as styles from './drawer.css';
import classnames from 'classnames';
import {Portal} from './portal';
import {useScreenSize} from './hooks';
import {useIsInViewport, useScreenSize, useTheme} from './hooks';
import FocusTrap from './focus-trap';
import {useSetModalStateEffect} from './modal-context-provider';
import ButtonLayout from './button-layout';
import {ButtonLink, ButtonPrimary, ButtonSecondary} from './button';
import Divider from './divider';
import {getPrefixedDataAttributes} from './utils/dom';
import * as tokens from './text-tokens';

import type {DataAttributes, TrackingEvent} from './utils/types';

Expand All @@ -25,6 +28,25 @@ const WIDTH_CONTENT = 388;
const WIDTH_DESKTOP = WIDTH_CONTENT + PADDING_X_DESKTOP * 2;
const WIDTH_TABLET = WIDTH_CONTENT + PADDING_X_TABLET * 2;

/**
* Renders divider or a div with transparent border to avoid the small but noticeable layout shift on scroll
*/
const MaybeDivider = ({show}: {show: boolean}) =>
show ? <Divider /> : <div style={{borderBottom: '1px solid transparent'}} />;

/**
* Restores the focus to the element that was focused before the Drawer was opened
*/
const useRestoreFocus = () => {
const activeElementRef = React.useRef<HTMLElement | null>(document.activeElement as HTMLElement);
React.useEffect(() => {
const elementToFocus = activeElementRef.current;
return () => {
elementToFocus?.focus?.();
};
}, []);
};

type DrawerLayoutProps = {
width?: number;
children: React.ReactNode;
Expand All @@ -40,6 +62,7 @@ type DrawerPropsRef = {
const DrawerLayout = React.forwardRef<DrawerPropsRef, DrawerLayoutProps>(
({width, children, onClose, onDismiss}, ref) => {
useSetModalStateEffect();
useRestoreFocus();
const {isMobile, isTablet} = useScreenSize();
const [isOpen, setIsOpen] = React.useState(false);

Expand Down Expand Up @@ -85,13 +108,15 @@ const DrawerLayout = React.forwardRef<DrawerPropsRef, DrawerLayoutProps>(
styles.overlay,
isOpen ? styles.overlayOpen : styles.overlayClosed
)}
{...getPrefixedDataAttributes({}, 'DrawerOverlay')}
/>
<div
ref={open}
style={{
width: isMobile ? 'unset' : width || (isTablet ? WIDTH_TABLET : WIDTH_DESKTOP),
}}
className={classnames(styles.container, isOpen ? styles.open : styles.closed)}
{...getPrefixedDataAttributes({}, 'DrawerLayout')}
>
{children}
</div>
Expand Down Expand Up @@ -128,6 +153,7 @@ type DrawerProps = {
button?: ButtonProps;
secondaryButton?: ButtonProps;
buttonLink?: ButtonProps;
dataAttributes?: DataAttributes;
};

const Drawer = ({
Expand All @@ -141,9 +167,15 @@ const Drawer = ({
button,
secondaryButton,
buttonLink,
dataAttributes,
}: DrawerProps): JSX.Element => {
const layoutRef = React.useRef<DrawerPropsRef>(null);
const hasButtons = !!(button || secondaryButton || buttonLink);
const [scrollableParentElement, setScrollableParentElement] = React.useState<HTMLElement | null>(null);
const topScrollSignalRef = React.useRef<HTMLDivElement>(null);
const bottomScrollSignalRef = React.useRef<HTMLDivElement>(null);
const {t, texts} = useTheme();

const paddingX = {
mobile: PADDING_X_MOBILE,
tablet: PADDING_X_TABLET,
Expand All @@ -154,81 +186,109 @@ const Drawer = ({
layoutRef.current?.close().then(pressHandlerFromProps);
};

const showTitleDivider = !useIsInViewport(topScrollSignalRef, true, {
root: scrollableParentElement,
});

const showButtonsDivider = !useIsInViewport(bottomScrollSignalRef, true, {
rootMargin: '1px', // bottomScrollSignal div has 0px height so we need a 1px margin to trigger the intersection observer
root: scrollableParentElement,
});

return (
<DrawerLayout width={width} ref={layoutRef} onClose={onClose} onDismiss={onDismiss}>
<div className={styles.drawer}>
<section
role="dialog"
aria-modal="true"
className={styles.drawer}
ref={setScrollableParentElement}
{...getPrefixedDataAttributes(dataAttributes, 'Drawer')}
>
{onDismiss && (
<div className={styles.closeButtonContainer}>
<IconButton
dataAttributes={{testid: 'dismissButton'}}
onPress={() => layoutRef.current?.dismiss()}
Icon={IconCloseRegular}
aria-label="Close drawer"
aria-label={texts.modalClose || t(tokens.modalClose)}
type="neutral"
backgroundType="transparent"
/>
</div>
)}
{title && (
<div style={{marginBottom: 16, flexShrink: 0, flexGrow: 0}}>
<div className={styles.titleContainer}>
<Box paddingX={paddingX}>
<Text5>{title}</Text5>
<Text5 dataAttributes={{testid: 'title'}}>{title}</Text5>
</Box>
</div>
)}
<MaybeDivider show={showTitleDivider} />
<div className={styles.scrollableSection}>
<div ref={topScrollSignalRef} />
<Box paddingX={paddingX}>
<Stack space={16}>
{subtitle && <Text4 regular>{subtitle}</Text4>}
{subtitle && (
<Text4 regular dataAttributes={{testid: 'subtitle'}}>
{subtitle}
</Text4>
)}
{description && (
<Text3 regular color={vars.colors.textSecondary}>
<Text3
regular
color={vars.colors.textSecondary}
dataAttributes={{testid: 'description'}}
>
{description}
</Text3>
)}
{children}
</Stack>
</Box>
<div ref={bottomScrollSignalRef} />
</div>
<MaybeDivider show={showButtonsDivider} />
<Box paddingBottom={16} />
{hasButtons && (
<Box paddingX={paddingX}>
<ButtonLayout
primaryButton={
button ? (
button && (
<ButtonPrimary
trackEvent={button.trackEvent}
trackingEvent={button.trackingEvent}
onPress={() => handleButtonPress(button.onPress)}
>
{button.text}
</ButtonPrimary>
) : undefined
)
}
secondaryButton={
secondaryButton ? (
secondaryButton && (
<ButtonSecondary
trackEvent={secondaryButton.trackEvent}
trackingEvent={secondaryButton.trackingEvent}
onPress={() => handleButtonPress(secondaryButton.onPress)}
>
{secondaryButton.text}
</ButtonSecondary>
) : undefined
)
}
link={
buttonLink ? (
buttonLink && (
<ButtonLink
trackEvent={buttonLink.trackEvent}
trackingEvent={buttonLink.trackingEvent}
onPress={() => handleButtonPress(buttonLink.onPress)}
>
{buttonLink.text}
</ButtonLink>
) : undefined
)
}
/>
</Box>
)}
</div>
</section>
</DrawerLayout>
);
};
Expand Down
25 changes: 9 additions & 16 deletions src/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,22 @@ export const Portal = ({children, className}: Props): JSX.Element | null => {
const [container, setContainer] = React.useState<HTMLDivElement | null>(null);

React.useEffect(() => {
if (!container) {
const newContainer = document.createElement('div');
newContainer.style.isolation = 'isolate';
setContainer(newContainer);
document.body.appendChild(newContainer);
}
const newContainer = document.createElement('div');
newContainer.style.isolation = 'isolate';
setContainer(newContainer);
document.body.appendChild(newContainer);

return () => {
if (container) {
document.body.removeChild(container);
}
document.body.removeChild(newContainer);
};
}, [container]);
}, []);

React.useEffect(() => {
if (container && className) {
container.classList.add(...className.split(' '));
}
const classes = className?.split(' ') || [];
container?.classList.add(...classes);

return () => {
if (container && className) {
container.classList.remove(...className.split(' '));
}
container?.classList.remove(...classes);
};
}, [className, container]);

Expand Down

0 comments on commit fda0071

Please sign in to comment.