diff --git a/src/components/Checkbox/README.md b/src/components/Checkbox/README.md
index 9770aad..b7800f6 100644
--- a/src/components/Checkbox/README.md
+++ b/src/components/Checkbox/README.md
@@ -20,13 +20,39 @@ import { BBBCheckbox } from 'bbb-ui-components';
```
+### Checkbox with Helper Text
+
+The helper text is shown below the label, and clicking it toggles the checkbox like the label does. Screen readers announce it as the checkbox's description; when there is no `label` or `ariaLabel`, it names the checkbox instead.
+
+```jsx
+import { BBBCheckbox } from 'bbb-ui-components';
+
+
+```
+
+### Checkbox with Tooltip
+
+Useful to explain why a disabled checkbox can't be changed. The tooltip shows on hover and keyboard focus, and its text is also exposed to screen readers as the input's description; when there is no `label`, `ariaLabel` or `helperText`, it names the checkbox instead.
+
+A disabled checkbox with a `tooltipLabel` stays in the tab order so keyboard users can reach the tooltip: the input gets `aria-disabled="true"` instead of the native `disabled` attribute, and clicks, label clicks and Space are ignored. Like a natively disabled input, it is left out of form submission and validation: its `name` and `required` are dropped while it is disabled.
+
+```jsx
+import { BBBCheckbox } from 'bbb-ui-components';
+
+
+```
+
## Props
| Property | Type | Default | Description |
| ---------- | ---------------------- | --------- | --------------------------------------------------------------------------- |
| `label` | `string` | | The text to be displayed as the label for the checkbox. |
| `layout` | `keyof typeof LAYOUTS` | `'right'` | The position of the label relative to the checkbox. |
+| `helperText` | `string` | | Secondary text displayed below the label. It is also exposed as the checkbox's accessible description. |
| `round` | `boolean` | `false` | If `true`, the checkbox will be rendered as a round radio-style button. |
| `ariaLabel`| `string` | | The accessible name for the checkbox. |
| `dataTest` | `string` | | The `data-test` attribute on the underlying `` element. |
+| `disabled` | `boolean` | `false` | If `true`, disables the checkbox and dims its label. |
+| `tooltipLabel` | `string` | | Text shown in a tooltip on hover and keyboard focus. When set, a disabled checkbox stays focusable (`aria-disabled`) so the tooltip can be reached. |
+| `tooltipPlacement` | `keyof typeof TOOLTIP_PLACEMENTS` | `'top'` | The placement of the tooltip relative to the checkbox. |
| `...props` | `Omit` | | Any other props will be passed down to the underlying Material-UI Checkbox component. Use `slotProps.input` to set attributes on the ``. |
diff --git a/src/components/Checkbox/component.stories.tsx b/src/components/Checkbox/component.stories.tsx
index c6f7d22..8b69e74 100644
--- a/src/components/Checkbox/component.stories.tsx
+++ b/src/components/Checkbox/component.stories.tsx
@@ -1,6 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react';
import BBBCheckbox from './component';
-import { LAYOUT_VALUES, DEFAULT_LAYOUT } from './constants';
+import {
+ LAYOUT_VALUES,
+ DEFAULT_LAYOUT,
+ TOOLTIP_PLACEMENT_VALUES,
+ DEFAULT_TOOLTIP_PLACEMENT,
+} from './constants';
const meta = {
title: 'BBBCheckbox',
@@ -17,10 +22,29 @@ const meta = {
description: 'Position of the label relative to the checkbox.',
table: { defaultValue: { summary: `${DEFAULT_LAYOUT}` } },
},
+ helperText: {
+ control: 'text',
+ description: "Secondary text displayed below the label; it is also exposed as the checkbox's accessible description.",
+ },
round: {
control: 'boolean',
description: 'When true, renders the checkbox as a round radio-style control.',
},
+ disabled: {
+ control: 'boolean',
+ description: 'When true, disables the checkbox and dims its label.',
+ table: { defaultValue: { summary: 'false' } },
+ },
+ tooltipLabel: {
+ control: 'text',
+ description: 'Text shown in a tooltip on hover and keyboard focus; when set, a disabled checkbox stays focusable (`aria-disabled`) so the tooltip can be reached.',
+ },
+ tooltipPlacement: {
+ control: 'select',
+ options: TOOLTIP_PLACEMENT_VALUES,
+ description: 'Placement of the tooltip when `tooltipLabel` is provided.',
+ table: { defaultValue: { summary: `${DEFAULT_TOOLTIP_PLACEMENT}` } },
+ },
ariaLabel: {
control: 'text',
description: 'Accessible name for the checkbox.',
@@ -49,3 +73,45 @@ export const Round: Story = {
round: true,
},
};
+
+/** Adds secondary text below the label via `helperText`. */
+export const WithHelperText: Story = {
+ args: {
+ label: 'Mirror my webcam',
+ helperText: 'Only you see the mirrored image',
+ },
+};
+
+/** Places the label and its helper text to the left of the checkbox. */
+export const WithHelperTextOnTheLeft: Story = {
+ args: {
+ label: 'Mirror my webcam',
+ helperText: 'Only you see the mirrored image',
+ layout: 'left',
+ },
+};
+
+/** Disabled checkbox: the box and the label are dimmed and the label shows a not-allowed cursor. */
+export const Disabled: Story = {
+ args: {
+ label: 'Disabled Checkbox',
+ disabled: true,
+ },
+};
+
+/** Shows a tooltip on hover and keyboard focus via `tooltipLabel`. */
+export const WithTooltip: Story = {
+ args: {
+ label: 'Checkbox with tooltip',
+ tooltipLabel: 'Extra information about this option',
+ },
+};
+
+/** Disabled checkbox that stays keyboard-focusable so its tooltip can explain why the option is unavailable. */
+export const DisabledWithTooltip: Story = {
+ args: {
+ label: 'Unavailable option',
+ disabled: true,
+ tooltipLabel: 'Only moderators can change this option',
+ },
+};
diff --git a/src/components/Checkbox/component.tsx b/src/components/Checkbox/component.tsx
index 87e74d3..751efcc 100644
--- a/src/components/Checkbox/component.tsx
+++ b/src/components/Checkbox/component.tsx
@@ -1,9 +1,16 @@
-import React, { JSX, useId } from 'react';
+import React, { JSX, useId, useRef } from 'react';
import { CheckboxOwnerState } from '@mui/material';
import { CheckboxProps } from './types';
import * as Styled from './styles';
-import { DEFAULT_LAYOUT, LAYOUTS } from './constants';
+import { DEFAULT_LAYOUT, DEFAULT_TOOLTIP_PLACEMENT, LAYOUTS } from './constants';
import { RadioButtonUnchecked, RadioButtonChecked } from '@mui/icons-material';
+import Tippy from '@tippyjs/react';
+import 'tippy.js/dist/tippy.css';
+
+// Cancelling the click (mouse, label or Space) keeps the box unchanged, and MUI skips
+// `onChange` for a prevented click. It sits on the root because MUI drops event handlers
+// passed through `slotProps.input`, except `onChange`.
+const preventToggle = (event: React.MouseEvent) => event.preventDefault();
/**
* A customizable Checkbox component.
@@ -16,40 +23,114 @@ import { RadioButtonUnchecked, RadioButtonChecked } from '@mui/icons-material';
function Checkbox({
label,
layout = DEFAULT_LAYOUT,
+ helperText,
round = false,
ariaLabel,
id,
dataTest,
slotProps,
+ disabled,
+ tooltipLabel,
+ tooltipPlacement = DEFAULT_TOOLTIP_PLACEMENT,
...props
}: CheckboxProps): JSX.Element {
const generatedId = useId();
+ const containerRef = useRef(null);
const checkboxId = id ?? generatedId;
- const ownInputProps = { 'aria-label': ariaLabel, 'data-test': dataTest };
+ const labelId = `${checkboxId}-label`;
+ const helperTextId = `${checkboxId}-helper`;
+ const tooltipDescriptionId = `${checkboxId}-tooltip`;
+ // The helper text is a second