Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
63b3170
docs(select): add Select Primitive PDR with purpose, patterns, access…
kawikabader Oct 23, 2025
6a10d3b
docs(select): update PDR with naming conventions, implementation patt…
kawikabader Oct 27, 2025
3b65f52
docs(select): clarify implementation pattern and update core concepts…
kawikabader Oct 27, 2025
236ef88
Merge branch 'main' into select-primitive-pdr
kbader-godaddy Oct 28, 2025
784b785
Merge branch 'main' into select-primitive-pdr
kbader-godaddy Oct 29, 2025
00b872a
docs(select): update Select primitive pdr
kawikabader Oct 29, 2025
29cd16d
docs(select): refine Select primitive documentation for clarity and a…
kawikabader Oct 29, 2025
72a05c6
Merge branch 'main' into select-primitive-pdr
kawikabader Oct 31, 2025
e099926
docs(select): align PDR with orchestration pattern and slot composition
kawikabader Nov 4, 2025
cbbc969
feat(select): add Select component with slot composition and examples
kawikabader Nov 5, 2025
44506c6
refactor(select): simplify hidden select option rendering for readabi…
kawikabader Nov 6, 2025
95f5d6c
docs(select): streamline Select primitive documentation for clarity a…
kawikabader Nov 6, 2025
d08cf1b
refactor(select): adopt container-based composition and enhance docs,…
kawikabader Nov 6, 2025
540c637
chore(select): remove unused dependency on @react-aria/utils from pac…
kawikabader Nov 6, 2025
a3aebe4
feat(select): introduce @bento/select component
kawikabader Nov 13, 2025
10128c5
feat(select): add multi-select support, new examples, and improved st…
kawikabader Nov 13, 2025
28b32e6
chore(select): update configuration files and improve test coverage s…
kawikabader Nov 13, 2025
6006ad2
refactor(select): consolidate examples and migrate from SelectOption …
kawikabader Nov 18, 2025
928b386
docs(select): refine Select primitive documentation, clarify attribut…
kawikabader Nov 18, 2025
1d7f97f
feat(select): add @bento/select with dynamic/grouped examples, improv…
kawikabader Dec 2, 2025
787e22f
fix(use-props): handle falsy values in renderProp function
kawikabader Dec 2, 2025
aaf4a86
refactor(pressable): extract interaction state before useProps
kawikabader Dec 2, 2025
9348b1a
refactor(button): use React Aria hooks directly instead of Pressable
kawikabader Dec 2, 2025
1e5bc94
chore(listbox): simplify example CSS and remove unused value prop
kawikabader Dec 2, 2025
db41418
refactor(select): update data-open attribute handling for improved co…
kawikabader Dec 2, 2025
e900637
refactor(button, select): remove unused React imports and enhance but…
kawikabader Dec 2, 2025
7e4d04c
Merge branch 'main' into select-primitive
kawikabader Dec 2, 2025
ffdffb0
Merge branch 'main' into select-primitive
kawikabader Dec 2, 2025
45347ab
docs: changeset
kawikabader Dec 2, 2025
d036dad
refactor(select, button): remove unused imports and update button pro…
kawikabader Dec 2, 2025
10d01c2
feat(use-props): add type-safe function overloads
kawikabader Dec 3, 2025
02eeeca
test(use-props): add coverage for falsy value handling in renderProp
kawikabader Dec 3, 2025
1aa03bc
refactor(button): single useProps call with explicit slot passthrough
kawikabader Dec 3, 2025
e32e552
refactor(select): use usePopover directly, remove wrapper hook
kawikabader Dec 3, 2025
bae2e48
fix(overlay): update example to use standard ref pattern
kawikabader Dec 3, 2025
5da142b
fix(text): use React.ElementType for as prop
kawikabader Dec 3, 2025
b991e42
chore: normalize package.json formatting
kawikabader Dec 3, 2025
3ab564c
fix(button): add aria-labelledby to slot passthrough props
kawikabader Dec 3, 2025
3e31e9c
test(select): add 100% coverage for example components
kawikabader Dec 3, 2025
cb65324
fix(select): improve ref handling in Popover component
kawikabader Dec 3, 2025
9952b25
refactor(pressable): merge slot props for improved React Aria hook in…
kawikabader Dec 3, 2025
576bd49
feat(button): improve examples and enhance prop handling in Button co…
kawikabader Dec 8, 2025
5a88b02
refactor(button): remove unused ButtonInFormExample from test suite
kawikabader Dec 8, 2025
47b3b13
Merge main into select-primitive
kawikabader Dec 9, 2025
17079f5
fix(button): improve test coverage for render prop example
kawikabader Dec 9, 2025
160d9e2
refactor(button): streamline Button implementation and improve prop h…
kawikabader Dec 15, 2025
8c40328
refactor(pressable): enhance Pressable component with forwarded ref a…
kawikabader Dec 15, 2025
412343b
refactor(select): improve type handling and add ref forwarding to Select
kawikabader Dec 15, 2025
8127c4d
refactor: update package imports and add watch script to multiple pac…
kawikabader Dec 15, 2025
78add36
Merge branch 'main' into select-primitive
kawikabader Dec 15, 2025
63e436d
refactor(select): add renderItem for dynamic collections and improve …
kawikabader Dec 15, 2025
32b3dad
refactor(listbox): update prop handling for render prop support in Li…
kawikabader Dec 15, 2025
91901fe
Merge branch 'main' into select-primitive
kbader-godaddy Dec 18, 2025
7bc29e9
refactor(select): streamline dynamic collection handling in examples …
kawikabader Dec 18, 2025
62d6ff2
refactor(button): enhance Button component with internal ref handling…
kawikabader Dec 18, 2025
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
29 changes: 29 additions & 0 deletions .changeset/mighty-cats-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@bento/visually-hidden": minor
"@bento/use-svg-sprite": minor
"@bento/illustration": minor
"@bento/field-error": minor
"@bento/scroll-lock": minor
"@bento/focus-lock": minor
"@bento/container": minor
"@bento/pressable": minor
"@bento/use-props": minor
"@bento/checkbox": minor
"@bento/dismiss": minor
"@bento/divider": minor
"@bento/forward": minor
"@bento/heading": minor
"@bento/listbox": minor
"@bento/overlay": minor
"@bento/button": minor
"@bento/portal": minor
"@bento/select": minor
"@bento/error": minor
"@bento/radio": minor
"@bento/slots": minor
"@bento/icon": minor
"@bento/text": minor
"@bento/box": minor
---

