Skip to content

Commit 764f731

Browse files
Fix file disappearing in userData field (#1908)
* Animate with `react-spring` – fix file state issue * Abstract and handle content changes * Fix accordion dynamic height * Fix test * Remove animation * Scroll into view on open * Revert "Fix test" This reverts commit 4d08e86. * inline some advanced accordion stuff * pull out a bit of static content for readability * comment --------- Co-authored-by: David Crespo <[email protected]>
1 parent 945619e commit 764f731

File tree

2 files changed

+100
-91
lines changed

2 files changed

+100
-91
lines changed

app/forms/instance-create.tsx

Lines changed: 100 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* Copyright Oxide Computer Company
77
*/
88
import * as Accordion from '@radix-ui/react-accordion'
9-
import { useEffect, useState } from 'react'
10-
import { useWatch } from 'react-hook-form'
9+
import cn from 'classnames'
10+
import { useEffect, useRef, useState } from 'react'
11+
import { useWatch, type Control } from 'react-hook-form'
1112
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1213
import type { SetRequired } from 'type-fest'
1314

@@ -421,44 +422,7 @@ export function CreateInstanceForm() {
421422
<FormDivider />
422423
<Form.Heading id="advanced">Advanced</Form.Heading>
423424

424-
<Accordion.Root type="multiple" className="mt-12">
425-
<Accordion.Item value="networking">
426-
<AccordionHeader id="networking">Networking</AccordionHeader>
427-
<AccordionContent>
428-
<NetworkInterfaceField control={control} disabled={isSubmitting} />
429-
430-
<TextField
431-
name="hostname"
432-
tooltipText="Will be generated if not provided"
433-
control={control}
434-
disabled={isSubmitting}
435-
/>
436-
</AccordionContent>
437-
</Accordion.Item>
438-
<Accordion.Item value="configuration">
439-
<AccordionHeader id="configuration">Configuration</AccordionHeader>
440-
<AccordionContent>
441-
<FileField
442-
id="user-data-input"
443-
description={
444-
<>
445-
Data or scripts to be passed to cloud-init as{' '}
446-
<a href={links.cloudInitFormat} target="_blank" rel="noreferrer">
447-
user data
448-
</a>{' '}
449-
<a href={links.cloudInitExamples} target="_blank" rel="noreferrer">
450-
(examples)
451-
</a>{' '}
452-
if the selected boot image supports it. Maximum size 32 KiB.
453-
</>
454-
}
455-
name="userData"
456-
label="User Data"
457-
control={control}
458-
/>
459-
</AccordionContent>
460-
</Accordion.Item>
461-
</Accordion.Root>
425+
<AdvancedAccordion control={control} isSubmitting={isSubmitting} />
462426

463427
<Form.Actions>
464428
<Form.Submit loading={createInstance.isPending}>Create instance</Form.Submit>
@@ -468,20 +432,90 @@ export function CreateInstanceForm() {
468432
)
469433
}
470434

471-
const AccordionHeader = ({ id, children }: { id: string; children: React.ReactNode }) => (
472-
<Accordion.Header id={id} className="max-w-lg">
473-
<Accordion.Trigger className="group flex w-full items-center justify-between border-t py-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90">
474-
<div className="text-secondary">{children}</div>
475-
<DirectionRightIcon className="transition-all text-secondary" />
476-
</Accordion.Trigger>
477-
</Accordion.Header>
478-
)
435+
const AdvancedAccordion = ({
436+
control,
437+
isSubmitting,
438+
}: {
439+
control: Control<InstanceCreateInput>
440+
isSubmitting: boolean
441+
}) => {
442+
// we track this state manually for the sole reason that we need to be able to
443+
// tell, inside AccordionItem, when an accordion is opened so we can scroll its
444+
// contents into view
445+
const [openItems, setOpenItems] = useState<string[]>([])
479446

480-
const AccordionContent = ({ children }: { children: React.ReactNode }) => (
481-
<Accordion.Content className="AccordionContent max-w-lg overflow-hidden">
482-
<div className="ox-accordion-content py-8">{children}</div>
483-
</Accordion.Content>
484-
)
447+
return (
448+
<Accordion.Root
449+
type="multiple"
450+
className="mt-12 max-w-lg"
451+
value={openItems}
452+
onValueChange={setOpenItems}
453+
>
454+
<AccordionItem
455+
value="networking"
456+
label="Networking"
457+
isOpen={openItems.includes('networking')}
458+
>
459+
<NetworkInterfaceField control={control} disabled={isSubmitting} />
460+
461+
<TextField
462+
name="hostname"
463+
tooltipText="Will be generated if not provided"
464+
control={control}
465+
disabled={isSubmitting}
466+
/>
467+
</AccordionItem>
468+
<AccordionItem
469+
value="configuration"
470+
label="Configuration"
471+
isOpen={openItems.includes('configuration')}
472+
>
473+
<FileField
474+
id="user-data-input"
475+
description={<UserDataDescription />}
476+
name="userData"
477+
label="User Data"
478+
control={control}
479+
/>
480+
</AccordionItem>
481+
</Accordion.Root>
482+
)
483+
}
484+
485+
type AccordionItemProps = {
486+
value: string
487+
isOpen: boolean
488+
label: string
489+
children: React.ReactNode
490+
}
491+
492+
function AccordionItem({ value, label, children, isOpen }: AccordionItemProps) {
493+
const contentRef = useRef<HTMLDivElement>(null)
494+
495+
useEffect(() => {
496+
if (isOpen && contentRef.current) {
497+
contentRef.current.scrollIntoView({ behavior: 'smooth' })
498+
}
499+
}, [isOpen])
500+
501+
return (
502+
<Accordion.Item value={value}>
503+
<Accordion.Header className="max-w-lg">
504+
<Accordion.Trigger className="group flex w-full items-center justify-between border-t py-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90">
505+
<div className="text-secondary">{label}</div>
506+
<DirectionRightIcon className="transition-all text-secondary" />
507+
</Accordion.Trigger>
508+
</Accordion.Header>
509+
<Accordion.Content
510+
ref={contentRef}
511+
forceMount
512+
className={cn('ox-accordion-content overflow-hidden py-8', { hidden: !isOpen })}
513+
>
514+
{children}
515+
</Accordion.Content>
516+
</Accordion.Item>
517+
)
518+
}
485519

486520
const SshKeysTable = () => {
487521
const keys = usePrefetchedApiQuery('currentUserSshKeyList', {}).data?.items || []
@@ -580,3 +614,16 @@ const PRESETS = [
580614

581615
{ category: 'custom', id: 'custom', memory: 0, ncpus: 0 },
582616
] as const
617+
618+
const UserDataDescription = () => (
619+
<>
620+
Data or scripts to be passed to cloud-init as{' '}
621+
<a href={links.cloudInitFormat} target="_blank" rel="noreferrer">
622+
user data
623+
</a>{' '}
624+
<a href={links.cloudInitExamples} target="_blank" rel="noreferrer">
625+
(examples)
626+
</a>{' '}
627+
if the selected boot image supports it. Maximum size 32 KiB.
628+
</>
629+
)

libs/ui/styles/index.css

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -72,44 +72,6 @@
7272
}
7373
}
7474

75-
@layer components {
76-
@media screen and (min-width: 720px) {
77-
.AccordionContent[data-state='open'] {
78-
animation: accordionSlideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
79-
}
80-
.AccordionContent[data-state='closed'] {
81-
animation: accordionSlideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
82-
}
83-
}
84-
85-
@media screen and (prefers-reduced-motion) {
86-
.AccordionContent[data-state='open'] {
87-
animation-name: none;
88-
}
89-
.AccordionContent[data-state='closed'] {
90-
animation-name: none;
91-
}
92-
}
93-
94-
@keyframes accordionSlideDown {
95-
from {
96-
height: 0;
97-
}
98-
to {
99-
height: var(--radix-accordion-content-height);
100-
}
101-
}
102-
103-
@keyframes accordionSlideUp {
104-
from {
105-
height: var(--radix-accordion-content-height);
106-
}
107-
to {
108-
height: 0;
109-
}
110-
}
111-
}
112-
11375
/**
11476
* Remove focus ring for non-explicit scenarios.
11577
*/

0 commit comments

Comments
 (0)