Skip to content

Commit

Permalink
fix(sbb-dialog): fix accessibility with option to hide the header on …
Browse files Browse the repository at this point in the history
…scroll (#2231)

In order to support the new feature of optionally hiding the header during scroll, we had to re structure all the dialog component.

BREAKING CHANGE: The `sbb-dialog` component now supports the dedicated inner elements `sbb-dialog-title`, `sbb-dialog-content`, and `sbb-dialog-actions`. Use these components to respectively provide a title, a content and, optionally, a footer with an action group. Moreover, the full-screen variant (which occurred when no title was provided to the dialog) has been removed. To achieve a full-screen overlay, please use the `sbb-overlay` component.
This was the previous implementation:
```html
<sbb-dialog title-content="Title">
  <p>Dialog content.</p>
  <sbb-action-group slot="action-group">...</sbb-action-group>
</sbb-dialog>
```
This is the new implementation:
```html
<sbb-dialog>
  <sbb-dialog-title>Title</sbb-dialog-title>
  <sbb-dialog-content>Dialog content</sbb-dialog-content>
  <sbb-dialog-actions>...</sbb-dialog-actions>
</sbb-dialog>
```
Previously, a ***full-screen*** dialog was displayed if no title was provided to the dialog component:
```html
<sbb-dialog>
  <p>Dialog content.</p>
</sbb-dialog>
```
It is now mandatory to use the `sbb-overlay` component to display the same variant:
```html
<sbb-overlay>
  <p>Overlay content.</p>
</sbb-overlay>
```
  • Loading branch information
dauriamarco authored Apr 10, 2024
1 parent 2e6470e commit 159f536
Show file tree
Hide file tree
Showing 36 changed files with 1,950 additions and 1,009 deletions.
17 changes: 14 additions & 3 deletions src/components/core/dom/breakpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isBrowser } from './platform.js';

export type Breakpoint = 'zero' | 'micro' | 'small' | 'medium' | 'wide' | 'large' | 'ultra';
export const breakpoints = ['zero', 'micro', 'small', 'medium', 'wide', 'large', 'ultra'] as const;
export type Breakpoint = (typeof breakpoints)[number];

