Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintain modal consistency with Dialog.Content component #146

Merged
merged 2 commits into from
May 23, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
Expand Down
165 changes: 99 additions & 66 deletions components/src/components/atoms/ScrollBox/ScrollBox.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
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: '<color>';
inherits: true;
initial-value: ${theme.colors.greyLight};
}

@property --top-line-color {
syntax: '<color>';
inherits: true;
initial-value: transparent;
}

@property --bottom-line-color {
syntax: '<color>';
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},
Expand Down Expand Up @@ -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<HTMLDivElement>

export const ScrollBox = ({
hideDividers = false,
alwaysShowDividers = false,
topTriggerPx = 16,
bottomTriggerPx = 16,
onReachedTop,
onReachedBottom,
horizontalPadding,
children,
...props
}: Props) => {
Expand All @@ -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 =
Expand All @@ -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?.()
Expand Down Expand Up @@ -184,18 +210,25 @@ export const ScrollBox = ({
}, [onReachedTop, onReachedBottom])

return (
<StyledScrollBox
data-bottom-line={showBottom}
data-top-line={showTop}
ref={ref}
{...props}
>
<IntersectElement data-testid="scrollbox-top-intersect" ref={topRef} />
{children}
<IntersectElement
data-testid="scrollbox-bottom-intersect"
ref={bottomRef}
<Container {...props}>
<StyledScrollBox $horizontalPadding={horizontalPadding} ref={ref}>
<IntersectElement data-testid="scrollbox-top-intersect" ref={topRef} />
{children}
<IntersectElement
data-testid="scrollbox-bottom-intersect"
ref={bottomRef}
/>
</StyledScrollBox>
<Divider
$horizontalPadding={horizontalPadding}
data-testid="scrollbox-top-line"
data-top-line={showTop}
/>
<Divider
$horizontalPadding={horizontalPadding}
data-bottom-line={showBottom}
data-testid="scrollbox-bottom-line"
/>
</StyledScrollBox>
</Container>
)
}
12 changes: 11 additions & 1 deletion components/src/components/organisms/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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']};
Expand All @@ -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']});
`)}
`,
)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -241,13 +249,14 @@ const Heading = ({
title,
subtitle,
alert,
fontVariant = 'headingFour',
}: TitleProps & StepProps & WithAlert) => {
return (
<TitleContainer>
{alert && <Icon alert={alert} />}
{title &&
((typeof title !== 'string' && title) || (
<Title fontVariant="headingFour">{title}</Title>
<Title fontVariant={fontVariant}>{title}</Title>
))}
{subtitle &&
((typeof subtitle !== 'string' && subtitle) || (
Expand Down Expand Up @@ -415,4 +424,5 @@ export const Dialog = ({
Dialog.displayName = 'Dialog'
Dialog.Footer = Footer
Dialog.Heading = Heading
Dialog.Content = DialogContent
Dialog.CloseButton = CloseButton
Loading
Loading