Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions src/components/Button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ import { BBButton } from 'bbb-ui-components-react';
</form>
```

### Full-width Button

```jsx
import { BBButton } from 'bbb-ui-components-react';

<BBButton label="Share" fullWidth onClick={() => (console.log('clicked'))}/>
```

### Extending with `styled()`

`className` is forwarded to the root element (the `<button>`, or the wrapper `<div>` in the `stacked` layout), so `BBButton` can be extended with `styled()`. This is an escape hatch while `BBButton` is being adopted: prefer native props such as `fullWidth`, and open an issue for any override you find yourself repeating, so it can become a prop.

```jsx
import styled from 'styled-components';
import { BBButton } from 'bbb-ui-components-react';

const FooterButton = styled(BBButton)`
min-width: 8.5rem;
`;
```

## Props

| Property | Type | Default | Description |
Expand All @@ -83,6 +104,8 @@ import { BBButton } from 'bbb-ui-components-react';
| `layout` | `keyof typeof LAYOUTS` | `'default'` | The layout of the button. |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | The native `type` attribute of the `<button>`. Defaults to `'button'` so it never submits an enclosing form unless `'submit'` is passed explicitly. |
| `disabled` | `boolean` | `false` | If `true`, the button will be disabled. |
| `fullWidth` | `boolean` | `false` | If `true`, the button fills the width of its container, keeping its content centered. Used for 'default' layout. |
| `className` | `string` | | Class name forwarded to the root element (the `<button>`, or the wrapper `<div>` in the 'stacked' layout), so `styled(BBButton)` works. Escape hatch while BBButton is being adopted; prefer native props and open an issue for recurring needs. |
| `children` | `React.ReactNode` | | The content of the button, typically used for the 'default' layout. |
| `icon` | `React.ReactNode` | | The icon to be displayed. Used for 'circle', 'squared' and 'stacked' layouts. |
| `iconStart` | `React.ReactNode` | | The icon to be displayed at the start of the button. Used for 'default' layout.|
Expand Down
43 changes: 43 additions & 0 deletions src/components/Button/component.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import styled from 'styled-components';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import BBButton from './component';
Expand Down Expand Up @@ -77,6 +78,15 @@ const meta = {
description: 'Disables interaction and applies disabled visuals. Works across all layouts.',
table: { defaultValue: { summary: 'false' } },
},
fullWidth: {
control: 'boolean',
description: `Makes the button fill the width of its container, keeping its content centered. Only used in the 'default' layout.`,
table: { defaultValue: { summary: 'false' } },
},
className: {
control: false,
description: `Class name forwarded to the root element (the \`<button>\`, or the wrapper \`<div>\` in the 'stacked' layout), so \`styled(BBButton)\` works. Escape hatch while BBButton is being adopted; prefer native props and open an issue for recurring needs.`,
},
icon: {
control: false,
description: `Main icon for the button. Commonly used for icon-only buttons (layouts 'circle' and 'squared') or in 'stacked' layout. When using a label, prefer \`iconStart\` or \`iconEnd\` in the 'default' layout.`,
Expand Down Expand Up @@ -378,6 +388,39 @@ export const Disabled: Story = {
},
};

/** Shows a `fullWidth` button filling its container, with the label centered. */
export const FullWidth: Story = {
args: {
label: 'Share',
fullWidth: true,
},
render: (args) => (
<div style={{ width: '20rem' }}>
<BBButton {...args} />
</div>
),
};

const StyledFooterButton = styled(BBButton)`
min-width: 8.5rem;
`;

const StyledWrappingButton = styled(BBButton)`
width: 10rem;
white-space: normal;
`;

/** Customizes the button through `styled(BBButton)`: a minimum width for a short label, and a fixed width that lets a long label wrap. */
export const StyledExtension: Story = {
name: 'Extended with styled()',
render: (args) => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<StyledFooterButton {...args} label="Yes" />
<StyledWrappingButton {...args} label="A long translated label that wraps" />
</div>
),
};

