From ccd1baa20d006a5b62afff53e0198b35a5e59c06 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 28 Jul 2026 14:51:30 -0300 Subject: [PATCH 1/2] feat(BBBHint): allow uncontrolled open/close state Adds an optional `open` prop so BBBHint can manage its own visibility and close itself when the close button is clicked, removing the need for external useState boilerplate in simple cases. Passing `open` switches to the existing controlled behavior, where the parent decides visibility via `onRequestClose`. Closes #29 --- src/components/Hint/README.md | 32 +++++++++++++++- src/components/Hint/component.stories.tsx | 46 ++++++++++++++++++++++- src/components/Hint/component.tsx | 19 ++++++++-- src/components/Hint/types.ts | 7 +++- 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/src/components/Hint/README.md b/src/components/Hint/README.md index 33a25ca..755acb6 100644 --- a/src/components/Hint/README.md +++ b/src/components/Hint/README.md @@ -26,13 +26,41 @@ import { BBBHint } from 'bbb-ui-components-react'; /> ``` +### Uncontrolled Hint (default) + +Without an `open` prop, the hint manages its own visibility and closes itself when the close button is clicked — no external state required. + +```jsx +import { BBBHint } from 'bbb-ui-components-react'; + + +``` + +### Controlled Hint + +Pass `open` to drive visibility externally; the hint calls `onRequestClose` instead of hiding itself, leaving the parent in charge of updating `open`. + +```jsx +import { useState } from 'react'; +import { BBBHint } from 'bbb-ui-components-react'; + +const [open, setOpen] = useState(true); + + setOpen(false)} + label="This hint's visibility is controlled externally." +/> +``` + ## Props | Property | Type | Default | Description | | ---------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | | `label` | `string` | | The main text content of the hint. | -| `title` | `string` | | An optional title for the hint. If provided, a close button will be displayed. | +| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | | `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | -| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked. | +| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | +| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | | `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | | `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | diff --git a/src/components/Hint/component.stories.tsx b/src/components/Hint/component.stories.tsx index 65a8a14..7a1a555 100644 --- a/src/components/Hint/component.stories.tsx +++ b/src/components/Hint/component.stories.tsx @@ -1,5 +1,7 @@ +import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import BBBHint from './component'; +import { BBButton } from '../Button'; const meta = { title: 'BBBHint', @@ -12,15 +14,19 @@ const meta = { }, title: { control: 'text', - description: 'Optional title; if provided, shows a close button.', + description: 'Optional title shown in the header; when set, label renders as a separate line below instead of inline.', }, icon: { control: false, description: 'Optional icon node displayed next to the title or label.', }, + open: { + control: 'boolean', + description: 'Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally.', + }, onRequestClose: { control: false, - description: 'Callback fired when the close button is clicked.', + description: 'Callback fired when the close button is clicked, in both controlled and uncontrolled mode.', }, children: { control: false, @@ -32,9 +38,45 @@ const meta = { export default meta; type Story = StoryObj; +/** + * Wrapper component so hooks can be used inside Storybook's render function, + * demonstrating the `open` prop driving visibility from outside the hint. + */ +const ControlledHintStory: React.FC> = (args) => { + const [open, setOpen] = useState(true); + + return ( +
+ setOpen(true)} /> + setOpen(false)} + /> +
+ ); +}; + /** Basic hint rendering with only a label and the default info icon. */ export const Default: Story = { args: { label: 'Helpful hint', }, }; + +/** Uncontrolled hint (no `open` prop): it manages its own visibility and closes itself when dismissed. */ +export const Uncontrolled: Story = { + args: { + title: 'Uncontrolled', + label: 'This hint manages its own visibility and closes itself when the close button is clicked.', + }, +}; + +/** Controlled hint: visibility is driven by the `open` prop, so the parent decides when it reappears. */ +export const Controlled: Story = { + args: { + title: 'Controlled', + label: "This hint's visibility is controlled externally via the open prop.", + }, + render: (args) => , +}; diff --git a/src/components/Hint/component.tsx b/src/components/Hint/component.tsx index 50bb01b..a45518e 100644 --- a/src/components/Hint/component.tsx +++ b/src/components/Hint/component.tsx @@ -1,4 +1,4 @@ -import React, { JSX } from 'react'; +import React, { JSX, useState } from 'react'; import * as Styled from './styles'; import { HintProps } from './types'; import { MdClose, MdInfo } from 'react-icons/md'; @@ -9,15 +9,28 @@ import { MdClose, MdInfo } from 'react-icons/md'; * This component provides a small contextual hint used to surface tips, short help text or dismissible messages. * It can be displayed with a title, an icon, and a close button. * + * Visibility is uncontrolled by default (the hint closes itself when its close button is clicked); pass `open` to control it externally instead. */ function Hint({ title, label, icon = , + open, onRequestClose, children, ...rest -}: HintProps): JSX.Element { +}: HintProps): JSX.Element | null { + const isControlled = open !== undefined; + const [internalOpen, setInternalOpen] = useState(true); + const isOpen = isControlled ? open : internalOpen; + + const handleClose = (): void => { + if (!isControlled) setInternalOpen(false); + onRequestClose?.(); + }; + + if (!isOpen) return null; + const renderedLabel = {label}{children}; return ( diff --git a/src/components/Hint/types.ts b/src/components/Hint/types.ts index eddf53e..2b9e4c6 100644 --- a/src/components/Hint/types.ts +++ b/src/components/Hint/types.ts @@ -2,13 +2,16 @@ export interface HintProps extends React.HTMLAttributes { /** Main text content of the hint. */ label: string; - /** Optional title; if provided, shows a close button. */ + /** Optional title shown in the header; when set, `label` renders as a separate line below instead of inline. */ title?: string; /** Optional icon node displayed next to the title or label. @default */ icon?: React.ReactNode; - /** Callback fired when the close button is clicked. */ + /** Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. */ + open?: boolean; + + /** Callback fired when the close button is clicked, in both controlled and uncontrolled mode. */ onRequestClose?: () => void; /** Optional additional content rendered under the label. */ From 10c72c7d87d122b046cfb9365c7c6602dc15c26c Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 28 Jul 2026 14:56:28 -0300 Subject: [PATCH 2/2] feat(BBBHint): add hideCloseButton prop Adds a `hideCloseButton` prop to hide the close (X) button for hints that shouldn't be manually dismissed, e.g. ones dismissed via another UI element or tooltip-style hints with no explicit dismiss action. The close button no longer depends on `title` being set, so label-only hints can now show one too unless hidden. Closes #30 --- src/components/Hint/README.md | 29 ++++++++++++++++------- src/components/Hint/component.stories.tsx | 13 ++++++++++ src/components/Hint/component.tsx | 3 ++- src/components/Hint/types.ts | 3 +++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/components/Hint/README.md b/src/components/Hint/README.md index 755acb6..c1b1bf5 100644 --- a/src/components/Hint/README.md +++ b/src/components/Hint/README.md @@ -53,14 +53,25 @@ const [open, setOpen] = useState(true); /> ``` +### Hint Without a Close Button + +Pass `hideCloseButton` for hints that shouldn't be manually dismissed — e.g. ones dismissed by interacting with another UI element, or tooltip-style hints with no explicit dismiss action. + +```jsx +import { BBBHint } from 'bbb-ui-components-react'; + + +``` + ## Props -| Property | Type | Default | Description | -| ---------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | -| `label` | `string` | | The main text content of the hint. | -| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | -| `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | -| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | -| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | -| `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | -| `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | +| Property | Type | Default | Description | +| ----------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | +| `label` | `string` | | The main text content of the hint. | +| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | +| `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | +| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | +| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | +| `hideCloseButton` | `boolean` | `false` | Hides the close (X) button, for hints that shouldn't be manually dismissed. | +| `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | +| `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | diff --git a/src/components/Hint/component.stories.tsx b/src/components/Hint/component.stories.tsx index 7a1a555..a8790e9 100644 --- a/src/components/Hint/component.stories.tsx +++ b/src/components/Hint/component.stories.tsx @@ -28,6 +28,10 @@ const meta = { control: false, description: 'Callback fired when the close button is clicked, in both controlled and uncontrolled mode.', }, + hideCloseButton: { + control: 'boolean', + description: "Hides the close (X) button, for hints that shouldn't be manually dismissed.", + }, children: { control: false, description: 'Optional additional content rendered under the label.', @@ -80,3 +84,12 @@ export const Controlled: Story = { }, render: (args) => , }; + +/** Hint with `hideCloseButton`: no close (X) button, for hints that shouldn't be manually dismissed. */ +export const WithoutCloseButton: Story = { + args: { + title: 'No close button', + label: 'This hint cannot be manually dismissed.', + hideCloseButton: true, + }, +}; diff --git a/src/components/Hint/component.tsx b/src/components/Hint/component.tsx index a45518e..ee61d3a 100644 --- a/src/components/Hint/component.tsx +++ b/src/components/Hint/component.tsx @@ -17,6 +17,7 @@ function Hint({ icon = , open, onRequestClose, + hideCloseButton = false, children, ...rest }: HintProps): JSX.Element | null { @@ -48,7 +49,7 @@ function Hint({ {title && {title}} {!title && renderedLabel} - {title && ( + {!hideCloseButton && ( { /** Callback fired when the close button is clicked, in both controlled and uncontrolled mode. */ onRequestClose?: () => void; + /** Hides the close (X) button, for hints that shouldn't be manually dismissed. @default false */ + hideCloseButton?: boolean; + /** Optional additional content rendered under the label. */ children?: React.ReactNode; }