diff --git a/src/components/dialog/dialog.scss b/src/components/dialog/dialog.scss index a7f42eb7eff..3652ba5f112 100644 --- a/src/components/dialog/dialog.scss +++ b/src/components/dialog/dialog.scss @@ -25,6 +25,8 @@ --sbb-dialog-backdrop-pointer-events: none; --sbb-dialog-backdrop-color: transparent; --sbb-dialog-header-padding-block: var(--sbb-spacing-responsive-s) 0; + --sbb-dialog-header-transition-duration: var(--sbb-animation-duration-6x); + --sbb-dialog-header-margin-block-start: 0; --sbb-dialog-footer-border: var(--sbb-border-width-1x) solid var(--sbb-color-cloud-default); position: fixed; @@ -59,6 +61,18 @@ } } +:host([data-hide-header]) { + --sbb-dialog-header-margin-block-start: calc(var(--sbb-dialog-header-height) * -1); + + // Hide transition + --sbb-dialog-header-transition-timing: cubic-bezier(0.4, 0, 1, 1); +} + +:host(:not([data-hide-header])) { + // Show transition + --sbb-dialog-header-transition-timing: cubic-bezier(0, 0, 0.2, 1); +} + :host([data-fullscreen]) { --sbb-dialog-backdrop-color: transparent; --sbb-dialog-max-width: 100%; @@ -198,6 +212,16 @@ pointer-events: all; } + // Apply show/hide animation unless it has a visible focus within. + &:not([data-has-visible-focus]:focus-within) { + margin-block-start: var(--sbb-dialog-header-margin-block-start); + transition: { + property: margin, box-shadow; + duration: var(--sbb-dialog-header-transition-duration); + timing-function: var(--sbb-dialog-header-transition-timing); + } + } + :host([data-fullscreen]) & { position: fixed; width: var(--sbb-dialog-width); @@ -206,6 +230,11 @@ padding-block-start: var(--sbb-spacing-responsive-xs); } + &[data-has-visible-focus]:focus-within, + :host([data-overflows]:not([data-fullscreen], [negative], [data-hide-header])) & { + @include sbb.shadow-level-9-soft; + } + @include sbb.mq($from: medium) { border-radius: var(--sbb-dialog-border-radius) var(--sbb-dialog-border-radius) 0 0; } @@ -244,15 +273,11 @@ margin-block-start: auto; background-color: var(--sbb-dialog-background-color); border-block-start: var(--sbb-dialog-footer-border); -} -// stylelint-disable selector-not-notation -:is(.sbb-dialog__header, .sbb-dialog__footer) { :host([data-overflows]:not([data-fullscreen], [negative])) & { @include sbb.shadow-level-9-soft; } } -// stylelint-enable selector-not-notation .sbb-screen-reader-only { @include sbb.screen-reader-only; diff --git a/src/components/dialog/dialog.ts b/src/components/dialog/dialog.ts index 3540b27a899..75ab5cec2d9 100644 --- a/src/components/dialog/dialog.ts +++ b/src/components/dialog/dialog.ts @@ -2,7 +2,12 @@ import { CSSResultGroup, html, LitElement, nothing, TemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js'; -import { FocusTrap, IS_FOCUSABLE_QUERY, setModalityOnNextFocus } from '../core/a11y'; +import { + FocusTrap, + IS_FOCUSABLE_QUERY, + setModalityOnNextFocus, + sbbInputModalityDetector, +} from '../core/a11y'; import { ScrollHandler, toggleDatasetEntry, @@ -144,15 +149,17 @@ export class SbbDialog extends LitElement { private _backClick: EventEmitter = new EventEmitter(this, SbbDialog.events.backClick); private _dialog: HTMLDivElement; - private _dialogWrapperElement: HTMLElement; + private _dialogHeaderElement: HTMLElement; private _dialogContentElement: HTMLElement; private _dialogCloseElement: HTMLElement; private _dialogController: AbortController; - private _windowEventsController: AbortController; + private _openDialogController: AbortController; private _focusTrap = new FocusTrap(); private _scrollHandler = new ScrollHandler(); private _returnValue: any; private _isPointerDownEventOnDialog: boolean; + private _overflows: boolean; + private _lastScroll = 0; private _dialogId = `sbb-dialog-${nextId++}`; // Last element which had focus before the dialog was opened. @@ -220,6 +227,42 @@ export class SbbDialog extends LitElement { } } + private _onContentScroll(): void { + const hasVisibleHeader = this.dataset.hideHeader === undefined; + const dialogHeaderHeight = this._dialogHeaderElement.clientHeight; + + // Check whether hiding the header would make the scrollbar disappear + // and prevent the hiding animation if so. + if ( + hasVisibleHeader && + this._dialogContentElement.clientHeight + dialogHeaderHeight >= + this._dialogContentElement.scrollHeight + ) { + return; + } + + const currentScroll = this._dialogContentElement.scrollTop; + if ( + Math.round(currentScroll + this._dialogContentElement.clientHeight) >= + this._dialogContentElement.scrollHeight + ) { + return; + } + // Check whether is scrolling down or up. + if (currentScroll > 0 && this._lastScroll < currentScroll) { + // Scrolling down + if (hasVisibleHeader) { + this.style.setProperty('--sbb-dialog-header-height', `${dialogHeaderHeight}px`); + } + toggleDatasetEntry(this, 'hideHeader', true); + } else { + // Scrolling up + toggleDatasetEntry(this, 'hideHeader', false); + } + // `currentScroll` can be negative, e.g. on mobile; this is not allowed. + this._lastScroll = currentScroll <= 0 ? 0 : currentScroll; + } + public override connectedCallback(): void { super.connectedCallback(); this._handlerRepository.connect(); @@ -244,7 +287,7 @@ export class SbbDialog extends LitElement { super.disconnectedCallback(); this._handlerRepository.disconnect(); this._dialogController?.abort(); - this._windowEventsController?.abort(); + this._openDialogController?.abort(); this._focusTrap.disconnect(); this._dialogContentResizeObserver.disconnect(); this._removeInstanceFromGlobalCollection(); @@ -260,8 +303,8 @@ export class SbbDialog extends LitElement { dialogRefs.splice(dialogRefs.indexOf(this as SbbDialog), 1); } - private _attachWindowEvents(): void { - this._windowEventsController = new AbortController(); + private _attachOpenDialogEvents(): void { + this._openDialogController = new AbortController(); // Remove dialog label as soon as it is not needed anymore to prevent accessing it with browse mode. window.addEventListener( 'keydown', @@ -270,12 +313,39 @@ export class SbbDialog extends LitElement { await this._onKeydownEvent(event); }, { - signal: this._windowEventsController.signal, + signal: this._openDialogController.signal, }, ); window.addEventListener('click', () => this._removeAriaLiveRefContent(), { - signal: this._windowEventsController.signal, + signal: this._openDialogController.signal, }); + // If the content overflows, apply the header animation on scroll. + if (this._overflows) { + this._dialogContentElement?.addEventListener('scroll', () => this._onContentScroll(), { + passive: true, + signal: this._openDialogController.signal, + }); + Array.from(this._dialogHeaderElement.querySelectorAll('sbb-button'))?.forEach((el) => { + el.addEventListener( + 'focusin', + () => { + toggleDatasetEntry( + this._dialogHeaderElement, + 'hasVisibleFocus', + sbbInputModalityDetector.mostRecentModality === 'keyboard', + ); + }, + { signal: this._openDialogController.signal }, + ); + el.addEventListener( + 'blur', + () => { + toggleDatasetEntry(this._dialogHeaderElement, 'hasVisibleFocus', false); + }, + { signal: this._openDialogController.signal }, + ); + }); + } } // Check if the pointerdown event target is triggered on the dialog. @@ -334,10 +404,11 @@ export class SbbDialog extends LitElement { setTimeout(() => this._setAriaLiveRefContent()); this._focusTrap.trap(this); this._dialogContentResizeObserver.observe(this._dialogContentElement); - this._attachWindowEvents(); + this._attachOpenDialogEvents(); } else if (event.animationName === 'close' && this._state === 'closing') { + toggleDatasetEntry(this, 'hideHeader', false); + this._dialogContentElement.scrollTo(0, 0); this._state = 'closed'; - this._dialogWrapperElement.querySelector('.sbb-dialog__content').scrollTo(0, 0); removeInertMechanism(); setModalityOnNextFocus(this._lastFocusedElement); // Manually focus last focused element @@ -346,7 +417,7 @@ export class SbbDialog extends LitElement { returnValue: this._returnValue, closeTarget: this._dialogCloseElement, }); - this._windowEventsController?.abort(); + this._openDialogController?.abort(); this._focusTrap.disconnect(); this._dialogContentResizeObserver.disconnect(); this._removeInstanceFromGlobalCollection(); @@ -382,11 +453,12 @@ export class SbbDialog extends LitElement { } private _setOverflowAttribute(): void { - toggleDatasetEntry( - this, - 'overflows', - this._dialogContentElement.scrollHeight > this._dialogContentElement.clientHeight, - ); + this._overflows = + this._dialogContentElement.scrollHeight > this._dialogContentElement.clientHeight; + if (!this._overflows && this.dataset.hideHeader === '') { + toggleDatasetEntry(this, 'hideHeader', false); + } + toggleDatasetEntry(this, 'overflows', this._overflows); } protected override render(): TemplateResult { @@ -420,7 +492,10 @@ export class SbbDialog extends LitElement { `; const dialogHeader = html` -
+
(this._dialogHeaderElement = dialogHeaderrRef as HTMLElement))} + > ${this.titleBackButton ? backButton : nothing} ${hasTitle ? html`
this._closeOnSbbDialogCloseClick(event)} - ${ref( - (dialogWrapperRef) => (this._dialogWrapperElement = dialogWrapperRef as HTMLElement), - )} class="sbb-dialog__wrapper" > ${dialogHeader}