Skip to content

Commit

Permalink
fix(side-modal): fix autofocus and improve animation (#3296)
Browse files Browse the repository at this point in the history
* fix(side-modal): fix autofocus and improve animation

* chore: add changeset

* feat(side-modal): add `hideOnEsc` prop and docs

* fix: change default hideOnEsc to true

* chore: update docs
  • Loading branch information
TheSisb committed Jun 28, 2023
1 parent 8cd2459 commit 771da3b
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 56 deletions.
6 changes: 6 additions & 0 deletions .changeset/eight-pumas-clap.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/side-modal': minor
'@twilio-paste/core': minor
---

[SideModal] Add new prop `hideOnEsc` which enables closing the SideModal when the "Escape" keyboard key is pressed
6 changes: 6 additions & 0 deletions .changeset/thin-lemons-report.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/side-modal': patch
'@twilio-paste/core': patch
---

[SideModal] Fix to correctly focus the close button when the SideModal first opens
105 changes: 50 additions & 55 deletions packages/paste-core/components/side-modal/src/SideModal.tsx
@@ -1,34 +1,13 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import type {BoxProps} from '@twilio-paste/box';
import {useTransition, animated} from '@twilio-paste/animation-library';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import {Box, safelySpreadBoxProps, type BoxProps} from '@twilio-paste/box';
import {StyledBase} from '@twilio-paste/theme';
import {NonModalDialogPrimitive} from '@twilio-paste/non-modal-dialog-primitive';

import {SideModalContext} from './SideModalContext';

const StyledSideModal = React.forwardRef<HTMLDivElement, BoxProps>(({style, ...props}, ref) => (
<Box
{...safelySpreadBoxProps(props)}
ref={ref}
style={style}
boxShadow="shadow"
width="size80"
zIndex="zIndex80"
position="fixed"
top="0 !important"
left="auto !important"
right="0 !important"
bottom="0 !important"
_focus={{
outline: 'none',
}}
/>
));
StyledSideModal.displayName = 'StyledSideModal';

const AnimatedStyledSideModal = animated(StyledSideModal);
export const AnimatedBox = animated(Box);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getAnimationStates = (): any => ({
Expand All @@ -37,20 +16,21 @@ const getAnimationStates = (): any => ({
leave: {opacity: 0, transform: `translateX(100%)`},
// https://www.react-spring.dev/docs/advanced/config
config: {
mass: 0.3,
tension: 288,
friction: 15,
mass: 0.5,
tension: 220,
friction: 20,
},
});

export interface SideModalProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
'aria-label': string;
element?: BoxProps['element'];
hideOnEsc?: boolean;
}

export const SideModal = React.forwardRef<HTMLDivElement, SideModalProps>(
({children, element = 'SIDE_MODAL', ...props}, ref) => {
({children, element = 'SIDE_MODAL', hideOnEsc = true, ...props}, ref) => {
const dialog = React.useContext(SideModalContext);
const transitions = useTransition(dialog.visible, getAnimationStates());

Expand All @@ -59,34 +39,49 @@ export const SideModal = React.forwardRef<HTMLDivElement, SideModalProps>(
* To enable an unmount animation we need to hardcode `visible={true}` on NonModalDialogPrimitive
* so that react-spring can manage cleanup. That said, I think it looks better to close instantly.
*/
return transitions((styles, item) => {
return (
item && (
<NonModalDialogPrimitive
{...dialog}
{...safelySpreadBoxProps(props)}
as={AnimatedStyledSideModal}
element={`${element}_CONTAINER`}
ref={ref}
preventBodyScroll={false}
hideOnClickOutside={false}
style={styles}
>
<StyledBase>
<Box
element={element}
display="grid"
gridTemplateRows="auto 1fr auto"
height="100vh"
backgroundColor="colorBackgroundBody"
>
{children}
</Box>
</StyledBase>
</NonModalDialogPrimitive>
)
);
});
return (
<NonModalDialogPrimitive
{...dialog}
{...safelySpreadBoxProps(props)}
preventBodyScroll={false}
hideOnClickOutside={false}
hideOnEsc={hideOnEsc}
element={`${element}_CONTAINER`}
ref={ref}
as={Box}
zIndex="zIndex80"
position="fixed"
top="0 !important"
right="0 !important"
bottom="0 !important"
left="auto !important"
transform="none !important"
_focus={{
outline: 'none',
}}
>
<StyledBase>
{transitions((styles, item) => {
return (
item && (
<AnimatedBox
style={styles}
element={element}
display="grid"
gridTemplateRows="auto 1fr auto"
height="100vh"
backgroundColor="colorBackgroundBody"
boxShadow="shadow"
width="size80"
>
{children}
</AnimatedBox>
)
);
})}
</StyledBase>
</NonModalDialogPrimitive>
);
}
);

Expand Down
Expand Up @@ -292,8 +292,9 @@ The SideModalButton renders a Button component and accepts all of its props, whi
| Prop | Type | Description | Default |
| ------------ | ----------------- | ----------------------------------------------------------------------------------------- | -------------- |
| `aria-label` | `string` | Title of the dialog for screen readers. | |
| `hideOnEsc?` | `boolean` | Set to `false` to disable closing the SideModal when the "Escape" key is pressed. | true |
| `element?` | `string` | Overrides the default element name to apply unique styles with the Customization Provider | `'SIDE_MODAL'` |
| `children?` | `React.ReactNode` | Child components to render into the SideModal | |
| `children?` | `React.ReactNode` | Child components to render into the SideModal |

##### SideModalHeader

Expand Down

0 comments on commit 771da3b

Please sign in to comment.