diff --git a/.changeset/field-drop-group-role.md b/.changeset/field-drop-group-role.md new file mode 100644 index 00000000..97d61aab --- /dev/null +++ b/.changeset/field-drop-group-role.md @@ -0,0 +1,6 @@ +--- +'@surfnet/curve-react': patch +'@surfnet/curve-angular': patch +--- + +`Field` / `hlmField` no longer renders `role="group"`. A single field isn't a group, and inside a choice-card label the role caused the radio or checkbox to lose its accessible name (axe "Form elements must have labels"). To group related fields, use `FieldSet` / `fieldset[hlmFieldSet]`. diff --git a/.changeset/radio-group-choice-card.md b/.changeset/radio-group-choice-card.md new file mode 100644 index 00000000..1176af16 --- /dev/null +++ b/.changeset/radio-group-choice-card.md @@ -0,0 +1,5 @@ +--- +'@surfnet/curve-react': minor +--- + +`FieldLabel` now becomes a selectable "choice card" when it wraps a `Field` (bordered, with a primary tint when the control inside is checked), matching `@surfnet/curve-angular`. Horizontal `Field`s with `FieldContent` now top-align their checkbox or radio. See the new RadioGroup **ChoiceCard** story. diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field.css b/packages/angular/src/lib/ui/field/src/lib/hlm-field.css index 83ef965e..b9a4abfb 100644 --- a/packages/angular/src/lib/ui/field/src/lib/hlm-field.css +++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field.css @@ -115,7 +115,7 @@ .curve-field-label:has(> [data-slot='field']) { width: 100%; flex-direction: column; - border: 1px solid var(--border); + border: 1px solid var(--input); border-radius: var(--radius-md, calc(var(--radius, 0.625rem) * 0.8)); } diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts index df20cbd6..d186b8a8 100644 --- a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts +++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts @@ -14,8 +14,9 @@ export type FieldVariants = { orientation?: FieldOrientationName | null }; @Directive({ selector: '[hlmField],hlm-field', hostDirectives: [{ directive: BrnField, inputs: ['data-invalid', 'forceInvalid'] }], + // CURVE: a11y — no role="group" (upstream adds it): a single field isn't a group, and inside a + // choice-card label it blanks the label's text for axe. Group fields with hlmFieldSet instead. host: { - role: 'group', 'data-slot': 'field', '[attr.data-orientation]': 'orientation()', }, diff --git a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css index 44b76a91..cf0ee5b2 100644 --- a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css +++ b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.css @@ -55,12 +55,22 @@ } .curve-radio-indicator-dot { + position: absolute; + top: 50%; + left: 50%; width: calc(var(--spacing, 0.25rem) * 2); height: calc(var(--spacing, 0.25rem) * 2); - border-radius: calc(infinity * 1px); + border-radius: 9999px; + transform: translate(-50%, -50%); background-color: transparent; } -.curve-radio[data-checked='true'] .curve-radio-indicator-dot { +.brn-radio-checked .curve-radio-indicator { + border-color: var(--primary); background-color: var(--primary); + color: var(--primary-foreground); +} + +.curve-radio[data-checked='true'] .curve-radio-indicator-dot { + background-color: var(--background); } diff --git a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts index 9aa68567..a4e6ab3e 100644 --- a/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts +++ b/packages/angular/src/lib/ui/radio-group/src/lib/hlm-radio-group.stories.ts @@ -104,6 +104,71 @@ export const InFieldset: Story = { }), }; +/** + * Choice cards: wrap each option's `hlmField` in a `label[hlmFieldLabel]` and the whole + * label becomes a selectable card. The selected card gets a primary-tinted border and + * background. The group is named by a visible `label[hlmLabel]` via `aria-labelledby`. + */ +export const ChoiceCard: Story = { + render: () => ({ + template: ` +