Skip to content

Commit 6fac706

Browse files
Make MorphemeBox forms row a single a11y control; fix open-state border
Only the first form cell is a real edit-breakdown button; remaining cells are presentational spans (aria-hidden) so assistive tech sees one control for the whole breakdown. Move tw:border-border into the always-on base classes so the open (popover) state keeps an explicit border color instead of falling back to the gray-200 default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4c32a90 commit 6fac706

1 file changed

Lines changed: 48 additions & 22 deletions

File tree

src/components/MorphemeBox.tsx

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,56 @@ export function MorphemeBox({
7777
return (
7878
<PopoverAnchor asChild>
7979
<div
80-
className={`tw:inline-grid tw:w-fit tw:items-center tw:gap-x-0.5 tw:gap-y-0.5 tw:rounded tw:border tw:bg-background tw:p-0.5${popoverOpen ? ' tw:ring-1 tw:ring-ring' : ' tw:border-border'}`}
80+
className={`tw:inline-grid tw:w-fit tw:items-center tw:gap-x-0.5 tw:gap-y-0.5 tw:rounded tw:border tw:border-border tw:bg-background tw:p-0.5${popoverOpen ? ' tw:ring-1 tw:ring-ring' : ''}`}
8181
style={{ gridTemplateColumns: `repeat(${morphemes.length}, minmax(1ch, auto))` }}
8282
>
83-
{/* Forms row: one clickable cell per morpheme, exposed to assistive tech as a single
84-
"edit breakdown" control. The cells share grid columns with the gloss inputs below so
85-
each form sits directly above its gloss. */}
86-
{morphemes.map((m, i) => (
87-
<button
88-
key={m.id}
89-
aria-label={i === 0 ? editLabel : undefined}
90-
className={`tw:flex tw:items-center tw:justify-center tw:whitespace-nowrap tw:rounded tw:px-0.5 tw:font-mono tw:text-xs tw:text-muted-foreground tw:transition-colors${disabled ? '' : ' tw:cursor-pointer'}${isFormsHovered && !disabled ? ' tw:bg-accent' : ''}`}
91-
style={{ gridColumn: i + 1, gridRow: 1 }}
92-
tabIndex={i === 0 ? 0 : -1}
93-
type="button"
94-
onClick={(e) => {
95-
e.preventDefault();
96-
if (!disabled) onEditBreakdown();
97-
}}
98-
onMouseEnter={() => setIsFormsHovered(true)}
99-
onMouseLeave={() => setIsFormsHovered(false)}
100-
>
101-
{m.form}
102-
</button>
103-
))}
83+
{/* Forms row. The first cell is the single accessible "edit breakdown" control (a real
84+
button); the rest are presentational form cells that share its click and hover behavior
85+
but carry no button semantics, so assistive tech sees one control for the whole
86+
breakdown. The cells share grid columns with the gloss inputs below so each form sits
87+
directly above its gloss. */}
88+
{morphemes.map((m, i) => {
89+
const formClassName = `tw:flex tw:items-center tw:justify-center tw:whitespace-nowrap tw:rounded tw:px-0.5 tw:font-mono tw:text-xs tw:text-muted-foreground tw:transition-colors${disabled ? '' : ' tw:cursor-pointer'}${isFormsHovered && !disabled ? ' tw:bg-accent' : ''}`;
90+
const formStyle = { gridColumn: i + 1, gridRow: 1 };
91+
const handleClick = () => {
92+
if (!disabled) onEditBreakdown();
93+
};
94+
const handleMouseEnter = () => setIsFormsHovered(true);
95+
const handleMouseLeave = () => setIsFormsHovered(false);
96+
97+
if (i === 0)
98+
return (
99+
<button
100+
key={m.id}
101+
aria-label={editLabel}
102+
className={formClassName}
103+
style={formStyle}
104+
type="button"
105+
onClick={(e) => {
106+
e.preventDefault();
107+
handleClick();
108+
}}
109+
onMouseEnter={handleMouseEnter}
110+
onMouseLeave={handleMouseLeave}
111+
>
112+
{m.form}
113+
</button>
114+
);
115+
116+
return (
117+
<span
118+
key={m.id}
119+
aria-hidden="true"
120+
className={formClassName}
121+
style={formStyle}
122+
onClick={handleClick}
123+
onMouseEnter={handleMouseEnter}
124+
onMouseLeave={handleMouseLeave}
125+
>
126+
{m.form}
127+
</span>
128+
);
129+
})}
104130
{/* Gloss row: each input fills its column and sits directly under its morpheme form. */}
105131
{morphemes.map((m, i) => (
106132
<MorphemeGlossInput

0 commit comments

Comments
 (0)