diff --git a/packages/beeq/src/components.d.ts b/packages/beeq/src/components.d.ts index fb1b777ad..cbe1737cb 100644 --- a/packages/beeq/src/components.d.ts +++ b/packages/beeq/src/components.d.ts @@ -77,6 +77,10 @@ export namespace Components { * If true accordion is expanded */ "expanded": boolean; + /** + * Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. + */ + "noAnimation": boolean; /** * If true accordion expand icon is rotate 180deg when expanded */ @@ -99,6 +103,10 @@ export namespace Components { * If true multiple accordions can be expanded at the same time */ "multiple": boolean; + /** + * Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. + */ + "noAnimation": boolean; /** * The size of accordion to be applied to all accordions */ @@ -2270,6 +2278,10 @@ declare namespace LocalJSX { * If true accordion is expanded */ "expanded"?: boolean; + /** + * Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. + */ + "noAnimation"?: boolean; /** * Handler to be called after the accordion is closed */ @@ -2317,6 +2329,10 @@ declare namespace LocalJSX { * If true multiple accordions can be expanded at the same time */ "multiple"?: boolean; + /** + * Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. + */ + "noAnimation"?: boolean; /** * The size of accordion to be applied to all accordions */ diff --git a/packages/beeq/src/components/accordion-group/_storybook/bq-accordion-group.stories.tsx b/packages/beeq/src/components/accordion-group/_storybook/bq-accordion-group.stories.tsx index 11819537c..2c6216e47 100644 --- a/packages/beeq/src/components/accordion-group/_storybook/bq-accordion-group.stories.tsx +++ b/packages/beeq/src/components/accordion-group/_storybook/bq-accordion-group.stories.tsx @@ -1,5 +1,6 @@ import type { Args, Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit-html'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; import mdx from '../../accordion/_storybook/bq-accordion.mdx'; import { ACCORDION_APPEARANCE, ACCORDION_SIZE } from '../../accordion/bq-accordion.types'; @@ -15,6 +16,7 @@ const meta: Meta = { argTypes: { appearance: { control: 'select', options: [...ACCORDION_APPEARANCE] }, 'expand-all': { control: 'boolean' }, + 'no-animation': { control: 'boolean' }, multiple: { control: 'boolean' }, size: { control: 'select', options: [...ACCORDION_SIZE] }, text: { control: 'text', table: { disable: true } }, @@ -22,6 +24,7 @@ const meta: Meta = { args: { appearance: 'filled', 'expand-all': false, + 'no-animation': false, multiple: false, size: 'medium', text: 'Header', @@ -34,10 +37,11 @@ type Story = StoryObj; export const Group: Story = { render: (args: Args) => html` ${args.text} diff --git a/packages/beeq/src/components/accordion-group/bq-accordion-group.tsx b/packages/beeq/src/components/accordion-group/bq-accordion-group.tsx index c91439312..8d3e370fc 100644 --- a/packages/beeq/src/components/accordion-group/bq-accordion-group.tsx +++ b/packages/beeq/src/components/accordion-group/bq-accordion-group.tsx @@ -27,23 +27,30 @@ export class BqAccordionGroup { // Public Property API // ======================== + /** The appearance style of accordion to be applied to all accordions */ + @Prop({ reflect: true, mutable: true }) appearance: TAccordionAppearance = 'filled'; + /** If true all accordions are expanded */ @Prop({ reflect: true }) expandAll: boolean; + /** + * Animation is set through JS when the browser does not support CSS calc-size() + * If true, the accordion animation, will be disabled. No animation will be applied. + */ + @Prop({ reflect: true }) noAnimation: boolean = false; + /** If true multiple accordions can be expanded at the same time */ @Prop({ reflect: true }) multiple: boolean = false; - /** The appearance style of accordion to be applied to all accordions */ - @Prop({ reflect: true, mutable: true }) appearance: TAccordionAppearance = 'filled'; - /** The size of accordion to be applied to all accordions */ @Prop({ reflect: true, mutable: true }) size: TAccordionSize = 'medium'; // Prop lifecycle events // ======================= - @Watch('expandAll') @Watch('appearance') + @Watch('expandAll') + @Watch('noAnimation') @Watch('size') checkPropValues() { this.bqAccordionElements.forEach((bqAccordionElement) => { @@ -52,6 +59,7 @@ export class BqAccordionGroup { bqAccordionElement.expanded = this.expandAll; } bqAccordionElement.appearance = this.appearance; + bqAccordionElement.noAnimation = this.noAnimation; bqAccordionElement.size = this.size; }); } diff --git a/packages/beeq/src/components/accordion-group/readme.md b/packages/beeq/src/components/accordion-group/readme.md index 99fd3f4cd..060e12e8c 100644 --- a/packages/beeq/src/components/accordion-group/readme.md +++ b/packages/beeq/src/components/accordion-group/readme.md @@ -7,12 +7,13 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------ | ------------ | ----------------------------------------------------------------- | --------------------- | ----------- | -| `appearance` | `appearance` | The appearance style of accordion to be applied to all accordions | `"filled" \| "ghost"` | `'filled'` | -| `expandAll` | `expand-all` | If true all accordions are expanded | `boolean` | `undefined` | -| `multiple` | `multiple` | If true multiple accordions can be expanded at the same time | `boolean` | `false` | -| `size` | `size` | The size of accordion to be applied to all accordions | `"medium" \| "small"` | `'medium'` | +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ----------- | +| `appearance` | `appearance` | The appearance style of accordion to be applied to all accordions | `"filled" \| "ghost"` | `'filled'` | +| `expandAll` | `expand-all` | If true all accordions are expanded | `boolean` | `undefined` | +| `multiple` | `multiple` | If true multiple accordions can be expanded at the same time | `boolean` | `false` | +| `noAnimation` | `no-animation` | Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. | `boolean` | `false` | +| `size` | `size` | The size of accordion to be applied to all accordions | `"medium" \| "small"` | `'medium'` | ## Shadow Parts diff --git a/packages/beeq/src/components/accordion/_storybook/bq-accordion.stories.tsx b/packages/beeq/src/components/accordion/_storybook/bq-accordion.stories.tsx index 49c30f4b5..94a38269f 100644 --- a/packages/beeq/src/components/accordion/_storybook/bq-accordion.stories.tsx +++ b/packages/beeq/src/components/accordion/_storybook/bq-accordion.stories.tsx @@ -17,11 +17,13 @@ const meta: Meta = { appearance: { control: 'select', options: [...ACCORDION_APPEARANCE] }, disabled: { control: 'boolean' }, expanded: { control: 'boolean' }, + 'no-animation': { control: 'boolean' }, rotate: { control: 'boolean' }, size: { control: 'select', options: [...ACCORDION_SIZE] }, // Event handlers bqBlur: { action: 'bqBlur' }, bqFocus: { action: 'bqFocus' }, + bqClick: { action: 'bqClick' }, bqOpen: { action: 'bqOpen' }, bqAfterOpen: { action: 'bqAfterOpen' }, bqClose: { action: 'bqClose' }, @@ -33,6 +35,7 @@ const meta: Meta = { appearance: 'filled', disabled: false, expanded: false, + 'no-animation': false, rotate: false, size: 'medium', // Not part of the component @@ -48,10 +51,12 @@ const Template = (args: Args) => html` appearance=${args.appearance} ?disabled=${args.disabled} ?expanded=${args.expanded} + ?no-animation=${args['no-animation']} ?rotate=${args.rotate} size=${args.size} @bqBlur=${args.bqBlur} @bqFocus=${args.bqFocus} + @bqClick=${args.bqClick} @bqOpen=${args.bqOpen} @bqAfterOpen=${args.bqAfterOpen} @bqClose=${args.bqClose} diff --git a/packages/beeq/src/components/accordion/bq-accordion.tsx b/packages/beeq/src/components/accordion/bq-accordion.tsx index 88948a27a..c2df4cef1 100644 --- a/packages/beeq/src/components/accordion/bq-accordion.tsx +++ b/packages/beeq/src/components/accordion/bq-accordion.tsx @@ -50,6 +50,12 @@ export class BqAccordion { /** If true accordion is expanded */ @Prop({ reflect: true, mutable: true }) expanded: boolean = false; + /** + * Animation is set through JS when the browser does not support CSS calc-size() + * If true, the accordion animation, will be disabled. No animation will be applied. + */ + @Prop({ reflect: true }) noAnimation: boolean = false; + /** If true accordion expand icon is rotate 180deg when expanded */ @Prop({ reflect: true }) rotate: boolean = false; @@ -67,15 +73,20 @@ export class BqAccordion { @Watch('expanded') handleExpandedChange() { - if (!this.accordion) return; - const event = this.expanded ? this.bqOpen.emit(this.el) : this.bqClose.emit(this.el); if (event.defaultPrevented) { this.expanded = !this.expanded; return; } - this.expanded ? this.accordion.open() : this.accordion.close(); + this.expanded ? this.accordion?.open() : this.accordion?.close(); + if (!this.isCssCalcSizeSupported) return; + + // NOTE: This is a workaround to trigger the transitionEnd event + // when the open/close animation is handled via CSS instead of JS + setTimeout(() => { + this.el.dispatchEvent(new CustomEvent('accordionTransitionEnd', { bubbles: false, composed: true })); + }, 200); } @Watch('disabled') @@ -85,6 +96,17 @@ export class BqAccordion { this.expanded = false; } + @Watch('noAnimation') + handleJsAnimation() { + if (this.isCssCalcSizeSupported) return; + + console.warn( + `[bq-accordion] calc-size() is not supported and animation will be set through JS + For vertical layout, consider using the 'noAnimation' prop ('no-animation' attribute) to disable it`, + ); + this.accordion = !this.noAnimation ? new Accordion(this.detailsElem) : null; + } + // Events section // Requires JSDocs for public API documentation // ============================================== @@ -119,7 +141,7 @@ export class BqAccordion { } componentDidLoad() { - this.accordion = new Accordion(this.detailsElem); + this.handleJsAnimation(); this.handleExpandedChange(); } @@ -128,6 +150,7 @@ export class BqAccordion { @Listen('accordionTransitionEnd') onAccordionTransitionEnd(event: CustomEvent) { + event.stopPropagation(); if (event.target !== this.el) return; this.expanded ? this.bqAfterOpen.emit(this.el) : this.bqAfterClose.emit(this.el); @@ -144,13 +167,14 @@ export class BqAccordion { // Internal business logic. // These methods cannot be called from the host element. // ======================================================= + private handleClick = (event: MouseEvent) => { event.preventDefault(); if (this.disabled) return; - this.expanded = !this.expanded; this.bqClick.emit(this.el); + this.expanded = !this.expanded; }; private handleFocus = () => { @@ -175,6 +199,10 @@ export class BqAccordion { return this.expanded && !this.disabled; } + private get isCssCalcSizeSupported() { + return window.CSS?.supports('(block-size: calc-size(auto))'); + } + // render() function // Always the last one in the class. // =================================== @@ -182,8 +210,13 @@ export class BqAccordion { render() { return (
(this.detailsElem = detailsElem)} + open={this.open} part="base" > @@ -232,7 +266,7 @@ export class BqAccordion { -
+
diff --git a/packages/beeq/src/components/accordion/helper/index.ts b/packages/beeq/src/components/accordion/helper/index.ts index 8b1bafa12..e77c2c200 100644 --- a/packages/beeq/src/components/accordion/helper/index.ts +++ b/packages/beeq/src/components/accordion/helper/index.ts @@ -10,7 +10,7 @@ export class Accordion { private isClosing: boolean; private isExpanding: boolean; private animationOptions = { - duration: 300, + duration: 200, easing: 'ease-in-out', }; diff --git a/packages/beeq/src/components/accordion/readme.md b/packages/beeq/src/components/accordion/readme.md index 33a206000..b11617299 100644 --- a/packages/beeq/src/components/accordion/readme.md +++ b/packages/beeq/src/components/accordion/readme.md @@ -7,13 +7,14 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------ | ------------ | ------------------------------------------------------------ | --------------------- | ---------- | -| `appearance` | `appearance` | The appearance style of accordion | `"filled" \| "ghost"` | `'filled'` | -| `disabled` | `disabled` | If true accordion is disabled | `boolean` | `false` | -| `expanded` | `expanded` | If true accordion is expanded | `boolean` | `false` | -| `rotate` | `rotate` | If true accordion expand icon is rotate 180deg when expanded | `boolean` | `false` | -| `size` | `size` | The size of accordion | `"medium" \| "small"` | `'medium'` | +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------- | +| `appearance` | `appearance` | The appearance style of accordion | `"filled" \| "ghost"` | `'filled'` | +| `disabled` | `disabled` | If true accordion is disabled | `boolean` | `false` | +| `expanded` | `expanded` | If true accordion is expanded | `boolean` | `false` | +| `noAnimation` | `no-animation` | Animation is set through JS when the browser does not support CSS calc-size() If true, the accordion animation, will be disabled. No animation will be applied. | `boolean` | `false` | +| `rotate` | `rotate` | If true accordion expand icon is rotate 180deg when expanded | `boolean` | `false` | +| `size` | `size` | The size of accordion | `"medium" \| "small"` | `'medium'` | ## Events diff --git a/packages/beeq/src/components/accordion/scss/bq-accordion.scss b/packages/beeq/src/components/accordion/scss/bq-accordion.scss index bb92398d7..6b121149b 100644 --- a/packages/beeq/src/components/accordion/scss/bq-accordion.scss +++ b/packages/beeq/src/components/accordion/scss/bq-accordion.scss @@ -18,12 +18,12 @@ } &.small .bq-accordion__header { - @apply gap-[--bq-accordion--small-gap] py-[--bq-accordion--small-padding-y] pe-[--bq-accordion--small-padding-end] ps-[--bq-accordion--small-padding-start]; + @apply gap-[--bq-accordion--small-gap] pe-[--bq-accordion--small-padding-end] ps-[--bq-accordion--small-padding-start] p-b-[--bq-accordion--small-padding-y]; @apply rounded-ee-[--bq-accordion--small-radius] rounded-es-[--bq-accordion--small-radius] rounded-se-[--bq-accordion--small-radius] rounded-ss-[--bq-accordion--small-radius]; } &.medium .bq-accordion__header { - @apply gap-[--bq-accordion--medium-gap] py-[--bq-accordion--medium-padding-y] pe-[--bq-accordion--medium-padding-end] ps-[--bq-accordion--medium-padding-start]; + @apply gap-[--bq-accordion--medium-gap] pe-[--bq-accordion--medium-padding-end] ps-[--bq-accordion--medium-padding-start] p-b-[--bq-accordion--medium-padding-y]; @apply rounded-ee-[--bq-accordion--medium-radius] rounded-es-[--bq-accordion--medium-radius] rounded-se-[--bq-accordion--medium-radius] rounded-ss-[--bq-accordion--medium-radius]; } @@ -91,22 +91,38 @@ } &.small.filled .bq-accordion__body { - @apply py-[--bq-accordion--panel-small-filled-padding-y] pe-[--bq-accordion--panel-small-filled-padding-end] ps-[--bq-accordion--panel-small-filled-padding-start]; + @apply pe-[--bq-accordion--panel-small-filled-padding-end] ps-[--bq-accordion--panel-small-filled-padding-start] p-b-[--bq-accordion--panel-small-filled-padding-y]; } &.medium.filled .bq-accordion__body { - @apply py-[--bq-accordion--panel-medium-filled-padding-y] pe-[--bq-accordion--panel-medium-filled-padding-end] ps-[--bq-accordion--panel-medium-filled-padding-start]; + @apply pe-[--bq-accordion--panel-medium-filled-padding-end] ps-[--bq-accordion--panel-medium-filled-padding-start] p-b-[--bq-accordion--panel-medium-filled-padding-y]; } &.small.ghost .bq-accordion__body { - @apply py-[--bq-accordion--panel-small-ghost-padding-y] pe-[--bq-accordion--panel-small-ghost-padding-end] ps-[--bq-accordion--panel-small-ghost-padding-start]; + @apply pe-[--bq-accordion--panel-small-ghost-padding-end] ps-[--bq-accordion--panel-small-ghost-padding-start] p-b-[--bq-accordion--panel-small-ghost-padding-y]; } &.medium.ghost .bq-accordion__body { - @apply py-[--bq-accordion--panel-medium-ghost-padding-y] pe-[--bq-accordion--panel-medium-ghost-padding-end] ps-[--bq-accordion--panel-medium-ghost-padding-start]; + @apply pe-[--bq-accordion--panel-medium-ghost-padding-end] ps-[--bq-accordion--panel-medium-ghost-padding-start] p-b-[--bq-accordion--panel-medium-ghost-padding-y]; } } +/* stylelint-disable-next-line selector-pseudo-element-no-unknown */ +.bq-accordion::details-content { + @apply block overflow-hidden transition-[block-size,content-visibility] duration-300 ease-in-out bs-0 [transition-behavior:allow-discrete]; +} + +/* stylelint-disable-next-line selector-pseudo-element-no-unknown */ +.bq-accordion[open]::details-content { + /* block-size: auto is just a fallback for browsers that don't support the calc-size() function */ + @apply bs-auto; +} + +/* stylelint-disable-next-line selector-pseudo-element-no-unknown */ +.bq-accordion[open]:not(.no-animation)::details-content { + @apply supports-[block-size:calc-size(auto)]:bs-[calc-size(auto)]; +} + .bq-accordion__header { // Since there's an overflow on the element, the focus outline is not visible, // so we force it to be inset to avoid the overflow hidden.