Fix shadows for static and mod modals

This commit is contained in:
Justin Edmund 2023-01-21 06:04:40 -08:00
parent c9315ab397
commit 7ad9032e86
12 changed files with 139 additions and 75 deletions

View file

@ -2,7 +2,7 @@
gap: 0; gap: 0;
padding-bottom: $unit; padding-bottom: $unit;
& > div:not(.DialogHeader) { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit-2x; gap: $unit-2x;

View file

@ -19,6 +19,7 @@ import './index.scss'
const AboutModal = () => { const AboutModal = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const headerRef = React.createRef<HTMLDivElement>()
return ( return (
<Dialog> <Dialog>
@ -29,11 +30,20 @@ const AboutModal = () => {
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="About" className="About"
title={t('menu.about')} headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}} onEscapeKeyDown={() => {}}
> >
<div className="sections"> <div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">{t('menu.about')}</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="content">
<section> <section>
<p> <p>
Granblue.team is a tool to save and share team comps for{' '} Granblue.team is a tool to save and share team comps for{' '}

View file

@ -85,6 +85,9 @@ const AccountModal = (props: Props) => {
const [languageOpen, setLanguageOpen] = useState(false) const [languageOpen, setLanguageOpen] = useState(false)
const [themeOpen, setThemeOpen] = useState(false) const [themeOpen, setThemeOpen] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
// UI management // UI management
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
@ -286,10 +289,11 @@ const AccountModal = (props: Props) => {
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="Account" className="Account"
headerref={headerRef}
onOpenAutoFocus={(event: Event) => {}} onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
> >
<div className="DialogHeader"> <div className="DialogHeader" ref={headerRef}>
<div className="DialogTop"> <div className="DialogTop">
<DialogTitle className="SubTitle"> <DialogTitle className="SubTitle">
{t('modals.settings.title')} {t('modals.settings.title')}

View file

@ -1,7 +1,7 @@
.Changelog.DialogContent { .Changelog.DialogContent {
gap: 0; gap: 0;
& > div:not(.DialogHeader) { .updates {
padding: 0 $unit-4x; padding: 0 $unit-4x;
} }

View file

@ -16,6 +16,7 @@ import './index.scss'
const ChangelogModal = () => { const ChangelogModal = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const headerRef = React.createRef<HTMLDivElement>()
return ( return (
<Dialog> <Dialog>
@ -27,9 +28,21 @@ const ChangelogModal = () => {
<DialogContent <DialogContent
className="Changelog" className="Changelog"
title={t('menu.changelog')} title={t('menu.changelog')}
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}} onEscapeKeyDown={() => {}}
> >
<div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">
{t('menu.changelog')}
</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="updates"> <div className="updates">
<section className="version" data-version="1.0"> <section className="version" data-version="1.0">
<div className="top"> <div className="top">

View file

@ -35,7 +35,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit-4x; gap: $unit-4x;
padding: 0 $unit-4x; padding: 0 $unit-4x $unit-2x;
section { section {
display: flex; display: flex;

View file

@ -81,45 +81,23 @@ const CharacterModal = ({
// UI state // UI state
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Classes // Classes
const headerClasses = classNames({ const headerClasses = classNames({
DialogHeader: true, DialogHeader: true,
Short: true, Short: true,
Scrolled: scrolled,
}) })
// Callbacks and Hooks // Callbacks and Hooks
const onScroll = useCallback((event: Event) => {
// const dialogContent = event.target as HTMLDivElement
// const { scrollTop } = dialogContent
// if (scrollTop > 150) {
// console.log(scrollTop, scrollTop % 5)
// if (scrollTop > 20) setScrolled(true)
// else setScrolled(false)
// console.log('scrollTop', scrollTop)
// }
}, [])
useEffect(() => { useEffect(() => {
setOpen(modalOpen) setOpen(modalOpen)
}, [modalOpen]) }, [modalOpen])
useEffect(() => {
//add eventlistener to window
// const dialogContent = document.querySelector('.DialogContent')
// if (dialogContent) {
// dialogContent.addEventListener('scroll', onScroll, { passive: true })
// // remove event on unmount to prevent a memory leak with the cleanup
// return () => {
// // what does passive do?
// dialogContent.removeEventListener('scroll', onScroll)
// }
// }
}, [])
// Character properties: Perpetuity // Character properties: Perpetuity
const [perpetuity, setPerpetuity] = useState(false) const [perpetuity, setPerpetuity] = useState(false)
@ -309,10 +287,12 @@ const CharacterModal = ({
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="Character" className="Character"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}} onEscapeKeyDown={() => {}}
> >
<div className={headerClasses}> <div className={headerClasses} ref={headerRef}>
<img <img
alt={gridCharacter.object.name[locale]} alt={gridCharacter.object.name[locale]}
className="DialogImage" className="DialogImage"
@ -339,7 +319,7 @@ const CharacterModal = ({
{earringSelect()} {earringSelect()}
{awakeningSelect()} {awakeningSelect()}
</div> </div>
<div className="DialogFooter"> <div className="DialogFooter" ref={footerRef}>
<Button <Button
contained={true} contained={true}
onClick={updateCharacter} onClick={updateCharacter}

View file

@ -143,6 +143,8 @@
align-items: flex-end; align-items: flex-end;
background: var(--dialog-bg); background: var(--dialog-bg);
bottom: 0; bottom: 0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.16);
border-top: 1px solid rgba(0, 0, 0, 0.24);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x; padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x;
@ -204,7 +206,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit * 2; gap: $unit-2x;
} }
.wrapper { .wrapper {

View file

@ -2,19 +2,17 @@ import React, { useEffect } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog' import * as DialogPrimitive from '@radix-ui/react-dialog'
import classNames from 'classnames' import classNames from 'classnames'
import { DialogClose, DialogTitle } from '~components/Dialog'
import Overlay from '~components/Overlay' import Overlay from '~components/Overlay'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
import debounce from 'lodash.debounce'
interface Props interface Props
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>, React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> { > {
header?: React.ReactNode headerref: React.RefObject<HTMLDivElement>
title?: string footerref?: React.RefObject<HTMLDivElement>
onEscapeKeyDown: (event: KeyboardEvent) => void onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void onOpenAutoFocus: (event: Event) => void
} }
@ -28,45 +26,91 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
DialogContent: true, DialogContent: true,
}) })
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const containerRef = React.createRef<HTMLDivElement>()
// Elements
const genericHeader = (
<div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">
{props.title ? props.title : ''}
</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
)
// Handlers // Handlers
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) { function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const headerElement = headerRef.current const scrollTop = event.currentTarget.scrollTop
const scrollTop = event.currentTarget?.scrollTop const scrollHeight = event.currentTarget.scrollHeight
const clientHeight = event.currentTarget.clientHeight
if (props.headerref && props.headerref.current)
manipulateHeaderShadow(props.headerref.current, scrollTop)
if (props.footerref && props.footerref.current)
manipulateFooterShadow(
props.footerref.current,
scrollTop,
scrollHeight,
clientHeight
)
}
function manipulateHeaderShadow(header: HTMLDivElement, scrollTop: number) {
const boxShadowBase = '0 2px 8px' const boxShadowBase = '0 2px 8px'
const maxValue = 50 const maxValue = 50
if (headerElement && scrollTop >= 0) { if (scrollTop >= 0) {
const input = scrollTop > maxValue ? maxValue : scrollTop const input = scrollTop > maxValue ? maxValue : scrollTop
const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16) const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16)
const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24) const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24)
console.log(
`Scroll top: ${scrollTop}, interpolated opacity: ${boxShadowOpacity}`
)
headerElement.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})` header.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
headerElement.style.borderBottomColor = `rgba(0, 0, 0, ${borderOpacity})` header.style.borderBottomColor = `rgba(0, 0, 0, ${borderOpacity})`
} }
} }
function manipulateFooterShadow(
footer: HTMLDivElement,
scrollTop: number,
scrollHeight: number,
clientHeight: number
) {
const boxShadowBase = '0 -2px 8px'
const minValue = scrollHeight - 200
const currentScroll = scrollTop + clientHeight
if (currentScroll >= minValue) {
const input = currentScroll < minValue ? minValue : currentScroll
const boxShadowOpacity = mapRange(
input,
minValue,
scrollHeight,
0.16,
0.0
)
const borderOpacity = mapRange(input, minValue, scrollHeight, 0.24, 0.0)
footer.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
footer.style.borderTopColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
const calculateFooterShadow = debounce(() => {
const boxShadowBase = '0 -2px 8px'
const scrollable = document.querySelector('.Scrollable')
const footer = props.footerref
if (footer && footer.current) {
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0)`
} else {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0.16)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0.24)`
}
}
}, 100)
useEffect(() => {
window.addEventListener('resize', calculateFooterShadow)
calculateFooterShadow()
return () => {
window.removeEventListener('resize', calculateFooterShadow)
}
}, [calculateFooterShadow])
function mapRange( function mapRange(
value: number, value: number,
low1: number, low1: number,
@ -87,12 +131,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, Props>(function dialog(
onEscapeKeyDown={props.onEscapeKeyDown} onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef} ref={forwardedRef}
> >
{props.title ? genericHeader : ''} <div className="Scrollable" onScroll={handleScroll}>
<div
className="Scrollable"
ref={containerRef}
onScroll={handleScroll}
>
{children} {children}
</div> </div>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View file

@ -24,7 +24,7 @@
} }
} }
& > div:not(.DialogHeader) { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit-2x; gap: $unit-2x;

View file

@ -17,6 +17,7 @@ import './index.scss'
const RoadmapModal = () => { const RoadmapModal = () => {
const { t } = useTranslation('roadmap') const { t } = useTranslation('roadmap')
const headerRef = React.createRef<HTMLDivElement>()
return ( return (
<Dialog> <Dialog>
@ -28,10 +29,20 @@ const RoadmapModal = () => {
<DialogContent <DialogContent
className="Roadmap" className="Roadmap"
title={t('title')} title={t('title')}
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => {}} onEscapeKeyDown={() => {}}
> >
<div> <div className="DialogHeader" ref={headerRef}>
<DialogTitle className="DialogTitle">{t('title')}</DialogTitle>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="content">
<section className="notes"> <section className="notes">
<p>{t('blurb')}</p> <p>{t('blurb')}</p>
<p>{t('link.intro')}</p> <p>{t('link.intro')}</p>

View file

@ -93,6 +93,10 @@ const WeaponModal = ({
const [ax2Open, setAx2Open] = useState(false) const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false) const [awakeningOpen, setAwakeningOpen] = useState(false)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
useEffect(() => { useEffect(() => {
setOpen(modalOpen) setOpen(modalOpen)
}, [modalOpen]) }, [modalOpen])
@ -350,10 +354,11 @@ const WeaponModal = ({
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="Weapon" className="Weapon"
headerref={headerRef}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
> >
<div className="DialogHeader Short"> <div className="DialogHeader Short" ref={headerRef}>
<img <img
alt={gridWeapon.object.name[locale]} alt={gridWeapon.object.name[locale]}
className="DialogImage" className="DialogImage"