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:
parent
b95d687904
commit
f214b0ac4d
10 changed files with 71 additions and 7 deletions
19
src/features/guides/components/FileUpload.css
Normal file
19
src/features/guides/components/FileUpload.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
0
src/features/guides/components/Frame.css
Normal file
0
src/features/guides/components/Frame.css
Normal 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>
|
||||||
|
|
|
||||||
0
src/features/guides/components/Step.css
Normal file
0
src/features/guides/components/Step.css
Normal 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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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} />}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline-offset: 0.2rem;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue