Fix being able to open multiple selects at once

This was miserable
This commit is contained in:
Justin Edmund 2022-12-26 03:34:22 -08:00
parent 4c73ebf294
commit 45f1bd291e
11 changed files with 215 additions and 76 deletions

View file

@ -80,6 +80,10 @@ const AccountModal = (props: Props) => {
// const [privateProfile, setPrivateProfile] = useState(false)
// Setup
const [pictureOpen, setPictureOpen] = useState(false)
const [genderOpen, setGenderOpen] = useState(false)
const [languageOpen, setLanguageOpen] = useState(false)
const [themeOpen, setThemeOpen] = useState(false)
// UI management
function openChange(open: boolean) {
@ -87,16 +91,10 @@ const AccountModal = (props: Props) => {
}
function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') {
const stateVars = selectOpenState
Object.keys(stateVars).forEach((key) => {
if (key === name) {
stateVars[name] = true
} else {
stateVars[key] = false
}
})
setSelectOpenState(stateVars)
setPictureOpen(name === 'picture' ? !pictureOpen : false)
setGenderOpen(name === 'gender' ? !genderOpen : false)
setLanguageOpen(name === 'language' ? !languageOpen : false)
setThemeOpen(name === 'theme' ? !themeOpen : false)
}
// Event handlers
@ -117,6 +115,14 @@ const AccountModal = (props: Props) => {
setAppTheme(value)
}
function onEscapeKeyDown(event: KeyboardEvent) {
if (pictureOpen || genderOpen || languageOpen || themeOpen) {
return event.preventDefault()
} else {
setOpen(false)
}
}
// API calls
function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
@ -189,9 +195,10 @@ const AccountModal = (props: Props) => {
description={t('modals.settings.descriptions.picture')}
className="Image"
label={t('modals.settings.labels.picture')}
open={selectOpenState.picture}
onClick={() => openSelect('picture')}
open={pictureOpen}
onOpenChange={() => openSelect('picture')}
onChange={handlePictureChange}
onClose={() => setPictureOpen(false)}
imageAlt={t('modals.settings.labels.image_alt')}
imageClass={pictureData.find((i) => i.filename === picture)?.element}
imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]}
@ -206,9 +213,10 @@ const AccountModal = (props: Props) => {
name="gender"
description={t('modals.settings.descriptions.gender')}
label={t('modals.settings.labels.gender')}
open={selectOpenState.gender}
onClick={() => openSelect('gender')}
open={genderOpen}
onOpenChange={() => openSelect('gender')}
onChange={handleGenderChange}
onClose={() => setGenderOpen(false)}
value={`${gender}`}
>
<SelectItem key="gran" value="0">
@ -224,9 +232,10 @@ const AccountModal = (props: Props) => {
<SelectTableField
name="language"
label={t('modals.settings.labels.language')}
open={selectOpenState.language}
onClick={() => openSelect('language')}
open={languageOpen}
onOpenChange={() => openSelect('language')}
onChange={handleLanguageChange}
onClose={() => setLanguageOpen(false)}
value={language}
>
<SelectItem key="en" value="en">
@ -242,9 +251,10 @@ const AccountModal = (props: Props) => {
<SelectTableField
name="theme"
label={t('modals.settings.labels.theme')}
open={selectOpenState.theme}
onClick={() => openSelect('theme')}
open={themeOpen}
onOpenChange={() => openSelect('theme')}
onChange={handleThemeChange}
onClose={() => setThemeOpen(false)}
value={theme}
>
<SelectItem key="system" value="system">
@ -274,7 +284,11 @@ const AccountModal = (props: Props) => {
<span>{t('menu.settings')}</span>
</li>
</DialogTrigger>
<DialogContent className="Account Dialog">
<DialogContent
className="Account Dialog"
onOpenAutoFocus={(event: Event) => {}}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader">
<div className="DialogTop">
<DialogTitle className="SubTitle">

View file

@ -17,6 +17,7 @@ interface Props {
object: 'character' | 'weapon'
awakeningType?: number
awakeningLevel?: number
onOpenChange: (open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (type: number, level: number) => void
}
@ -81,6 +82,11 @@ const AwakeningSelect = (props: Props) => {
// Classes
function changeOpen() {
setOpen(!open)
props.onOpenChange(!open)
}
function onClose() {
props.onOpenChange(false)
}
function generateOptions(object: 'character' | 'weapon') {
@ -165,7 +171,8 @@ const AwakeningSelect = (props: Props) => {
value={`${awakeningType}`}
open={open}
onValueChange={handleSelectChange}
onClick={() => changeOpen()}
onOpenChange={() => changeOpen()}
onClose={onClose}
triggerClass="modal"
>
{generateOptions(props.object)}

View file

@ -20,6 +20,7 @@ interface ErrorMap {
interface Props {
axType: number
currentSkills?: SimpleAxSkill[]
onOpenChange: (index: 1 | 2, open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (
primaryAxModifier: number,
@ -193,9 +194,22 @@ const AXSelect = (props: Props) => {
: undefined
}
function openSelect(ref: ForwardedRef<HTMLButtonElement>) {
if (ref === primaryAxModifierSelect) setOpenAX1(!openAX1)
if (ref === secondaryAxModifierSelect) setOpenAX2(!openAX2)
function openSelect(index: 1 | 2) {
if (index === 1) {
setOpenAX1(!openAX1)
setOpenAX2(false)
props.onOpenChange(1, !openAX1)
props.onOpenChange(2, false)
} else if (index === 2) {
setOpenAX2(!openAX2)
setOpenAX1(false)
props.onOpenChange(2, !openAX2)
props.onOpenChange(1, false)
}
}
function onClose(index: 1 | 2) {
props.onOpenChange(index, false)
}
function generateOptions(modifierSet: number) {
@ -392,8 +406,9 @@ const AXSelect = (props: Props) => {
key="ax1"
value={`${primaryAxModifier}`}
open={openAX1}
onClose={() => onClose(1)}
onOpenChange={() => openSelect(1)}
onValueChange={handleAX1SelectChange}
onClick={() => openSelect(primaryAxModifierSelect)}
triggerClass="modal"
>
{generateOptions(0)}
@ -419,8 +434,9 @@ const AXSelect = (props: Props) => {
key="ax2"
value={`${secondaryAxModifier}`}
open={openAX2}
onClose={() => onClose(2)}
onOpenChange={() => openSelect(2)}
onValueChange={handleAX2SelectChange}
onClick={() => openSelect(secondaryAxModifierSelect)}
triggerClass="modal"
ref={secondaryAxModifierSelect}
>

View file

@ -8,7 +8,10 @@ interface Props
extends React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDivElement>,
HTMLDivElement
> {}
> {
onEscapeKeyDown: (event: KeyboardEvent) => void
onOpenAutoFocus: (event: Event) => void
}
export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
function dialog({ children, ...props }, forwardedRef) {
@ -25,6 +28,8 @@ export const DialogContent = React.forwardRef<HTMLDivElement, Props>(
<DialogPrimitive.Content
className={classes}
{...props}
onOpenAutoFocus={props.onOpenAutoFocus}
onEscapeKeyDown={props.onEscapeKeyDown}
ref={forwardedRef}
>
{children}

View file

@ -60,12 +60,18 @@ const FilterBar = (props: Props) => {
props.onFilter({ raidSlug: slug })
}
function onSelectChange(name: 'element' | 'recency') {
setElementOpen(name === 'element' ? !elementOpen : false)
setRecencyOpen(name === 'recency' ? !recencyOpen : false)
}
return (
<div className={classes}>
{props.children}
<Select
value={`${props.element}`}
open={elementOpen}
onOpenChange={() => onSelectChange('element')}
onValueChange={elementSelectChanged}
onClick={openElementSelect}
>
@ -106,6 +112,7 @@ const FilterBar = (props: Props) => {
value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen}
onOpenChange={() => onSelectChange('recency')}
onValueChange={recencySelectChanged}
onClick={openRecencySelect}
>

View file

@ -106,6 +106,7 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
placeholder={'Select a class...'}
open={open}
onClick={openJobSelect}
onOpenChange={() => setOpen(!open)}
onValueChange={handleChange}
triggerClass="Job"
>

View file

@ -123,6 +123,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
value={props.currentRaid}
placeholder={'Select a raid...'}
open={open}
onOpenChange={() => setOpen(!open)}
onClick={openRaidSelect}
onValueChange={handleChange}
>

View file

@ -15,8 +15,9 @@ interface Props
open: boolean
trigger?: React.ReactNode
children?: React.ReactNode
onClick?: () => void
onOpenChange?: () => void
onValueChange?: (value: string) => void
onClose?: () => void
triggerClass?: string
}
@ -24,8 +25,13 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
props: Props,
forwardedRef
) {
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
useEffect(() => {
setOpen(props.open)
}, [props.open])
useEffect(() => {
if (props.value && props.value !== '') setValue(`${props.value}`)
else setValue('')
@ -36,10 +42,27 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
if (props.onValueChange) props.onValueChange(newValue)
}
function onCloseAutoFocus() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onEscapeKeyDown() {
setOpen(false)
if (props.onClose) props.onClose()
}
function onPointerDownOutside() {
setOpen(false)
if (props.onClose) props.onClose()
}
return (
<RadixSelect.Root
open={open}
value={value !== '' ? value : undefined}
onValueChange={onValueChange}
onOpenChange={props.onOpenChange}
>
<RadixSelect.Trigger
className={classNames('SelectTrigger', props.triggerClass)}
@ -53,7 +76,11 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
</RadixSelect.Trigger>
<RadixSelect.Portal className="Select">
<RadixSelect.Content>
<RadixSelect.Content
onCloseAutoFocus={onCloseAutoFocus}
onEscapeKeyDown={onEscapeKeyDown}
onPointerDownOutside={onPointerDownOutside}
>
<RadixSelect.ScrollUpButton className="Scroll Up">
<ArrowIcon />
</RadixSelect.ScrollUpButton>

View file

@ -15,8 +15,9 @@ interface Props {
imageClass?: string
imageSrc?: string[]
children: React.ReactNode
onClick: () => void
onOpenChange: () => void
onChange: (value: string) => void
onClose: () => void
}
const SelectTableField = (props: Props) => {
@ -53,8 +54,9 @@ const SelectTableField = (props: Props) => {
<Select
name={props.name}
open={props.open}
onClick={props.onClick}
onOpenChange={props.onOpenChange}
onValueChange={props.onChange}
onClose={props.onClose}
triggerClass={classNames({ Bound: true, Table: true })}
value={value}
>

View file

@ -9,10 +9,13 @@ import './index.scss'
// Props
interface Props {
open: boolean
currentValue?: WeaponKey
series: number
slot: number
onChange?: (value: string, slot: number) => void
onOpenChange: () => void
onClose?: () => void
}
const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
@ -64,10 +67,6 @@ const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
fetchWeaponKeys()
}, [props.series, props.slot])
function openSelect() {
setOpen(!open)
}
function weaponKeyGroup(index: number) {
;['α', 'β', 'γ', 'Δ'].sort((a, b) => a.localeCompare(b, 'el'))
@ -125,9 +124,10 @@ const WeaponKeySelect = React.forwardRef<HTMLButtonElement, Props>(
<Select
key={`weapon-key-${props.slot}`}
value={props.currentValue ? props.currentValue.id : 'no-key'}
open={open}
open={props.open}
onClose={props.onClose}
onOpenChange={props.onOpenChange}
onValueChange={handleChange}
onClick={openSelect}
ref={ref}
triggerClass="modal"
>

View file

@ -4,7 +4,13 @@ import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog'
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
DialogTrigger,
} from '~components/Dialog'
import AXSelect from '~components/AxSelect'
import AwakeningSelect from '~components/AwakeningSelect'
@ -74,6 +80,14 @@ const WeaponModal = (props: Props) => {
const [weaponKey2Id, setWeaponKey2Id] = useState('')
const [weaponKey3Id, setWeaponKey3Id] = useState('')
const [weaponKey1Open, setWeaponKey1Open] = useState(false)
const [weaponKey2Open, setWeaponKey2Open] = useState(false)
const [weaponKey3Open, setWeaponKey3Open] = useState(false)
const [weaponKey4Open, setWeaponKey4Open] = useState(false)
const [ax1Open, setAx1Open] = useState(false)
const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false)
useEffect(() => {
setElement(props.gridWeapon.element)
@ -188,16 +202,35 @@ const WeaponModal = (props: Props) => {
)
}
function openSelect(index: 1 | 2 | 3 | 4) {
setWeaponKey1Open(index === 1 ? !weaponKey1Open : false)
setWeaponKey2Open(index === 2 ? !weaponKey2Open : false)
setWeaponKey3Open(index === 3 ? !weaponKey3Open : false)
setWeaponKey4Open(index === 4 ? !weaponKey4Open : false)
}
function receiveAxOpen(index: 1 | 2, isOpen: boolean) {
if (index === 1) setAx1Open(isOpen)
if (index === 2) setAx2Open(isOpen)
}
function receiveAwakeningOpen(isOpen: boolean) {
setAwakeningOpen(isOpen)
}
const keySelect = () => {
return (
<section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey1Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(1)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey1Open(false)}
/>
) : (
''
@ -205,10 +238,13 @@ const WeaponModal = (props: Props) => {
{[2, 3, 17].includes(props.gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey2Open}
currentValue={weaponKey2 != null ? weaponKey2 : undefined}
series={props.gridWeapon.object.series}
slot={1}
onOpenChange={() => openSelect(2)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey2Open(false)}
/>
) : (
''
@ -216,10 +252,13 @@ const WeaponModal = (props: Props) => {
{props.gridWeapon.object.series == 17 ? (
<WeaponKeySelect
open={weaponKey3Open}
currentValue={weaponKey3 != null ? weaponKey3 : undefined}
series={props.gridWeapon.object.series}
slot={2}
onOpenChange={() => openSelect(3)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey3Open(false)}
/>
) : (
''
@ -228,10 +267,13 @@ const WeaponModal = (props: Props) => {
{props.gridWeapon.object.series == 24 &&
props.gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect
open={weaponKey4Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined}
series={props.gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(4)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey4Open(false)}
/>
) : (
''
@ -247,6 +289,7 @@ const WeaponModal = (props: Props) => {
<AXSelect
axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax}
onOpenChange={receiveAxOpen}
sendValidity={receiveValidity}
sendValues={receiveAxValues}
/>
@ -262,6 +305,7 @@ const WeaponModal = (props: Props) => {
object="weapon"
awakeningType={props.gridWeapon.awakening?.type}
awakeningLevel={props.gridWeapon.awakening?.level}
onOpenChange={receiveAwakeningOpen}
sendValidity={receiveValidity}
sendValues={receiveAwakeningValues}
/>
@ -278,49 +322,64 @@ const WeaponModal = (props: Props) => {
setOpen(open)
}
const anySelectOpen =
weaponKey1Open ||
weaponKey2Open ||
weaponKey3Open ||
weaponKey4Open ||
ax1Open ||
ax2Open ||
awakeningOpen
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else {
setOpen(false)
}
}
return (
// TODO: Refactor into Dialog component
<Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content
className="Weapon Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader">
<div className="DialogTop">
<Dialog.Title className="SubTitle">
{t('modals.weapon.title')}
</Dialog.Title>
<Dialog.Title className="DialogTitle">
{props.gridWeapon.object.name[locale]}
</Dialog.Title>
</div>
<Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
<Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent
className="Weapon Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown}
>
<div className="DialogHeader">
<div className="DialogTop">
<DialogTitle className="SubTitle">
{t('modals.weapon.title')}
</DialogTitle>
<DialogTitle className="DialogTitle">
{props.gridWeapon.object.name[locale]}
</DialogTitle>
</div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect()
: ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ''}
{props.gridWeapon.awakening ? awakeningSelect() : ''}
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
</div>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
<div className="mods">
{props.gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
? keySelect()
: ''}
{props.gridWeapon.object.ax > 0 ? axSelect() : ''}
{props.gridWeapon.awakening ? awakeningSelect() : ''}
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
</div>
</DialogContent>
</Dialog>
)
}