Skip to content

Commit

Permalink
feat(styles): Added toggle button component (#3744) (#3889)
Browse files Browse the repository at this point in the history
  • Loading branch information
veyaromain authored Dec 2, 2024
1 parent b6a6e7a commit d8b3c9c
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/chilled-owls-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@swisspost/design-system-documentation': minor
'@swisspost/design-system-components': minor
---

Added the `post-togglebutton` component.
114 changes: 114 additions & 0 deletions packages/components/cypress/e2e/togglebutton.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const TOGGLE_BUTTON_ID = '1a6f47c2-5e8a-45a0-b1c3-9f7e2b834c24';

describe('togglebutton', () => {
beforeEach(() => {
cy.visit('/iframe.html?id=snapshots--toggle-button');
cy.get('post-togglebutton', { timeout: 30000 }).should('be.visible');
});

describe('default behavior', () => {
it('should toggle state when clicked', () => {
cy.get('post-togglebutton')
.first()
.as('button')
.shadow()
.find('slot[name="untoggled"]')
.should('exist');

cy.get('@button').click();

cy.get('@button').shadow().find('slot[name="toggled"]').should('exist');
});

it('should toggle state when pressing Enter key', () => {
cy.get('post-togglebutton')
.first()
.as('button')
.shadow()
.find('slot[name="untoggled"]')
.should('exist');

cy.get('@button').trigger('keydown', { key: 'Enter' });

cy.get('@button').shadow().find('slot[name="toggled"]').should('exist');
});

it('should have correct ARIA attributes', () => {
cy.get('post-togglebutton')
.first()
.as('button')
.should('have.attr', 'role', 'button')
.and('have.attr', 'aria-pressed', 'false')
.and('have.attr', 'tabindex', '0');

cy.get('@button').click();

cy.get('@button').should('have.attr', 'aria-pressed', 'true');
});
});

describe('initial state', () => {
it('should respect initial toggled state', () => {
cy.get('post-togglebutton[toggled]')
.first()
.as('toggledButton')
.shadow()
.find('slot[name="toggled"]')
.should('exist');

cy.get('@toggledButton').should('have.attr', 'aria-pressed', 'true');
});

it('should respect untoggled state', () => {
cy.get('post-togglebutton:not([toggled])')
.first()
.as('untoggledButton')
.shadow()
.find('slot[name="untoggled"]')
.should('exist');

cy.get('@untoggledButton').should('have.attr', 'aria-pressed', 'false');
});
});

describe('slot content', () => {
it('should display correct slot content based on toggle state', () => {
cy.get('post-togglebutton').first().as('button');

cy.get('@button').shadow().find('slot[name="untoggled"]').should('exist');

cy.get('@button').click();

cy.get('@button').shadow().find('slot[name="toggled"]').should('exist');

cy.get('@button').click();

cy.get('@button').shadow().find('slot[name="untoggled"]').should('exist');
});
});

describe('version attribute', () => {
it('should have the correct version data attribute', () => {
cy.get('post-togglebutton').first().should('have.attr', 'data-version');
});
});

describe('Accessibility', () => {
beforeEach(() => {
cy.injectAxe();
});

it('Has no detectable a11y violations on load for all variants', () => {
cy.checkA11y('#root-inner');
});

it('Should be keyboard navigable', () => {
cy.get('post-togglebutton')
.first()
.focus()
.should('have.focus')
.trigger('keydown', { key: 'Enter' })
.should('have.attr', 'aria-pressed', 'true');
});
});
});
21 changes: 21 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ export namespace Components {
*/
"variant": 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow';
}
interface PostTogglebutton {
/**
* If `true`, the button is in the "on" state, otherwise it is in the "off" state.
*/
"toggled": boolean;
}
interface PostTooltip {
/**
* Wheter or not to display a little pointer arrow
Expand Down Expand Up @@ -762,6 +768,12 @@ declare global {
prototype: HTMLPostTagElement;
new (): HTMLPostTagElement;
};
interface HTMLPostTogglebuttonElement extends Components.PostTogglebutton, HTMLStencilElement {
}
var HTMLPostTogglebuttonElement: {
prototype: HTMLPostTogglebuttonElement;
new (): HTMLPostTogglebuttonElement;
};
interface HTMLPostTooltipElement extends Components.PostTooltip, HTMLStencilElement {
}
var HTMLPostTooltipElement: {
Expand Down Expand Up @@ -797,6 +809,7 @@ declare global {
"post-tab-panel": HTMLPostTabPanelElement;
"post-tabs": HTMLPostTabsElement;
"post-tag": HTMLPostTagElement;
"post-togglebutton": HTMLPostTogglebuttonElement;
"post-tooltip": HTMLPostTooltipElement;
}
}
Expand Down Expand Up @@ -1136,6 +1149,12 @@ declare namespace LocalJSX {
*/
"variant"?: 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow';
}
interface PostTogglebutton {
/**
* If `true`, the button is in the "on" state, otherwise it is in the "off" state.
*/
"toggled"?: boolean;
}
interface PostTooltip {
/**
* Wheter or not to display a little pointer arrow
Expand Down Expand Up @@ -1179,6 +1198,7 @@ declare namespace LocalJSX {
"post-tab-panel": PostTabPanel;
"post-tabs": PostTabs;
"post-tag": PostTag;
"post-togglebutton": PostTogglebutton;
"post-tooltip": PostTooltip;
}
}
Expand Down Expand Up @@ -1220,6 +1240,7 @@ declare module "@stencil/core" {
"post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes<HTMLPostTabPanelElement>;
"post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes<HTMLPostTabsElement>;
"post-tag": LocalJSX.PostTag & JSXBase.HTMLAttributes<HTMLPostTagElement>;
"post-togglebutton": LocalJSX.PostTogglebutton & JSXBase.HTMLAttributes<HTMLPostTogglebuttonElement>;
"post-tooltip": LocalJSX.PostTooltip & JSXBase.HTMLAttributes<HTMLPostTooltipElement>;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, Host, h, Prop } from '@stencil/core';
import { version } from '@root/package.json';

/**
* @slot toggled - Slot for content displayed when the button is in the "on" state.
* @slot untoggled - Slot for content displayed when the button is in the "off" state.
*/

@Component({
tag: 'post-togglebutton',
styleUrl: 'post-togglebutton.scss',
shadow: true,
})
export class PostTogglebutton {
/**
* If `true`, the button is in the "on" state, otherwise it is in the "off" state.
*/
@Prop({ reflect: true, mutable: true }) toggled: boolean = false;

private handleClick = () => {
this.toggled = !this.toggled;
};

private handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
this.toggled = !this.toggled;
}
};

render() {
return (
<Host
slot="post-togglebutton"
tabindex="0"
data-version={version}
role="button"
aria-pressed={this.toggled.toString()}
onClick={this.handleClick}
onKeyDown={this.handleKeydown}
>
<span hidden={this.toggled}>
<slot name="untoggled" />
</span>
<span hidden={!this.toggled}>
<slot name="toggled" />
</span>
</Host>
);
}
}
25 changes: 25 additions & 0 deletions packages/components/src/components/post-togglebutton/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# post-togglebutton



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| --------- | --------- | ------------------------------------------------------------------------------- | --------- | ------- |
| `toggled` | `toggled` | If `true`, the button is in the "on" state, otherwise it is in the "off" state. | `boolean` | `false` |


