Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10503ae
WIP feat(checkbox)/adds checkbox component in ui with story and unit …
sachin-thakur-bruno Aug 3, 2026
b57a055
updated nord border color
sachin-thakur-bruno Aug 3, 2026
1a70b70
updated check icon path
sachin-thakur-bruno Aug 3, 2026
c4e3c81
added indeterminate prop and story for it
sachin-thakur-bruno Aug 4, 2026
00e3412
removed default test id
sachin-thakur-bruno Aug 4, 2026
76be3fe
updated radius
sachin-thakur-bruno Aug 4, 2026
ef81c5d
memoize the refs
sachin-thakur-bruno Aug 4, 2026
74fd445
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 4, 2026
0aa7043
updated tests
sachin-thakur-bruno Aug 4, 2026
5cce5dd
Merge branch 'main' into chore/checkbox-ui
sachin-thakur-bruno Aug 5, 2026
d290395
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 12, 2026
aa97b28
Merge branch 'chore/checkbox-ui' of github.com:sachin-thakur-bruno/br…
sachin-thakur-bruno Aug 12, 2026
4efb610
used use layout effect
sachin-thakur-bruno Aug 12, 2026
994e47e
removed bprder 3
sachin-thakur-bruno Aug 24, 2026
d35525e
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 24, 2026
9659b61
added border 3 for dark and light themes
sachin-thakur-bruno Aug 24, 2026
cbef029
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 26, 2026
9d8779d
Merge remote-tracking branch 'upstream/main' into chore/checkbox-ui
sachin-thakur-bruno Aug 26, 2026
2de1bc2
updated intermediate
sachin-thakur-bruno Aug 26, 2026
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
201 changes: 201 additions & 0 deletions packages/bruno-app/src/ui/Checkbox/Checkbox.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import '@testing-library/jest-dom';
import React, { createRef, useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from 'styled-components';
import Checkbox from './index';

const theme = {
mode: 'light',
text: '#343434',
border: { border3: '#B1B1B1' },
primary: { solid: '#D37F17' },
button2: { color: { primary: { text: '#ffffff' } } },
font: { size: { sm: '0.75rem' } }
};

const renderWithTheme = (ui) => render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);

const renderCheckbox = (props = {}) => {
const hasAccessibleName = 'label' in props || 'ariaLabel' in props || 'ariaLabelledBy' in props;
return renderWithTheme(
<Checkbox onChange={() => {}} {...(hasAccessibleName ? {} : { ariaLabel: 'Checkbox' })} {...props} />
);
};

describe('Checkbox', () => {
let user;

beforeEach(() => {
user = userEvent.setup();
});

it('renders unchecked by default', () => {
renderCheckbox();
expect(screen.getByRole('checkbox')).not.toBeChecked();
});

it('reflects the checked prop', () => {
renderCheckbox({ checked: true });
expect(screen.getByRole('checkbox')).toBeChecked();
});

it('calls onChange when clicked', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange });

await user.click(screen.getByRole('checkbox'));

expect(onChange).toHaveBeenCalledTimes(1);
});

it('does not call onChange when disabled', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange, disabled: true });

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeDisabled();
await user.click(checkbox);

expect(onChange).not.toHaveBeenCalled();
});

it('updates when driven as a controlled component', async () => {
const Controlled = () => {
const [checked, setChecked] = useState(false);
return <Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />;
};
renderWithTheme(<Controlled />);

const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();

await user.click(checkbox);
expect(checkbox).toBeChecked();

await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});

it('renders the optional label and associates it with the input', () => {
renderCheckbox({ label: 'Accept terms' });

expect(screen.getByText('Accept terms')).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Accept terms' })).toBeInTheDocument();
});

it('toggles via keyboard (space)', async () => {
const onChange = jest.fn();
renderCheckbox({ onChange });

const checkbox = screen.getByRole('checkbox');
checkbox.focus();
await user.keyboard(' ');

expect(onChange).toHaveBeenCalledTimes(1);
});

it('applies the data-testid to the input', () => {
renderCheckbox({ 'data-testid': 'my-checkbox' });
expect(screen.getByTestId('my-checkbox')).toBe(screen.getByRole('checkbox'));
});

it('forwards the ref to the input element', () => {
const ref = createRef();
renderCheckbox({ ref });

expect(ref.current).toBe(screen.getByRole('checkbox'));
});

it('forwards additional props to the input', () => {
renderCheckbox({ required: true, name: 'terms' });

const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeRequired();
expect(checkbox).toHaveAttribute('name', 'terms');
});

