-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
639 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { useDragItem } from "@/hooks/useDrag"; | ||
import { fr } from "@codegouvfr/react-dsfr"; | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import Input from "@codegouvfr/react-dsfr/Input"; | ||
import { tss } from "tss-react"; | ||
|
||
export interface KeyValue { | ||
key?: string; | ||
value: string | null; | ||
} | ||
|
||
export type KeyValues = KeyValue[]; | ||
|
||
interface KeyValueListProps { | ||
index: number; | ||
keyValue?: string; | ||
onChange: (index: number, key: string, value: string) => void; | ||
onRemove: (index: number) => void; | ||
useKeys: boolean; | ||
value: string | null; | ||
} | ||
|
||
function KeyValueItem(props: KeyValueListProps) { | ||
const { index, keyValue, onChange, onRemove, useKeys, value } = props; | ||
const { dragIndex, ref, startDrag } = useDragItem(); | ||
const { classes, cx } = useStyles({ active: dragIndex === index }); | ||
|
||
function handleMove(index: number) { | ||
return (event) => startDrag(event, index); | ||
} | ||
|
||
function handleChange(index: number, field: string) { | ||
return (event) => onChange(index, field, event.target.value); | ||
} | ||
|
||
function handleRemove(index: number) { | ||
return () => onRemove(index); | ||
} | ||
|
||
return ( | ||
<div className={cx(fr.cx("fr-pb-2w"), classes.row, classes.drag)} ref={ref}> | ||
<div className={classes.buttons}> | ||
<Button | ||
title="Ordonner" | ||
priority={"tertiary no outline"} | ||
iconId={"ri-draggable"} | ||
nativeButtonProps={{ onMouseDown: handleMove(index) }} | ||
type="button" | ||
/> | ||
</div> | ||
{useKeys && ( | ||
<div className={classes.cell}> | ||
<Input | ||
label="Clé" | ||
nativeInputProps={{ | ||
value: keyValue, | ||
onChange: handleChange(index, "key"), | ||
}} | ||
/> | ||
</div> | ||
)} | ||
<div className={classes.cell}> | ||
<Input | ||
label="Valeur" | ||
nativeInputProps={{ | ||
value: value ?? "", | ||
onChange: handleChange(index, "value"), | ||
}} | ||
/> | ||
</div> | ||
<div className={classes.buttons}> | ||
<Button title="Supprimer" priority={"tertiary no outline"} iconId={"fr-icon-delete-line"} onClick={handleRemove(index)} /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default KeyValueItem; | ||
|
||
const useStyles = tss.withParams<{ active: boolean }>().create(({ active }) => ({ | ||
drag: { | ||
position: "relative", | ||
zIndex: active ? 1 : 0, | ||
// ...(!active && { transition: "translate 200ms ease-out" }), | ||
// transition: "scale 200ms ease-out", | ||
// scale: active? 1.01 : 1, | ||
}, | ||
row: { | ||
display: "flex", | ||
gap: "1.5rem", | ||
alignItems: "center", | ||
}, | ||
cell: { | ||
flex: 1, | ||
}, | ||
buttons: { | ||
alignSelf: "flex-end", | ||
}, | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
/* eslint-disable @typescript-eslint/no-namespace */ | ||
import { fr } from "@codegouvfr/react-dsfr"; | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; | ||
import { Dispatch, SetStateAction } from "react"; | ||
import { useDrag } from "@/hooks/useDrag"; | ||
import { DragProvider } from "@/contexts/drag"; | ||
|
||
import KeyValueItem, { KeyValues } from "./KeyValueItem"; | ||
|
||
// export type KeyValues = (string | null)[] | Record<string, string | null>; | ||
|
||
type KeyValueListProps = KeyValueListProps.Common & (KeyValueListProps.Array | KeyValueListProps.Object); | ||
|
||
export namespace KeyValueListProps { | ||
export interface Common { | ||
id?: string; | ||
label: string; | ||
hintText?: string; | ||
setUseKeys: Dispatch<SetStateAction<boolean>>; | ||
state?: "default" | "error" | "success"; | ||
stateRelatedMessage?: string; | ||
useKeys: boolean; | ||
} | ||
export interface Array<T = (string | null)[]> { | ||
useKeys: false; | ||
onChange: (value: T) => void; | ||
value: T; | ||
} | ||
export interface Object<T = Record<string, string | null>> { | ||
useKeys: true; | ||
onChange: (value: T) => void; | ||
value: T; | ||
} | ||
} | ||
|
||
function KeyValueList2(props: KeyValueListProps) { | ||
const { label, hintText, onChange, setUseKeys, state, stateRelatedMessage, useKeys, value } = props; | ||
const arrayValue: KeyValues = | ||
value instanceof Array ? value.map((item) => ({ value: item ?? "" })) : Object.entries(value).map(([key, value]) => ({ key, value: value ?? "" })); | ||
const drag = useDrag((callback) => setData(callback(arrayValue))); | ||
|
||
function setData(newArray: KeyValues) { | ||
if (useKeys) { | ||
onChange(Object.fromEntries(newArray.map(({ key, value }) => [key, value || null]))); | ||
} else { | ||
onChange(newArray.map(({ value }) => value || null)); | ||
} | ||
} | ||
|
||
function handleAdd() { | ||
if (useKeys) { | ||
onChange({ ...value, "": "" }); | ||
} else { | ||
onChange([...value, ""]); | ||
} | ||
} | ||
|
||
function handleChange(index: number, key: string, val: string) { | ||
setData( | ||
arrayValue.map((item, i) => { | ||
if (index === i) { | ||
return { ...item, [key]: val }; | ||
} | ||
return item; | ||
}) | ||
); | ||
} | ||
|
||
function handleRemove(index: number) { | ||
setData(arrayValue.filter((_, i) => i !== index)); | ||
} | ||
|
||
return ( | ||
<div className={fr.cx("fr-pb-2w")}> | ||
<div className={fr.cx("fr-input-group", "fr-mb-1v")}> | ||
<label className={fr.cx("fr-label")}> | ||
{label} | ||
{hintText && <span className={fr.cx("fr-hint-text")}>{hintText}</span>} | ||
</label> | ||
</div> | ||
<div className={fr.cx("fr-mt-1w", "fr-mb-1w")}> | ||
<ToggleSwitch label="Définir les clés" inputTitle="Définir les clés" checked={useKeys} onChange={setUseKeys} showCheckedHint={false} /> | ||
</div> | ||
<DragProvider value={drag}> | ||
{arrayValue.map(({ key, value }, i) => { | ||
return <KeyValueItem key={i} index={i} keyValue={key} onChange={handleChange} onRemove={handleRemove} useKeys={useKeys} value={value} />; | ||
})} | ||
</DragProvider> | ||
<div> | ||
<Button className={fr.cx("fr-mr-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleAdd} type="button"> | ||
Ajouter | ||
</Button> | ||
</div> | ||
{state !== "default" && ( | ||
<p | ||
className={fr.cx( | ||
(() => { | ||
switch (state) { | ||
case "error": | ||
return "fr-error-text"; | ||
case "success": | ||
return "fr-valid-text"; | ||
} | ||
})() | ||
)} | ||
> | ||
{stateRelatedMessage} | ||
</p> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default KeyValueList2; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { createContext, PropsWithChildren, useContext } from "react"; | ||
|
||
export interface IDragContext { | ||
dragIndex: number; | ||
register: (ref: HTMLElement) => void; | ||
startDrag: (event: MouseEvent, index: number) => void; | ||
} | ||
|
||
const dragContext = createContext<IDragContext | null>(null); | ||
|
||
export function DragProvider({ children, value }: PropsWithChildren<{ value: IDragContext }>) { | ||
return <dragContext.Provider value={value}>{children}</dragContext.Provider>; | ||
} | ||
|
||
export function useDragContext() { | ||
const context = useContext(dragContext); | ||
if (!context) { | ||
throw new Error("useDragContext must be used within a DragProvider"); | ||
} | ||
return context; | ||
} |
Oops, something went wrong.