/** Renders the same button with a tooltip in each of the available `tooltipPlacement` positions. */
export const WithTooltip: Story = {
name: 'With Tooltip (All Placements)',
Expand Down
12 changes: 10 additions & 2 deletions src/components/Button/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function Button(props: ButtonProps): JSX.Element {
showFeedback = false,
feedbackContent = <MdCheckCircle fontSize="small" />,
feedbackDuration = 2000,
className,
} = props;

const feedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -95,6 +96,7 @@ function Button(props: ButtonProps): JSX.Element {
return (
<Styled.Button
id={id}
className={className}
type={type}
data-test={testId}
onClick={handleClick}
Expand Down Expand Up @@ -125,7 +127,11 @@ function Button(props: ButtonProps): JSX.Element {
const testId = dataTest || `${LAYOUTS.STACKED}-button-${id || label || 'default'}`;

return (
<Styled.ButtonWrapper data-test={testId} $layout={layout}>
<Styled.ButtonWrapper
className={className}
data-test={testId}
$layout={layout}
>
<Styled.Button
id={id}
type={type}
Expand Down Expand Up @@ -172,12 +178,13 @@ function Button(props: ButtonProps): JSX.Element {
);
}

const { iconStart, iconEnd } = props;
const { iconStart, iconEnd, fullWidth = false } = props;
const testId = dataTest || `button-${id || label || 'default'}`;

return (
<Styled.Button
id={id}
className={className}
type={type}
data-test={testId}
onClick={handleClick}
Expand All @@ -187,6 +194,7 @@ function Button(props: ButtonProps): JSX.Element {
$variant={variant}
$size={size}
$layout={layout}
$fullWidth={fullWidth}
disabled={disabled}
>
{isFeedbackVisible ? (
Expand Down
13 changes: 7 additions & 6 deletions src/components/Button/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,14 @@ const commonButtonStyles = css<StyledButtonProps>`
color: ${color};
}
color: ${color};
${background ? `background-color: ${background}` : `background-color: none`};
border: ${border ? `1px solid ${border}` : 'none'};
${background ? `background-color: ${background}` : `background-color: transparent`};
border: ${border ? `1px solid ${border}` : '1px solid transparent'};

&&:hover,
&&:active {
color: ${color};
${background ? `background-color: ${background}` : `background-color: none`};
border: ${border ? `1px solid ${border}` : 'none'};
${background ? `background-color: ${background}` : `background-color: transparent`};
border: ${border ? `1px solid ${border}` : '1px solid transparent'};
}

&&:focus {
Expand Down Expand Up @@ -145,8 +145,9 @@ const squaredLayoutStyles = css<StyledButtonProps>`
border-radius: ${borderRadiusSmall};
`;

const defaultLayoutStyles = css`
display: inline-flex;
const defaultLayoutStyles = css<StyledButtonProps>`
display: ${({ $fullWidth }) => ($fullWidth ? 'flex' : 'inline-flex')};
${({ $fullWidth }) => $fullWidth && 'width: 100%;'}
justify-content: center;
align-items: center;
gap: ${spacingSmall};
Expand Down
7 changes: 7 additions & 0 deletions src/components/Button/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface StyledButtonProps {
$color?: ColorType;
$size?: SizeType;
$layout?: LayoutType;
$fullWidth?: boolean;
disabled: boolean;
}

Expand Down Expand Up @@ -124,6 +125,9 @@ type BaseButtonProps = {

/** How long, in milliseconds, the click feedback stays visible. @default 2000 */
feedbackDuration?: number;

/** Class name forwarded to the root element (the `<button>`, or the wrapper `<div>` in the `stacked` layout), so `styled(BBButton)` works. Escape hatch while BBButton is being adopted; prefer native props and open an issue for recurring needs. */
className?: string;
}

type DefaultLayoutProps = BaseButtonProps & {
Expand All @@ -135,6 +139,9 @@ type DefaultLayoutProps = BaseButtonProps & {

/** Icon rendered after the label (right side); effective when a label is present. */
iconEnd?: React.ReactNode;

/** Makes the button fill the width of its container, keeping its content centered. @default false */
fullWidth?: boolean;
};

type StackedLayoutProps = BaseButtonProps & {
Expand Down
2 changes: 1 addition & 1 deletion src/stylesheets/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const colorBrand2 = `var(--color-brand-2, ${colorBrand1_base})`;
export const colorBrand3 = `var(--color-brand-3, ${colorBrand1_base})`;
// Mapped to core css vars
export const colorBrandLight = `var(--color-blue-aux, ${colorBrandLight_base})`;
export const colorBrandAux = `var(--color-brand-aux, ${colorBrandLight_base})`;
export const colorBrandAux = `var(--color-brand-aux, var(--color-hover-light, ${colorBrandLight_base}))`;


// Semantic colors
Expand Down
Loading