describe('indeterminate', () => {
it('sets the DOM indeterminate property when the prop is true', () => {
renderCheckbox({ indeterminate: true });
expect(screen.getByRole('checkbox').indeterminate).toBe(true);
});

it('does not set the DOM indeterminate property by default', () => {
renderCheckbox();
expect(screen.getByRole('checkbox').indeterminate).toBe(false);
});

it('clears the DOM indeterminate property when the prop turns false', () => {
const { rerender } = renderCheckbox({ indeterminate: true });
expect(screen.getByRole('checkbox').indeterminate).toBe(true);

rerender(
<ThemeProvider theme={theme}>
<Checkbox onChange={() => {}} ariaLabel="Checkbox" indeterminate={false} />
</ThemeProvider>
);

expect(screen.getByRole('checkbox').indeterminate).toBe(false);
});

it('re-applies the DOM property on re-render even though the browser clears it on click', async () => {
const onChange = jest.fn();
const { rerender } = renderCheckbox({ indeterminate: true, onChange });

const checkbox = screen.getByRole('checkbox');
await user.click(checkbox);
// Clicking a native checkbox always resets `indeterminate` to false in the browser,
// regardless of what the app does with the click. This is the behavior our effect
// (re-applied on every render, no dependency array) must correct for.
checkbox.indeterminate = false;

rerender(
<ThemeProvider theme={theme}>
<Checkbox onChange={onChange} ariaLabel="Checkbox" indeterminate={true} />
</ThemeProvider>
);

expect(checkbox.indeterminate).toBe(true);
});

it('re-asserts the DOM property from the change handler even when the click causes no rerender', async () => {
const onChange = jest.fn();
renderCheckbox({ indeterminate: true, onChange });

const checkbox = screen.getByRole('checkbox');
expect(checkbox.indeterminate).toBe(true);

checkbox.indeterminate = false;
await user.click(checkbox);

expect(onChange).toHaveBeenCalledTimes(1);
expect(checkbox.indeterminate).toBe(true);
});

it('still forwards the ref to the input element when indeterminate', () => {
const ref = createRef();
renderCheckbox({ indeterminate: true, ref });

expect(ref.current).toBe(screen.getByRole('checkbox'));
expect(ref.current.indeterminate).toBe(true);
});
});

describe('accessible name', () => {
it('is derived from ariaLabel when there is no visible label', () => {
renderCheckbox({ ariaLabel: 'Select all' });
expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeInTheDocument();
});

it('is derived from an external element via ariaLabelledBy', () => {
renderWithTheme(
<>
<span id="ext-label">Select all</span>
<Checkbox onChange={() => {}} ariaLabelledBy="ext-label" />
</>
);
expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeInTheDocument();
});
});
});
154 changes: 154 additions & 0 deletions packages/bruno-app/src/ui/Checkbox/Checkbox.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import Checkbox from './index';

export default {
title: 'Components/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['sm', 'md', 'lg', 'xl'],
description: 'The size of the checkbox'
},
checked: {
control: 'boolean',
description: 'Whether the checkbox is checked'
},
disabled: {
control: 'boolean',
description: 'Whether the checkbox is disabled'
},
indeterminate: {
control: 'boolean',
description: 'Tri-state "some, not all" visual, e.g. for a "select all" checkbox. Does not affect `checked`.'
},
label: {
control: 'text',
description: 'Optional label rendered next to the checkbox'
},
ariaLabel: {
control: 'text',
description: 'Accessible name when there is no visible label (e.g. icon-only checkboxes)'
},
ariaLabelledBy: {
control: 'text',
description: 'ID of an external element to use as the accessible name, instead of ariaLabel'
},
onChange: { action: 'changed' }
}
};

const Controlled = (args) => {
const [checked, setChecked] = useState(!!args.checked);
return <Checkbox {...args} checked={checked} onChange={(e) => setChecked(e.target.checked)} />;
};

export const Default = {
render: (args) => <Controlled {...args} />,
args: {
label: 'Accept terms and conditions'
}
};

export const Sizes = {
tags: ['!dev'],
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<Checkbox size="sm" checked label="Small" onChange={() => {}} />
<Checkbox size="md" checked label="Medium" onChange={() => {}} />
<Checkbox size="lg" checked label="Large" onChange={() => {}} />
<Checkbox size="xl" checked label="Extra Large" onChange={() => {}} />
</div>
)
};

export const States = {
tags: ['!dev'],
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<Checkbox label="Unchecked" checked={false} onChange={() => {}} />
<Checkbox label="Checked" checked onChange={() => {}} />
<Checkbox label="Disabled unchecked" checked={false} disabled onChange={() => {}} />
<Checkbox label="Disabled checked" checked disabled onChange={() => {}} />
</div>
)
};

export const WithoutLabel = {
tags: ['!dev'],
args: {
checked: true
},
render: (args) => <Controlled {...args} />
};

export const Indeterminate = {
tags: ['!dev'],
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<Checkbox size="sm" indeterminate label="Small" onChange={() => {}} />
<Checkbox size="md" indeterminate label="Medium" onChange={() => {}} />
<Checkbox size="lg" indeterminate label="Large" onChange={() => {}} />
<Checkbox size="xl" indeterminate label="Extra Large" onChange={() => {}} />
</div>
)
};

const SelectAllDemo = ({ size, disabled, label = 'Select all' }) => {
const [items, setItems] = useState([
{ id: 1, label: 'Item 1', selected: true },
{ id: 2, label: 'Item 2', selected: false },
{ id: 3, label: 'Item 3', selected: false }
]);

const selectedCount = items.filter((item) => item.selected).length;
const allSelected = selectedCount === items.length;
const someSelected = selectedCount > 0 && !allSelected;

const toggleAll = (e) => {
const next = e.target.checked;
setItems(items.map((item) => ({ ...item, selected: next })));
};

const toggleOne = (id) => {
setItems(items.map((item) => (item.id === id ? { ...item, selected: !item.selected } : item)));
};

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Checkbox
size={size}
disabled={disabled}
label={label}
checked={allSelected}
indeterminate={someSelected}
onChange={toggleAll}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', paddingLeft: '20px' }}>
{items.map((item) => (
<Checkbox
key={item.id}
size={size}
disabled={disabled}
label={item.label}
checked={item.selected}
onChange={() => toggleOne(item.id)}
/>
))}
</div>
</div>
);
};

export const SelectAll = {
args: {
size: 'md',
disabled: false,
label: 'Select all'
},
render: (args) => <SelectAllDemo {...args} />
};
Loading
Loading