Skip to content

Add Snackbar #721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jun 3, 2019
Merged
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
1 change: 1 addition & 0 deletions src/common/example/example.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@import '../../theme/slide-pane.m.css';
@import '../../slider/styles/slider.m.css';
@import '../../theme/slider.m.css';
@import '../../theme/snackbar.m.css';
@import '../../split-pane/styles/split-pane.m.css';
@import '../../theme/split-pane.m.css';
@import '../../theme/tab-controller.m.css';
Expand Down
1 change: 1 addition & 0 deletions src/common/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const modules = [
'select',
'slide-pane',
'slider',
'snackbar',
'split-pane',
'tab-controller',
'text-area',
Expand Down
1 change: 1 addition & 0 deletions src/common/tests/unit/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../../../radio/tests/unit/Radio';
import '../../../select/tests/unit/Select';
import '../../../slide-pane/tests/unit/SlidePane';
import '../../../slider/tests/unit/Slider';
import '../../../snackbar/tests/unit/Snackbar';
import '../../../split-pane/tests/unit/SplitPane';
import '../../../range-slider/tests/unit/RangeSlider';
import '../../../tab/tests/unit/Tab';
Expand Down
57 changes: 57 additions & 0 deletions src/snackbar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# @dojo/widgets/snackbar widget

Dojo's `Snackbar` widget creates a brief display message that is positioned at the bottom of the screen.

## Features

- Provides a simple way to present information to a user
- Provides an easy API for dismissing snackbar messages

## Example usage

```tsx
// basic usage
<Snackbar open={true} message="Text to display"/>

// Display a success-styled message
<Snackbar open={true} message="Text to display" type="success"/>

// Display a error-styled message
<Snackbar open={true} message="Text to display" type="error"/>

// Handle closing the message
let open = true;
<Snackbar
open={open}
message="Text to display"
type="error"
actionsRenderer={() => <Button onClick={() => open = false}>Dismiss</Button>}
/>
```

## Properties


- `open: boolean` - Whether the snackbar is open and displayed
- `message: string` - The message to display on the snackbar
- `actionsRenderer?: () => RenderResult` - A callback that returns what to render in the snackbar's actions section
- `type?: 'success' | 'error'` - The variant of snackbar to render. Can be `"success"` or `"error"`
- `leading?: boolean` - If true, render the snackbar on the leading side of the page
- `stacked?: boolean` - If true, stack the snackbar's message on top of the actions

## Theming

The following CSS classes are available on the `Snackbar` widget for use with custom themes:

- `root`: Applied to the top-level wrapping of the Snackbar
- `content` - Applied to the wrapper around the label and actions of the Snackbar
- `label` - Applied to the element displaying the message portion of the Snackbar
- `actions` - Applied to the wrapper around the content rendered in the `actionsRenderer` property.

*Conditional classes*

- `open` - Applied to the top-level element of the widget when the Snackbar is displayed
- `success` - Applied to the top-level element of the widget when `type` is `success`
- `error` - Applied to the top-level element of the widget when `type` is `error`
- `leading` - When applied, the Snackbar will be aligned to the leading side of the screen
- `stacked` - When applied, the snackbar actions will appear below the message instead of beside it
119 changes: 119 additions & 0 deletions src/snackbar/example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { DNode } from '@dojo/framework/widget-core/interfaces';
import { tsx } from '@dojo/framework/widget-core/tsx';
import watch from '@dojo/framework/widget-core/decorators/watch';
import Snackbar from '../index';
import Button from '../../button/index';

export default class App extends WidgetBase {
@watch()
private _showSuccess = false;

@watch()
private _showLeading = false;

@watch()
private _showStacked = false;

@watch()
private _showError = false;

@watch()
private _showPlain = false;

@watch()
private _showAutoclose = false;

private _timeoutHandle: any;

render(): DNode {
return (
<div>
<h2>Snackbar Examples</h2>
<div id="example-plain">
<h3>Snackbar</h3>
<Button onClick={() => (this._showPlain = true)}>Show Plain Snackbar</Button>
<Snackbar
open={this._showPlain}
message="Test Snackbar"
actionsRenderer={() => (
<Button onClick={() => (this._showPlain = false)}>Dismiss</Button>
)}
/>
</div>
<div id="example-success">
<h3>Success Snackbar</h3>
<Button onClick={() => (this._showSuccess = true)}>Show Success</Button>
<Snackbar
type="success"
open={this._showSuccess}
message="Test Snackbar Success"
actionsRenderer={() => (
<Button onClick={() => (this._showSuccess = false)}>X</Button>
)}
/>
</div>
<div id="example-error">
<h3>Error Snackbar</h3>
<Button onClick={() => (this._showError = true)}>Show Error</Button>
<Snackbar
type="error"
open={this._showError}
message="Test Snackbar Error"
actionsRenderer={() => (
<Button onClick={() => (this._showError = false)}>X</Button>
)}
/>
</div>
<div id="example-leading">
<h3>Leading Snackbar</h3>
<Button onClick={() => (this._showLeading = true)}>Show Leading</Button>
<Snackbar
leading={true}
open={this._showLeading}
message="Test leading snackbar"
actionsRenderer={() => (
<Button onClick={() => (this._showLeading = false)}>X</Button>
)}
/>
</div>
<div id="example-stacked">
<h3>Stacked Snackbar</h3>
<Button onClick={() => (this._showStacked = true)}>Show Stacked</Button>
<Snackbar
stacked={true}
open={this._showStacked}
message="Test stacked Snackbar"
actionsRenderer={() => (
<Button onClick={() => (this._showStacked = false)}>Close</Button>
)}
/>
</div>
<div id="example-autoclose">
<h3>Multiple Actions</h3>
<Button
onClick={() => {
this._showAutoclose = true;
this._timeoutHandle = setTimeout(() => {
this._showAutoclose = false;
}, 5000);
}}
>
Show Snackbar
</Button>
<Snackbar
type="success"
open={this._showAutoclose}
message="Test Snackbar auto close"
actionsRenderer={() => [
<Button onClick={() => clearTimeout(this._timeoutHandle)}>
Clear Timeout
</Button>,
<Button onClick={() => (this._showAutoclose = false)}>Close</Button>
]}
/>
</div>
</div>
);
}
}
53 changes: 53 additions & 0 deletions src/snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { DNode, RenderResult } from '@dojo/framework/widget-core/interfaces';
import { theme, ThemedMixin } from '@dojo/framework/widget-core/mixins/Themed';
import { customElement } from '@dojo/framework/widget-core/decorators/customElement';
import { tsx } from '@dojo/framework/widget-core/tsx';
import * as css from '../theme/snackbar.m.css';

export interface SnackbarProperties {
open: boolean;
message: string;
actionsRenderer?: () => RenderResult;
type?: 'success' | 'error';
leading?: boolean;
stacked?: boolean;
}

@theme(css)
@customElement<SnackbarProperties>({
tag: 'dojo-snackbar',
properties: ['actionsRenderer', 'leading', 'open', 'stacked'],
attributes: ['message', 'type']
})
export class Snackbar extends ThemedMixin(WidgetBase)<SnackbarProperties> {
protected render(): DNode {
const { type, open, leading, stacked, message, actionsRenderer } = this.properties;

return (
<div
key="root"
classes={[
css.root,
open ? css.open : null,
type ? css[type] : null,
leading ? css.leading : null,
stacked ? css.stacked : null
]}
>
<div key="content" classes={css.content}>
<div key="label" classes={css.label} role="status" aria-live="polite">
{message}
</div>
{actionsRenderer && (
<div key="actions" classes={css.actions}>
{actionsRenderer()}
</div>
)}
</div>
</div>
);
}
}

export default Snackbar;
122 changes: 122 additions & 0 deletions src/snackbar/tests/unit/Snackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const { describe, it } = intern.getInterface('bdd');

import assertationTemplate from '@dojo/framework/testing/assertionTemplate';
import harness from '@dojo/framework/testing/harness';
import { tsx } from '@dojo/framework/widget-core/tsx';
import Snackbar from '../../index';
import * as css from '../../../theme/snackbar.m.css';
import Button from '../../../button/index';

describe('Snackbar', () => {
const template = assertationTemplate(() => {
return (
<div key="root" classes={[css.root, css.open, null, null, null]}>
<div key="content" classes={css.content}>
<div
key="label"
assertion-key="label"
classes={css.label}
role="status"
aria-live="polite"
>
test
</div>
</div>
</div>
);
});

it('renders', () => {
const h = harness(() => <Snackbar message="test" open={true} />);
h.expect(template);
});

it('renders closed', () => {
const h = harness(() => <Snackbar message="test" open={false} />);
const openTemplate = template.setProperty('@root', 'classes', [
css.root,
null,
null,
null,
null
]);
h.expect(openTemplate);
});

it('renders success', () => {
const h = harness(() => <Snackbar type="success" message="test" open={true} />);
const successTemplate = template.setProperty('@root', 'classes', [
css.root,
css.open,
css.success,
null,
null
]);
h.expect(successTemplate);
});

it('renders leading', () => {
const h = harness(() => <Snackbar leading message="test" open={true} />);
const successTemplate = template.setProperty('@root', 'classes', [
css.root,
css.open,
null,
css.leading,
null
]);
h.expect(successTemplate);
});

it('renders stacked', () => {
const h = harness(() => <Snackbar stacked message="test" open={true} />);
const successTemplate = template.setProperty('@root', 'classes', [
css.root,
css.open,
null,
null,
css.stacked
]);
h.expect(successTemplate);
});

it('renders error', () => {
const h = harness(() => <Snackbar message="test" type="error" open={true} />);
const errorTemplate = template.setProperty('@root', 'classes', [
css.root,
css.open,
css.error,
null,
null
]);
h.expect(errorTemplate);
});

it('renders a single action', () => {
const h = harness(() => (
<Snackbar message="test" open={true} actionsRenderer={() => <Button>Dismiss</Button>} />
));
const actionsTemplate = template.insertAfter('~label', [
<div key="actions" classes={css.actions}>
<Button>Dismiss</Button>
</div>
]);
h.expect(actionsTemplate);
});

it('renders more than one action', () => {
const h = harness(() => (
<Snackbar
message="test"
open={true}
actionsRenderer={() => [<Button>Retry</Button>, <Button>Close</Button>]}
/>
));
const actionsTemplate = template.insertAfter('~label', [
<div key="actions" classes={css.actions}>
<Button>Retry</Button>
<Button>Close</Button>
</div>
]);
h.expect(actionsTemplate);
});
});
Loading