diff --git a/packages/ibm-products-web-components/src/components/side-panel/_story-assets/index.ts b/packages/ibm-products-web-components/src/components/side-panel/_story-assets/index.ts index a2fe8714703..90119fbd634 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/_story-assets/index.ts +++ b/packages/ibm-products-web-components/src/components/side-panel/_story-assets/index.ts @@ -129,26 +129,43 @@ export const getActionToolbarItems = (index) => { // TODO: There are problems switching this export const getActionItems = (index) => { switch (index) { - case 1: - return html`Primary`; - case 2: + case 2: // One button (ghost) + return html`Ghost`; + case 3: // One button (danger) + return html`Danger`; + case 4: // Two buttons + return html` + Secondary + Primary + `; + case 5: // Two buttons with ghost return html` Ghost Primary `; - case 3: - return html` Danger Primary`; - case 4: - return html` Ghost { Primary`; - case 5: - return html`Danger - Secondary - Primary`; - case 6: - return html`Danger - TertiarySecondary - Secondary - Primary`; default: diff --git a/packages/ibm-products-web-components/src/components/side-panel/defs.ts b/packages/ibm-products-web-components/src/components/side-panel/defs.ts index bacc45894bb..4ef25596f3c 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/defs.ts +++ b/packages/ibm-products-web-components/src/components/side-panel/defs.ts @@ -31,6 +31,11 @@ export enum SIDE_PANEL_SIZE { */ LARGE = 'lg', + /** + * Extra Large size. + */ + EXTRA_LARGE = 'xl', + /** * 2X-Large size. */ diff --git a/packages/ibm-products-web-components/src/components/side-panel/side-panel.scss b/packages/ibm-products-web-components/src/components/side-panel/side-panel.scss index ecae77c6cde..80cec659564 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/side-panel.scss +++ b/packages/ibm-products-web-components/src/components/side-panel/side-panel.scss @@ -77,7 +77,7 @@ $block-class-action-set: #{$prefix}--action-set; @extend .#{$block-class}--right-placement; /* remove if https://github.com/carbon-design-system/ibm-products/pull/3983 merged */ - border-inline-end: 1px solid $border-subtle-02; + border-inline-end: none; inset-inline-end: 0; } @@ -86,7 +86,7 @@ $block-class-action-set: #{$prefix}--action-set; @extend .#{$block-class}--left-placement; /* remove if https://github.com/carbon-design-system/ibm-products/pull/3983 merged */ - border-inline-end: 1px solid $border-subtle-02; + border-inline-end: 1px solid $border-subtle-01; inset-inline-start: 0; } @@ -167,9 +167,17 @@ $block-class-action-set: #{$prefix}--action-set; content: initial; /* remove border below */ } + .#{$block-class}__title-text { + padding-inline-end: 0; + } + .#{$block-class}__subtitle-text { + @extend .#{$block-class}__subtitle-text; + &[hidden] { @extend .#{carbon-config.$prefix}--visually-hidden !optional; + + display: none; } } @@ -180,6 +188,10 @@ $block-class-action-set: #{$prefix}--action-set; &[hidden] { @extend .#{carbon-config.$prefix}--visually-hidden !optional; } + + ::slotted(.#{$block-class}__action-toolbar-leading-button) { + margin-inline-end: $spacing-03; + } } .#{$block-class} .#{$block-class}__action-toolbar[hidden] { @@ -190,6 +202,23 @@ $block-class-action-set: #{$prefix}--action-set; @extend .#{$block-class}--has-slug; } + .#{$block-class}[has-slug] [scrolls] { + /* override carbon ai removing background gradient */ + background: + linear-gradient( + to top, + var(--cds-layer) 0%, + $ai-aura-start 0%, + 15%, + $ai-aura-end 50% + ) + padding-box, + linear-gradient(to top, var(--cds-layer), var(--cds-layer)) padding-box, + linear-gradient(to bottom, $ai-border-start, $ai-border-end) border-box, + linear-gradient(to top, var(--cds-layer), var(--cds-layer)) border-box; + box-shadow: inset 0 -80px 70px -65px $ai-inner-shadow; + } + [scrolls] { @extend .#{$block-class}--scrolls; } @@ -244,7 +273,7 @@ $block-class-action-set: #{$prefix}--action-set; } &[actions-multiple='triple'] { - --flex-direction: column; + --flex-direction: column-reverse; ::slotted(#{carbon-config.$prefix}-button) { flex: initial; @@ -256,7 +285,7 @@ $block-class-action-set: #{$prefix}--action-set; // -1 in @container query is for 1px left border @container (width <= #{map.get(spv.$side-panel-sizes, sm)}) { - --flex-direction: column; + --flex-direction: column-reverse; ::slotted(#{carbon-config.$prefix}-button) { flex: initial; @@ -271,6 +300,7 @@ $block-class-action-set: #{$prefix}--action-set; @include actions-placement(); display: flex; + gap: 1px; inline-size: 100%; ::slotted(#{carbon-config.$prefix}-button) { diff --git a/packages/ibm-products-web-components/src/components/side-panel/side-panel.stories.ts b/packages/ibm-products-web-components/src/components/side-panel/side-panel.stories.ts index 0f05daddb47..7dbce6295e0 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/side-panel.stories.ts +++ b/packages/ibm-products-web-components/src/components/side-panel/side-panel.stories.ts @@ -50,6 +50,8 @@ const sizes = { [`Small size (${SIDE_PANEL_SIZE.SMALL})`]: SIDE_PANEL_SIZE.SMALL, [`Medium size (default) (${SIDE_PANEL_SIZE.MEDIUM})`]: SIDE_PANEL_SIZE.MEDIUM, [`Large size (${SIDE_PANEL_SIZE.LARGE})`]: SIDE_PANEL_SIZE.LARGE, + [`Extra Large size (${SIDE_PANEL_SIZE.EXTRA_LARGE})`]: + SIDE_PANEL_SIZE.EXTRA_LARGE, [`Extra Extra Large size (${SIDE_PANEL_SIZE.EXTRA_EXTRA_LARGE})`]: SIDE_PANEL_SIZE.EXTRA_EXTRA_LARGE, }; @@ -97,13 +99,16 @@ const actionToolbarItems = { }; const actionItems = { - 'No actions': 0, 'One button': 1, - 'Two buttons with ghost': 2, - 'Two buttons with danger': 3, - 'Three buttons with ghost': 4, - 'Three buttons with danger': 5, - 'Too many buttons': 6, + 'One button (ghost)': 2, + 'One button (danger)': 3, + 'Two buttons': 4, + 'Two buttons with ghost': 5, + 'Two buttons with danger': 6, + 'Three buttons with ghost': 7, + 'Three buttons with danger': 8, + 'Three buttons': 9, + None: 0, }; const slugs = { diff --git a/packages/ibm-products-web-components/src/components/side-panel/side-panel.test.ts b/packages/ibm-products-web-components/src/components/side-panel/side-panel.test.ts index e71569c5d45..a873c4ab544 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/side-panel.test.ts +++ b/packages/ibm-products-web-components/src/components/side-panel/side-panel.test.ts @@ -342,7 +342,7 @@ describe('c4p-side-panel', () => { `.${prefix}--action-set__action-button` ); - expect(actionItems).toHaveLength(3); + expect(actionItems).toHaveLength(2); }); it('should display a close button by default', async () => { diff --git a/packages/ibm-products-web-components/src/components/side-panel/side-panel.ts b/packages/ibm-products-web-components/src/components/side-panel/side-panel.ts index 128137d130f..953d80105fa 100644 --- a/packages/ibm-products-web-components/src/components/side-panel/side-panel.ts +++ b/packages/ibm-products-web-components/src/components/side-panel/side-panel.ts @@ -144,33 +144,132 @@ class CDSSidePanel extends HostListenerMixin(LitElement) { last: HTMLElement | undefined; all: HTMLElement[]; } { - const elements: NodeListOf[] = []; + const elements: HTMLElement[] = []; - // Add slug elements if present + // Add back button if present (shadow DOM) + if (this.currentStep > 0) { + const backButton = this.shadowRoot?.querySelector( + `.${blockClass}__navigation-back-button` + ); + if (backButton) { + elements.push(backButton); + } + } + + // Add tabbable elements from above-title slot (light DOM - breadcrumbs, etc.) + const aboveTitleSlot = this.shadowRoot?.querySelector( + 'slot[name="above-title"]' + ); + if (aboveTitleSlot) { + const aboveTitleElements = aboveTitleSlot + .assignedElements({ flatten: true }) + .flatMap((el) => + Array.from(el.querySelectorAll(selectorTabbable)) + ); + elements.push(...aboveTitleElements); + } + + // Add label text if present (shadow DOM) + const labelText = this.shadowRoot?.querySelector( + `.${blockClass}__label-text` + ); + if (labelText) { + elements.push(labelText); + } + + // Add title if present (shadow DOM) + const titleText = this.shadowRoot?.querySelector( + `.${blockClass}__title-text` + ); + if (titleText) { + elements.push(titleText); + } + + // Add slug elements if present (light DOM) if (this._hasSlug) { - elements.push(this.querySelectorAll(`${carbonPrefix}-slug`)); + const slugElements = Array.from( + this.querySelectorAll(`${carbonPrefix}-slug`) + ); + elements.push(...slugElements); } - // Add close button if not hidden + // Add close button if not hidden (shadow DOM) if (!this.hideCloseButton) { - const closeButtons = this.shadowRoot?.querySelectorAll( - `${carbonPrefix}-icon-button` + const closeButton = this.shadowRoot?.querySelector( + `.${blockClass}__close-button` ); - if (closeButtons) { - elements.push(closeButtons); + if (closeButton) { + elements.push(closeButton); } } - // Add tabbable elements inside light DOM - const _tabbableItems = this.querySelectorAll(selectorTabbable); - if (_tabbableItems) { - elements.push(_tabbableItems); + // Add subtitle if present (shadow DOM) + const subtitleText = this.shadowRoot?.querySelector( + `.${blockClass}__subtitle-text` + ); + if (subtitleText && !subtitleText.hidden) { + elements.push(subtitleText); + } + + // Add tabbable elements from below-title slot (light DOM) + const belowTitleSlot = this.shadowRoot?.querySelector( + 'slot[name="below-title"]' + ); + if (belowTitleSlot) { + const belowTitleElements = belowTitleSlot + .assignedElements({ flatten: true }) + .flatMap((el) => + Array.from(el.querySelectorAll(selectorTabbable)) + ); + elements.push(...belowTitleElements); } - // Flatten NodeList arrays and filter for focusable items - const all = elements - ?.flatMap((nodeList) => Array.from(nodeList)) - ?.filter((el): el is HTMLElement => typeof el?.focus === 'function'); + // Add action toolbar elements (light DOM) + const actionToolbarSlot = this.shadowRoot?.querySelector( + 'slot[name="action-toolbar"]' + ); + if (actionToolbarSlot) { + const actionToolbarElements = actionToolbarSlot + .assignedElements({ flatten: true }) + .filter( + (el): el is HTMLElement => + el instanceof HTMLElement && + typeof (el as HTMLElement).focus === 'function' + ); + elements.push(...actionToolbarElements); + } + + // Add body content tabbable elements (light DOM - default slot) + const defaultSlot = + this.shadowRoot?.querySelector('slot:not([name])'); + if (defaultSlot) { + const bodyElements = defaultSlot + .assignedElements({ flatten: true }) + .flatMap((el) => + Array.from(el.querySelectorAll(selectorTabbable)) + ); + elements.push(...bodyElements); + } + + // Add action buttons (light DOM) + const actionsSlot = this.shadowRoot?.querySelector( + 'slot[name="actions"]' + ); + if (actionsSlot) { + const actionElements = actionsSlot + .assignedElements({ flatten: true }) + .filter( + (el): el is HTMLElement => + el instanceof HTMLElement && + typeof (el as HTMLElement).focus === 'function' + ); + elements.push(...actionElements); + } + + // Filter for focusable items + const all = elements.filter( + (el): el is HTMLElement => typeof el?.focus === 'function' + ); return { first: all[0], @@ -405,9 +504,21 @@ class CDSSidePanel extends HostListenerMixin(LitElement) { this._hasActionToolbar = toolbarActions && toolbarActions.length > 0; if (this._hasActionToolbar) { - for (const toolbarAction of toolbarActions) { + for (let i = 0; i < toolbarActions.length; i++) { + const toolbarAction = toolbarActions[i]; // toolbar actions size should always be sm toolbarAction.setAttribute('size', 'sm'); + + // Add leading button class to first button + if (i === 0) { + toolbarAction.classList.add( + `${blockClass}__action-toolbar-leading-button` + ); + } else { + toolbarAction.classList.remove( + `${blockClass}__action-toolbar-leading-button` + ); + } } } } @@ -709,7 +820,9 @@ class CDSSidePanel extends HostListenerMixin(LitElement) { class=${`${blockClass}__title`} ?no-label=${!!labelText} > -

${title}

+

+ ${title} +

${this._doAnimateTitle ? html`

${title?.length && labelText?.length - ? html`

${labelText}

` + ? html`

+ ${labelText} +

` : ''} @@ -787,6 +902,7 @@ class CDSSidePanel extends HostListenerMixin(LitElement) { ?no-title-animation=${!this._doAnimateTitle} ?no-action-toolbar=${!this._hasActionToolbar} ?no-title=${!title} + tabindex="0" >