/**
* Checks whether the document matches a particular media query.
Expand All @@ -10,7 +11,11 @@ export type Breakpoint = 'zero' | 'micro' | 'small' | 'medium' | 'wide' | 'large
* @param to The breakpoint corresponding to the `max-width` value of the media query (optional).
* @returns A boolean indicating whether the window matches the breakpoint.
*/
export function isBreakpoint(from?: Breakpoint, to?: Breakpoint): boolean {
export function isBreakpoint(
from?: Breakpoint,
to?: Breakpoint,
properties?: { includeMaxBreakpoint: boolean },
): boolean {
if (!isBrowser()) {
// TODO: Remove and decide case by case what should be done on consuming end
return false;
Expand All @@ -19,7 +24,13 @@ export function isBreakpoint(from?: Breakpoint, to?: Breakpoint): boolean {
const computedStyle = getComputedStyle(document.documentElement);
const breakpointMin = from ? computedStyle.getPropertyValue(`--sbb-breakpoint-${from}-min`) : '';
const breakpointMax = to
? `${parseFloat(computedStyle.getPropertyValue(`--sbb-breakpoint-${to}-min`)) - 0.0625}rem`
? `${
parseFloat(
computedStyle.getPropertyValue(
`--sbb-breakpoint-${to}-${properties?.includeMaxBreakpoint ? 'max' : 'min'}`,
),
) - (properties?.includeMaxBreakpoint ? 0 : 0.0625)
}rem`
: ''; // subtract 1px (0.0625rem) from the max-width breakpoint

const minWidth = breakpointMin && `(min-width: ${breakpointMin})`;
Expand Down
74 changes: 0 additions & 74 deletions src/components/dialog/__snapshots__/dialog.spec.snap.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* @web/test-runner snapshot v1 */
export const snapshots = {};

snapshots["sbb-dialog-actions renders"] =
`<sbb-dialog-actions
align-group="start"
button-size="l"
horizontal-from="medium"
link-size="m"
orientation="horizontal"
>
</sbb-dialog-actions>
`;
/* end snapshot sbb-dialog-actions renders */

snapshots["sbb-dialog-actions A11y tree Chrome"] =
`<p>
{
"role": "WebArea",
"name": ""
}
</p>
`;
/* end snapshot sbb-dialog-actions A11y tree Chrome */

snapshots["sbb-dialog-actions A11y tree Firefox"] =
`<p>
{
"role": "document",
"name": ""
}
</p>
`;
/* end snapshot sbb-dialog-actions A11y tree Firefox */

snapshots["sbb-dialog-actions A11y tree Safari"] =
`<p>
{
"role": "WebArea",
"name": ""
}
</p>
`;
/* end snapshot sbb-dialog-actions A11y tree Safari */

21 changes: 21 additions & 0 deletions src/components/dialog/dialog-actions/dialog-actions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use '../../core/styles' as sbb;

:host {
display: contents;

@include sbb.if-forced-colors {
--sbb-dialog-actions-border: var(--sbb-border-width-1x) solid CanvasText;
}
}

.sbb-dialog-actions {
padding-inline: var(--sbb-dialog-padding-inline);
padding-block: var(--sbb-spacing-responsive-s);
margin-block-start: auto;
background-color: var(--sbb-dialog-background-color);
border-block-start: var(--sbb-dialog-actions-border);

:host([data-overflows]:not([data-negative])) & {
@include sbb.shadow-level-9-soft;
}
}
24 changes: 24 additions & 0 deletions src/components/dialog/dialog-actions/dialog-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from '@open-wc/testing';
import { html } from 'lit/static-html.js';

import { fixture, testA11yTreeSnapshot } from '../../core/testing/private/index.js';
import './dialog-actions.js';

describe('sbb-dialog-actions', () => {
it('renders', async () => {
const root = await fixture(html`<sbb-dialog-actions></sbb-dialog-actions>`);

await expect(root).dom.to.equalSnapshot();

expect(root).shadowDom.to.be.equal(`
<div class="sbb-dialog-actions">
<div class="sbb-action-group">
<slot>
</slot>
</div>
</div>
`);
});

testA11yTreeSnapshot(html`<sbb-dialog-actions></sbb-dialog-actions>`);
});
45 changes: 45 additions & 0 deletions src/components/dialog/dialog-actions/dialog-actions.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { withActions } from '@storybook/addon-actions/decorator';
import type { Decorator, Meta, StoryObj } from '@storybook/web-components';
import type { TemplateResult } from 'lit';
import { html } from 'lit';

import './dialog-actions.js';
import readme from './readme.md?raw';

import '../../button/button/index.js';
import '../../button/secondary-button/index.js';
import '../../link/index.js';

const Template = (): TemplateResult =>
html`<sbb-dialog-actions align-group="stretch" orientation="vertical" horizontal-from="medium">
<sbb-block-link
align-self="start"
icon-name="chevron-small-left-small"
href="https://www.sbb.ch/en/"
sbb-dialog-close
>
Link
</sbb-block-link>
<sbb-secondary-button sbb-dialog-close> Cancel </sbb-secondary-button>
<sbb-button sbb-dialog-close> Confirm </sbb-button>
</sbb-dialog-actions>`;

export const Default: StoryObj = { render: Template };

const meta: Meta = {
decorators: [
(story) => html` <div style="padding: 2rem;">${story()}</div> `,
withActions as Decorator,
],
parameters: {
backgrounds: {
disable: true,
},
docs: {
extractComponentDescription: () => readme,
},
},
title: 'components/sbb-dialog/sbb-dialog-actions',
};

export default meta;
28 changes: 28 additions & 0 deletions src/components/dialog/dialog-actions/dialog-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { CSSResultGroup, TemplateResult } from 'lit';
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';

import { SbbActionGroupElement } from '../../action-group/index.js';

import style from './dialog-actions.scss?lit&inline';

/**
* Use this component to display a footer into an `sbb-dialog` with an action group.
*
* @slot - Use the unnamed slot to add `sbb-block-link` or `sbb-button` elements to the `sbb-dialog-actions`.
*/
@customElement('sbb-dialog-actions')
export class SbbDialogActionsElement extends SbbActionGroupElement {
public static override styles: CSSResultGroup = [SbbActionGroupElement.styles, style];

protected override render(): TemplateResult {
return html` <div class="sbb-dialog-actions">${super.render()}</div> `;
}
}

declare global {
interface HTMLElementTagNameMap {
// eslint-disable-next-line @typescript-eslint/naming-convention
'sbb-dialog-actions': SbbDialogActionsElement;
}
}
1 change: 1 addition & 0 deletions src/components/dialog/dialog-actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dialog-actions.js';
29 changes: 29 additions & 0 deletions src/components/dialog/dialog-actions/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
The `sbb-dialog-actions` component extends the [sbb-action-group](/docs/components-sbb-action-group--docs) component. Use it in combination with the [sbb-dialog](/docs/components-sbb-dialog--docs) to display a footer with an action group.

```html
<sbb-dialog>
<sbb-dialog-action>
<sbb-block-link sbb-dialog-close>Link</sbb-block-link>
<sbb-secondary-button sbb-dialog-close> Cancel </sbb-secondary-button>
<sbb-button sbb-dialog-close> Confirm </sbb-button>
</sbb-dialog-actions>
</sbb-dialog>
```

<!-- Auto Generated Below -->

## Properties

| Name | Attribute | Privacy | Type | Default | Description |
| ---------------- | ----------------- | ------- | ------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `alignGroup` | `align-group` | public | `'start' \| 'center' \| 'stretch' \| 'end'` | `'start'` | Set the slotted `<sbb-action-group>` children's alignment. |
| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom` | `'medium'` | Overrides the behaviour of `orientation` property. |
| `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Indicates the orientation of the components inside the `<sbb-action-group>`. |
| `buttonSize` | `button-size` | public | `SbbButtonSize` | `'l'` | Size of the nested sbb-button instances. This will overwrite the size attribute of nested sbb-button instances. |
| `linkSize` | `link-size` | public | `SbbLinkSize` | `'m'` | Size of the nested sbb-block-link instances. This will overwrite the size attribute of nested sbb-block-link instances. |

## Slots

| Name | Description |
| ---- | -------------------------------------------------------------------------------------------------- |
| | Use the unnamed slot to add `sbb-block-link` or `sbb-button` elements to the `sbb-dialog-actions`. |
26 changes: 26 additions & 0 deletions src/components/dialog/dialog-content/dialog-content.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@use '../../core/styles' as sbb;

:host {
display: contents;
}

.sbb-dialog-content {
@include sbb.scrollbar-rules;

padding-inline: var(--sbb-dialog-padding-inline);
padding-block: var(--sbb-dialog-padding-block);
overflow: auto;
transform: translateY(var(--sbb-dialog-header-margin-block-start));
margin-block: 0 calc(var(--sbb-dialog-header-height) * -1);
transition: var(--sbb-dialog-content-transition);
z-index: -1;

// In order to improve the header transition on mobile (especially iOS) we use
// a combination of the transform and margin properties on touch devices,
// while on desktop we use just the margin-block for a better transition of the visible scrollbar.
@include sbb.mq($from: medium) {
transform: unset;
margin-block: var(--sbb-dialog-header-margin-block-start) 0;
transition: margin var(--sbb-dialog-animation-duration) var(--sbb-dialog-animation-easing);
}
}
17 changes: 17 additions & 0 deletions src/components/dialog/dialog-content/dialog-content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import './dialog-content.js';

describe('sbb-dialog-content', () => {
it('renders', async () => {
const root = await fixture(html`<sbb-dialog-content>Content</sbb-dialog-content>`);

expect(root).dom.to.be.equal(`<sbb-dialog-content>Content</sbb-dialog-content>`);

expect(root).shadowDom.to.be.equal(`
<div class="sbb-dialog-content">
<slot></slot>
</div>
`);
});
});
30 changes: 30 additions & 0 deletions src/components/dialog/dialog-content/dialog-content.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { withActions } from '@storybook/addon-actions/decorator';
import type { Decorator, Meta, StoryObj } from '@storybook/web-components';
import type { TemplateResult } from 'lit';
import { html } from 'lit';

import './dialog-content.js';
import readme from './readme.md?raw';

const Template = (): TemplateResult =>
html`<sbb-dialog-content>This is a dialog content.</sbb-dialog-content>`;

export const Default: StoryObj = { render: Template };

const meta: Meta = {
decorators: [
(story) => html` <div style="padding: 2rem;">${story()}</div> `,
withActions as Decorator,
],
parameters: {
backgrounds: {
disable: true,
},
docs: {
extractComponentDescription: () => readme,
},
},
title: 'components/sbb-dialog/sbb-dialog-content',
};

export default meta;
Loading

0 comments on commit 159f536

Please sign in to comment.