Add @bento/select primitive. Includes Button refactor for forwardRef support and renderProp falsy value fix.
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@bento/pressable": "*",
"@bento/radio": "*",
"@bento/scroll-lock": "*",
"@bento/select": "*",
"@bento/slots": "*",
"@bento/storybook-addon-helpers": "*",
"@bento/svg-parser": "*",
Expand Down
43 changes: 35 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 36 additions & 8 deletions packages/button/examples/button.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import { Button } from '@bento/button';
/* v8 ignore next */
import React from 'react';
import { Button, type ButtonProps } from '@bento/button';

export function ButtonExample() {
export function ButtonExample(props: ButtonProps) {
return <Button {...props}>{props.children || 'Click me'}</Button>;
}

export function ButtonWithAriaExample() {
return (
<Button
onPress={function handlePress() {
console.log('button pressed!');
}}
>
Click me!
<Button aria-label="Close dialog" aria-expanded="false" aria-haspopup="dialog">
Close
</Button>
);
}

export function ButtonWithDataAttributesExample() {
return (
<Button data-testid="my-button" data-foo="bar" data-select-trigger="true">
Trigger
</Button>
);
}

export function ButtonInFormExample() {
return (
<form id="test-form">
<Button type="submit" form="test-form" name="action" value="submit">
Submit
</Button>
</form>
);
}

export function DisabledButtonExample() {
return <Button isDisabled>Disabled Button</Button>;
}

export function ButtonWithRenderPropExample() {
return (
<Button onPress={() => console.log('pressed')}>{({ isHovered }) => (isHovered ? 'Hover!' : 'Click me')}</Button>
);
}
4 changes: 3 additions & 1 deletion packages/button/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"module": "./dist/index.js",
"scripts": {
"build": "tsup-node",
"dev": "tsup-node --watch",
"lint": "biome lint && tsc",
"posttest": "npm run lint",
"prepublishOnly": "node ../../scripts/compile-readme.ts",
Expand Down Expand Up @@ -41,9 +42,10 @@
"package.json"
],
"dependencies": {
"@bento/pressable": "^0.1.3",
"@bento/slots": "^0.2.0",
"@bento/use-data-attributes": "^0.1.0",
"@bento/use-props": "^0.2.0",
"@react-aria/utils": "^3.30.0",
"react-aria": "^3.44.0"
},
"peerDependencies": {
Expand Down
118 changes: 91 additions & 27 deletions packages/button/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,102 @@
import { useProps } from '@bento/use-props';
import { withSlots } from '@bento/slots';
import { Pressable, type PressableProps } from '@bento/pressable';
import { useButton } from 'react-aria';
import React, { ComponentProps } from 'react';
import { useProps } from '@bento/use-props';
import { useDataAttributes } from '@bento/use-data-attributes';
import { useButton, useFocusRing, useHover, mergeProps, type AriaButtonProps, type HoverEvents } from 'react-aria';
import { mergeRefs } from '@react-aria/utils';
/* v8 ignore next */
import React from 'react';

export interface ButtonProps
extends Omit<PressableProps, 'children'>,
Omit<ComponentProps<'button'>, keyof PressableProps> {
/** A ref to the button element. This is useful if you want to access the button element directly. */
childRef?: React.Ref<HTMLButtonElement>;

extends Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>,
HoverEvents,
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof AriaButtonProps | 'children'> {
/** The content to display inside the button. */
children: React.ReactNode;
children: React.ReactNode | ((props: ButtonRenderProps) => React.ReactNode);
}

export interface ButtonRenderProps {
/** Whether the button is currently pressed. */
isPressed: boolean;
/** Whether the button is currently hovered. */
isHovered: boolean;
/** Whether the button is focused. */
isFocused: boolean;
/** Whether the button is keyboard focused. */
isFocusVisible: boolean;
}

/**
* A complete button component built on top of the Pressable primitive.
* Renders as a native button element with all accessibility and interaction features.
* A button component built on React Aria's useButton.
*
* @example
* <Button onPress={() => console.log('pressed')}>Click me</Button>
*
* @example
* ```tsx
* <Button onPress={() => console.log('Button pressed!')}>Click me</Button>
* ```
* <Button>{({ isPressed }) => isPressed ? 'Pressing...' : 'Click me'}</Button>
*/
export const Button = withSlots('BentoButton', function Button(args: ButtonProps) {
const { props } = useProps(args);
const { children, childRef, ...restProps } = props;
const { buttonProps } = useButton(restProps, childRef);

return (
<Pressable {...restProps} slot="pressable">
<button {...buttonProps} ref={childRef}>
{children}
export const Button = withSlots(
'BentoButton',
function Button(args: ButtonProps, forwardedRef: React.Ref<HTMLButtonElement>) {
// First pass: merge slot props so React Aria hooks see complete props
const { props: mergedProps, ref: mergedRef } = useProps(args, {}, forwardedRef);
const buttonRef = React.useRef<HTMLButtonElement>(null);

// React Aria hooks receive slot-merged props and internal ref
const { buttonProps, isPressed } = useButton(mergedProps, buttonRef);
const { focusProps, isFocused, isFocusVisible } = useFocusRing(mergedProps);
const { hoverProps, isHovered } = useHover(mergedProps);

// Render state for children render function
const renderState: ButtonRenderProps = {
isPressed,
isHovered,
isFocused,
isFocusVisible
};

// Execute children render prop explicitly (before useProps to prevent incorrect execution)
const content = typeof args.children === 'function' ? args.children(renderState) : args.children;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here we are deviating from how Bento handles the "function" props, no? https://github.com/godaddy/bento/blob/main/packages/use-props/src/index.ts#L221-L226

// from this
<button>(({ props, state }) => ...)</button>

// to this
<button>(({ isHovered, isPressed, ... }) => ...)</button>

Not sure if exposing those states in the second argument of useProps is the right call but I'd look into it:

const { props: mergedProps, ref: mergedRef } = useProps(args, renderState, forwardedRef);


// Second pass: apply user props with render state via apply()
// Apply merges React Aria props as defaults that slots can override
const { apply } = useProps(args, renderState);

const dataAttrs = useDataAttributes({
pressed: isPressed,
hovered: isHovered,
focused: isFocused,
focusVisible: isFocusVisible,
disabled: mergedProps.isDisabled
});

return (
<button
{...apply(
{
...mergeProps(buttonProps, focusProps, hoverProps)
},
[
'ref',
'children',
'isDisabled',
'excludeFromTabOrder',
'preventFocusOnPress',
'onPress',
'onPressStart',
'onPressEnd',
'onPressUp',
'onPressChange',
'onHoverStart',
'onHoverEnd',
'onHoverChange',
'onFocusChange'
]
)}
{...dataAttrs}
ref={mergeRefs(buttonRef, mergedRef)}
>
{content}
</button>
</Pressable>
);
});
);
}
);
Loading