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