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
8 changes: 4 additions & 4 deletions bestax-mcp/data/components/Field.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions bestax-mcp/data/components/Taginput.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"code": "function example() {\n return (\n <Field\n horizontal\n label=\"Tags\"\n labelProps={{ htmlFor: 'tags-field-control' }}\n >\n <Field.Body>\n <Field>\n <Control>\n <Taginput\n id=\"tags-field-control\"\n icon=\"tags\"\n defaultValue={['React', 'TypeScript']}\n placeholder=\"Add a tag...\"\n tagColor=\"primary\"\n />\n </Control>\n </Field>\n </Field.Body>\n </Field>\n );\n}"
}
],
"accessibility": "- The `label` prop is automatically associated with the inner text input (`htmlFor` plus a generated `id`, or your own `id` if you pass one). While the label is wired it also names the input; without one, the input falls back to `aria-label=\"Add tag\"`. Inside a labeled `Field`, that `Field`'s label names the input the same way and replaces the fallback.\n- A user-supplied `id` is applied to the inner input (the labellable control), not the wrapper div.\n- When `maxTags` is reached the text input is not rendered, so the label renders without a `for` attribute.\n- Tags are keyboard navigable\n- Delete buttons have proper `aria-label`\n- Autocomplete dropdown follows ARIA listbox pattern",
"accessibility": "- The `label` prop is automatically associated with the inner text input (`htmlFor` plus a generated `id`, or your own `id` if you pass one). While the label is wired it also names the input; without one, the input falls back to `aria-label=\"Add tag\"`. Inside a labeled `Field`, that `Field`'s label names the input the same way and replaces the fallback.\n- A user-supplied `id` is applied to the inner input (the labellable control), not the wrapper div.\n- When `maxTags` is reached the text input is not rendered, so the label renders without a `for` attribute.\n- Tags are keyboard navigable\n- Delete buttons have proper `aria-label`\n- With `data`, the input is a `role=\"combobox\"` with `aria-autocomplete=\"list\"` and `aria-haspopup=\"listbox\"`. Its `aria-expanded` is true only while the suggestion list is on screen, and while it is, `aria-controls` points at the list. While no suggestion matches there's no list, so it stays false. Without `data` there's never a list, so the input stays a plain text field\n- Focus stays in the input, which points `aria-activedescendant` at the highlighted suggestion as the arrow keys or the mouse move it\n- The list is a `role=\"listbox\"` named through `aria-labelledby` by the label that names the input, when that label has an id: the `label` prop's label always has one, and so does a `Field`'s label that the `Field` wires itself. A `Field` label you wire by hand with `labelProps.htmlFor`, which is how the horizontal examples above name the Taginput, needs an `id` in its `labelProps` as well. Otherwise the list falls back to `aria-label=\"Suggestions\"`\n- The list holds only the suggestions, each a `role=\"option\"` with an id of its own, and the highlighted one has `aria-selected=\"true\"`",
"related": ["Autocomplete", "Input", "Tag"],
"storybook": "https://bestax.io/storybook/?path=/story/form-taginput",
"summary": "The `Taginput` component provides a tag/chip input field for managing multiple tags.",
Expand All @@ -75,7 +75,7 @@
"name": "label",
"type": "React.ReactNode",
"default": null,
"description": "Field label. Automatically associated with the text input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Not wired when the tag limit is reached (the input is not rendered). Dropped inside an outer `Field`, whose own label associates instead when that `Field` generates a target id (not `grouped`/`hasAddons`, no explicit `labelProps.htmlFor`) and you set no `id` here; it then replaces the `aria-label=\"Add tag\"` fallback too.",
"description": "Field label. Automatically associated with the text input via `htmlFor` — uses your `id` when provided, otherwise a generated one. Not wired when the tag limit is reached (the input is not rendered). Dropped inside an outer `Field`, whose own label associates instead when that `Field` generates a target id (not `grouped`/`hasAddons`, no explicit `labelProps.htmlFor`) and you set no `id` here; it then replaces the `aria-label=\"Add tag\"` fallback too. With `data`, the label naming the input also names the open suggestion list, through `aria-labelledby`, when it has an id: this label always does, and an outer `Field`'s does unless you wired it by hand without an `id` in its `labelProps`. Otherwise the list is named \"Suggestions\".",
"required": false,
"inherited": false,
"deprecated": false
Expand All @@ -84,7 +84,7 @@
"name": "labelProps",
"type": "React.LabelHTMLAttributes<HTMLLabelElement> & { [key: string]: unknown; }",
"default": null,
"description": "Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then).",
"description": "Props for the label element. An explicit `htmlFor` here overrides the automatic association (no id is generated then). With `data`, while the label names the input, the suggestion list's `aria-labelledby` points at it, through an `id` here when you give one and a generated one otherwise.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down Expand Up @@ -119,8 +119,8 @@
{
"name": "data",
"type": "string[]",
"default": "[]",
"description": "Autocomplete suggestions.",
"default": null,
"description": "Autocomplete suggestions. With `data` set, even to an empty array, the text input is a `role=\"combobox\"` that controls the suggestion list and points `aria-activedescendant` at the highlighted suggestion. Without it there's no list, so the input stays a plain text field.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
4 changes: 2 additions & 2 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"promptName": "form",
"dir": "bestax-form",
"references": [
{ "id": "api", "file": "references/api.md", "bytes": 13008 },
{ "id": "patterns", "file": "references/patterns.md", "bytes": 7509 }
{ "id": "api", "file": "references/api.md", "bytes": 13036 },
{ "id": "patterns", "file": "references/patterns.md", "bytes": 7525 }
],
"examples": []
},
Expand Down
4 changes: 2 additions & 2 deletions bulma-ui/src/__tests__/shadow-root.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('Taginput in a shadow root', () => {
const { getByRole, queryByRole } = renderInShadowRoot(
<Taginput data={['React', 'Vue']} onChange={onChange} />
);
fireEvent.change(getByRole('textbox'), { target: { value: 'r' } });
fireEvent.change(getByRole('combobox'), { target: { value: 'r' } });
const option = getByRole('option', { name: 'React' });

fireEvent.mouseDown(option);
Expand All @@ -145,7 +145,7 @@ describe('Taginput in a shadow root', () => {
<button type="button">Elsewhere</button>
</>
);
fireEvent.change(getByRole('textbox'), { target: { value: 'r' } });
fireEvent.change(getByRole('combobox'), { target: { value: 'r' } });
expect(queryByRole('listbox')).toBeInTheDocument();

fireEvent.mouseDown(getByRole('button', { name: 'Elsewhere' }));
Expand Down
10 changes: 5 additions & 5 deletions bulma-ui/src/form/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ export interface FieldProps
hasAddons?: boolean | 'centered' | 'right';
/** Constrains the field to its content's width (used inside horizontal field bodies). */
narrow?: boolean;
/** Field label, rendered above the widget. Automatically names the one control the Field holds: a composed `InputBase`, `SelectBase`, `TextAreaBase`, `DateInputBase`, `TimeInputBase` or `DateTimeInputBase` (an `inline` picker has no input, so it takes nothing), or a bestax input that renders a single input of its own (`Input`, `Select`, `TextArea`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`, `DateTimeInput`, `Autocomplete`, `Taginput`, `File`), adopts a generated id that the label's `htmlFor` points at, and a group (`Radios`, `Checkboxes`, `Rate`, `DateRangeInput`, or a composed `DateRangeInputBase`, `inline` or not) points `aria-labelledby` at the label's own id unless you gave the group an `aria-label` or `aria-labelledby`. A range `Slider` takes the id on its low thumb and also starts each thumb's name with the label through `aria-labelledby`, unless its `ariaLabel` names that thumb or its own `aria-label` or `aria-labelledby` takes the label's place. An `Autocomplete` also names its open suggestion list after the label through `aria-labelledby`. Nothing else takes the label: `Checkbox`, `Radio` and `Switch` are named by their own children, and the label names one only when you wire it as you would any control with an `id` of its own, `labelProps={{ htmlFor }}` plus that control's `id`. Only the single inputs take the `htmlFor`, so once the Field's content has mounted the label keeps it only while one of them holds the id, and drops it over anything else: a group, a `Checkbox`, `Radio` or `Switch`, an `inline` picker, an inner `Field`, an input with an `id` of its own, markup of your own, or nothing at all. A server render, and the first render while hydrating, still write the `htmlFor`, which keeps hydration matching, and an input that mounts later puts it back. The label names an input with an `id` of your own only through `labelProps={{ htmlFor }}` set to that `id`, a `for` it always keeps; `labelProps={{ htmlFor: undefined }}` opts out. Skipped for `grouped`/`hasAddons` fields (multiple controls), and a nested `Field` starts its own scope, so a horizontal Field whose body holds an inner `Field` names the control there only when you wire it. Two controls in one plain labeled Field would both adopt the id, so give each an `id` of its own. */
/** Field label, rendered above the widget. Automatically names the one control the Field holds: a composed `InputBase`, `SelectBase`, `TextAreaBase`, `DateInputBase`, `TimeInputBase` or `DateTimeInputBase` (an `inline` picker has no input, so it takes nothing), or a bestax input that renders a single input of its own (`Input`, `Select`, `TextArea`, `Numberinput`, `Slider`, `DateInput`, `TimeInput`, `DateTimeInput`, `Autocomplete`, `Taginput`, `File`), adopts a generated id that the label's `htmlFor` points at, and a group (`Radios`, `Checkboxes`, `Rate`, `DateRangeInput`, or a composed `DateRangeInputBase`, `inline` or not) points `aria-labelledby` at the label's own id unless you gave the group an `aria-label` or `aria-labelledby`. A range `Slider` takes the id on its low thumb and also starts each thumb's name with the label through `aria-labelledby`, unless its `ariaLabel` names that thumb or its own `aria-label` or `aria-labelledby` takes the label's place. An `Autocomplete` or a `Taginput` also names its open suggestion list after the label through `aria-labelledby`. Nothing else takes the label: `Checkbox`, `Radio` and `Switch` are named by their own children, and the label names one only when you wire it as you would any control with an `id` of its own, `labelProps={{ htmlFor }}` plus that control's `id`. Only the single inputs take the `htmlFor`, so once the Field's content has mounted the label keeps it only while one of them holds the id, and drops it over anything else: a group, a `Checkbox`, `Radio` or `Switch`, an `inline` picker, an inner `Field`, an input with an `id` of its own, markup of your own, or nothing at all. A server render, and the first render while hydrating, still write the `htmlFor`, which keeps hydration matching, and an input that mounts later puts it back. The label names an input with an `id` of your own only through `labelProps={{ htmlFor }}` set to that `id`, a `for` it always keeps; `labelProps={{ htmlFor: undefined }}` opts out. Skipped for `grouped`/`hasAddons` fields (multiple controls), and a nested `Field` starts its own scope, so a horizontal Field whose body holds an inner `Field` names the control there only when you wire it. Two controls in one plain labeled Field would both adopt the id, so give each an `id` of its own. */
label?: React.ReactNode;
/** Size for the label. */
labelSize?: 'small' | 'normal' | 'medium' | 'large';
/** Props for the label element. An explicit `htmlFor` key — even set to `undefined` — takes over the association. While the association is on, the label renders with the `id` given here, or a generated one, and a group control, a range `Slider`'s thumbs and an `Autocomplete`'s suggestion list point `aria-labelledby` at it. A label you take over gets no generated id, and no group points at it on its own. With an `id` here, though, a range `Slider` or an `Autocomplete` whose `id` your `htmlFor` names still points its thumbs or its suggestion list at it, in a `grouped` or `hasAddons` Field too. To point a group in an inner `Field` at this label by hand, pass that `id`. Nothing in the inner `Field` takes the generated `htmlFor`, so the label drops it after mounting, and adding `htmlFor: undefined` keeps it out of server-rendered markup too. */
/** Props for the label element. An explicit `htmlFor` key — even set to `undefined` — takes over the association. While the association is on, the label renders with the `id` given here, or a generated one, and a group control, a range `Slider`'s thumbs and an `Autocomplete`'s or `Taginput`'s suggestion list point `aria-labelledby` at it. A label you take over gets no generated id, and no group points at it on its own. With an `id` here, though, a range `Slider`, an `Autocomplete` or a `Taginput` whose `id` your `htmlFor` names still points its thumbs or its suggestion list at it, in a `grouped` or `hasAddons` Field too. To point a group in an inner `Field` at this label by hand, pass that `id`. Nothing in the inner `Field` takes the generated `htmlFor`, so the label drops it after mounting, and adding `htmlFor: undefined` keeps it out of server-rendered markup too. */
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement> & {
[key: string]: unknown;
};
Expand Down Expand Up @@ -100,9 +100,9 @@ export interface FieldBodyProps
* `id`, or give the horizontal `Field` a `label` prop, which renders this
* column and its `<label>` for you. A `<label>` you put here names the
* control through its `htmlFor` alone: a range `Slider`'s thumbs and an
* `Autocomplete`'s suggestion list point `aria-labelledby` at a `Field`'s
* label only when its `label` prop renders it and it names the control.
* Where it would not name the control on its own, as in an inner `Field`,
* `Autocomplete`'s or `Taginput`'s suggestion list point `aria-labelledby` at
* a `Field`'s label only when its `label` prop renders it and it names the
* control. Where it would not name the control on its own, as in an inner `Field`,
* pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
*
* @function
Expand Down
3 changes: 2 additions & 1 deletion bulma-ui/src/form/FormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ const FieldLabelForContext = createContext<FieldLabelFor>({});
* renders with, if it has one. An unlabeled Field passes both through
* together, so a control in the inner Field of a horizontal layout can tell
* the outer label names it, and point at that label by its id, as a range
* Slider's thumbs (#981) and Autocomplete's suggestion list (#998) do. The
* Slider's thumbs (#981) and Autocomplete's and Taginput's suggestion lists
* (#998, #1082) do. The
* `htmlFor` is only ever compared with a control's own id, never adopted, so
* it hands out no id for a control. It keeps the generated id after the
* label drops a `for` nothing took (#1004), which no control's id matches
Expand Down
Loading
Loading