Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/storybook/src/patterns/RepeatableRowsGuidance.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,8 +781,8 @@ function RepeatableRowsGuidancePage({ globalTheme }: { globalTheme: string }) {
Each control is named by its row and its column, such as “Condition 2 Field”.
</GuidanceItem>
<GuidanceItem>
Stacked rows, in narrow panels: each field gets its own small label, a{' '}
<InlineCode>FormFieldHeader</InlineCode> with the muted variant. Column labels
Stacked rows, in narrow panels: each field gets its own label, the same{' '}
<InlineCode>FormFieldLabel</InlineCode> as cards and other form fields. Column labels
can&apos;t reach a value that has dropped to its own line.
</GuidanceItem>
<GuidanceItem>
Expand All @@ -796,9 +796,10 @@ function RepeatableRowsGuidancePage({ globalTheme }: { globalTheme: string }) {
Build labels from the Field Anatomy parts. A standalone field is a{' '}
<InlineCode>FormField</InlineCode> with a <InlineCode>FormFieldLabel</InlineCode>, or a{' '}
<InlineCode>FormFieldHeader</InlineCode> when its label row carries a badge or actions.
Shared column labels are one muted <InlineCode>FormFieldHeader</InlineCode> per column,
outside any field, and each control names itself with that label through{' '}
<InlineCode>aria-labelledby</InlineCode>.
Shared column labels are one <InlineCode>FormFieldLabel</InlineCode> per column. Wrap
the column labels and the row list together in a <InlineCode>FormField</InlineCode> so
the first row sits the same distance below its label as any other field. Each control
names itself with its column label through <InlineCode>aria-labelledby</InlineCode>.
</GuidanceItem>
</GuidanceList>
<div className="mt-6 grid gap-5 md:grid-cols-2">
Expand Down
215 changes: 112 additions & 103 deletions apps/storybook/src/patterns/repeatable-row-primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import {
type ValueMode,
ValueModeIndicator,
} from '@/components/ui/field-addons';
import { FormField, FormFieldError, FormFieldHeader } from '@/components/ui/form-field';
import {
FormField,
FormFieldError,
FormFieldHeader,
FormFieldLabel,
} from '@/components/ui/form-field';
import { Input } from '@/components/ui/input';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group';
import { MultiSelect } from '@/components/ui/multi-select';
Expand Down Expand Up @@ -553,12 +558,12 @@ function ConditionColumnHeaders({
return (
<div className="flex items-center gap-2">
<div className={cn('grid min-w-0 flex-1 gap-2', COLUMNS_INLINE)}>
<FormFieldHeader variant="muted" label="Field" labelId={ids.field} />
<FormFieldHeader variant="muted" label="Operator" labelId={ids.operator} />
<FormFieldHeader variant="muted" label="Value" labelId={ids.value} />
<FormFieldLabel id={ids.field}>Field</FormFieldLabel>
<FormFieldLabel id={ids.operator}>Operator</FormFieldLabel>
<FormFieldLabel id={ids.value}>Value</FormFieldLabel>
</div>
{/* Keeps the labels over their columns: the delete button's width. */}
<span aria-hidden="true" className="size-8 shrink-0" />
<span aria-hidden="true" className="h-4 w-8 shrink-0" />
</div>
);
}
Expand Down Expand Up @@ -831,24 +836,24 @@ export function ConditionRow({
<div className="flex items-start gap-2">
<div className="grid min-w-0 flex-1 grid-cols-[minmax(0,3fr)_minmax(0,2fr)] gap-2">
<FormField>
<FormFieldHeader variant="muted" label="Field" labelId={labelIds.field} />
<FormFieldLabel id={labelIds.field}>Field</FormFieldLabel>
Comment thread
dbacomputer marked this conversation as resolved.
{fieldControl}
</FormField>
<FormField>
<FormFieldHeader variant="muted" label="Operator" labelId={labelIds.operator} />
<FormFieldLabel id={labelIds.operator}>Operator</FormFieldLabel>
{operatorControl}
</FormField>
{valueControl && (
<FormField className="col-span-2">
<FormFieldHeader variant="muted" label="Value" labelId={labelIds.value} />
<FormFieldLabel id={labelIds.value}>Value</FormFieldLabel>
{valueControl}
</FormField>
)}
</div>
{/* A label-height spacer, then a control-height box, so the icon centres on the first
line of controls rather than on the labels. */}
<FormField>
<span aria-hidden="true" className="min-h-6" />
<span aria-hidden="true" className="h-4" />
<div className="flex h-9 items-center future:h-10">{deleteControl}</div>
</FormField>
</div>
Expand Down Expand Up @@ -1087,59 +1092,61 @@ function GroupBody({
</div>
)}

{columns && <ConditionColumnHeaders ids={columns} />}
<ConditionColumnsContext.Provider value={columns}>
<div ref={focus.listRef} className="flex flex-col gap-3">
{node.children.length === 0 ? (
<EmptyRows>{nested ? 'No conditions in this group.' : emptyText}</EmptyRows>
) : (
node.children.map((child, position) => {
if (child.kind === 'group') {
nestedGroups += 1;
<FormField>
{columns && <ConditionColumnHeaders ids={columns} />}
<ConditionColumnsContext.Provider value={columns}>
<div ref={focus.listRef} className="flex flex-col gap-3">
{node.children.length === 0 ? (
<EmptyRows>{nested ? 'No conditions in this group.' : emptyText}</EmptyRows>
) : (
node.children.map((child, position) => {
if (child.kind === 'group') {
nestedGroups += 1;
return (
<GroupBody
key={child.id}
node={child}
root={root}
onRootChange={onRootChange}
depth={depth + 1}
maxDepth={maxDepth}
matchMode={matchMode}
layout={layout}
forceErrors={forceErrors}
emptyText={emptyText}
variables={variables}
label={label}
numbers={numbers}
groupIndex={nestedGroups}
onDeleteGroup={() => deleteChild(child.id, position)}
/>
);
}
return (
<GroupBody
<ConditionRow
key={child.id}
node={child}
root={root}
onRootChange={onRootChange}
depth={depth + 1}
maxDepth={maxDepth}
matchMode={matchMode}
index={numbers.get(child.id) ?? position + 1}
condition={child}
layout={layout}
forceErrors={forceErrors}
emptyText={emptyText}
forceError={forceErrors}
variables={variables}
label={label}
numbers={numbers}
groupIndex={nestedGroups}
onDeleteGroup={() => deleteChild(child.id, position)}
onChange={(patch) =>
onRootChange(
updateNode(
root,
child.id,
(current) => ({ ...current, ...patch }) as Condition
)
)
}
onDelete={() => deleteChild(child.id, position)}
/>
);
}
return (
<ConditionRow
key={child.id}
index={numbers.get(child.id) ?? position + 1}
condition={child}
layout={layout}
forceError={forceErrors}
variables={variables}
onChange={(patch) =>
onRootChange(
updateNode(
root,
child.id,
(current) => ({ ...current, ...patch }) as Condition
)
)
}
onDelete={() => deleteChild(child.id, position)}
/>
);
})
)}
</div>
</ConditionColumnsContext.Provider>
})
)}
</div>
</ConditionColumnsContext.Provider>
</FormField>

<div className="flex flex-wrap items-center gap-4">
<AddRowButton ref={focus.addRef} onClick={() => addNode(condition())}>
Expand Down Expand Up @@ -1335,56 +1342,58 @@ export function KeyValueList({
</span>
}
/>
{pairs.length > 0 && (
// Visible column labels: the persistent labels for every row's Name and Value.
<div className="flex items-center gap-2">
{reorderable && <span aria-hidden="true" className="size-8 shrink-0" />}
<div className="grid min-w-0 flex-1 grid-cols-2 gap-2">
<FormFieldHeader variant="muted" label="Name" labelId={`${baseId}-name`} />
<FormFieldHeader variant="muted" label="Value" labelId={`${baseId}-value`} />
</div>
<span aria-hidden="true" className="size-8 shrink-0" />
</div>
)}
<div ref={focus.listRef} className="flex flex-col gap-2">
{pairs.map((pair, position) => (
<div key={pair.id} data-row="" className="flex items-center gap-2">
<span id={`${baseId}-${pair.id}`} className="sr-only">
{label} {position + 1}
</span>
{reorderable && (
<RowDragHandle
label={`Reorder ${itemName} ${position + 1}`}
onMoveUp={position > 0 ? () => move(position, position - 1) : undefined}
onMoveDown={
position < pairs.length - 1 ? () => move(position, position + 1) : undefined
}
/>
)}
<FormField>
{pairs.length > 0 && (
// Visible column labels: the persistent labels for every row's Name and Value.
<div className="flex items-center gap-2">
{reorderable && <span aria-hidden="true" className="h-4 w-8 shrink-0" />}
<div className="grid min-w-0 flex-1 grid-cols-2 gap-2">
<Input
data-row-first=""
aria-labelledby={`${baseId}-${pair.id} ${baseId}-name`}
defaultValue={pair.key}
placeholder="Content-Type"
/>
<Input
aria-labelledby={`${baseId}-${pair.id} ${baseId}-value`}
defaultValue={pair.value}
placeholder="application/json"
/>
<FormFieldLabel id={`${baseId}-name`}>Name</FormFieldLabel>
<FormFieldLabel id={`${baseId}-value`}>Value</FormFieldLabel>
</div>
<RowDeleteButton
label={`Remove ${itemName} ${position + 1}`}
onClick={() => {
focus.afterDelete(position);
setPairs((current) => current.filter((row) => row.id !== pair.id));
}}
/>
<span aria-hidden="true" className="h-4 w-8 shrink-0" />
</div>
))}
{pairs.length === 0 && <EmptyRows>No {itemName}s.</EmptyRows>}
</div>
)}
<div ref={focus.listRef} className="flex flex-col gap-2">
{pairs.map((pair, position) => (
<div key={pair.id} data-row="" className="flex items-center gap-2">
<span id={`${baseId}-${pair.id}`} className="sr-only">
{label} {position + 1}
</span>
{reorderable && (
<RowDragHandle
label={`Reorder ${itemName} ${position + 1}`}
onMoveUp={position > 0 ? () => move(position, position - 1) : undefined}
onMoveDown={
position < pairs.length - 1 ? () => move(position, position + 1) : undefined
}
/>
)}
<div className="grid min-w-0 flex-1 grid-cols-2 gap-2">
<Input
data-row-first=""
aria-labelledby={`${baseId}-${pair.id} ${baseId}-name`}
defaultValue={pair.key}
placeholder="Content-Type"
/>
<Input
aria-labelledby={`${baseId}-${pair.id} ${baseId}-value`}
defaultValue={pair.value}
placeholder="application/json"
/>
</div>
<RowDeleteButton
label={`Remove ${itemName} ${position + 1}`}
onClick={() => {
focus.afterDelete(position);
setPairs((current) => current.filter((row) => row.id !== pair.id));
}}
/>
</div>
))}
{pairs.length === 0 && <EmptyRows>No {itemName}s.</EmptyRows>}
</div>
</FormField>
<div>
<AddRowButton
ref={focus.addRef}
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/patterns/repeatable-rows-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function EntityFilterPanel({ onClose }: { onClose: () => void }) {
</FormField>

<FormField>
<FormFieldHeader label="How to look up the record" labelId="lookup-mode-label" />
<FormFieldLabel id="lookup-mode-label">How to look up the record</FormFieldLabel>
<RadioGroup
aria-labelledby="lookup-mode-label"
defaultValue="multiple"
Expand Down
Loading