mirror of https://github.com/langgenius/dify.git
feat(web): refactor pricing modal scrolling and accessibility (#34011)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
parent
0c3d11f920
commit
d14635625c
|
|
@ -1,12 +1,14 @@
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Dialog } from '@/app/components/base/ui/dialog'
|
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||||
import Header from '../header'
|
import Header from '../header'
|
||||||
|
|
||||||
function renderHeader(onClose: () => void) {
|
function renderHeader(onClose: () => void) {
|
||||||
return render(
|
return render(
|
||||||
<Dialog open>
|
<Dialog open>
|
||||||
<Header onClose={onClose} />
|
<DialogContent>
|
||||||
|
<Header onClose={onClose} />
|
||||||
|
</DialogContent>
|
||||||
</Dialog>,
|
</Dialog>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +26,7 @@ describe('Header', () => {
|
||||||
|
|
||||||
expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument()
|
expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument()
|
||||||
expect(screen.getByText('billing.plansCommon.title.description')).toBeInTheDocument()
|
expect(screen.getByText('billing.plansCommon.title.description')).toBeInTheDocument()
|
||||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ describe('Header', () => {
|
||||||
const handleClose = vi.fn()
|
const handleClose = vi.fn()
|
||||||
renderHeader(handleClose)
|
renderHeader(handleClose)
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button'))
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||||
|
|
||||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
@ -41,11 +43,11 @@ describe('Header', () => {
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
it('should render structural elements with translation keys', () => {
|
it('should render structural elements with translation keys', () => {
|
||||||
const { container } = renderHeader(vi.fn())
|
renderHeader(vi.fn())
|
||||||
|
|
||||||
expect(container.querySelector('span')).toBeInTheDocument()
|
expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument()
|
||||||
expect(container.querySelector('p')).toBeInTheDocument()
|
expect(screen.getByText('billing.plansCommon.title.description')).toBeInTheDocument()
|
||||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ describe('Pricing', () => {
|
||||||
it('should render pricing header and localized footer link', () => {
|
it('should render pricing header and localized footer link', () => {
|
||||||
render(<Pricing onCancel={vi.fn()} />)
|
render(<Pricing onCancel={vi.fn()} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('dialog', { name: 'billing.plansCommon.title.plans' })).toBeInTheDocument()
|
||||||
expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument()
|
expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', 'https://dify.ai/en/pricing#plans-and-features')
|
expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', 'https://dify.ai/en/pricing#plans-and-features')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,9 @@ const Footer = ({
|
||||||
<span className="flex h-fit items-center gap-x-1 text-saas-dify-blue-accessible">
|
<span className="flex h-fit items-center gap-x-1 text-saas-dify-blue-accessible">
|
||||||
<Link
|
<Link
|
||||||
href={pricingPageURL}
|
href={pricingPageURL}
|
||||||
className="system-md-regular"
|
className="system-md-regular hover:underline focus-visible:underline focus-visible:outline-none"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{t('plansCommon.comparePlanAndFeatures', { ns: 'billing' })}
|
{t('plansCommon.comparePlanAndFeatures', { ns: 'billing' })}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { DialogDescription, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import Button from '../../base/button'
|
import Button from '../../base/button'
|
||||||
import DifyLogo from '../../base/logo/dify-logo'
|
import DifyLogo from '../../base/logo/dify-logo'
|
||||||
|
|
@ -18,24 +19,25 @@ const Header = ({
|
||||||
<div className="flex min-h-[105px] w-full justify-center px-10">
|
<div className="flex min-h-[105px] w-full justify-center px-10">
|
||||||
<div className="relative flex max-w-[1680px] grow flex-col justify-end gap-y-1 border-x border-divider-accent p-6 pt-8">
|
<div className="relative flex max-w-[1680px] grow flex-col justify-end gap-y-1 border-x border-divider-accent p-6 pt-8">
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<div className="py-[5px]">
|
<div aria-hidden="true" className="py-[5px]">
|
||||||
<DifyLogo className="h-[27px] w-[60px]" />
|
<DifyLogo className="h-[27px] w-[60px]" />
|
||||||
</div>
|
</div>
|
||||||
<span
|
<DialogTitle
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-billing-plan-title-bg bg-clip-text px-1.5 text-[37px] leading-[1.2] text-transparent',
|
'bg-billing-plan-title-bg bg-clip-text px-1.5 text-[37px] leading-[1.2] text-transparent',
|
||||||
styles.instrumentSerif,
|
styles.instrumentSerif,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t('plansCommon.title.plans', { ns: 'billing' })}
|
{t('plansCommon.title.plans', { ns: 'billing' })}
|
||||||
</span>
|
</DialogTitle>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-text-tertiary system-sm-regular">
|
<DialogDescription className="text-text-tertiary system-sm-regular">
|
||||||
{t('plansCommon.title.description', { ns: 'billing' })}
|
{t('plansCommon.title.description', { ns: 'billing' })}
|
||||||
</p>
|
</DialogDescription>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="absolute bottom-[40.5px] right-[-18px] z-10 size-9 rounded-full p-2"
|
className="absolute bottom-[40.5px] right-[-18px] z-10 size-9 rounded-full p-2"
|
||||||
|
aria-label={t('operation.close', { ns: 'common' })}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" className="i-ri-close-line size-5" />
|
<span aria-hidden="true" className="i-ri-close-line size-5" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ import type { Category } from './types'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
import { Dialog, DialogContent } from '@/app/components/base/ui/dialog'
|
||||||
|
import {
|
||||||
|
ScrollAreaContent,
|
||||||
|
ScrollAreaCorner,
|
||||||
|
ScrollAreaRoot,
|
||||||
|
ScrollAreaScrollbar,
|
||||||
|
ScrollAreaThumb,
|
||||||
|
ScrollAreaViewport,
|
||||||
|
} from '@/app/components/base/ui/scroll-area'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useGetPricingPageLanguage } from '@/context/i18n'
|
import { useGetPricingPageLanguage } from '@/context/i18n'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
|
@ -19,6 +27,15 @@ type PricingProps = {
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pricingScrollAreaClassNames = {
|
||||||
|
root: 'relative h-full w-full overflow-hidden [--scroll-area-edge-hint-bg:var(--color-saas-background)]',
|
||||||
|
viewport: 'overscroll-contain',
|
||||||
|
content: 'min-h-full min-w-[1200px]',
|
||||||
|
verticalScrollbar: 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:me-1',
|
||||||
|
horizontalScrollbar: 'data-[orientation=horizontal]:mx-2 data-[orientation=horizontal]:mb-0.5',
|
||||||
|
corner: 'bg-saas-background',
|
||||||
|
} as const
|
||||||
|
|
||||||
const Pricing: FC<PricingProps> = ({
|
const Pricing: FC<PricingProps> = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -42,30 +59,46 @@ const Pricing: FC<PricingProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="inset-0 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 overflow-auto rounded-none border-none bg-saas-background p-0 shadow-none"
|
className="inset-0 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 overflow-hidden rounded-none border-none bg-saas-background p-0 shadow-none"
|
||||||
>
|
>
|
||||||
<div className="relative grid min-h-full min-w-[1200px] grid-rows-[1fr_auto_auto_1fr] overflow-hidden">
|
<ScrollAreaRoot className={pricingScrollAreaClassNames.root}>
|
||||||
<div className="absolute -top-12 left-0 right-0 -z-10">
|
<ScrollAreaViewport className={pricingScrollAreaClassNames.viewport}>
|
||||||
<NoiseTop />
|
<ScrollAreaContent className={pricingScrollAreaClassNames.content}>
|
||||||
</div>
|
<div className="relative grid min-h-full grid-rows-[1fr_auto_auto_1fr] overflow-hidden">
|
||||||
<Header onClose={onCancel} />
|
<div className="absolute -top-12 left-0 right-0 -z-10">
|
||||||
<PlanSwitcher
|
<NoiseTop />
|
||||||
currentCategory={currentCategory}
|
</div>
|
||||||
onChangeCategory={setCurrentCategory}
|
<Header onClose={onCancel} />
|
||||||
currentPlanRange={planRange}
|
<PlanSwitcher
|
||||||
onChangePlanRange={setPlanRange}
|
currentCategory={currentCategory}
|
||||||
/>
|
onChangeCategory={setCurrentCategory}
|
||||||
<Plans
|
currentPlanRange={planRange}
|
||||||
plan={plan}
|
onChangePlanRange={setPlanRange}
|
||||||
currentPlan={currentCategory}
|
/>
|
||||||
planRange={planRange}
|
<Plans
|
||||||
canPay={canPay}
|
plan={plan}
|
||||||
/>
|
currentPlan={currentCategory}
|
||||||
<Footer pricingPageURL={pricingPageURL} currentCategory={currentCategory} />
|
planRange={planRange}
|
||||||
<div className="absolute -bottom-12 left-0 right-0 -z-10">
|
canPay={canPay}
|
||||||
<NoiseBottom />
|
/>
|
||||||
</div>
|
<Footer pricingPageURL={pricingPageURL} currentCategory={currentCategory} />
|
||||||
</div>
|
<div className="absolute -bottom-12 left-0 right-0 -z-10">
|
||||||
|
<NoiseBottom />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollAreaContent>
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
<ScrollAreaScrollbar className={pricingScrollAreaClassNames.verticalScrollbar}>
|
||||||
|
<ScrollAreaThumb className="rounded-full" />
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
<ScrollAreaScrollbar
|
||||||
|
orientation="horizontal"
|
||||||
|
className={pricingScrollAreaClassNames.horizontalScrollbar}
|
||||||
|
>
|
||||||
|
<ScrollAreaThumb className="rounded-full" />
|
||||||
|
</ScrollAreaScrollbar>
|
||||||
|
<ScrollAreaCorner className={pricingScrollAreaClassNames.corner} />
|
||||||
|
</ScrollAreaRoot>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue