Skip to content
Merged
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/Avatar.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"name": "as",
"type": "React.ElementType",
"default": null,
"description": "Element/component to render as. Defaults to `'a'` when `href` is set, else `'figure'`. This also decides whether the avatar is treated as interactive, which is what keeps `role=\"img\"` and the `alt=\"\"` decorative opt-out off a link or button. A custom component counts — a router link takes `to` rather than this component's `href`, so its own props cannot say — while `'a'`, `'button'`, and a custom element given an `href` count for the reason they read. If your custom component renders something that really is just a picture, say so with `role=\"img\"` and it is treated as one, `alt=\"\"` included. A truthy `aria-hidden` says it too, and so do `role=\"presentation\"` and `role=\"none\"` — though ARIA's own conflict resolution drops those two whenever the avatar still carries a name, so prefer `role=\"img\"`. A role claiming the opposite, such as `\"button\"`, says nothing here, and neither does an `href`: that settles it on its own. A genuine `'a'`/`'button'`/`href` avatar keeps its accessible name either way.",
"description": "Element/component to render as. Defaults to `'a'` when `href` is set, else `'figure'`. This also decides whether the avatar is treated as interactive, which is what keeps `role=\"img\"` and the `alt=\"\"` decorative opt-out off a link or button. A custom component counts — a router link takes `to` rather than this component's `href`, so its own props cannot say — while `'a'`, `'button'`, and a custom element given an `href` count for the reason they read. If your custom component renders something that really is just a picture, say so with `role=\"img\"` and it is treated as one, `alt=\"\"` included. A truthy `aria-hidden` says it too, and so do `role=\"presentation\"` and `role=\"none\"` — though ARIA's own conflict resolution drops those two whenever the avatar still carries a name, so prefer `role=\"img\"`. A role claiming the opposite, such as `\"button\"`, says nothing here, and neither does an `href`: that settles it on its own. A genuine `'a'`/`'button'`/`href` avatar keeps its accessible name either way. `'button'` renders `type=\"button\"` unless you pass `type=\"submit\"` or `type=\"reset\"`, so an avatar inside a form does not submit it.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/Avatars.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"name": "as",
"type": "React.ElementType",
"default": null,
"description": "Element/component to render as. Defaults to `'a'` when `href` is set, else `'figure'`. This also decides whether the avatar is treated as interactive, which is what keeps `role=\"img\"` and the `alt=\"\"` decorative opt-out off a link or button. A custom component counts — a router link takes `to` rather than this component's `href`, so its own props cannot say — while `'a'`, `'button'`, and a custom element given an `href` count for the reason they read. If your custom component renders something that really is just a picture, say so with `role=\"img\"` and it is treated as one, `alt=\"\"` included. A truthy `aria-hidden` says it too, and so do `role=\"presentation\"` and `role=\"none\"` — though ARIA's own conflict resolution drops those two whenever the avatar still carries a name, so prefer `role=\"img\"`. A role claiming the opposite, such as `\"button\"`, says nothing here, and neither does an `href`: that settles it on its own. A genuine `'a'`/`'button'`/`href` avatar keeps its accessible name either way.",
"description": "Element/component to render as. Defaults to `'a'` when `href` is set, else `'figure'`. This also decides whether the avatar is treated as interactive, which is what keeps `role=\"img\"` and the `alt=\"\"` decorative opt-out off a link or button. A custom component counts — a router link takes `to` rather than this component's `href`, so its own props cannot say — while `'a'`, `'button'`, and a custom element given an `href` count for the reason they read. If your custom component renders something that really is just a picture, say so with `role=\"img\"` and it is treated as one, `alt=\"\"` included. A truthy `aria-hidden` says it too, and so do `role=\"presentation\"` and `role=\"none\"` — though ARIA's own conflict resolution drops those two whenever the avatar still carries a name, so prefer `role=\"img\"`. A role claiming the opposite, such as `\"button\"`, says nothing here, and neither does an `href`: that settles it on its own. A genuine `'a'`/`'button'`/`href` avatar keeps its accessible name either way. `'button'` renders `type=\"button\"` unless you pass `type=\"submit\"` or `type=\"reset\"`, so an avatar inside a form does not submit it.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
4 changes: 2 additions & 2 deletions bestax-mcp/data/components/Dropdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"code": "<Dropdown label=\"Dropdown Menu\" active>\n <Dropdown.Item>First Item</Dropdown.Item>\n <Dropdown.Item>Second Item</Dropdown.Item>\n <Dropdown.Divider />\n <Dropdown.Item>Third Item</Dropdown.Item>\n</Dropdown>"
}
],
"accessibility": "- The dropdown root is a `<div class=\"dropdown\">` with ARIA roles/attributes for menu and trigger.\n- The trigger button uses `aria-haspopup`, `aria-controls`, and `aria-expanded`.\n- Menu items are focusable and use `role=\"menuitem\"`. An item given `role=\"menuitemcheckbox\"` or `role=\"menuitemradio\"` stays in the arrow-key order.\n- Clicking outside closes the dropdown in most cases.\n\n`Dropdown` implements the [WAI-ARIA Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)\nout of the box:\n\n- On the trigger: <kbd>ArrowDown</kbd> or <kbd>Enter</kbd>/<kbd>Space</kbd> open the menu and\n focus the first item; <kbd>ArrowUp</kbd> opens the menu and focuses the last item;\n <kbd>Escape</kbd> closes an open menu.\n- Inside the menu: <kbd>ArrowDown</kbd>/<kbd>ArrowUp</kbd> move between items and wrap around;\n <kbd>Home</kbd>/<kbd>End</kbd> jump to the first/last item; <kbd>Escape</kbd> closes the menu\n and returns focus to the trigger; <kbd>Tab</kbd> closes the menu and lets focus continue\n naturally.\n- On an item: <kbd>Enter</kbd> or <kbd>Space</kbd> activates it the way a click does, so its\n `onClick` runs and the menu closes unless `closeOnClick` is `false`. That includes the\n default anchor without an `href` and `as=\"div\"`, which the browser would not activate on\n its own.\n- Disabled items (a `Dropdown.Item` rendered with a native `disabled` attribute or\n `aria-disabled=\"true\"`) and dividers are skipped during arrow-key navigation.\n\n:::note\nThere is no typeahead and no built-in outside-click handling inside the menu beyond the\nexisting click-outside-to-close behavior — add those yourself if your use case needs them.\n:::",
"accessibility": "- The dropdown root is a `<div class=\"dropdown\">` with ARIA roles/attributes for menu and trigger.\n- The trigger button uses `aria-haspopup`, `aria-controls`, and `aria-expanded`.\n- Menu items are focusable and use `role=\"menuitem\"`. An item given `role=\"menuitemcheckbox\"` or `role=\"menuitemradio\"` stays in the arrow-key order.\n- `Dropdown.Item as=\"button\"` defaults to `type=\"button\"`, so a menu item inside a form doesn't\n submit it. Pass `type=\"submit\"` or `type=\"reset\"` and yours is used instead; any other value\n renders `type=\"button\"`.\n- Clicking outside closes the dropdown in most cases.\n\n`Dropdown` implements the [WAI-ARIA Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)\nout of the box:\n\n- On the trigger: <kbd>ArrowDown</kbd> or <kbd>Enter</kbd>/<kbd>Space</kbd> open the menu and\n focus the first item; <kbd>ArrowUp</kbd> opens the menu and focuses the last item;\n <kbd>Escape</kbd> closes an open menu.\n- Inside the menu: <kbd>ArrowDown</kbd>/<kbd>ArrowUp</kbd> move between items and wrap around;\n <kbd>Home</kbd>/<kbd>End</kbd> jump to the first/last item; <kbd>Escape</kbd> closes the menu\n and returns focus to the trigger; <kbd>Tab</kbd> closes the menu and lets focus continue\n naturally.\n- On an item: <kbd>Enter</kbd> or <kbd>Space</kbd> activates it the way a click does, so its\n `onClick` runs and the menu closes unless `closeOnClick` is `false`. That includes the\n default anchor without an `href` and `as=\"div\"`, which the browser would not activate on\n its own.\n- Disabled items (a `Dropdown.Item` rendered with a native `disabled` attribute or\n `aria-disabled=\"true\"`) and dividers are skipped during arrow-key navigation.\n\n:::note\nThere is no typeahead and no built-in outside-click handling inside the menu beyond the\nexisting click-outside-to-close behavior — add those yourself if your use case needs them.\n:::",
"related": ["Button", "useBulmaClasses"],
"storybook": "https://bestax.io/storybook/?path=/story/components-dropdown--default",
"summary": "The `Dropdown` component provides Bulma's versatile dropdown menu for your Bulma React UI.",
Expand Down Expand Up @@ -183,7 +183,7 @@
"name": "as",
"type": "'a' | 'div' | 'button'",
"default": "'a'",
"description": "The element type to render.",
"description": "The element type to render. `'button'` renders `type=\"button\"` unless you pass `type=\"submit\"` or `type=\"reset\"`, so an item inside a form does not submit it.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
4 changes: 2 additions & 2 deletions bestax-mcp/data/components/Menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"code": "<Menu>\n <Menu.Label>General</Menu.Label>\n <Menu.List>\n <Menu.Item active>Dashboard</Menu.Item>\n <Menu.Item>Customers</Menu.Item>\n </Menu.List>\n</Menu>"
}
],
"accessibility": "- The root `Menu` renders as `<aside class=\"menu\">`.\n- Use semantic nesting (`Menu.List` inside `Menu.Item`) for submenus.\n- Ensure each item is focusable and accessible if interactive.\n\n:::note\nFor router integrations, pass the correct `as` and `to`/`href` props to `Menu.Item`.\n:::",
"accessibility": "- The root `Menu` renders as `<aside class=\"menu\">`.\n- Use semantic nesting (`Menu.List` inside `Menu.Item`) for submenus.\n- Ensure each item is focusable and accessible if interactive.\n- `Menu.Item as=\"button\"` defaults to `type=\"button\"`, so a menu item inside a form doesn't\n submit it. Pass `type=\"submit\"` or `type=\"reset\"` and yours is used instead; any other value\n renders `type=\"button\"`.\n\n:::note\nFor router integrations, pass the correct `as` and `to`/`href` props to `Menu.Item`.\n:::",
"related": ["Panel", "Sidebar", "useBulmaClasses"],
"storybook": "https://bestax.io/storybook/?path=/story/components-menu--basic",
"summary": "The `Menu` component provides Bulma's vertical navigation menu: a simple, accessible sidebar or section menu for your Bulma React UI.",
Expand Down Expand Up @@ -131,7 +131,7 @@
"name": "as",
"type": "React.ElementType",
"default": "'a'",
"description": "Custom link component (e.g. `Link` from router).",
"description": "Custom link component (e.g. `Link` from router). `'button'` renders `type=\"button\"` unless you pass `type=\"submit\"` or `type=\"reset\"`, so an item inside a form does not submit it.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
45 changes: 38 additions & 7 deletions bulma-ui/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { classNames, usePrefixedClassNames } from '../helpers/classNames';
import { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';
import { buttonType } from '../helpers/buttonType';
import {
isCustomElement,
type PolymorphicComponent,
Expand Down Expand Up @@ -235,6 +236,9 @@ export type AvatarProps<T extends React.ElementType = 'figure'> = Omit<
* `role="img"`. A role claiming the opposite, such as `"button"`, says nothing here, and
* neither does an `href`: that settles it on its own. A genuine `'a'`/`'button'`/`href`
* avatar keeps its accessible name either way.
*
* `'button'` renders `type="button"` unless you pass `type="submit"` or `type="reset"`, so
* an avatar inside a form does not submit it.
*/
as?: T;
};
Expand All @@ -248,6 +252,8 @@ type AvatarImplProps = AvatarOwnProps & {
as?: React.ElementType;
role?: React.AriaRole;
'aria-hidden'?: React.AriaAttributes['aria-hidden'];
'aria-label'?: string;
type?: React.ButtonHTMLAttributes<HTMLButtonElement>['type'];
};

/**
Expand Down Expand Up @@ -465,32 +471,57 @@ export const Avatar = forwardRef(function Avatar(
// button must always expose an accessible name.
const isDecorative = alt === '' && !isInteractive;

const a11yProps = showImage
const a11yDefaults: {
'aria-label'?: string;
role?: 'img';
'aria-hidden'?: true;
} = showImage
? isInteractive && !accessibleName
? // The img alt normally names the control; with no alt/name (an API
// returning only a photo URL) the link/button would be nameless.
{ 'aria-label': name || 'Avatar' }
: {}
: isDecorative
? { 'aria-hidden': true as const }
? { 'aria-hidden': true }
: {
...(isInteractive ? {} : { role: 'img' as const }),
'aria-label': accessibleName || name || 'Avatar',
};

// A clickable avatar inside a form must not submit it; default the native
// button type (an explicit type passed through rest still wins).
const buttonTypeProps = Tag === 'button' ? { type: 'button' as const } : {};
// Each default yields to a value the caller passed, and is applied after
// `rest` reading through it rather than spread before it. React treats an
// `undefined` attribute as "remove it", and a spread carrying the key with no
// value is how that arrives, so a default spread first was erased by it: an
// `aria-label: undefined` left a button avatar nameless, and `role` and
// `aria-hidden` went the same way. The button `type` below has the same
// shape (#690).
const a11yProps = Object.fromEntries(
Object.entries(a11yDefaults).map(([key, fallback]) => [
key,
rest[key as keyof typeof a11yDefaults] ?? fallback,
])
);

return (
<Tag
ref={ref}
className={combinedClasses}
style={{ ...sizeStyle, ...style }}
{...buttonTypeProps}
{...linkProps}
{...a11yProps}
{...rest}
{...a11yProps}
// A clickable avatar inside a form must not submit it, and `<button>`
// defaults to type="submit". A caller's `submit` or `reset` still wins.
// Anything else becomes `button`, since HTML reads a value it does not
// define for a button as submit too; `buttonType` says more.
//
// After `rest`, reading through it rather than before it: React treats
// `type={undefined}` as "remove the attribute", and a spread carrying the
// key with no value is how that arrives. Spreading the default first let
// such a spread erase it (#690), so the guard only held for callers who
// passed nothing. `Dropdown.Item` and `Menu.Item` apply their button
// type after the spread too.
{...(Tag === 'button' ? { type: buttonType(rest.type) } : {})}
>
{showImage && (
<img
Expand Down
14 changes: 4 additions & 10 deletions bulma-ui/src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ANCHOR_ONLY_ATTRS,
omitAttrs,
} from '../helpers/anchorAttrs';
import { buttonType } from '../helpers/buttonType';
import {
useBulmaClasses,
BulmaClassesProps,
Expand Down Expand Up @@ -686,7 +687,6 @@ const CardFooterItem: React.FC<CardFooterItemProps> = ({
const forwarded = omitAttrs(rest, STRIP_FROM_NON_ANCHOR);

if (as === 'button') {
const forwardedType = (forwarded as { type?: string }).type;
return (
<button
className={itemClasses}
Expand All @@ -699,15 +699,9 @@ const CardFooterItem: React.FC<CardFooterItemProps> = ({
// typed here as the `<a>` MIME string (`STRIP_FROM_NON_ANCHOR` above
// says why it is not withheld), so `as="button" type="text/html"`
// compiles, and HTML's INVALID-value default for a button's `type` is
// submit too. So anything that is not one of the three native button
// types falls back to `button` rather than being forwarded.
type={
forwardedType === 'submit' ||
forwardedType === 'reset' ||
forwardedType === 'button'
? forwardedType
: 'button'
}
// submit too. `buttonType` keeps `button`, `submit` and `reset` and
// turns anything else into `button`.
type={buttonType((forwarded as { type?: string }).type)}
>
{children}
</button>
Expand Down
24 changes: 12 additions & 12 deletions bulma-ui/src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ANCHOR_ONLY_ATTRS,
omitAttrs,
} from '../helpers/anchorAttrs';
import { buttonType } from '../helpers/buttonType';

/**
* Checks if code is running in a browser environment.
Expand Down Expand Up @@ -445,7 +446,12 @@ export type DropdownItemProps<
T extends DropdownItemElement = DropdownItemElement,
> = DropdownItemOwnProps &
Omit<React.ComponentPropsWithoutRef<T>, keyof DropdownItemOwnProps | 'as'> & {
/** The element type to render. */
/**
* The element type to render.
*
* `'button'` renders `type="button"` unless you pass `type="submit"` or `type="reset"`, so
* an item inside a form does not submit it.
*/
as?: T;
};

