Skip to content

Commit

Permalink
Feat(web): Introduce the Toast component #DS-1112
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkudrna committed Feb 27, 2024
1 parent 45aec24 commit c98a39e
Show file tree
Hide file tree
Showing 10 changed files with 871 additions and 23 deletions.
27 changes: 4 additions & 23 deletions packages/web/src/scss/components/Grid/_tools.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,6 @@
@use '@tokens' as tokens;
@use '../../tools/breakpoint';

// Function to get the breakpoint name as a infix or suffix
// Example: -get-breakpoint-infix('tablet', 768px) will return 'tablet--'
// Example: -get-breakpoint-infix('mobile', 0) will return ''
// Parameters are:
// * $name: the breakpoint name
// * $value: the breakpoint value
// * $infix: whether to return the suffix or infix
@function -get-breakpoint-name($name, $value, $infix: true) {
@if $value > 0 {
@if $infix {
@return unquote($name + '--');
}

@return unquote('-' + $name);
}

@return '';
}

// Generates grid-gap values for each breakpoint
// Parameters are:
// * $gutters: the gutters map
Expand All @@ -46,7 +27,7 @@
@each $breakpoint-name, $breakpoint-value in $breakpoints {
@include breakpoint.up($breakpoint-value) {
@each $column in $column-count {
.Grid--#{-get-breakpoint-name($breakpoint-name, $breakpoint-value)}cols-#{$column} {
.Grid--#{breakpoint.get-modifier('infix', $breakpoint-name, $breakpoint-value)}cols-#{$column} {
grid-template-columns: repeat(#{$column}, 1fr);
}
}
Expand All @@ -63,7 +44,7 @@
@each $breakpoint-name, $breakpoint-value in $breakpoints {
@include breakpoint.up($breakpoint-value) {
@each $option in $grid-span-variants {
.Grid__span--#{-get-breakpoint-name($breakpoint-name, $breakpoint-value)}over-#{$option} {
.Grid__span--#{breakpoint.get-modifier('infix', $breakpoint-name, $breakpoint-value)}over-#{$option} {
$start: 1 + math.div(($grid-columns - $option), 2);

grid-column: $start / span $option;
Expand All @@ -83,7 +64,7 @@
$row-end: 'initial';

@each $breakpoint-name, $breakpoint-value in $breakpoints {
$suffix: -get-breakpoint-name($breakpoint-name, $breakpoint-value, false);
$suffix: breakpoint.get-modifier('suffix', $breakpoint-name, $breakpoint-value);

@include breakpoint.up($breakpoint-value) {
$column-start: var(--grid-item-column-start#{$suffix}, $column-start);
Expand All @@ -102,7 +83,7 @@

:where(.GridItem .GridItem) {
@each $breakpoint-name, $breakpoint-value in $breakpoints {
$suffix: -get-breakpoint-name($breakpoint-name, $breakpoint-value, false);
$suffix: breakpoint.get-modifier('suffix', $breakpoint-name, $breakpoint-value);

--grid-item-column-start#{$suffix}: initial;
--grid-item-column-end#{$suffix}: initial;
Expand Down
240 changes: 240 additions & 0 deletions packages/web/src/scss/components/Toast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# Toast

Toast displays a brief, temporary notification that appears at a prescribed location of an application window.

Toast is a composition of a few subcomponents:

- [Toast](#toast)
- [ToastBar](#toastbar)

## Toast

The Toast component is a container for the ToastBar component. It is responsible for positioning the
[ToastBar](#toastbar) component, even when there are [multiple of them](#toast-queue).

```html
<div class="Toast" aria-live="polite">
<div class="Toast__queue">
<!-- ToastBar components go here -->
</div>
</div>
```

### Accessibility

The wrapping Toast container has the [`aria-live="polite"`][mdn-aria-live] attribute set, which will announce any
**dynamic changes** inside the container as they happen. In order for this to work, the Toast component **must be
present in the DOM** on the initial page load, even when empty.

👉 Unless you are absolutely sure that your toast messages are critical to interrupt the user, you should keep the
`polite` value of the `aria-live` attribute. When set to `assertive`, assistive technologies immediately notify the
user, potentially clearing the speech queue of previous updates.

### Alignment

The Toast component is positioned at the bottom of the screen by default. It is also fixed to the bottom of the screen,
so it will always be visible, even when the user scrolls. Available alignment options are derived from the
[AlignmentX and AlignmentY][dictionary-alignment] dictionaries and are as follows:

- `top` `left`,
- `top` `center`,
- `top` `right`,
- `bottom` `left`,
- `bottom` `center` (default),
- `bottom` `right`.

Use the `Toast--<alignmentX>` and `Toast--<alignmentY>` modifiers to change the alignment of the Toast component:

| AlignmentX/Y | left | center | right |
| ------------ | --------------------------- | ----------------------------- | ---------------------------- |
| top | `Toast--top Toast--left` | `Toast--top Toast--center` | `Toast--top Toast--right` |
| bottom | `Toast--bottom Toast--left` | `Toast--bottom Toast--center` | `Toast--bottom Toast--right` |

ℹ️ The `center` vertical alignment is not supported, as it would not make sense for a toast notification to be in the
middle of the screen.

Example:

```html
<div class="Toast Toast--bottom Toast--right" aria-live="polite">
<div class="Toast__queue">
<!-- ToastBar components go here -->
</div>
</div>
```

### Responsive Alignment

The Toast container can be aligned differently on different screen sizes. Use the `Toast--<breakpoint>--<alignmentX/Y>`
modifiers to change the alignment of the Toast component starting on a specific screen size, e.g. `Toast--tablet--top`,
`Tablet--desktop--left`, etc. (leave the breakpoint empty for alignment on all screen sizes, including mobile screens).

Example:

```html
<div class="Toast Toast--bottom Toast--center Toast--tablet--right" aria-live="polite">
<div class="Toast__queue">
<!-- ToastBar components go here -->
</div>
</div>
```

### Mobile Screens

Positioning becomes trickier on mobile screens due to the presence of notches, rounded corners, and the virtual
keyboard. The Toast component tries to find the best position to be visible using the following detection mechanisms:

1. On devices with rounded displays and/or notches (e.g. iPhone X and newer), the Toast component is pushed inwards to
avoid the rounded corners. The `viewport-fit="cover"` meta tag is required for this feature to work:

```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
```

2. Android Chrome only: When the vertical alignment is set to `bottom` and the virtual keyboard is open, the Toast
component is pushed upwards to avoid being covered by the keyboard. This feature requires the following JavaScript
snippet and is currently supported only in Chrome 94 on Android and later.

```js
// Enable CSS to detect the presence of virtual keyboard:
if ('virtualKeyboard' in navigator) {
navigator.virtualKeyboard.overlaysContent = true;
}
```

### Toast Queue

When multiple ToastBar components are present, they stack up in a queue, separated by a gap. The ToastBar components are
sorted from top to bottom for the `top` vertical alignment, and from bottom to top for the `bottom` vertical alignment.
When the queue does not fit the screen, it becomes scrollable.

👉 Please note that the initial scroll position is always at the **top** of the queue.

👉 Please note that scrolling is only possible over the toast message boxes and not over the entire container, so the
page behind the toast messages is still accessible.

## ToastBar

The ToastBar component is the actual toast notification. It is a simple container with a message and a few optional
elements.

Minimum example:

```html
<div class="ToastBar ToastBar--inverted">
<div class="ToastBar__content">
<div class="ToastBar__message">Message only</div>
</div>
</div>
```

### Optional Icon

An icon can be added to the ToastBar component:

```html
<div class="ToastBar ToastBar--inverted">
<div class="ToastBar__content">
<svg width="20" height="20" aria-hidden="true">
<use xlink:href="/icons/svg/sprite.svg#info" />
</svg>
<div class="ToastBar__message">Message with icon</div>
</div>
</div>
```

### Action Link

An action link can be added to the ToastBar component:

```html
<div class="ToastBar ToastBar--inverted">
<div class="ToastBar__content">
<div class="ToastBar__message">
Message with action
<a href="#" class="link-inverted link-underlined">Action</a>
</div>
</div>
</div>
```

### Colors

The ToastBar component is available in all [emotion colors][dictionary-color], plus the `inverted` variant (default).
Use the `ToastBar--<color>` modifier class to change the color of the ToastBar component.

For example:

```html
<div class="ToastBar ToastBar--success">
<div class="ToastBar__content">
<div class="ToastBar__message">Success message</div>
</div>
</div>
```

### Dismissible ToastBar

To make the ToastBar dismissible, add the `ToastBar--dismissible` modifier class, the `id` attribute, and a close button:

```html
<div id="my-dismissible-toast" class="ToastBar ToastBar--inverted ToastBar--dismissible">
<div class="ToastBar__content">
<div class="ToastBar__message">Dismissible message</div>
</div>
<button
type="button"
class="Button Button--small Button--square Button--inverted"
data-spirit-dismiss="toast"
aria-controls="my-dismissible-toast"
aria-expanded="true"
>
<svg width="24" height="24" aria-hidden="true">
<use xlink:href="/icons/svg/sprite.svg#close" />
</svg>
<span class="accessibility-hidden">Close</span>
</button>
</div>
```

⚠️ The JavaScript functionality for dismissing the ToastBar is yet to be implemented.

## Full Example

```html
<!-- Toast: start -->
<div class="Toast Toast--bottom Toast--center" aria-live="polite">
<div class="Toast__queue">
<!-- ToastBar: start -->
<div id="my-dismissible-toast" class="ToastBar ToastBar--inverted ToastBar--dismissible">
<div class="ToastBar__content">
<svg width="20" height="20" aria-hidden="true">
<use xlink:href="/icons/svg/sprite.svg#info" />
</svg>
<div class="ToastBar__message">
Toast message
<a href="#" class="link-inverted link-underlined">Action</a>
</div>
</div>
<button
type="button"
class="Button Button--small Button--square Button--inverted"
data-spirit-dismiss="toast"
aria-controls="my-dismissible-toast"
aria-expanded="true"
>
<svg width="24" height="24" aria-hidden="true">
<use xlink:href="/icons/svg/sprite.svg#close" />
</svg>
<span class="accessibility-hidden">Close</span>
</button>
</div>
<!-- ToastBar: end -->
</div>
</div>
<!-- Toast: end -->
```

[mdn-aria-live]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live
[dictionary-alignment]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#alignment
[dictionary-color]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#color

0 comments on commit c98a39e

Please sign in to comment.