feat: add frame reordering and improve file upload accessibility

- Add Up/Down reorder controls to Frame component
- Add handleReOrderFrames hook using splice swap
- Improve FileUpload: keyboard support, aria attributes, fix accept=image/* typo
- Extract component-level CSS for Frame, Step, FileUpload
- Update focus-visible styles; scope border style to .file-upload
This commit is contained in:
johannes-hernehult 2026-06-15 19:49:00 +02:00
parent b95d687904
commit f214b0ac4d
10 changed files with 71 additions and 7 deletions

View file

@ -0,0 +1,19 @@
.file-upload {
display: grid;
place-items: center;
cursor: pointer;
border-style: dashed;
padding: 2rem;
font-weight: bold;
&:hover,
&:focus-visible {
border-color: #ff770080;
color: #ff7700;
background-color: #ff770010;
}
}
.file-upload input {
display: none;
}

View file

@ -1,17 +1,31 @@
import "./FileUpload.css";
export default function FileUpload({ export default function FileUpload({
onChange, onChange,
}: { }: {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) { }) {
return ( return (
<label> <label
<span>File</span> className="file-upload"
role="button"
tabIndex={0}
aria-label="Upload images"
aria-describedby="upload-hint"
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.currentTarget.querySelector("input")?.click();
}
}}
>
<span aria-hidden="true">Click to upload images</span>
<input <input
type="file" type="file"
name="file" name="file"
accept="images/*" accept="image/*"
multiple multiple
onChange={(e) => onChange(e)} onChange={onChange}
/> />
</label> </label>
); );

View file

View file

@ -1,23 +1,35 @@
import "./Frame.css";
import type { FrameType, StepType } from "../../../lib/schemas"; import type { FrameType, StepType } from "../../../lib/schemas";
import Step from "./Step"; import Step from "./Step";
export default function Frame({ export default function Frame({
frame, frame,
frameIndex,
onRemove, onRemove,
addStep, addStep,
removeStep, removeStep,
updateStep, updateStep,
updateTitle, updateTitle,
reOrder,
}: { }: {
frame: FrameType; frame: FrameType;
frameIndex: number;
onRemove: (frameId: string) => void; onRemove: (frameId: string) => void;
addStep: (frameId: string) => void; addStep: (frameId: string) => void;
removeStep: (stepId: string) => void; removeStep: (stepId: string) => void;
updateStep: (stepId: string, text: string) => void; updateStep: (stepId: string, text: string) => void;
updateTitle: (frameId: string, title: string) => void; updateTitle: (frameId: string, title: string) => void;
reOrder: (startIndex: number, endIndex: number) => void;
}) { }) {
return ( return (
<div> <div>
<button type="button" onClick={() => reOrder(frameIndex, frameIndex - 1)}>
Up
</button>
<button type="button" onClick={() => reOrder(frameIndex, frameIndex + 1)}>
Down
</button>
<button type="button" onClick={() => onRemove(frame.id)}> <button type="button" onClick={() => onRemove(frame.id)}>
Remove Frame Remove Frame
</button> </button>

View file

View file

@ -1,3 +1,4 @@
import "./Step.css";
import type { StepType } from "../../../lib/schemas"; import type { StepType } from "../../../lib/schemas";
export default function Step({ export default function Step({

View file

@ -54,6 +54,15 @@ export default function useNewGuide() {
setFrames((prev) => prev.filter((frame) => frame.id !== frameId)); setFrames((prev) => prev.filter((frame) => frame.id !== frameId));
}; };
const handleReOrderFrames = (startIndex: number, endIndex: number) => {
setFrames((prev) => {
const newOrder = [...prev];
const [movedFrame] = newOrder.splice(startIndex, 1);
newOrder.splice(endIndex, 0, movedFrame);
return newOrder;
});
};
const handleAddSteps = (frameId: string) => { const handleAddSteps = (frameId: string) => {
setFrames((prev) => setFrames((prev) =>
prev.map((frame) => prev.map((frame) =>
@ -123,6 +132,7 @@ export default function useNewGuide() {
handleUpdateDescription, handleUpdateDescription,
handleAddFrames, handleAddFrames,
handleUpdateFrameTitle, handleUpdateFrameTitle,
handleReOrderFrames,
handleAddSteps, handleAddSteps,
handleRemoveFrame, handleRemoveFrame,
handleUpdateStep, handleUpdateStep,

View file

@ -12,6 +12,7 @@ export default function NewGuide() {
handleUpdateFrameTitle, handleUpdateFrameTitle,
handleAddSteps, handleAddSteps,
handleRemoveFrame, handleRemoveFrame,
handleReOrderFrames,
handleRemoveStep, handleRemoveStep,
handleUpdateStep, handleUpdateStep,
handleSaveGuide, handleSaveGuide,
@ -42,16 +43,18 @@ export default function NewGuide() {
frames.map((frame, index) => ( frames.map((frame, index) => (
<Frame <Frame
key={index} key={index}
frameIndex={index}
frame={frame} frame={frame}
onRemove={handleRemoveFrame} onRemove={handleRemoveFrame}
addStep={handleAddSteps} addStep={handleAddSteps}
updateStep={handleUpdateStep} updateStep={handleUpdateStep}
removeStep={handleRemoveStep} removeStep={handleRemoveStep}
updateTitle={handleUpdateFrameTitle} updateTitle={handleUpdateFrameTitle}
reOrder={handleReOrderFrames}
/> />
))} ))}
{frames.length === 0 && <p>No frames added yet.</p>} {frames.length === 0 && <p>No images added yet.</p>}
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />} {frames.length > 0 && <FileUpload onChange={handleAddFrames} />}

View file

@ -70,7 +70,8 @@ textarea {
} }
input, input,
textarea { textarea,
.file-upload {
border: 3px solid var(--color-border); border: 3px solid var(--color-border);
border-radius: 4px; border-radius: 4px;
padding: 0.25rem; padding: 0.25rem;
@ -80,3 +81,7 @@ textarea {
resize: none; resize: none;
height: 5rem; height: 5rem;
} }
:focus-visible {
border-color: #ff7700 !important;
}

View file

@ -136,6 +136,6 @@
} }
:focus-visible { :focus-visible {
outline-offset: 0.2rem; outline: none;
} }
} }