From 4eb1e84a4158cb64303fe26d8eaed40bf57b1d06 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Sat, 11 May 2024 00:18:35 +0800 Subject: [PATCH 1/2] add Dialog.Content component and updated scrollbox --- .../components/atoms/ScrollBox/ScrollBox.tsx | 160 ++++++++++-------- .../components/organisms/Dialog/Dialog.tsx | 12 +- .../organisms/Dialog/DialogContent.tsx | 122 +++++++++++++ components/src/tokens/space.ts | 2 +- .../reference/mdx/organisms/Dialog.docs.mdx | 134 ++++++++++++++- 5 files changed, 360 insertions(+), 70 deletions(-) create mode 100644 components/src/components/organisms/Dialog/DialogContent.tsx diff --git a/components/src/components/atoms/ScrollBox/ScrollBox.tsx b/components/src/components/atoms/ScrollBox/ScrollBox.tsx index e6f7b2bb..2401cd56 100644 --- a/components/src/components/atoms/ScrollBox/ScrollBox.tsx +++ b/components/src/components/atoms/ScrollBox/ScrollBox.tsx @@ -1,10 +1,30 @@ import * as React from 'react' import styled, { css } from 'styled-components' -const StyledScrollBox = styled.div( +import { Space } from '../../../tokens/index' + +const Container = styled.div( ({ theme }) => css` + position: relative; + border: solid ${theme.space.px} transparent; + width: 100%; + height: 100%; + border-left-width: 0; + border-right-width: 0; + `, +) + +const StyledScrollBox = styled.div<{ $horizontalPadding?: Space }>( + ({ theme, $horizontalPadding }) => css` overflow: auto; position: relative; + width: 100%; + height: 100%; + + ${$horizontalPadding && + css` + padding: 0 ${theme.space[$horizontalPadding]}; + `} @property --scrollbar { syntax: ''; @@ -12,18 +32,6 @@ const StyledScrollBox = styled.div( initial-value: ${theme.colors.greyLight}; } - @property --top-line-color { - syntax: ''; - inherits: true; - initial-value: transparent; - } - - @property --bottom-line-color { - syntax: ''; - inherits: true; - initial-value: transparent; - } - /* stylelint-disable custom-property-no-missing-var-function */ transition: --scrollbar 0.15s ease-in-out, height 0.15s ${theme.transitionTimingFunction.popIn}, @@ -57,68 +65,74 @@ const StyledScrollBox = styled.div( &:hover { --scrollbar: ${theme.colors.greyBright}; } + `, +) - &[data-top-line='true'] { - --top-line-color: ${theme.colors.greyLight}; - &::before { - z-index: 100; - } - } +const IntersectElement = styled.div( + () => css` + display: block; + height: 0px; + `, +) - &[data-bottom-line='true'] { - --bottom-line-color: ${theme.colors.greyLight}; - &::after { - z-index: 100; - } - } +const Divider = styled.div<{ $horizontalPadding?: Space }>( + ({ theme, $horizontalPadding }) => css` + position: absolute; + left: 0; + height: 1px; + width: ${theme.space.full}; + background: transparent; + transition: background-color 0.15s ease-in-out; - ::-webkit-scrollbar-track { - border-top: solid ${theme.space['px']} var(--top-line-color); - border-bottom: solid ${theme.space['px']} var(--bottom-line-color); - } + ${$horizontalPadding && + css` + left: ${theme.space[$horizontalPadding]}; + width: calc(100% - 2 * ${theme.space[$horizontalPadding]}); + `} - &::before, - &::after { - content: ''; - position: sticky; - left: 0; - width: 100%; - display: block; - height: ${theme.space.px}; + &[data-top-line] { + top: -${theme.space.px}; } - &::before { - top: 0; - background-color: var(--top-line-color); + &[data-top-line='true'] { + background: ${theme.colors.border}; } - &::after { - bottom: 0; - background-color: var(--bottom-line-color); + + &[data-bottom-line] { + bottom: -${theme.space.px}; } - `, -) -const IntersectElement = styled.div( - () => css` - display: block; - height: 0px; + &[data-bottom-line='true'] { + background: ${theme.colors.border}; + } `, ) type Props = { + /** If true, the dividers will be hidden */ hideDividers?: boolean | { top?: boolean; bottom?: boolean } + /** If true, the dividers will always be shown */ + alwaysShowDividers?: boolean | { top?: boolean; bottom?: boolean } + /** The number of pixels below the top of the content where events such as showing/hiding dividers and onReachedTop will be executed */ topTriggerPx?: number + /** The number of pixels above the bottom of the content where events such as showing/hiding dividers and onReachedTop will be executed */ bottomTriggerPx?: number + /** A callback function that is fired when the content reaches topTriggerPx */ onReachedTop?: () => void + /** A callback function that is fired when the content reaches bottomTriggerPx */ onReachedBottom?: () => void + /** The amount of horizontal padding to apply to the scrollbox. This will decrease the content area as well as the width of the overflow indicator dividers*/ + horizontalPadding?: Space } & React.HTMLAttributes export const ScrollBox = ({ hideDividers = false, + alwaysShowDividers = false, topTriggerPx = 16, bottomTriggerPx = 16, onReachedTop, onReachedBottom, + horizontalPadding, children, ...props }: Props) => { @@ -130,18 +144,26 @@ export const ScrollBox = ({ typeof hideDividers === 'boolean' ? hideDividers : !!hideDividers?.top const hideBottom = typeof hideDividers === 'boolean' ? hideDividers : !!hideDividers?.bottom + const alwaysShowTop = + typeof alwaysShowDividers === 'boolean' + ? alwaysShowDividers + : !!alwaysShowDividers?.top + const alwaysShowBottom = + typeof alwaysShowDividers === 'boolean' + ? alwaysShowDividers + : !!alwaysShowDividers?.bottom const funcRef = React.useRef<{ onReachedTop?: () => void onReachedBottom?: () => void }>({ onReachedTop, onReachedBottom }) - const [showTop, setShowTop] = React.useState(false) - const [showBottom, setShowBottom] = React.useState(false) + const [showTop, setShowTop] = React.useState(alwaysShowTop) + const [showBottom, setShowBottom] = React.useState(alwaysShowBottom) const handleIntersect: IntersectionObserverCallback = (entries) => { - const intersectingTop = [false, -1] - const intersectingBottom = [false, -1] + const intersectingTop: [boolean, number] = [false, -1] + const intersectingBottom: [boolean, number] = [false, -1] for (let i = 0; i < entries.length; i += 1) { const entry = entries[i] const iref = @@ -151,9 +173,13 @@ export const ScrollBox = ({ iref[1] = entry.time } } - intersectingTop[1] !== -1 && !hideTop && setShowTop(!intersectingTop[0]) + intersectingTop[1] !== -1 && + !hideTop && + !alwaysShowTop && + setShowTop(!intersectingTop[0]) intersectingBottom[1] !== -1 && !hideBottom && + !alwaysShowBottom && setShowBottom(!intersectingBottom[0]) intersectingTop[0] && funcRef.current.onReachedTop?.() intersectingBottom[0] && funcRef.current.onReachedBottom?.() @@ -184,18 +210,20 @@ export const ScrollBox = ({ }, [onReachedTop, onReachedBottom]) return ( - - - {children} - + + + {children} + + + + - + ) } diff --git a/components/src/components/organisms/Dialog/Dialog.tsx b/components/src/components/organisms/Dialog/Dialog.tsx index 3c517588..9ab04820 100644 --- a/components/src/components/organisms/Dialog/Dialog.tsx +++ b/components/src/components/organisms/Dialog/Dialog.tsx @@ -6,7 +6,10 @@ import { mq } from '@/src/utils/responsiveHelpers' import { WithAlert } from '@/src/types' +import { FontSize } from '@/src/tokens/typography' + import { Modal, Typography } from '../..' +import { DialogContent } from './DialogContent' const IconCloseContainer = styled.button( ({ theme }) => css` @@ -44,6 +47,7 @@ const StyledCard = styled.div( display: flex; flex-direction: column; align-items: center; + overflow: hidden; gap: ${theme.space['4']}; padding: ${theme.space['4']}; border-radius: ${theme.radii['3xLarge']}; @@ -52,12 +56,15 @@ const StyledCard = styled.div( background-color: ${theme.colors.background}; position: relative; width: 100%; + max-height: 80vh; + ${mq.sm.min(css` min-width: ${theme.space['64']}; max-width: 80vw; border-radius: ${theme.radii['3xLarge']}; padding: ${theme.space['6']}; gap: ${theme.space['6']}; + max-height: min(90vh, ${theme.space['144']}); `)} `, ) @@ -201,6 +208,7 @@ const StepItem = styled.div<{ $type: StepType }>( type TitleProps = { title?: string | React.ReactNode subtitle?: string | React.ReactNode + fontVariant?: FontSize } & WithAlert type StepProps = { @@ -241,13 +249,14 @@ const Heading = ({ title, subtitle, alert, + fontVariant = 'headingFour', }: TitleProps & StepProps & WithAlert) => { return ( {alert && } {title && ((typeof title !== 'string' && title) || ( - {title} + {title} ))} {subtitle && ((typeof subtitle !== 'string' && subtitle) || ( @@ -415,4 +424,5 @@ export const Dialog = ({ Dialog.displayName = 'Dialog' Dialog.Footer = Footer Dialog.Heading = Heading +Dialog.Content = DialogContent Dialog.CloseButton = CloseButton diff --git a/components/src/components/organisms/Dialog/DialogContent.tsx b/components/src/components/organisms/Dialog/DialogContent.tsx new file mode 100644 index 00000000..ab5cfa9f --- /dev/null +++ b/components/src/components/organisms/Dialog/DialogContent.tsx @@ -0,0 +1,122 @@ +import * as React from 'react' +import styled, { css } from 'styled-components' + +import { mq } from '@/src/utils/responsiveHelpers' + +import { Space } from '@/src/tokens' + +import { ScrollBox } from '../..' + +type NativeFromProps = React.FormHTMLAttributes + +type BaseProps = React.ComponentProps & { + as?: 'form' + gap?: Space + fullWidth?: boolean +} + +type WithForm = { + as: 'form' + target?: NativeFromProps['target'] + action?: NativeFromProps['action'] + method?: NativeFromProps['method'] + onSubmit?: NativeFromProps['onSubmit'] +} + +type WithoutForm = { + as?: never + target?: never + onSubmit?: never + method?: never + action?: never +} + +type DialogContentProps = BaseProps & (WithForm | WithoutForm) + +const Container = styled.div<{ + $fullWidth?: boolean + $horizontalPadding: Space +}>( + ({ theme, $fullWidth, $horizontalPadding }) => css` + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: ${theme.space['4']}; + max-height: 60vh; + max-width: 100vw; + overflow: hidden; + + width: calc(100% + 2 * ${theme.space[$horizontalPadding]}); + margin: 0 -${theme.space[$horizontalPadding]}; + + ${$fullWidth && + css` + width: calc(100% + 2 * ${theme.space['4']}); + margin: 0 -${theme.space['4']}; + `} + + ${mq.sm.min(css` + width: calc( + 80vw - 2 * ${theme.space['6']} + 2 * ${theme.space[$horizontalPadding]} + ); + max-width: calc( + ${theme.space['128']} + 2 * ${theme.space[$horizontalPadding]} + ); + + ${$fullWidth && + css` + width: 80vw; + margin: 0 -${theme.space['6']}; + max-width: calc(${theme.space['128']} + 2 * ${theme.space['6']}); + `} + `)} + `, +) + +const ScrollBoxContent = styled.div<{ $gap: Space }>( + ({ theme, $gap }) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: ${theme.space.full}; + gap: ${theme.space[$gap]}; + `, +) + +export const DialogContent = React.forwardRef< + HTMLFormElement, + React.PropsWithChildren +>( + ( + { + as: asProp, + target, + method, + action, + onSubmit, + gap = '4', + fullWidth, + horizontalPadding = '2', + children, + ...props + }: React.PropsWithChildren, + ref, + ) => { + return ( + + + {children} + + + ) + }, +) diff --git a/components/src/tokens/space.ts b/components/src/tokens/space.ts index e9993c24..1bb11514 100644 --- a/components/src/tokens/space.ts +++ b/components/src/tokens/space.ts @@ -1,5 +1,5 @@ export const space = { - '0': '0', + '0': '0rem', px: '1px', '0.25': '0.0625rem', '0.5': '0.125rem', diff --git a/docs/src/reference/mdx/organisms/Dialog.docs.mdx b/docs/src/reference/mdx/organisms/Dialog.docs.mdx index a68ab1d6..59222170 100644 --- a/docs/src/reference/mdx/organisms/Dialog.docs.mdx +++ b/docs/src/reference/mdx/organisms/Dialog.docs.mdx @@ -218,11 +218,141 @@ import { Dialog } from '@ensdomains/thorin' onDismiss={() => toggleState('dialog-components')} > -
Content
+ + +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque cursus + posuere lacinia. Proin mollis nisl a lacus mollis, a volutpat tortor + consectetur. Vivamus mattis augue eu nulla molestie, a euismod nunc + placerat. Fusce vitae elit sed dolor auctor faucibus luctus condimentum + risus. Aenean pharetra mauris sodales ligula sodales, a tristique magna + rhoncus. Fusce sit amet arcu suscipit, molestie dolor nec, blandit ipsum. + Sed sodales blandit elit eget ultrices. Aliquam non blandit lacus. Etiam + eget tellus vitae risus vestibulum pulvinar at a nibh. Orci varius natoque + penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam + volutpat elit odio. Fusce placerat nibh nec ante aliquam, id dictum turpis + tempor. Sed maximus quam ut ipsum blandit, vel lobortis neque venenatis. + Maecenas dignissim massa nec risus vestibulum bibendum. Duis malesuada, est + dapibus porttitor aliquet, massa arcu fermentum nunc, nec feugiat erat magna + viverra orci. Suspendisse eu ultrices mauris. Aliquam scelerisque mollis + orci id volutpat. Nunc ac vestibulum odio, vitae feugiat ligula. Nunc + lacinia nisi in gravida volutpat. Vivamus dui ex, ornare ut ante vel, + dapibus tempor est. Proin congue sapien non dolor hendrerit, in dignissim + risus scelerisque. Pellentesque quis egestas ex. Suspendisse quis diam nec + dui sagittis sollicitudin. In hac habitasse platea dictumst. Vestibulum + vestibulum dignissim tincidunt. Donec congue nibh lectus, nec facilisis + tortor tempus sit amet. Phasellus varius mattis metus porta posuere. Integer + posuere dapibus libero. Maecenas euismod sem sed blandit consectetur.
+
Leading} trailing={} /> - + + ``` + +### Dialog.Content + +The Dialog.Content component is a ScrollBox component that is used to contain the content of the Dialog. It is also responsible for setting the responsive sizing of the dialog. It can be used to display a form element by setting the as prop to "form". The fullWidth prop can be used to make the content area the width of the dialog. + +#### Props + +This component inherits the props of [ScrollBox](/components/atoms/ScrollBox/). The additional unique props are listed below. + + + +#### as + +```tsx live=true +<> + + toggleState('dialog-components')} + > + + alert('submitted')}> + Click this button to simulate an submit + + + Leading} + trailing={} + /> + + + +``` + +#### fullWidth + +If true, the width of the scrollbox will be the width of the dialog. By default the content area will have a horizontal padding of 8px to cover the overflow from inputs. To achieve true edge to edge content, set horizontalPadding to "0". + +```tsx live=true +<> + + toggleState('dialog-components')} + > + + +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque cursus + posuere lacinia. Proin mollis nisl a lacus mollis, a volutpat tortor + consectetur. Vivamus mattis augue eu nulla molestie, a euismod nunc + placerat. Fusce vitae elit sed dolor auctor faucibus luctus condimentum + risus. Aenean pharetra mauris sodales ligula sodales, a tristique magna + rhoncus. Fusce sit amet arcu suscipit, molestie dolor nec, blandit ipsum. + Sed sodales blandit elit eget ultrices. Aliquam non blandit lacus. Etiam + eget tellus vitae risus vestibulum pulvinar at a nibh. Orci varius natoque + penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam + volutpat elit odio. Fusce placerat nibh nec ante aliquam, id dictum turpis + tempor. Sed maximus quam ut ipsum blandit, vel lobortis neque venenatis. + Maecenas dignissim massa nec risus vestibulum bibendum. Duis malesuada, est + dapibus porttitor aliquet, massa arcu fermentum nunc, nec feugiat erat magna + viverra orci. Suspendisse eu ultrices mauris. Aliquam scelerisque mollis + orci id volutpat. Nunc ac vestibulum odio, vitae feugiat ligula. Nunc + lacinia nisi in gravida volutpat. Vivamus dui ex, ornare ut ante vel, + dapibus tempor est. Proin congue sapien non dolor hendrerit, in dignissim + risus scelerisque. Pellentesque quis egestas ex. Suspendisse quis diam nec + dui sagittis sollicitudin. In hac habitasse platea dictumst. Vestibulum + vestibulum dignissim tincidunt. Donec congue nibh lectus, nec facilisis + tortor tempus sit amet. Phasellus varius mattis metus porta posuere. Integer + posuere dapibus libero. Maecenas euismod sem sed blandit consectetur.
+
+ Leading} + trailing={} + /> + + + +``` \ No newline at end of file From ea101845d19befc44a68d5e64e9c8fd7c8ecdd4f Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 15 May 2024 18:03:07 +0800 Subject: [PATCH 2/2] fix scrollbox unit test --- .../src/components/atoms/ScrollBox/ScrollBox.test.tsx | 2 +- components/src/components/atoms/ScrollBox/ScrollBox.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/components/src/components/atoms/ScrollBox/ScrollBox.test.tsx b/components/src/components/atoms/ScrollBox/ScrollBox.test.tsx index a5eccb2f..f04e88e2 100644 --- a/components/src/components/atoms/ScrollBox/ScrollBox.test.tsx +++ b/components/src/components/atoms/ScrollBox/ScrollBox.test.tsx @@ -44,7 +44,7 @@ const mockIntersectionObserver = makeMockIntersectionObserver( ) const expectLine = (e: 'top' | 'bottom', visible: boolean) => - expect(screen.getByTestId('scroll-box')).toHaveAttribute( + expect(screen.getByTestId(`scrollbox-${e}-line`)).toHaveAttribute( `data-${e}-line`, visible ? 'true' : 'false', ) diff --git a/components/src/components/atoms/ScrollBox/ScrollBox.tsx b/components/src/components/atoms/ScrollBox/ScrollBox.tsx index 2401cd56..58196b8c 100644 --- a/components/src/components/atoms/ScrollBox/ScrollBox.tsx +++ b/components/src/components/atoms/ScrollBox/ScrollBox.tsx @@ -219,10 +219,15 @@ export const ScrollBox = ({ ref={bottomRef} /> - + )