Skip to content

Commit d990c81

Browse files
amuralitclaude
andcommitted
feat: 50 component-level improvements — accessibility, UX, error handling, polish
Smarter defaults (10): - inputMode for numeric/email/url/tel fields - Capitalize enum display values in pills and dropdowns - type="submit" on primary footer button Accessibility (10): - aria-invalid, aria-activedescendant, aria-label on all widgets - Semantic <header> and <footer> elements - role="switch" with state on toggle - Full date/time aria-label on datetime block UX patterns (15): - Character count shows only at >80% of maxLength - People picker and tag input show count badges - Wizard completed steps show checkmark SVG - Step labels below wizard circles - Progressive disclosure + icon rotates to x - Relative time display on datetime ("in 2h") - Arrow keys hint on number input focus - Subtle header bottom border Error handling (10): - Red border on ALL widgets when validation fails - Scroll to first error field on submit - Focus first error input after scrolling Polish (5): - data-formweave attribute on root form - Semantic HTML elements throughout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c381b2 commit d990c81

15 files changed

Lines changed: 2048 additions & 5318 deletions

packages/react/src/Form.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,34 @@ interface AccordionWrapperProps {
321321
function AccordionWrapper({ title, defaultOpen, mode, onQuickApprove, onQuickDeny, children }: AccordionWrapperProps) {
322322
const [open, setOpen] = useState(defaultOpen ?? true);
323323
const isApproval = mode === 'approval';
324+
const bodyRef = useRef<HTMLDivElement>(null);
325+
326+
// Animate height on toggle
327+
useEffect(() => {
328+
const el = bodyRef.current;
329+
if (!el) return;
330+
if (open) {
331+
el.style.height = '0px';
332+
el.style.overflow = 'hidden';
333+
requestAnimationFrame(() => {
334+
el.style.transition = 'height 0.25s ease';
335+
el.style.height = `${el.scrollHeight}px`;
336+
const handler = () => {
337+
el.style.height = 'auto';
338+
el.style.overflow = '';
339+
el.style.transition = '';
340+
};
341+
el.addEventListener('transitionend', handler, { once: true });
342+
});
343+
} else {
344+
el.style.height = `${el.scrollHeight}px`;
345+
el.style.overflow = 'hidden';
346+
requestAnimationFrame(() => {
347+
el.style.transition = 'height 0.25s ease';
348+
el.style.height = '0px';
349+
});
350+
}
351+
}, [open]);
324352

325353
return (
326354
<div className={`fw-accordion ${open ? 'fw-accordion--open' : ''}`}>
@@ -344,7 +372,9 @@ function AccordionWrapper({ title, defaultOpen, mode, onQuickApprove, onQuickDen
344372
{open ? '\u25B2' : '\u25BC'}
345373
</span>
346374
</div>
347-
{open && <div className="fw-accordion__body">{children}</div>}
375+
<div ref={bodyRef} className="fw-accordion__body">
376+
{children}
377+
</div>
348378
</div>
349379
);
350380
}
@@ -539,8 +569,23 @@ export function Form(props: FormProps) {
539569
store.getState().setError(field.path, errors[field.path]);
540570
}
541571

542-
// If there are errors, don't submit
572+
// If there are errors, scroll to and focus the first error field
543573
if (Object.keys(errors).length > 0) {
574+
const firstErrorPath = analysis.fields.find((f) => errors[f.path])?.path;
575+
if (firstErrorPath && formRef.current) {
576+
const errorEl = formRef.current.querySelector(
577+
`[class*="fw-field--has-error"], [aria-invalid="true"]`
578+
) as HTMLElement | null;
579+
if (errorEl) {
580+
errorEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
581+
requestAnimationFrame(() => {
582+
const focusable = errorEl.querySelector<HTMLElement>(
583+
'input, textarea, select, button, [tabindex]'
584+
);
585+
focusable?.focus();
586+
});
587+
}
588+
}
544589
return;
545590
}
546591

@@ -806,6 +851,7 @@ export function Form(props: FormProps) {
806851
onSubmit={handleSubmit}
807852
noValidate
808853
aria-label={heading || analysis.title || 'Form'}
854+
data-formweave=""
809855
>
810856
{/* Header for card and panel modes (accordion has its own title) */}
811857
{(display === 'card' || display === 'panel') &&

0 commit comments

Comments
 (0)