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` -
- Filter -
1
-
+ render: (_args, context) => html` + ${chipMeta.render?.({ ...chipMeta.args, badge: true }, context)}
@@ -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.
There are accessibility concerns with the disabled state.
Please read our disabled state accessibility guide.
', + 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` + + 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` +
+ + +
+ `; +} + +function getDismissibleChip(args: Args, updateArgs: (args: Args) => void) { + return html` + + `; +} + +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