Skip to content

Commit

Permalink
Work on Banner component.
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrause committed Dec 28, 2024
1 parent c3fcee3 commit 26f3660
Show file tree
Hide file tree
Showing 23 changed files with 388 additions and 193 deletions.
1 change: 1 addition & 0 deletions .vscode/custom.css-data.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
// https://github.com/microsoft/vscode-custom-data
"version": 1.1,
"properties": [
{ "name": "interpolate-size" },
Expand Down
7 changes: 5 additions & 2 deletions src/components/actions/Button/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
cursor: pointer;

margin: 0;
padding: calc(bk.$spacing-2 - 0.125lh) bk.$spacing-3; /* Note: compensate for line-height difference with Figma */
padding: 0;
&:not(.bk-button--compact) {
padding: calc(bk.$spacing-2 - 0.125lh) bk.$spacing-3; /* Note: compensate for line-height difference with Figma */
}

/* Transparent border for consistency with other variants that have a border */
border: bk.$size-1 solid transparent;
Expand Down Expand Up @@ -67,7 +70,7 @@
--bk-button-color-contrast: #{bk.$theme-button-primary-text-non-active};
cursor: not-allowed;
}

@media (prefers-reduced-motion: no-preference) {
transition: none 150ms ease-in-out;
transition-property: border, background, color;
Expand Down
12 changes: 7 additions & 5 deletions src/components/actions/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as React from 'react';
import { ErrorBoundary } from 'react-error-boundary';

import { Icon } from '../../graphics/Icon/Icon.tsx';
import { Alert } from '../../containers/Alert/Alert.tsx';
import { Banner } from '../../containers/Banner/Banner.tsx';

import { Button } from './Button.tsx';

Expand Down Expand Up @@ -40,10 +40,12 @@ export default {
Story => (
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) =>
<Alert kind="error" style={{ width: '80cqi' }}>
<p>Error: {error?.message}</p>
<p><Button variant="tertiary" label="Reset" onClick={resetErrorBoundary}/></p>
</Alert>
<Banner variant="error" style={{ width: '60cqi' }}
title="Error"
actions={<Banner.ActionButton label="Reset" onPress={resetErrorBoundary}/>}
>
{error?.message}
</Banner>
}
>
<Story/>
Expand Down
9 changes: 9 additions & 0 deletions src/components/actions/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export type ButtonProps = React.PropsWithChildren<Omit<ComponentProps<'button'>,
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** Whether this tooltip should be rendered compact (minimal padding). */
compact?: undefined | boolean,

/**
* The label of the button. Additional UI elements may be added, e.g. a loading indicator. If full control over
* the button content is desired, use `children` instead, this overrides the `label`.
Expand Down Expand Up @@ -48,6 +51,7 @@ export const Button = (props: ButtonProps) => {
const {
children,
unstyled = false,
compact = false,
label,
nonactive = false,
variant = 'tertiary',
Expand Down Expand Up @@ -112,14 +116,19 @@ export const Button = (props: ButtonProps) => {
buttonType = 'submit';
}

// If both children and label are specified, use the `label` as the accessible name by default
const accessibleName = typeof children !== 'undefined' && label ? label : undefined;

return (
<button
aria-label={accessibleName}
{...propsRest}
type={buttonType} // Not overridable
disabled={!isInteractive}
className={cx({
bk: true,
[cl['bk-button']]: !unstyled,
[cl['bk-button--compact']]: compact,
[cl['bk-button--primary']]: variant === 'primary',
[cl['bk-button--secondary']]: variant === 'secondary',
[cl['bk-button--nonactive']]: isNonactive,
Expand Down
23 changes: 12 additions & 11 deletions src/components/containers/Banner/Banner.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
--bk-banner-indent: #{bk.$spacing-9};

overflow: hidden;
overflow-y: scroll;

padding-block: bk.$spacing-2;
padding-inline: bk.$spacing-4 - bk.$radius-2;
Expand Down Expand Up @@ -71,30 +72,26 @@

.bk-banner__actions {
display: flex;
flex-direction: row-reverse;
flex-direction: row;
align-items: center;
gap: bk.$spacing-3 bk.$spacing-7;
gap: bk.$spacing-3 calc(bk.$spacing-7 / 2);

> * {
> button {
// Keep each item to a max of 1lh, so that in the case of all items being on one line, all the items
// are properly aligned with the title (which implicitly has height `1lh`).
height: 1lh;
}

.bk-banner__action {
display: flex;
flex-direction: row;
align-items: center;
flex: none;

button {
border: 0;
font-weight: bk.$font-weight-semibold;
padding: 0;
}
.bk-banner__action--button { --keep: ; }
.bk-banner__action--icon { --keep: ; }
}

.bk-banner__action-close {
flex: none;
margin-inline-start: calc(bk.$spacing-7 / 2); // Total gap should be `bk.$spacing-7`

display: flex;
color: bk.$theme-banner-icon-default;
Expand Down Expand Up @@ -122,4 +119,8 @@
--bk-banner-color-background: #{bk.$theme-banner-success-background-default};
}
}

.bk-banner + .bk-banner {
margin-block-start: bk.$spacing-2;
}
}
133 changes: 100 additions & 33 deletions src/components/containers/Banner/Banner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,51 @@
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import * as React from 'react';
import { idToCssIdent } from '../../../util/reactUtil.ts';
import { startViewTransition } from '../../../util/reactDomUtil.ts';

import type { Meta, StoryObj } from '@storybook/react';
import { LayoutDecorator } from '../../../util/storybook/LayoutDecorator.tsx';
import { loremIpsum, LoremIpsum } from '../../../util/storybook/LoremIpsum.tsx';

import { Banner, BannerAction } from './Banner.tsx';
import { Icon } from '../../graphics/Icon/Icon.tsx';
import { Button } from '../../actions/Button/Button.tsx';
import { SegmentedControl } from '../../forms/controls/SegmentedControl/SegmentedControl.tsx';

import { Banner } from './Banner.tsx';


type BannerArgs = React.ComponentProps<typeof Banner>;
type Story = StoryObj<BannerArgs>;

// Controlled version of `Banner` (handles close state)
const BannerControlled = (props: React.ComponentProps<typeof Banner>) => {
const viewTransitionName = idToCssIdent(React.useId());
const [isVisible, setIsVisible] = React.useState(true);

// Use a view transition to get an exit animation in supported browsers
const handleClose = React.useCallback(() => {
startViewTransition(() => {
setIsVisible(false);
props.onClose?.();
});
}, [props.onClose]);

if (!isVisible) { return null; }

return (
<Banner
{...props}
onClose={props.onClose ? handleClose : undefined}
style={{
viewTransitionName,
//resize: 'inline',
...(props.style ?? {}),
}}
/>
);
};

export default {
component: Banner,
parameters: {
Expand All @@ -28,23 +61,14 @@ export default {
args: {
title: 'Banner title',
},
render: (args) => <Banner {...args}/>,
render: (args) => <BannerControlled {...args}/>,
} satisfies Meta<BannerArgs>;


// Utility components
const BannerWithCloseState = (props: React.ComponentProps<typeof Banner>) => {
const [isVisible, setIsVisible] = React.useState(true);

if (!isVisible) { return null; }
return <Banner {...props} onClose={() => { setIsVisible(false); }}/>;
};

const BannerActionExample = () => (
<BannerAction>
<Button variant="tertiary" onPress={() => alert('clicked')}>Button</Button>
</BannerAction>
);
const ExampleActionButton = () =>
<Banner.ActionButton onPress={() => alert('clicked')}>Button</Banner.ActionButton>;
const ExampleActionIcon = () =>
<Banner.ActionIcon label="Copy" onPress={() => alert('clicked')}><Icon icon="copy"/></Banner.ActionIcon>;


export const BannerStandard: Story = {};
Expand All @@ -53,21 +77,33 @@ export const BannerWithCloseButton: Story = {
args: {
onClose: () => {},
},
render: (args) => <BannerWithCloseState {...args}/>,
};

export const BannerWithMessage: Story = {
args: {
message: 'This is the main message of the banner.',
children: 'This is the main message of the banner.',
onClose: () => {},
},
};

export const BannerWithButton: Story = {
args: {
message: 'Message text',
children: 'Message text',
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerWithButtonAndIcon: Story = {
args: {
children: 'Message text',
onClose: () => {},
actions: (
<>
<ExampleActionButton/>
<ExampleActionIcon/>
</>
),
},
};

Expand All @@ -76,63 +112,94 @@ export const BannerWithTitleOverflow: Story = {
args: {
title: loremIpsum(),
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerWithTextWrap: Story = {
args: {
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerWithTitleOverflowAndTextWrap: Story = {
args: {
title: loremIpsum(),
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerWithThemedContent: Story = {
args: {
title: loremIpsum(),
children: (
<article>
<p className="bk-body-text">
Banners look visually light, even in dark mode. The following components should always have a light theme:
</p>
<p>
<Button nonactive variant="primary" onPress={() => alert('clicked')}>Button</Button>
</p>
<SegmentedControl
options={['Test 1', 'Test 2']}
defaultValue="Test 1"
/>
</article>
),
},
};

export const BannerInformational: Story = {
args: {
variant: 'informational',
variant: 'info',
title: loremIpsum(),
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerSuccess: Story = {
args: {
variant: 'success',
title: loremIpsum(),
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerWarning: Story = {
args: {
variant: 'warning',
title: loremIpsum(),
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannerError: Story = {
args: {
variant: 'error',
title: loremIpsum(),
message: <LoremIpsum/>,
children: <LoremIpsum/>,
onClose: () => {},
actions: <BannerActionExample/>,
actions: <ExampleActionButton/>,
},
};

export const BannersStacked: Story = {
render: args => (
<>
<BannerControlled {...args} variant="informational" onClose={() => {}}/>

Check failure on line 199 in src/components/containers/Banner/Banner.stories.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Type '"informational"' is not assignable to type 'BannerVariant | undefined'.

Check failure on line 199 in src/components/containers/Banner/Banner.stories.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Type '"informational"' is not assignable to type 'BannerVariant | undefined'.

Check failure on line 199 in src/components/containers/Banner/Banner.stories.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Type '"informational"' is not assignable to type 'BannerVariant | undefined'.
<BannerControlled {...args} variant="warning" onClose={() => {}}/>
<BannerControlled {...args} variant="error" onClose={() => {}}/>
<BannerControlled {...args} variant="success" onClose={() => {}}/>
</>
),
};
Loading

0 comments on commit 26f3660

Please sign in to comment.