Skip to content

Commit

Permalink
feat(components): segmented-button (#3879)
Browse files Browse the repository at this point in the history
Co-authored-by: Zherdetska Alona, IT21.1 <[email protected]>
Co-authored-by: Oliver Schürch <[email protected]>
Co-authored-by: Philipp Gfeller <[email protected]>
Co-authored-by: Philipp Gfeller <[email protected]>
  • Loading branch information
5 people authored Nov 25, 2024
1 parent 6ca7f64 commit 47570b0
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changeset/big-hats-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@swisspost/design-system-documentation': minor
'@swisspost/design-system-styles': minor
---

Added a new `segmented-button` component, which allows users to toggle between two or more content sections within the same area on the screen.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
describe('Segmented Button', () => {
describe('Accessibility', () => {
beforeEach(() => {
cy.visit('/iframe.html?id=snapshots--segmented-button');
cy.get('.segmented-button', { timeout: 30000 }).should('be.visible');
cy.injectAxe();
});

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

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

it('Displays vertical layout when viewport is narrower than 600px', () => {
cy.viewport(500, 600);
cy.get('.segmented-button')
.should('have.css', 'flex-direction', 'column');
});
});

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

it('Allows selecting an input and updates the state', () => {
cy.get('.segmented-button label').first().click();

cy.get('.segmented-button label input').first().should('be.checked');

cy.get('.segmented-button label').eq(1).click();

cy.get('.segmented-button label input').eq(1).should('be.checked');

cy.get('.segmented-button label input').first().should('not.be.checked');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
describe('Segmented-button', () => {
it('default', () => {
cy.visit('/iframe.html?id=snapshots--segmented-button');
cy.get('.segmented-button', { timeout: 30000 }).should('be.visible');
cy.percySnapshot('Segmented-button', { widths: [1440] });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Meta, Canvas, Controls } from '@storybook/blocks';
import * as SegmentedButtonStories from './segmented-button.stories';
import StylesPackageImport from '@/shared/styles-package-import.mdx';

<Meta of={SegmentedButtonStories} />

<div className="docs-title">
# Segmented Button

<nav>
<link-design of={JSON.stringify(SegmentedButtonStories)}></link-design>
</nav>
</div>

The segmented button is a single-select component.
It allows users to toggle between two or more content sections within the same area on the screen.

<div role="alert" class="alert alert-info">
<p>
If the labels are too long or the number of options is excessive for the available space,
consider using a select component. Keep in mind that long labels may cause layout issues,
such as text overflowing or word breaks.
</p>
</div>

<Canvas of={SegmentedButtonStories.TextExample} sourceState="shown" />
<Controls of={SegmentedButtonStories.TextExample} />

## Segmented icon button

<Canvas of={SegmentedButtonStories.IconExample} sourceState="shown" />
<Controls of={SegmentedButtonStories.IconExample} />

<StylesPackageImport components={['segmented-button']} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { StoryObj } from '@storybook/web-components';
import meta from './segmented-button.stories';
import { html } from 'lit';

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

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

type Story = StoryObj;

export const SegmentedButton: Story = {
render: () => {
const labelCounts = [2, 4, 6, 8];
const themes = ['bg-light', 'bg-dark'];

return html`
<div class="gap-4">
${themes.map(
(theme) => html`
<div class="${theme} d-flex flex-column w-75 gap-16 p-16">
${labelCounts.map((count) => {
const labels = Array.from({ length: count }, (_, i) => `Label ${i + 1}`);
return html`
<div class="segmented-button-container">
<fieldset class="segmented-button">
${labels.map(
(label) => html`
<label class="segmented-button-label">
<input name="snapshot-${count}-${theme}" type="radio" />
${label}
</label>
`
)}
</fieldset>
</div>
`;
})}
</div>
`
)}
</div>
`;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Args, StoryObj } from '@storybook/web-components';
import { html, nothing } from 'lit';
import { MetaComponent } from '@root/types';

const MAX_LABELS = 8;

const meta: MetaComponent = {
id: '78509712-d45e-462c-bde3-405cfaff5421',
title: 'Components/Segmented button',
tags: ['package:HTML'],
parameters: {
badges: [],
design: {
type: 'figma',
url: 'https://www.figma.com/design/JIT5AdGYqv6bDRpfBPV8XR/Foundations-%26-Components-Next-Level?node-id=2864-83396&node-type=instance&m=dev',
},
},
args: {
labelCount: 4,
},
argTypes: {
labelCount: {
name: 'Number of segments',
description: `Defines the number of segments for the segmented button. The maximum number of supported segments is 8. If you need more options, please refer to the select component.`,
control: { type: 'number', min: 1, max: MAX_LABELS },
table: { category: 'Content' },
},
},
};

export default meta;

type Story = StoryObj;

export const TextExample: Story = {
render: (args: Args) => {
const labelCount = Math.min(args.labelCount || 0, MAX_LABELS);
const labelsArray = Array.from({ length: labelCount }, (_, i) => `Label ${i + 1}`);
const name = `segmented-button-${Math.random().toString(36).slice(-6)}`;

return html`
<div class="segmented-button-container">
<fieldset class="segmented-button">
<legend>Choose one of the options</legend>
${labelsArray.map(
(label, index) => html`
<label class="segmented-button-label">
<input name="${name}" type="radio" checked="${index === 0 ? '' : nothing}" />
${label}
</label>
`,
)}
</fieldset>
</div>
`;
},
};

export const IconExample: Story = {
render: (args: Args) => {
const labelCount = Math.min(args.labelCount || 0, MAX_LABELS);
const name = `segmented-button-${Math.random().toString(36).slice(-6)}`;

return html`
<div class="segmented-button-container">
<fieldset class="segmented-button">
<legend>Choose one of the options</legend>
${Array.from(
{ length: labelCount },
(_undefined, index) => html`
<label class="segmented-button-label">
<input type="radio" name="${name}" checked="${index === 0 ? '' : nothing}" />
<post-icon name="${1000 + index}" />
</label>
`,
)}
</fieldset>
</div>
`;
},
};
1 change: 1 addition & 0 deletions packages/styles/src/elements/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
@use 'list-bullet';
@use 'paragraph';
@use 'fieldset-legend';
@use 'segmented-button';
@use 'list';
@use 'heading';
Loading

0 comments on commit 47570b0

Please sign in to comment.