feat(simulations): clavier caractères spéciaux sticky + flex-wrap + auto-resize textarea
This commit is contained in:
parent
4f786dd44b
commit
869668a1ba
2 changed files with 35 additions and 5 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
* Règle H : aucune logique métier — le composant reçoit tache, handlers et états par props.
|
* Règle H : aucune logique métier — le composant reçoit tache, handlers et états par props.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useEffect, useRef, useState, type FormEvent } from 'react'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { Button } from '@/shared/components/ui/button'
|
import { Button } from '@/shared/components/ui/button'
|
||||||
|
|
@ -14,6 +14,7 @@ import { formatTache } from '@/entities/production/lib'
|
||||||
import type { SujetData, Tache } from '@/entities/production/types'
|
import type { SujetData, Tache } from '@/entities/production/types'
|
||||||
import type { ApiError } from '@/shared/types/api'
|
import type { ApiError } from '@/shared/types/api'
|
||||||
import { SujetDisplay } from './SujetDisplay'
|
import { SujetDisplay } from './SujetDisplay'
|
||||||
|
import { SpecialCharsKeyboard } from './SpecialCharsKeyboard'
|
||||||
|
|
||||||
const textSchema = z.object({
|
const textSchema = z.object({
|
||||||
texte: z
|
texte: z
|
||||||
|
|
@ -47,9 +48,34 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, onBack }: Props) {
|
export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, onBack }: Props) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [texte, setTexte] = useState('')
|
const [texte, setTexte] = useState('')
|
||||||
const [fieldError, setFieldError] = useState<string | null>(null)
|
const [fieldError, setFieldError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = textareaRef.current
|
||||||
|
if (!el) return
|
||||||
|
el.style.height = 'auto'
|
||||||
|
el.style.height = `${el.scrollHeight}px`
|
||||||
|
}, [texte])
|
||||||
|
|
||||||
|
function handleInsert(char: string) {
|
||||||
|
const el = textareaRef.current
|
||||||
|
if (!el) {
|
||||||
|
setTexte((prev) => prev + char)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const start = el.selectionStart ?? texte.length
|
||||||
|
const end = el.selectionEnd ?? texte.length
|
||||||
|
const next = texte.slice(0, start) + char + texte.slice(end)
|
||||||
|
setTexte(next)
|
||||||
|
const caret = start + char.length
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.focus()
|
||||||
|
el.setSelectionRange(caret, caret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setFieldError(null)
|
setFieldError(null)
|
||||||
|
|
@ -96,16 +122,20 @@ export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, on
|
||||||
<label htmlFor="texte" className="text-sm font-medium text-ink-2">
|
<label htmlFor="texte" className="text-sm font-medium text-ink-2">
|
||||||
Votre production
|
Votre production
|
||||||
</label>
|
</label>
|
||||||
|
<div className="sticky top-0 z-10 bg-canvas pb-1">
|
||||||
|
<SpecialCharsKeyboard onInsert={handleInsert} disabled={isSubmitting} />
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
id="texte"
|
id="texte"
|
||||||
rows={12}
|
rows={8}
|
||||||
value={texte}
|
value={texte}
|
||||||
onChange={(e) => setTexte(e.target.value)}
|
onChange={(e) => setTexte(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
placeholder="Rédigez votre texte ici…"
|
placeholder="Rédigez votre texte ici…"
|
||||||
aria-invalid={!!fieldError}
|
aria-invalid={!!fieldError}
|
||||||
aria-describedby={fieldError ? 'texte-error' : undefined}
|
aria-describedby={fieldError ? 'texte-error' : undefined}
|
||||||
className="w-full resize-none rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
className="w-full resize-none overflow-y-hidden rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
{fieldError ? (
|
{fieldError ? (
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function SpecialCharsKeyboard({ onInsert, disabled = false }: Props) {
|
||||||
<div
|
<div
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label="Caractères spéciaux"
|
aria-label="Caractères spéciaux"
|
||||||
className="flex gap-1.5 overflow-x-auto rounded-md border border-line bg-canvas-2 p-2"
|
className="flex flex-wrap gap-1.5 rounded-md border border-line bg-canvas-2 p-2"
|
||||||
>
|
>
|
||||||
{SPECIAL_CHARS.map((char) => (
|
{SPECIAL_CHARS.map((char) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -37,7 +37,7 @@ export function SpecialCharsKeyboard({ onInsert, disabled = false }: Props) {
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => onInsert(char)}
|
onClick={() => onInsert(char)}
|
||||||
aria-label={`Insérer le caractère ${char}`}
|
aria-label={`Insérer le caractère ${char}`}
|
||||||
className="size-10 shrink-0 rounded-md border border-line bg-surface text-sm font-medium text-ink-1 transition-colors hover:border-expria hover:bg-expria-50 hover:text-expria focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
className="size-8 shrink-0 rounded-md border border-line bg-surface text-sm font-medium text-ink-1 transition-colors hover:border-expria hover:bg-expria-50 hover:text-expria focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{char}
|
{char}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue