* Add ellipsis icon * Reduce size of tokens * Move UpdateToast to toasts folder * Update variables.scss * Add reps for grid objects These reps act like the existing PartyRep except for Characters and Summons, as well as a new component just for Weapons. They only render the grid of objects and nothing else. Eventually PartyRep will use WeaponRep * Added RepSegment This is a Character, Weapon or Summon rep wrapped with an input and label for use in a SegmentedControl * Modify PartySegmentedControl to use RepSegments This will not work on mobile yet, where it should gracefully degrade to a normal SegmentedControl with only text * Extract URL copied and Remixed toasts into files * Extract delete team alert into a file Also, to support this: * Added `Destructive` class to Button * Added `primaryActionClassName` prop to Alert * Added an alert for when remixing teams * Began refactoring PartyDetails into several files * PartyHeader will live at the top, above the new segmented control * PartyDetails stays below, only showing remixed teams and the description * PartyDropdown handles the new ... menu * Remove duplicated code This is description and remix code that is still in `PartyDetails` * Small fixes for weapon grid * Add placeholder image for guidebooks * Add localizations * Add Guidebook type and update other types * Update gitignore Don't commit guidebook images * Indicate if a dialog is scrollable We had broken paging in the infinite scroll component. Turning off "scrolling" at the dialog levels fixes it without adding scrollbars in environments that persistently show them * Add ExtraContainer This is the purple container that will contain additional weapons and sephira guidebooks * Move ExtraWeapons to ExtraWeaponsGrid And put it in ExtraContainer * Added GuidebooksGrid and GuidebookUnit These are the display components for Guidebooks in the WeaponGrid * Visual adjustments to summon grid * Add Empty class to weapons when unit is unfilled * Implement GuidebooksGrid in WeaponGrid * Remove extra switch * Remove old dependencies and props * Implement searching for/adding guidebooks to party * Update styles * Fix dependency * Properly determine when extra container should display * Change to 1-indexing for guidebooks * Add support for removing guidebooks * Display guidebook validation error * Move read only buttons to PartyHeader Also broke up tokens and made them easier to render * Add guidebooks to DetailsObject * Remove preview when on mobile sizes
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
import React, { useEffect } from 'react'
|
|
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
import classNames from 'classnames'
|
|
import debounce from 'lodash.debounce'
|
|
|
|
import Overlay from '~components/common/Overlay'
|
|
import './index.scss'
|
|
|
|
interface Props
|
|
extends React.DetailedHTMLProps<
|
|
React.DialogHTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
> {
|
|
headerref?: React.RefObject<HTMLDivElement>
|
|
footerref?: React.RefObject<HTMLDivElement>
|
|
scrollable?: boolean
|
|
onEscapeKeyDown: (event: KeyboardEvent) => void
|
|
onOpenAutoFocus: (event: Event) => void
|
|
}
|
|
|
|
const DialogContent = React.forwardRef<HTMLDivElement, Props>(function Dialog(
|
|
{ scrollable, children, ...props },
|
|
forwardedRef
|
|
) {
|
|
// Classes
|
|
const classes = classNames(props.className, {
|
|
DialogContent: true,
|
|
})
|
|
|
|
// Handlers
|
|
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
|
|
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 maxValue = 50
|
|
|
|
if (scrollTop >= 0) {
|
|
const input = scrollTop > maxValue ? maxValue : scrollTop
|
|
|
|
const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16)
|
|
const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24)
|
|
|
|
header.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
|
|
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(
|
|
value: number,
|
|
low1: number,
|
|
high1: number,
|
|
low2: number,
|
|
high2: number
|
|
) {
|
|
return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1)
|
|
}
|
|
|
|
return (
|
|
<DialogPrimitive.Portal>
|
|
<dialog className="Dialog">
|
|
<DialogPrimitive.Content
|
|
{...props}
|
|
className={classes}
|
|
onOpenAutoFocus={props.onOpenAutoFocus}
|
|
onEscapeKeyDown={props.onEscapeKeyDown}
|
|
ref={forwardedRef}
|
|
>
|
|
<div
|
|
className={classNames({
|
|
Container: true,
|
|
Scrollable: scrollable,
|
|
})}
|
|
onScroll={handleScroll}
|
|
>
|
|
{children}
|
|
</div>
|
|
</DialogPrimitive.Content>
|
|
</dialog>
|
|
<Overlay visible={true} open={true} />
|
|
</DialogPrimitive.Portal>
|
|
)
|
|
})
|
|
|
|
DialogContent.defaultProps = {
|
|
scrollable: true,
|
|
}
|
|
|
|
export default DialogContent
|