## Slots

| Slot | Description |
| ------------- | ----------------------------------------------------------------- |
| `"toggled"` | Slot for content displayed when the button is in the "on" state. |
| `"untoggled"` | Slot for content displayed when the button is in the "off" state. |


----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
2 changes: 1 addition & 1 deletion packages/components/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function getComponentDefinitions(dir: string, files: string[] = []) {
return files;
}

describe('Index.js', () => {
describe('packages/components/src/index.ts', () => {
componentDefinitions.forEach(def => {
const component = componentExports.find(exp => exp === def);

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { PostTabHeader } from './components/post-tab-header/post-tab-header';
export { PostTabPanel } from './components/post-tab-panel/post-tab-panel';
export { PostTooltip } from './components/post-tooltip/post-tooltip';
export { PostTag } from './components/post-tag/post-tag';
export { PostTogglebutton } from './components/post-togglebutton/post-togglebutton';
export { PostList } from './components/post-list/post-list';
export { PostListItem } from './components/post-list-item/post-list-item';
export { PostHeader } from './components/post-header/post-header';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
describe('Toggle Button', () => {
it('default', () => {
cy.visit('/iframe.html?id=snapshots--toggle-button');
cy.get('post-togglebutton[data-hydrated]', { timeout: 30000 }).should('be.visible');
cy.percySnapshot('Toggle Buttons', { widths: [320, 600, 1024] });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Canvas, Controls, Meta } from '@storybook/blocks';
import * as toggleButtonStories from './togglebutton.stories';

<Meta of={toggleButtonStories} />

# Toggle Button

<p className="lead">A toggle button component to switch between two states and display different content based on the current state.</p>

<Canvas sourceState="shown" of={toggleButtonStories.Default} />
<div className="hide-col-default">
<Controls of={toggleButtonStories.Default} />
</div>

## Installation

The `<post-togglebutton>` element is part of the `@swisspost/design-system-components` package. For more information, read the [getting started with components guide](/?path=/docs/edfb619b-fda1-4570-bf25-20830303d483--docs).

## Examples

### Initial Toggled State

You can set the button to be toggled initially by using the `toggled` attribute. When set, the component will start in the toggled state and display the corresponding content.

<Canvas of={toggleButtonStories.InitiallyToggled} />

### Icon content

You can also add an icon to the content (for a toggle menu, for example).

<Canvas of={toggleButtonStories.IconContent} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { StoryObj } from '@storybook/web-components';
import meta from './togglebutton.stories';
import { html } from 'lit';

const { id, ...metaWithoutId } = meta;

export default {
...metaWithoutId,
title: 'Snapshots',
};

const SCHEME = ['light', 'dark'];
const BTN = ['btn-primary', 'btn-secondary', 'btn-terciary'];
const SIZES = ['', 'btn-sm', 'btn-lg'];

type Story = StoryObj;

export const ToggleButton: Story = {
render: () => {
const TOGGLED = [false, true];

return html`
${SCHEME.map(
scheme => html`
<div data-color-scheme="${scheme}" class="palette-default px-5">
${BTN.map(btn =>
SIZES.map(size =>
TOGGLED.map(
isToggled => html`
${meta.render({
variant: btn,
size: size || 'null',
toggled: isToggled,
contentWhenUntoggled: 'Untoggled',
contentWhenToggled: 'Toggled',
})}
`,
),
),
)}
</div>
`,
)}
`;
},
};
Loading

0 comments on commit d8b3c9c

Please sign in to comment.