From bde567597f1d285f7e12cd93e32084feb97efb5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aliz=C3=A9=20Debray?=
<33580481+alizedebray@users.noreply.github.com>
Date: Wed, 10 Apr 2024 13:38:02 +0200
Subject: [PATCH] feat(styles, docs, migrations): create the chip component
(#2855)
---
.changeset/quiet-mugs-design.md | 5 +
.changeset/selfish-lions-tap.md | 5 +
.changeset/sweet-clouds-scream.md | 5 +
packages/demo/package.json | 1 +
.../snapshots/components/badge.snapshot.ts | 7 -
.../snapshots/components/chip.snapshot.ts | 7 +
.../stories/components/badge/badge.stories.ts | 10 +-
.../src/stories/components/chip/chip.docs.mdx | 42 +++
.../components/chip/chip.snapshot.stories.ts | 43 +++
.../stories/components/chip/chip.stories.ts | 263 ++++++++++++++++++
packages/migrations/src/migrations.json | 5 +
.../src/migrations/bootstrap/chip/index.ts | 75 +++++
.../src/migrations/v7/badge/index.ts | 106 +++++++
packages/styles/src/components/_index.scss | 1 +
packages/styles/src/components/badge.scss | 9 +-
packages/styles/src/components/chip.scss | 157 +++++++++++
.../styles/src/components/form-check.scss | 5 +-
.../styles/src/components/form-range.scss | 8 +-
packages/styles/src/mixins/_badge.scss | 31 ---
packages/styles/src/mixins/_chip.scss | 36 +++
packages/styles/src/mixins/_index.scss | 2 +-
packages/styles/src/mixins/_utilities.scss | 43 ++-
packages/styles/src/placeholders/_badge.scss | 60 +---
.../bootstrap/_overrides-variables.scss | 1 +
packages/styles/src/variables/_commons.scss | 2 +
.../src/variables/components/_badge.scss | 25 +-
.../src/variables/components/_chip.scss | 50 ++++
.../src/variables/components/_index.scss | 1 +
pnpm-lock.yaml | 3 +
29 files changed, 863 insertions(+), 145 deletions(-)
create mode 100644 .changeset/quiet-mugs-design.md
create mode 100644 .changeset/selfish-lions-tap.md
create mode 100644 .changeset/sweet-clouds-scream.md
delete mode 100644 packages/documentation/cypress/snapshots/components/badge.snapshot.ts
create mode 100644 packages/documentation/cypress/snapshots/components/chip.snapshot.ts
create mode 100644 packages/documentation/src/stories/components/chip/chip.docs.mdx
create mode 100644 packages/documentation/src/stories/components/chip/chip.snapshot.stories.ts
create mode 100644 packages/documentation/src/stories/components/chip/chip.stories.ts
create mode 100644 packages/migrations/src/migrations/bootstrap/chip/index.ts
create mode 100644 packages/migrations/src/migrations/v7/badge/index.ts
create mode 100644 packages/styles/src/components/chip.scss
delete mode 100644 packages/styles/src/mixins/_badge.scss
create mode 100644 packages/styles/src/mixins/_chip.scss
create mode 100644 packages/styles/src/variables/components/_chip.scss
diff --git a/.changeset/quiet-mugs-design.md b/.changeset/quiet-mugs-design.md
new file mode 100644
index 0000000000..77f0f40af0
--- /dev/null
+++ b/.changeset/quiet-mugs-design.md
@@ -0,0 +1,5 @@
+---
+'@swisspost/design-system-migrations': minor
+---
+
+Added migrations to turn badges into chips.
diff --git a/.changeset/selfish-lions-tap.md b/.changeset/selfish-lions-tap.md
new file mode 100644
index 0000000000..e09f6a5ab2
--- /dev/null
+++ b/.changeset/selfish-lions-tap.md
@@ -0,0 +1,5 @@
+---
+'@swisspost/design-system-documentation': major
+---
+
+Renamed badge into "chip" and improved related examples.
diff --git a/.changeset/sweet-clouds-scream.md b/.changeset/sweet-clouds-scream.md
new file mode 100644
index 0000000000..7eab6f60de
--- /dev/null
+++ b/.changeset/sweet-clouds-scream.md
@@ -0,0 +1,5 @@
+---
+'@swisspost/design-system-styles': major
+---
+
+Renamed the badge into "chip", added a disable state and updated its styles.
diff --git a/packages/demo/package.json b/packages/demo/package.json
index fe5fd9ba4e..5dfc9f4c17 100644
--- a/packages/demo/package.json
+++ b/packages/demo/package.json
@@ -26,6 +26,7 @@
"@popperjs/core": "2.11.8",
"@swimlane/ngx-datatable": "20.1.0",
"@swisspost/design-system-intranet-header": "workspace:5.0.11",
+ "@swisspost/design-system-migrations": "workspace:1.0.2",
"@swisspost/design-system-styles": "workspace:6.6.4",
"bootstrap": "5.3.2",
"core-js": "3.36.1",
diff --git a/packages/documentation/cypress/snapshots/components/badge.snapshot.ts b/packages/documentation/cypress/snapshots/components/badge.snapshot.ts
deleted file mode 100644
index e17f11bc34..0000000000
--- a/packages/documentation/cypress/snapshots/components/badge.snapshot.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-describe('Badge', () => {
- it('default', () => {
- cy.visit('/iframe.html?id=snapshots--badge');
- cy.get('.badge', { timeout: 30000 }).should('be.visible');
- cy.percySnapshot('Badges', { widths: [1440] });
- });
-});
diff --git a/packages/documentation/cypress/snapshots/components/chip.snapshot.ts b/packages/documentation/cypress/snapshots/components/chip.snapshot.ts
new file mode 100644
index 0000000000..aaa1d6b082
--- /dev/null
+++ b/packages/documentation/cypress/snapshots/components/chip.snapshot.ts
@@ -0,0 +1,7 @@
+describe('Chip', () => {
+ it('default', () => {
+ cy.visit('/iframe.html?id=snapshots--chip');
+ cy.get('.chip', { timeout: 30000 }).should('be.visible');
+ cy.percySnapshot('Chips', { widths: [1440] });
+ });
+});
diff --git a/packages/documentation/src/stories/components/badge/badge.stories.ts b/packages/documentation/src/stories/components/badge/badge.stories.ts
index 99f1510f7d..3545cd1626 100644
--- a/packages/documentation/src/stories/components/badge/badge.stories.ts
+++ b/packages/documentation/src/stories/components/badge/badge.stories.ts
@@ -3,6 +3,7 @@ import { html, nothing } from 'lit';
import { MetaComponent } from '../../../../types';
import backgroundColors from '../../../shared/background-colors.module.scss';
import { coloredBackground } from '../../../shared/decorators/dark-background';
+import chipMeta from '../chip/chip.stories';
const meta: MetaComponent = {
id: 'bec68e8b-445e-4760-8bd7-1b9970206d8d',
@@ -120,11 +121,8 @@ export const LargeNumber: Story = {
};
export const Position: Story = {
- render: args => html`
-
@@ -133,7 +131,7 @@ export const Position: Story = {
`,
decorators: [
(story: StoryFn, { args, context }: StoryContext) => html`
-
${story(args, context)}
+
${story(args, context)}
`,
],
};
diff --git a/packages/documentation/src/stories/components/chip/chip.docs.mdx b/packages/documentation/src/stories/components/chip/chip.docs.mdx
new file mode 100644
index 0000000000..1a6de376b9
--- /dev/null
+++ b/packages/documentation/src/stories/components/chip/chip.docs.mdx
@@ -0,0 +1,42 @@
+import { Canvas, Controls, Meta } from '@storybook/blocks';
+import { PostAlert } from '@swisspost/design-system-components-react';
+import * as ChipStories from './chip.stories';
+import StylesPackageImport from '../../../shared/styles-package-import.mdx';
+
+
+
+# Chip
+
+
+ Display small pieces of information with which users can interact.
+
+
+
+
+
+
+
+
+
+## Examples
+
+### Filter Chip
+
+Filter chips provide a simple means to refine content or search results based on specific attributes.
+They are personalized checkboxes, allowing users to toggle them to filter content effectively.
+
+
+
+Alternatively, filter chips may be used with radio inputs when a single filter selection is needed.
+
+
+
+### Dismissible Chip
+
+Dismissible chips represent pieces of information entered by a user.
+Each chip includes a close button, enabling users to conveniently remove them from view.
+
+It's important to note that the close icon lacks visible text.
+Therefore, it's imperative to include a visually hidden label to ensure accessibility for users relying on assistive technologies.
+
+
diff --git a/packages/documentation/src/stories/components/chip/chip.snapshot.stories.ts b/packages/documentation/src/stories/components/chip/chip.snapshot.stories.ts
new file mode 100644
index 0000000000..6763721904
--- /dev/null
+++ b/packages/documentation/src/stories/components/chip/chip.snapshot.stories.ts
@@ -0,0 +1,43 @@
+import type { Args, StoryContext, StoryObj } from '@storybook/web-components';
+import meta from './chip.stories';
+import { html } from 'lit';
+import { bombArgs } from '../../../utils';
+
+const { id, ...metaWithoutId } = meta;
+
+export default {
+ ...metaWithoutId,
+ title: 'Snapshots',
+};
+
+type Story = StoryObj;
+
+export const Chip: Story = {
+ render: (_args: Args, context: StoryContext) => {
+ return html`
+
+ ${['bg-white', 'bg-dark'].map(
+ bg => html`
+
+ ${bombArgs({
+ text: [
+ 'Malakceptebla Insigno',
+ 'Contentus momentus vero siteos et accusam iretea et justo.',
+ ],
+ size: context.argTypes.size.options,
+ type: context.argTypes.type.options,
+ badge: [false, true],
+ active: [false, true],
+ disabled: [false, true],
+ dismissed: [false],
+ })
+ .filter(args => !(args.type !== 'filter' && args.active === true))
+ .filter(args => !(args.type !== 'filter' && args.badge === true))
+ .map((args: Args) => meta.render?.({ ...context.args, ...args }, context))}
+
+ `,
+ )}
+
+ `;
+ },
+};
diff --git a/packages/documentation/src/stories/components/chip/chip.stories.ts b/packages/documentation/src/stories/components/chip/chip.stories.ts
new file mode 100644
index 0000000000..13acc113be
--- /dev/null
+++ b/packages/documentation/src/stories/components/chip/chip.stories.ts
@@ -0,0 +1,263 @@
+import { useArgs } from '@storybook/preview-api';
+import type { Args, StoryContext, StoryObj } from '@storybook/web-components';
+import { html, nothing } from 'lit';
+import { MetaComponent } from '../../../../types';
+
+const meta: MetaComponent = {
+ id: '12576d97-52c3-49ec-be7b-6d37728b75f5',
+ title: 'Components/Chip',
+ tags: ['package:HTML'],
+ render: renderChip,
+ parameters: {
+ controls: {
+ exclude: ['dismissed', 'number', 'radio'],
+ },
+ },
+ args: {
+ text: 'Insigno',
+ size: 'Large',
+ type: 'filter',
+ disabled: false,
+ active: false,
+ badge: false,
+ dismissed: false,
+ number: 1,
+ radio: false,
+ },
+ argTypes: {
+ text: {
+ name: 'Text',
+ description: 'The text contained in the chip.',
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Content',
+ },
+ },
+ size: {
+ name: 'Size',
+ description: 'The size of the chip.',
+ control: {
+ type: 'radio',
+ },
+ options: ['Large', 'Small'],
+ table: {
+ category: 'General',
+ },
+ },
+ type: {
+ name: 'Type',
+ description: 'Defines how the chip can be interacted with.',
+ control: {
+ type: 'radio',
+ labels: {
+ filter: 'Filter Chip',
+ dismissible: 'Dismissible Chip',
+ },
+ },
+ options: ['filter', 'dismissible'],
+ table: {
+ category: 'General',
+ },
+ },
+ disabled: {
+ name: 'Disabled',
+ description:
+ 'If `true`, the chip is disabled.
',
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ category: 'General',
+ },
+ },
+ active: {
+ name: 'Active',
+ description: 'If `true`, the chip is active.',
+ if: {
+ arg: 'type',
+ eq: 'filter',
+ },
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ category: 'General',
+ },
+ },
+ badge: {
+ name: 'Nested Badge',
+ description: 'If `true`, a badge is displayed inside the chip.',
+ if: {
+ arg: 'type',
+ eq: 'filter',
+ },
+ control: {
+ type: 'boolean',
+ },
+ table: {
+ category: 'General',
+ },
+ },
+ },
+};
+
+export default meta;
+
+// DECORATORS
+function externalControl(story: any, { args }: StoryContext) {
+ const [_, updateArgs] = useArgs();
+
+ const button = html`
+
{
+ e.preventDefault();
+ updateArgs({ dismissed: false });
+ }}"
+ >
+ Show chip
+
+ `;
+
+ return html` ${args.dismissed ? button : nothing} ${story()} `;
+}
+
+// RENDERER
+function getFilterChip(
+ args: Args,
+ updateArgs: (args: Args) => void,
+ context: StoryContext,
+ index?: number,
+) {
+ const inputName = `chip-example--${context.name.replace(/ /g, '-').toLowerCase()}`;
+ const inputId = typeof index !== 'undefined' ? `${inputName}-${index}` : inputName;
+
+ const handleChange = (e: Event) => {
+ updateArgs({ active: !args.active });
+
+ if (document.activeElement === e.target) {
+ setTimeout(() => {
+ const element: HTMLInputElement | null = document.querySelector(`#${inputId}`);
+ if (element) element.focus();
+ }, 25);
+ }
+ };
+
+ return html`
+
+
+
+ ${args.text}
+ ${args.badge ? html` ${args.number} ` : nothing}
+
+
+ `;
+}
+
+function getDismissibleChip(args: Args, updateArgs: (args: Args) => void) {
+ return html`
+
updateArgs({ dismissed: true })}"
+ ?disabled="${args.disabled}"
+ >
+ ${args.text}
+ Dismiss
+
+ `;
+}
+
+function renderChip(args: Args, context: StoryContext, index?: number) {
+ const [_, updateArgs] = useArgs();
+
+ if (args.dismissed) return html` ${nothing} `;
+
+ if (args.type === 'dismissible') return getDismissibleChip(args, updateArgs);
+
+ return getFilterChip(args, updateArgs, context, index);
+}
+
+// STORIES
+type Story = StoryObj;
+
+export const Default: Story = {
+ decorators: [externalControl],
+};
+
+export const FilterCheckboxChip: Story = {
+ render: ({ active, ...args }, context) => {
+ const checkboxChips = [
+ { text: 'Aventuro', active: true },
+ { text: 'Familio' },
+ { text: 'Vidoj' },
+ ];
+
+ return html`
+
+ Migrandaj Itineroj
+
+ ${checkboxChips.map(({ text, active }, index) =>
+ renderChip({ ...args, text, active }, context, index),
+ )}
+
+
+ `;
+ },
+ decorators: [story => html`
${story()}
`],
+ args: {
+ type: 'filter',
+ },
+};
+
+export const FilterRadioChip: Story = {
+ render: ({ active, ...args }, context) => {
+ const radioChips = [
+ { number: 253, text: 'Ĉiuj' },
+ { number: 12, text: 'Artikoloj', active: true },
+ { number: 5, text: 'Iloj' },
+ { number: 236, text: 'Dokumentoj' },
+ ];
+
+ return html`
+
+ Serĉrezultoj
+
+ ${radioChips.map(({ text, number, active }, index) =>
+ renderChip({ ...args, text, number, active }, context, index),
+ )}
+
+
+ `;
+ },
+ decorators: [story => html`
${story()}
`],
+ args: {
+ type: 'filter',
+ radio: true,
+ size: 'Small',
+ badge: true,
+ },
+};
+
+export const Dismissible: Story = {
+ render: ({ dismissed, ...args }, context) => html`
+
+ ${renderChip({ ...args, text: 'Unua uzanta enigo' }, context)}
+ ${renderChip({ ...args, text: 'Dua uzanta enigo' }, context)}
+ ${renderChip({ ...args, text: 'Tria uzanta enigo' }, context)}
+ ${renderChip({ ...args, text: 'Fora uzanta enigo' }, context)}
+
+ `,
+ args: {
+ type: 'dismissible',
+ },
+};
diff --git a/packages/migrations/src/migrations.json b/packages/migrations/src/migrations.json
index 1af81682aa..2c0eb03364 100644
--- a/packages/migrations/src/migrations.json
+++ b/packages/migrations/src/migrations.json
@@ -100,6 +100,11 @@
"version": "6.0.0",
"description": "Improves the post stepper accessibility.",
"factory": "./migrations/post/stepper"
+ },
+ "migration-badge-to-chip-or-tag": {
+ "version": "7.0.0",
+ "description": "Migrates badges to chip or tag depending on the content.",
+ "factory": "./migrations/v7/badge"
}
}
}
diff --git a/packages/migrations/src/migrations/bootstrap/chip/index.ts b/packages/migrations/src/migrations/bootstrap/chip/index.ts
new file mode 100644
index 0000000000..1ec22d8620
--- /dev/null
+++ b/packages/migrations/src/migrations/bootstrap/chip/index.ts
@@ -0,0 +1,75 @@
+import { Rule } from '@angular-devkit/schematics';
+import type { AnyNode, Cheerio, CheerioAPI } from 'cheerio';
+import { DomUpdate, getDomMigrationRule } from '../../../utils/dom-migration';
+
+export default function (): Rule {
+ return getDomMigrationRule(new BadgeCheckToChipCheckUpdate(), new BadgeToChipUpdate());
+}
+
+class BadgeCheckToChipCheckUpdate implements DomUpdate {
+ selector = '.badge-check';
+
+ update($elements: Cheerio
, $: CheerioAPI) {
+ $elements.each((_i, element) => {
+ const $element = $(element);
+
+ $element.removeClass('badge-check').addClass('chip-filter');
+
+ const $label = $element.children('.badge-check-label');
+ if ($label) $label.removeClass('badge-check-label').addClass('chip-filter-label');
+
+ const $input = $element.children('.badge-check-input');
+ if ($input) $input.removeClass('badge-check-input').addClass('chip-filter-input');
+ });
+ }
+}
+
+class BadgeToChipUpdate implements DomUpdate {
+ selector = '.badge';
+
+ update($elements: Cheerio, $: CheerioAPI) {
+ $elements.each((_i, element) => {
+ const $element = $(element);
+
+ // do not update nested badges
+ const $parent = $element.parent();
+ if ($parent.hasClass('chip') || $parent.hasClass('chip-filter-label')) {
+ return;
+ }
+
+ $element.removeClass('badge').addClass('chip');
+
+ // remove obsolete badge classes
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const isBgClass = cssClass.match(/^bg-\w+$/);
+ const isBorderClass = cssClass.match(/^border(-\w+)?$/);
+ const isRoundedClass = cssClass.match(/^rounded(-\w+)?$/);
+
+ if (isBgClass || isBorderClass || isRoundedClass) {
+ $element.removeClass(cssClass);
+
+ if (isBgClass && isBgClass[1] === 'active') {
+ $element.addClass('active');
+ }
+ }
+ });
+
+ if ($element.hasClass('badge-sm')) {
+ $element.removeClass('badge-sm').addClass('chip-sm');
+ }
+
+ if ($element.hasClass('bg-active')) {
+ $element.removeClass('bg-active').addClass('active');
+ }
+
+ // move the close button to be the first child
+ const $closeBtn = $element.children('.btn-close');
+ if ($closeBtn && $closeBtn.is(':last-child')) {
+ $element.prepend($closeBtn);
+ }
+ });
+ }
+}
diff --git a/packages/migrations/src/migrations/v7/badge/index.ts b/packages/migrations/src/migrations/v7/badge/index.ts
new file mode 100644
index 0000000000..bb10137f16
--- /dev/null
+++ b/packages/migrations/src/migrations/v7/badge/index.ts
@@ -0,0 +1,106 @@
+import { Rule } from '@angular-devkit/schematics';
+import type { AnyNode, Cheerio, CheerioAPI } from 'cheerio';
+import { DomUpdate, getDomMigrationRule } from '../../../utils/dom-migration';
+import { themeColors } from '../../../utils/constants';
+
+export default function (): Rule {
+ return getDomMigrationRule(
+ new BadgeCheckToChipFilterUpdate(),
+ new BadgeToChipDismissibleUpdate(),
+ new BadgeToTagUpdate(),
+ );
+}
+
+class BadgeCheckToChipFilterUpdate implements DomUpdate {
+ selector = '.badge-check';
+
+ update($elements: Cheerio, $: CheerioAPI) {
+ $elements.each((_i, element) => {
+ const $element = $(element);
+
+ $element.removeClass('badge-check').addClass('chip-filter');
+
+ const $input = $element.children('.badge-check-input');
+ if ($input) $input.removeClass('badge-check-input').addClass('chip-filter-input');
+
+ const $label = $element.children('.badge-check-label');
+ if ($label) $label.removeClass('badge-check-label').addClass('chip-filter-label');
+
+ addChipTextClass($label);
+ });
+ }
+}
+
+class BadgeToChipDismissibleUpdate implements DomUpdate {
+ selector = '.badge';
+
+ update($elements: Cheerio, $: CheerioAPI) {
+ $elements.each((_i, element) => {
+ const $element = $(element);
+
+ // only update badge with close button
+ const $closeBtn = $element.children('.btn-close');
+ if (!$closeBtn.length) {
+ return;
+ }
+
+ addChipTextClass($element);
+
+ $closeBtn.removeAttr('class').addClass('chip chip-dismissible');
+
+ const ariaLabel = $closeBtn.attr('aria-label');
+ if (ariaLabel) {
+ $closeBtn.removeAttr('aria-label');
+ $closeBtn.append(`${ariaLabel} `);
+ }
+
+ $closeBtn.append($element.children()).data($element.data());
+
+ $element.replaceWith($closeBtn);
+ });
+ }
+}
+
+class BadgeToTagUpdate implements DomUpdate {
+ selector = '.badge';
+
+ bgClassRegex: RegExp = new RegExp(`^bg-(${themeColors.join('|')})$`);
+
+ update($elements: Cheerio, $: CheerioAPI) {
+ $elements.each((_i, element) => {
+ const $element = $(element);
+
+ // do not update nested badges
+ const $parent = $element.parent();
+ if ($parent.hasClass('tag')) {
+ return;
+ }
+
+ $element.removeClass('badge').addClass('tag');
+
+ if ($element.hasClass('badge-sm')) $element.removeClass('badge-sm').addClass('tag-sm');
+
+ $element
+ .attr('class')
+ ?.split(' ')
+ .forEach(cssClass => {
+ const [_, bgColor] = cssClass.match(this.bgClassRegex) ?? [];
+
+ if (!bgColor) return;
+
+ if (bgColor === 'active' || bgColor === 'yellow') $element.addClass('tag-yellow');
+ if (bgColor === 'white') $element.addClass('tag-white');
+ if (bgColor === 'info') $element.addClass('tag-info');
+ if (bgColor === 'success') $element.addClass('tag-success');
+ if (bgColor === 'danger') $element.addClass('tag-danger');
+ if (bgColor === 'warning') $element.addClass('tag-warning');
+
+ $element.removeClass(cssClass);
+ });
+ });
+ }
+}
+
+function addChipTextClass($element: Cheerio) {
+ $element.children('span:not(.badge)').addClass('chip-text');
+}
diff --git a/packages/styles/src/components/_index.scss b/packages/styles/src/components/_index.scss
index 5f18753943..287e24d725 100644
--- a/packages/styles/src/components/_index.scss
+++ b/packages/styles/src/components/_index.scss
@@ -9,6 +9,7 @@
@use 'button';
@use 'button-group';
@use 'card';
+@use 'chip';
@use 'choice-control-card';
@use 'carousel';
@use 'close';
diff --git a/packages/styles/src/components/badge.scss b/packages/styles/src/components/badge.scss
index b4c9fcff7e..08c5ac9ecf 100644
--- a/packages/styles/src/components/badge.scss
+++ b/packages/styles/src/components/badge.scss
@@ -1,6 +1,7 @@
@forward './../variables/options';
@use './../mixins/color' as color-mx;
+@use './../placeholders/badge' as badge-ph;
@use './../variables/components/badge';
.badge {
@@ -24,10 +25,8 @@
--post-badge-height: #{badge.$badge-height-empty};
--post-badge-padding-x: #{badge.$badge-padding-x-empty};
}
-}
-.badge-sm {
- --post-badge-height: #{badge.$badge-height-sm};
- --post-badge-padding-x: #{badge.$badge-padding-x-sm};
- font-size: badge.$badge-font-size-sm;
+ &.badge-sm {
+ @extend %badge-sm;
+ }
}
diff --git a/packages/styles/src/components/chip.scss b/packages/styles/src/components/chip.scss
new file mode 100644
index 0000000000..fa0c61e98f
--- /dev/null
+++ b/packages/styles/src/components/chip.scss
@@ -0,0 +1,157 @@
+@forward './../variables/options';
+
+@use './../variables/components/chip';
+@use './../mixins/utilities';
+@use './../mixins/chip' as chip-mx;
+@use './../mixins/icons' as icons-mx;
+@use './../placeholders/badge' as badge-ph;
+
+.chip-dismissible {
+ @include chip-mx.chip-styles();
+ position: relative;
+
+ &::before,
+ &::after {
+ content: '';
+ display: inline-block;
+ flex: 0 0 auto;
+ height: chip.$chip-close-button-height;
+ width: chip.$chip-close-button-height;
+ transition: chip.$chip-transition;
+ }
+
+ &::before {
+ border-radius: chip.$chip-close-button-border-radius;
+ }
+
+ &:hover::before {
+ background-color: chip.$chip-hover-bg;
+ }
+
+ &::after {
+ @include icons-mx.icon(chip.$chip-close-button-icon);
+ position: absolute;
+ top: 50%;
+ left: chip.$chip-padding-x;
+ transform: translateY(-50%);
+ }
+
+ // set the focus ring on the close button only
+ @include utilities.focus-style-none();
+ @include utilities.focus-style('::before') {
+ background-color: chip.$chip-hover-bg;
+ }
+
+ @include utilities.disabled-style();
+}
+
+.chip-filter {
+ display: inline-block;
+
+ &-label {
+ @include chip-mx.chip-styles();
+ cursor: pointer;
+
+ > .badge {
+ color: chip.$chip-hover-color;
+ background-color: chip.$chip-hover-bg;
+ border-color: transparent;
+ transition: chip.$chip-transition;
+ }
+ }
+
+ &-input {
+ @include utilities.visuallyhidden;
+
+ &:checked {
+ + .chip-filter-label {
+ color: chip.$chip-active-color;
+ background-color: chip.$chip-active-bg;
+ border-color: transparent;
+
+ > .badge {
+ background-color: chip.$chip-bg;
+ }
+
+ @include utilities.high-contrast-mode() {
+ border-color: Highlight;
+ }
+ }
+
+ &:disabled + .chip-filter-label {
+ background-color: chip.$chip-disabled-active-bg;
+
+ @include utilities.high-contrast-mode() {
+ > .chip-text {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ &:not(:disabled) {
+ + .chip-filter-label > .chip-text {
+ text-decoration: underline;
+ transition: text-decoration 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ + .chip-filter-label:hover > .chip-text {
+ text-decoration-color: transparent;
+
+ @include utilities.high-contrast-mode() {
+ text-decoration-color: initial;
+ }
+ }
+
+ @include utilities.focus-style('+ .chip-filter-label') {
+ > .chip-text {
+ text-decoration-color: transparent;
+ }
+ }
+ }
+ }
+
+ &:not(:checked) + .chip-filter-label:hover {
+ color: chip.$chip-hover-color;
+ background-color: chip.$chip-hover-bg;
+
+ > .badge {
+ background-color: chip.$chip-bg;
+ }
+ }
+
+ @include utilities.disabled-style('+ .chip-filter-label') {
+ background-color: chip.$chip-disabled-bg;
+
+ @include utilities.high-contrast-mode() {
+ > .badge {
+ color: GrayText;
+ border-color: GrayText;
+ }
+ }
+ }
+ }
+}
+
+.chip-sm {
+ &.chip-dismissible {
+ @include chip-mx.chip-styles-sm();
+
+ &::before,
+ &::after {
+ height: chip.$chip-close-button-height-sm;
+ width: chip.$chip-close-button-height-sm;
+ }
+
+ &::after {
+ left: chip.$chip-padding-x-sm;
+ }
+ }
+
+ &.chip-filter > .chip-filter-label {
+ @include chip-mx.chip-styles-sm();
+
+ > .badge {
+ @extend %badge-sm;
+ }
+ }
+}
diff --git a/packages/styles/src/components/form-check.scss b/packages/styles/src/components/form-check.scss
index d0eb245bcf..f170586bc2 100644
--- a/packages/styles/src/components/form-check.scss
+++ b/packages/styles/src/components/form-check.scss
@@ -1,6 +1,7 @@
@forward './../variables/options';
@use '../variables/color';
+@use '../variables/commons';
@use '../variables/type';
@use '../variables/spacing';
@use '../variables/animation';
@@ -14,7 +15,9 @@
row-gap: form-check.$form-check-row-gap;
margin-bottom: form-check.$form-check-margin-bottom;
- @include utility-mx.focus-style();
+ @include utility-mx.focus-style() {
+ border-radius: commons.$border-radius !important;
+ }
&-inline {
display: inline-flex;
diff --git a/packages/styles/src/components/form-range.scss b/packages/styles/src/components/form-range.scss
index 66f698fdf2..b1754e3f39 100644
--- a/packages/styles/src/components/form-range.scss
+++ b/packages/styles/src/components/form-range.scss
@@ -15,12 +15,16 @@ $webkit-progress-height-adjustment: 2px;
$webkit-thumb-width: 32px;
:has(> .form-range) {
- @include utilities.focus-style();
+ @include utilities.focus-style() {
+ border-radius: commons.$border-radius !important;
+ }
}
@supports not selector(:has(> .form-range)) {
.form-range {
- @include utilities.focus-style();
+ @include utilities.focus-style() {
+ border-radius: commons.$border-radius !important;
+ }
}
}
diff --git a/packages/styles/src/mixins/_badge.scss b/packages/styles/src/mixins/_badge.scss
deleted file mode 100644
index 22a8d652e8..0000000000
--- a/packages/styles/src/mixins/_badge.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-@use './../variables/components/badge';
-@use './../mixins/utilities';
-
-@mixin badge-hover-state {
- color: badge.$badge-hover-color;
- background-color: badge.$badge-hover-bg-color;
- border-color: transparent;
-
- @include utilities.high-contrast-mode() {
- background-color: Highlight;
- border-color: Highlight;
- color: HighlightText;
- forced-color-adjust: none; // Disable "readability backplate" on blink browser that interferes with the colors on this case
- }
-}
-
-@mixin badge-active-state {
- color: badge.$badge-active-color;
- background-color: badge.$badge-active-bg-color;
- border-color: transparent;
-
- > .badge {
- background-color: badge.$badge-nested-active-bg-color;
- }
-
- @include utilities.high-contrast-mode() {
- background-color: SelectedItem;
- color: SelectedItemText;
- forced-color-adjust: none; // Disable "readability backplate" on blink browser that interferes with the colors on this case
- }
-}
diff --git a/packages/styles/src/mixins/_chip.scss b/packages/styles/src/mixins/_chip.scss
new file mode 100644
index 0000000000..1e5751aaa7
--- /dev/null
+++ b/packages/styles/src/mixins/_chip.scss
@@ -0,0 +1,36 @@
+@use './../lic/bootstrap-license';
+@use './../themes/bootstrap/core' as *;
+
+@use './../variables/components/badge';
+@use './../variables/components/chip';
+@use './../mixins/utilities';
+
+@mixin chip-styles {
+ @include border-radius(chip.$chip-border-radius);
+ display: inline-flex;
+ align-items: center;
+ height: chip.$chip-height;
+ max-width: chip.$chip-max-width;
+ padding-inline: chip.$chip-padding-x;
+ border: chip.$chip-border-width solid chip.$chip-border-color;
+ gap: chip.$chip-gap;
+ line-height: chip.$chip-line-height;
+ font-size: chip.$chip-font-size;
+ font-weight: chip.$chip-font-weight;
+ color: chip.$chip-color;
+ background-color: chip.$chip-bg;
+ transition: chip.$chip-transition;
+
+ > .chip-text {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+}
+
+@mixin chip-styles-sm {
+ height: chip.$chip-height-sm;
+ font-size: chip.$chip-font-size-sm;
+ gap: chip.$chip-gap-sm;
+ padding-inline: chip.$chip-padding-x-sm;
+}
diff --git a/packages/styles/src/mixins/_index.scss b/packages/styles/src/mixins/_index.scss
index c7521115e2..8916e48f1c 100644
--- a/packages/styles/src/mixins/_index.scss
+++ b/packages/styles/src/mixins/_index.scss
@@ -1,6 +1,6 @@
@forward 'animation';
-@forward 'badge';
@forward 'button';
+@forward 'chip';
@forward 'color';
@forward 'focus';
@forward 'form-checks';
diff --git a/packages/styles/src/mixins/_utilities.scss b/packages/styles/src/mixins/_utilities.scss
index 63d4363bc3..b28b1d2ae9 100644
--- a/packages/styles/src/mixins/_utilities.scss
+++ b/packages/styles/src/mixins/_utilities.scss
@@ -87,16 +87,17 @@
outline: none;
}
-@mixin focus-style($vendor-prefix: '') {
- outline-style: none;
- outline-offset: spacing.$size-line;
- outline-width: spacing.$size-line;
- outline-color: var(--post-focus-color);
+@mixin focus-style($additional-selector: '') {
+ {$additional-selector} {
+ outline-style: none;
+ outline-offset: spacing.$size-line;
+ outline-width: spacing.$size-line;
+ outline-color: var(--post-focus-color);
+ }
// :has(:focus-visible) mimic a focus-visible-within pseudo-class
- &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$vendor-prefix} {
+ &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$additional-selector} {
outline-style: solid;
- border-radius: commons.$border-radius !important;
@include high-contrast-mode() {
outline-color: Highlight;
@@ -108,9 +109,8 @@
// When a browser doesn't support :has, use focus-within as a fallback. This means that focus state is displayed on focus and not on focus-visible only (except some browsers like Safari).
@supports not selector(:has(:focus-visible)) {
- &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} {
+ &:is(:focus-visible, :focus-within, .pretend-focus)#{$additional-selector} {
outline-style: solid;
- border-radius: commons.$border-radius !important;
@include high-contrast-mode() {
outline-color: Highlight;
@@ -122,16 +122,35 @@
}
}
-@mixin focus-style-custom($vendor-prefix: '') {
+@mixin focus-style-custom($additional-selector: '') {
// :has(:focus-visible) mimic a focus-visible-within pseudo-class
- &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$vendor-prefix} {
+ &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$additional-selector} {
@content;
}
// When a browser doesn't support :has, use focus-within as a fallback. This means that focus state is displayed on focus and not on focus-visible only (except some browsers like Safari).
@supports not selector(:has(:focus-visible)) {
- &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} {
+ &:is(:focus-visible, :focus-within, .pretend-focus)#{$additional-selector} {
@content;
}
}
}
+
+@mixin disabled-style($additional-selector: '') {
+ &:disabled#{$additional-selector} {
+ pointer-events: none;
+ color: var(--post-gray-40);
+ border-color: var(--post-gray-40);
+ border-style: dashed;
+ background-clip: padding-box;
+ text-decoration: line-through;
+
+ @include high-contrast-mode() {
+ color: GrayText;
+ border-color: GrayText;
+ }
+
+ // In case rules need to be slightly adjusted
+ @content;
+ }
+}
diff --git a/packages/styles/src/placeholders/_badge.scss b/packages/styles/src/placeholders/_badge.scss
index a98cfd3afa..0b67c3c02c 100644
--- a/packages/styles/src/placeholders/_badge.scss
+++ b/packages/styles/src/placeholders/_badge.scss
@@ -1,59 +1,7 @@
-@use './../themes/bootstrap/core' as *;
-
@use './../variables/components/badge';
-%badge {
- @include border-radius($badge-border-radius);
- display: inline-flex;
- justify-content: flex-start;
- align-items: center;
- gap: badge.$badge-gap;
- padding: $badge-padding-y $badge-padding-x;
- border: badge.$badge-border;
- height: badge.$badge-height;
- font-size: badge.$badge-font-size;
- font-weight: $badge-font-weight;
- line-height: inherit;
- color: $badge-color;
- text-align: center;
- vertical-align: baseline;
- max-width: 100%;
-
- > span {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
-
- > .badge {
- padding: $size-micro;
- height: badge.$badge-nested-height;
- min-width: badge.$badge-nested-height;
- color: badge.$badge-nested-color;
- background-color: badge.$badge-nested-bg-color;
- border-color: badge.$badge-nested-border-color;
- font-size: badge.$badge-nested-font-size;
- }
-
- > .badge,
- > .btn-close {
- margin-right: -1 * (badge.$badge-padding-x - badge.$badge-nested-translate-x);
- }
-
- &.badge-sm {
- height: badge.$badge-height-sm;
- font-size: badge.$badge-font-size-sm;
- gap: badge.$badge-gap-sm;
-
- > .badge,
- > .btn-close {
- margin-right: -1 * (badge.$badge-padding-x - badge.$badge-nested-translate-x-sm);
- }
- }
-
- // Quick fix for badges in buttons
- .btn & {
- position: relative;
- top: -1px;
- }
+%badge-sm {
+ --post-badge-height: #{badge.$badge-height-sm};
+ --post-badge-padding-x: #{badge.$badge-padding-x-sm};
+ font-size: badge.$badge-font-size-sm;
}
diff --git a/packages/styles/src/themes/bootstrap/_overrides-variables.scss b/packages/styles/src/themes/bootstrap/_overrides-variables.scss
index 27f3553c28..ca9d63b291 100644
--- a/packages/styles/src/themes/bootstrap/_overrides-variables.scss
+++ b/packages/styles/src/themes/bootstrap/_overrides-variables.scss
@@ -15,6 +15,7 @@
@forward './../../variables/components/button';
@forward './../../variables/components/card';
@forward './../../variables/components/carousel';
+@forward './../../variables/components/chip';
@forward './../../variables/components/close';
@forward './../../variables/components/datepicker';
@forward './../../variables/components/dropdowns';
diff --git a/packages/styles/src/variables/_commons.scss b/packages/styles/src/variables/_commons.scss
index 3d29df6cdc..e8ed38d95e 100644
--- a/packages/styles/src/variables/_commons.scss
+++ b/packages/styles/src/variables/_commons.scss
@@ -11,6 +11,8 @@ $border-radius: 4px !default;
$border-radius-sm: $border-radius !default;
$border-radius-rg: $border-radius !default;
$border-radius-lg: $border-radius !default;
+$border-radius-pill: 50rem !default;
+$border-radius-round: 50% !default;
$box-shadow-sm: 0 0 4px 0 rgba(color.$black, 0.4) !default;
$box-shadow: 0 0 5px 0 rgba(color.$black, 0.3) !default;
diff --git a/packages/styles/src/variables/components/_badge.scss b/packages/styles/src/variables/components/_badge.scss
index 49ea1bfbad..d4c5a14145 100644
--- a/packages/styles/src/variables/components/_badge.scss
+++ b/packages/styles/src/variables/components/_badge.scss
@@ -7,6 +7,7 @@
@use './../../functions/sizing';
$badge-border-radius: 50rem;
+$badge-line-height: type.$line-height-copy;
$badge-color: color.$white;
$badge-bg: color.$error;
$badge-border: color.$white solid commons.$border-thick;
@@ -21,27 +22,3 @@ $badge-padding-x-empty: 0%; // needs a unit for the calculated min-width
$badge-font-size: type.$font-size-12;
$badge-font-size-sm: 10px;
-
-// DEPRECATED
-$badge-gap: spacing.$size-mini;
-$badge-transition:
- color 250ms,
- background-color 250ms,
- border-color 250ms;
-$badge-hover-color: color.$black;
-$badge-hover-bg-color: color.$gray-10;
-$badge-active-color: color.$black;
-$badge-active-bg-color: color.$yellow;
-$badge-gap-sm: sizing.px-to-rem(6px);
-$badge-nested-height: sizing.px-to-rem(22px);
-$badge-nested-color: color.$gray-60;
-$badge-nested-bg-color: color.$gray-10;
-$badge-nested-border-color: color.$white;
-$badge-nested-font-size: sizing.px-to-rem(10px);
-$badge-nested-translate-x: ($badge-height - $badge-nested-height) * 0.5;
-$badge-nested-active-bg-color: color.$white;
-$badge-nested-translate-x-sm: ($badge-height-sm - $badge-nested-height) * 0.5;
-$badge-check-input-height: spacing.$size-small-large;
-$badge-check-input-bg-color: color.$white;
-$badge-font-weight: type.$font-weight-normal;
-$badge-padding-y: 0;
diff --git a/packages/styles/src/variables/components/_chip.scss b/packages/styles/src/variables/components/_chip.scss
new file mode 100644
index 0000000000..57fca6157c
--- /dev/null
+++ b/packages/styles/src/variables/components/_chip.scss
@@ -0,0 +1,50 @@
+@use './button';
+@use './../animation';
+@use './../color';
+@use './../commons';
+@use './../spacing';
+@use './../type';
+
+@use './../../functions/sizing';
+
+$chip-color: color.$gray-80;
+$chip-bg: color.$white;
+$chip-border-color: color.$gray-60;
+$chip-border-width: commons.$border-thick;
+$chip-border-radius: commons.$border-radius-pill;
+
+$chip-height: button.$btn-height-rg;
+$chip-max-width: sizing.px-to-rem(296px);
+$chip-padding-x: spacing.$size-regular;
+$chip-gap: spacing.$size-mini;
+
+$chip-font-size: type.$font-size-16;
+$chip-font-weight: type.$font-weight-normal;
+$chip-line-height: type.$line-height-copy;
+
+$chip-transition:
+ color animation.$transition-base-timing,
+ background-color animation.$transition-base-timing,
+ border-color animation.$transition-base-timing;
+
+$chip-height-sm: button.$btn-height-sm;
+$chip-gap-sm: sizing.px-to-rem(6px);
+$chip-font-size-sm: type.$font-size-14;
+$chip-padding-x-sm: spacing.$size-small-regular;
+
+$chip-hover-color: color.$black;
+$chip-hover-bg: color.$gray-20;
+
+$chip-active-color: color.$black;
+$chip-active-bg: color.$yellow;
+
+$chip-disabled-bg: color.$white;
+$chip-disabled-active-bg: color.$gray;
+
+$chip-text-transition: text-decoration animation.$transition-time-simple
+ animation.$transition-easing-default;
+
+$chip-close-button-icon: 2043;
+$chip-close-button-border-radius: commons.$border-radius-round;
+$chip-close-button-height: spacing.$size-large;
+$chip-close-button-height-sm: spacing.$size-regular;
diff --git a/packages/styles/src/variables/components/_index.scss b/packages/styles/src/variables/components/_index.scss
index 7b970be652..69d5d76351 100644
--- a/packages/styles/src/variables/components/_index.scss
+++ b/packages/styles/src/variables/components/_index.scss
@@ -4,6 +4,7 @@
@forward 'button';
@forward 'card';
@forward 'carousel';
+@forward 'chip';
@forward 'close';
@forward 'datepicker';
@forward 'dropdowns';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a96be7891..c65f44a7ba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -344,6 +344,9 @@ importers:
'@swisspost/design-system-intranet-header':
specifier: workspace:5.0.11
version: link:../intranet-header-workspace/dist/intranet-header
+ '@swisspost/design-system-migrations':
+ specifier: workspace:1.0.2
+ version: link:../migrations
'@swisspost/design-system-styles':
specifier: workspace:6.6.4
version: link:../styles/dist