Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-pans-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ultraviolet/ui": patch
---

`Modal`, `Dialog`, `Drawer`: improve opening/closing animations
4 changes: 3 additions & 1 deletion e2e/tests/componentsWithinModal/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ test('open modal, select an option, open nested modal through select input, clos
await page.getByTestId('select-input-color').click()

await page.getByRole('button', { name: 'Open Nested Modal' }).click()
await page.locator('dialog').filter({ hasText: 'This is the nested modal' }).getByLabel('close').click()
const nestedModal = page.locator('dialog').filter({ hasText: 'This is the nested modal' })
await nestedModal.getByLabel('close').click()
await expect(nestedModal).not.toBeAttached()

await page.getByLabel('close').click()
await expect(page.locator('dialog')).not.toBeVisible()
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/Dialog/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from '@testing-library/react'
import { screen, waitFor } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { renderWithTheme } from '@utils/test'
import { describe, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -45,7 +45,9 @@ describe('dialog', () => {

const cancelButton = screen.getByRole('button', { name: 'Cancel' })
await userEvent.click(cancelButton)
expect(screen.queryByRole('heading', { name: 'Title Test' })).not.toBeInTheDocument()
await waitFor(() => {
expect(screen.queryByRole('heading', { name: 'Title Test' })).not.toBeInTheDocument()
})
})

it('[CancelButton] : should handle click', async () => {
Expand Down
12 changes: 10 additions & 2 deletions packages/ui/src/components/Dialog/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import type { ComponentProps, ReactNode } from 'react'
import type { ComponentProps, MouseEvent, ReactNode } from 'react'
import { Button } from '../../Button'
import { useDialogContext } from '../Context'

Expand All @@ -11,8 +11,16 @@ type DialogButtonProps = {
export const DialogButton = ({ children, onClick, disabled, tooltip, style }: DialogButtonProps) => {
const context = useDialogContext()

function onButtonClick(e: MouseEvent<HTMLElement>) {
const dialog = (e.target as HTMLElement).closest('dialog')
if (dialog) {
dialog.dataset['closeAction'] = 'confirm'
}
onClick?.(e)
}

return (
<Button disabled={disabled} onClick={onClick} sentiment={context.sentiment} style={style} tooltip={tooltip}>
<Button disabled={disabled} onClick={onButtonClick} sentiment={context.sentiment} style={style} tooltip={tooltip}>
{children}
</Button>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,25 @@ exports[`drawer > renders with disclosure and onClose 1`] = `
</div>
<div>
<div
class="backdrop-drawer styles__lrojhc7 styles__lrojhc6 styles_open_true__lrojhc8 styles_visible_true__lrojhc9"
class="backdrop-drawer styles__lrojhc7"
data-testid="test-backdrop"
data-visible="true"
>
<dialog
aria-label="drawer-test"
aria-modal="true"
class="styles_medium__1iezf7y4 styles__1iezf7yb styles__lrojhca styles_placement_top-right__lrojhcl styles_size_medium__lrojhco"
class="styles_medium__1iezf7y1 styles__1iezf7y8 styles__lrojhc8 styles_placement_right__lrojhcf styles_size_medium__lrojhcl"
data-size="medium"
data-testid="test"
id="drawer-test"
open=""
style="--lrojhc0: -0px; --lrojhc1: -50px;"
style="--lrojhc0: -0px; --lrojhc1: 1.1;"
tabindex="0"
>
<div
class="styles__1iezf7y6 styles__toi52u0 styles_flexDirection_column_xxsmall__toi52u2d styles_gap_1rem_xxsmall__toi52u3v"
class="styles__1iezf7y3 styles__toi52u0 styles_flexDirection_column_xxsmall__toi52u2d styles_gap_1rem_xxsmall__toi52u3v"
>
<h2
class="styles__1iezf7y9 style__m4c9ow0 style_prominence_default__m4c9ow4 style_sentiment_neutral__m4c9owb style_variant_headingSmallStrong__m4c9ow16 style_undefined_compound_0__m4c9ow1a style_undefined_compound_64__m4c9ow32"
class="styles__1iezf7y6 style__m4c9ow0 style_prominence_default__m4c9ow4 style_sentiment_neutral__m4c9owb style_variant_headingSmallStrong__m4c9ow16 style_undefined_compound_0__m4c9ow1a style_undefined_compound_64__m4c9ow32"
>
header
</h2>
Expand All @@ -204,10 +203,10 @@ exports[`drawer > renders with disclosure and onClose 1`] = `
style="--bu43fb0: 1px;"
/>
<div
class="styles__1iezf7y7"
class="styles__1iezf7y4"
>
<div
class="styles__1iezf7y8"
class="styles__1iezf7y5"
>
<div>
drawer
Expand All @@ -216,7 +215,7 @@ exports[`drawer > renders with disclosure and onClose 1`] = `
</div>
</div>
<div
class="styles__lrojhc2"
class="styles__lrojhc3"
>
<button
aria-label="close"
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const BaseDrawer = ({
onClose={onClose}
onOpen={onOpenPush}
open={open}
placement="top-right"
placement="right"
preventBodyScroll={!push}
ref={modalRef}
size={size}
Expand Down
38 changes: 7 additions & 31 deletions packages/ui/src/components/Drawer/styles.css.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,18 @@
import { theme } from '@ultraviolet/themes'
import { globalStyle, keyframes, style, styleVariants } from '@vanilla-extract/css'
import { globalStyle, style, styleVariants } from '@vanilla-extract/css'

export const SIZES = {
large: 75.5,
medium: 49,
small: 22.25,
} as const

export const DURATION = {
large: 0.3,
medium: 0.25,
small: 0.15,
} as const

const slideIn = () =>
keyframes({
'0%': {
transform: 'translateX(90%)',
},
'100%': {
transform: 'translateX(0)',
},
})

export type SizeProp = keyof typeof SIZES

function makeStyleSize(size: SizeProp) {
return {
height: '100%',
transform: 'translateX(0)',
animation: `${slideIn()} linear ${DURATION[size]}s`,
}
}
const drawer = styleVariants({
large: makeStyleSize('large'),
medium: makeStyleSize('medium'),
small: makeStyleSize('small'),
large: {},
medium: {},
small: {},
})

const contentWrapper = style({
Expand Down Expand Up @@ -63,25 +40,24 @@ const footer = style({
paddingTop: 0,
})

const base = style({})
const base = style({
height: '100%',
})

const push = style({})

const contentToPushStyle = style({})

globalStyle(`${contentToPushStyle}[data-drawer="small"]`, {
paddingRight: `${SIZES.small}rem`,
transition: `padding-right ${DURATION.small}s !important`,
})

globalStyle(`${contentToPushStyle}[data-drawer="medium"]`, {
transition: `padding-right ${DURATION.small}s !important`,
paddingRight: `${SIZES.medium}rem`,
})

globalStyle(`${contentToPushStyle}[data-drawer="large"]`, {
paddingRight: `${SIZES.large}rem`,
transition: `padding-right ${DURATION.large}s !important`,
})
export const drawerStyle = {
drawer,
Expand Down
37 changes: 30 additions & 7 deletions packages/ui/src/components/Modal/ModalContent.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
'use client'

import { CloseIcon } from '@ultraviolet/icons/CloseIcon'
import { useEffect, useState } from 'react'
import type { ComponentProps, CSSProperties } from 'react'
import type { Modal } from '.'
import { Button } from '../Button'
import { Dialog } from './components/Dialog'
import type { ModalPlacement, ModalSize } from './types'
import { ANIMATION_DURATION_MS } from './constants.css'
import { modalStyle } from './styles.css'

type ModalContentProps = ComponentProps<typeof Modal> & {
visible: boolean
open: boolean
placement: ModalPlacement
finalSize: ModalSize
Expand All @@ -23,7 +24,6 @@ type ModalContentProps = ComponentProps<typeof Modal> & {
}

export const ModalContent = ({
visible,
open,
placement,
finalSize,
Expand All @@ -45,8 +45,14 @@ export const ModalContent = ({
style,
ref,
isDrawer,
}: ModalContentProps) =>
visible || open ? (
}: ModalContentProps) => {
const shouldRender = useDelayUnmount(open, ANIMATION_DURATION_MS)

if (!shouldRender) {
return null
}

return (
<Dialog
ariaLabel={ariaLabel}
backdropClassName={backdropClassName}
Expand All @@ -58,7 +64,7 @@ export const ModalContent = ({
image={image}
isDrawer={isDrawer}
onClose={handleClose}
open={visible || open}
open={open}
placement={placement}
preventBodyScroll={preventBodyScroll}
ref={ref}
Expand All @@ -71,7 +77,7 @@ export const ModalContent = ({
modalId: finalId,
show: handleOpen,
toggle: handleToggle,
visible,
visible: open,
})
: children}
<div className={modalStyle.container}>
Expand All @@ -89,4 +95,21 @@ export const ModalContent = ({
) : null}
</div>
</Dialog>
) : null
)
}

function useDelayUnmount(open?: boolean, delayTime?: number) {
const [shouldRender, setShouldRender] = useState(open)

useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>
if (open && !shouldRender) {
setShouldRender(true)
} else if (!open && shouldRender) {
timeoutId = setTimeout(() => setShouldRender(false), delayTime)
}
return () => clearTimeout(timeoutId)
}, [open, delayTime, shouldRender])

return shouldRender
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StoryFn } from '@storybook/react-vite'
import { Modal } from '..'
import { Button } from '../../Button'
import { MODAL_PLACEMENT } from '../constants'
import { MODAL_PLACEMENT } from '../constants.css'

export const Placement: StoryFn = props => (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StoryFn } from '@storybook/react-vite'
import { Modal } from '..'
import { Button } from '../../Button'
import { MODAL_WIDTH } from '../constants'
import { MODAL_WIDTH } from '../constants.css'

export const Size: StoryFn = props => (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,26 +95,25 @@ exports[`modal > renders with disclosure and onClose 1`] = `
</div>
<div>
<div
class="styles__lrojhc7 styles__lrojhc6 styles_open_true__lrojhc8 styles_visible_true__lrojhc9"
class="styles__lrojhc7"
data-testid="test-backdrop"
data-visible="true"
>
<dialog
aria-label="modal-test"
aria-modal="true"
class="styles__lrojhca styles_placement_center__lrojhcg styles_size_small__lrojhcp"
class="styles__lrojhc8 styles_placement_center__lrojhcd styles_size_small__lrojhcm"
data-size="small"
data-testid="test"
id="modal-test"
open=""
style="--lrojhc0: -0px; --lrojhc1: 0px;"
style="--lrojhc0: -0px; --lrojhc1: 1;"
tabindex="0"
>
<div>
modal
</div>
<div
class="styles__lrojhc2"
class="styles__lrojhc3"
>
<button
aria-label="close"
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/components/Modal/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHook, screen } from '@testing-library/react'
import { renderHook, screen, waitFor } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { consoleLightTheme } from '@ultraviolet/themes'
import { renderWithTheme } from '@utils/test'
Expand Down Expand Up @@ -127,8 +127,10 @@ describe('modal', () => {
await userEvent.click(screen.getByRole('button', { name: 'Open modal' }))
const closeButton = screen.getByRole('button', { name: 'close' })
await userEvent.click(closeButton)
expect(count).toBe(1)

await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())

expect(count).toBe(1)
expect(asFragment()).toMatchSnapshot()
})

Expand Down
Loading
Loading