Expand Down Expand Up @@ -540,24 +546,18 @@ export const DropdownItem = ((itemProps: DropdownItemProps) => {
role={(forwarded as { role?: React.AriaRole }).role ?? 'menuitem'}
// A menu item inside a form must not submit it. `<button>` defaults to
// type="submit", and a filter or sort menu sitting in a form is ordinary.
// Dropdown's own trigger sets it. `Avatar` and `Menu.Item` do NOT get this
// right — Avatar spreads its default BEFORE `rest`, so `type={undefined}`
// arriving through a spread erases it, and Menu defaults none at all.
// Dropdown's own trigger sets it, and `Avatar` and `Menu.Item` default it
// the same way. A caller's `submit` or `reset` still wins, and anything
// else becomes `button`, since HTML reads a value it does not define for
// a button as submit too; `buttonType` says more.
//
// After `forwarded`, reading through it rather than before it: React
// treats `type={undefined}` as "remove the attribute", and a spread
// carrying an absent key is how that arrives. Spreading the default first
// let such a spread erase it and restore the submit behaviour, so the
// guard only held for callers who passed nothing.
{...(Component === 'button'
? {
type:
(
forwarded as {
type?: React.ButtonHTMLAttributes<HTMLButtonElement>['type'];
}
).type ?? 'button',
}
? { type: buttonType((forwarded as { type?: unknown }).type) }
: {})}
>
{children}
Expand Down
Loading
Loading