Skip to content

Commit

Permalink
refactor: create base class for overlay functionality (#2599)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: type 'SbbDialogCloseEventDetails' has been renamed to 'SbbOverlayCloseEventDetails'
  • Loading branch information
DavideMininni-Fincons authored Jun 6, 2024
1 parent 058489a commit 2059719
Show file tree
Hide file tree
Showing 26 changed files with 604 additions and 905 deletions.
81 changes: 20 additions & 61 deletions src/elements/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import {
type CSSResultGroup,
html,
isServer,
LitElement,
nothing,
type PropertyValues,
type TemplateResult,
} from 'lit';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
import { html, isServer, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ref } from 'lit/directives/ref.js';

import { getNextElementIndex } from '../core/a11y.js';
import { SbbOpenCloseBaseElement } from '../core/base-elements.js';
import { SbbConnectedAbortController } from '../core/controllers.js';
import { hostAttributes } from '../core/decorators.js';
import { findReferencedElement, getDocumentWritingMode, isSafari } from '../core/dom.js';
import { EventEmitter } from '../core/eventing.js';
import type { SbbOpenedClosedState } from '../core/interfaces.js';
import { SbbHydrationMixin, SbbNegativeMixin } from '../core/mixins.js';
import {
isEventOnElement,
Expand Down Expand Up @@ -53,14 +45,10 @@ const ariaRoleOnHost = isSafari;
dir: getDocumentWritingMode(),
role: ariaRoleOnHost ? 'listbox' : null,
})
export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement)) {
export class SbbAutocompleteElement extends SbbNegativeMixin(
SbbHydrationMixin(SbbOpenCloseBaseElement),
) {
public static override styles: CSSResultGroup = style;
public static readonly events = {
willOpen: 'willOpen',
didOpen: 'didOpen',
willClose: 'willClose',
didClose: 'didClose',
} as const;

/**
* The element where the autocomplete will attach; accepts both an element's id or an HTMLElement.
Expand All @@ -79,29 +67,6 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L
@property({ attribute: 'preserve-icon-space', reflect: true, type: Boolean })
public preserveIconSpace?: boolean;

/* The state of the autocomplete. */
private set _state(state: SbbOpenedClosedState) {
this.setAttribute('data-state', state);
}
private get _state(): SbbOpenedClosedState {
return this.getAttribute('data-state') as SbbOpenedClosedState;
}

/** Emits whenever the `sbb-autocomplete` starts the opening transition. */
private _willOpen: EventEmitter = new EventEmitter(this, SbbAutocompleteElement.events.willOpen);

/** Emits whenever the `sbb-autocomplete` is opened. */
private _didOpen: EventEmitter = new EventEmitter(this, SbbAutocompleteElement.events.didOpen);

/** Emits whenever the `sbb-autocomplete` begins the closing transition. */
private _willClose: EventEmitter = new EventEmitter(
this,
SbbAutocompleteElement.events.willClose,
);

/** Emits whenever the `sbb-autocomplete` is closed. */
private _didClose: EventEmitter = new EventEmitter(this, SbbAutocompleteElement.events.didClose);

private _overlay!: HTMLElement;
private _optionContainer!: HTMLElement;

Expand Down Expand Up @@ -139,32 +104,27 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L

/** Opens the autocomplete. */
public open(): void {
if (
this._state !== 'closed' ||
!this._overlay ||
this._options.length === 0 ||
this._readonly
) {
if (this.state !== 'closed' || !this._overlay || this._options.length === 0 || this._readonly) {
return;
}
if (!this._willOpen.emit()) {
if (!this.willOpen.emit()) {
return;
}

this._state = 'opening';
this.state = 'opening';
this._setOverlayPosition();
}

/** Closes the autocomplete. */
public close(): void {
if (this._state !== 'opened') {
if (this.state !== 'opened') {
return;
}
if (!this._willClose.emit()) {
if (!this.willClose.emit()) {
return;
}

this._state = 'closing';
this.state = 'closing';
this._openPanelEventsController.abort();
}

Expand Down Expand Up @@ -228,7 +188,6 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L
this.id ||= this._overlayId;
}

this._state ||= 'closed';
const signal = this._abort.signal;
const formField = this.closest?.('sbb-form-field') ?? this.closest?.('[data-form-field]');

Expand Down Expand Up @@ -396,26 +355,26 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L
* To avoid entering a corrupt state, exit when state is not expected.
*/
private _onAnimationEnd(event: AnimationEvent): void {
if (event.animationName === 'open' && this._state === 'opening') {
if (event.animationName === 'open' && this.state === 'opening') {
this._onOpenAnimationEnd();
} else if (event.animationName === 'close' && this._state === 'closing') {
} else if (event.animationName === 'close' && this.state === 'closing') {
this._onCloseAnimationEnd();
}
}

private _onOpenAnimationEnd(): void {
this._state = 'opened';
this.state = 'opened';
this._attachOpenPanelEvents();
this.triggerElement?.setAttribute('aria-expanded', 'true');
this._didOpen.emit();
this.didOpen.emit();
}

private _onCloseAnimationEnd(): void {
this._state = 'closed';
this.state = 'closed';
this.triggerElement?.setAttribute('aria-expanded', 'false');
this._resetActiveElement();
this._optionContainer.scrollTop = 0;
this._didClose.emit();
this.didClose.emit();
}

private _attachOpenPanelEvents(): void {
Expand Down Expand Up @@ -466,7 +425,7 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L
};

private _closedPanelKeyboardInteraction(event: KeyboardEvent): void {
if (this._state !== 'closed') {
if (this.state !== 'closed') {
return;
}

Expand All @@ -480,7 +439,7 @@ export class SbbAutocompleteElement extends SbbNegativeMixin(SbbHydrationMixin(L
}

private _openedPanelKeyboardInteraction(event: KeyboardEvent): void {
if (this._state !== 'opened') {
if (this.state !== 'opened') {
return;
}

Expand Down
20 changes: 10 additions & 10 deletions src/elements/autocomplete/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,19 @@ using `aria-activedescendant` to support navigation though the autocomplete opti

## Methods

| Name | Privacy | Description | Parameters | Return | Inherited From |
| ------- | ------- | ------------------------ | ---------- | ------ | -------------- |
| `close` | public | Closes the autocomplete. | | `void` | |
| `open` | public | Opens the autocomplete. | | `void` | |
| Name | Privacy | Description | Parameters | Return | Inherited From |
| ------- | ------- | ------------------------ | ---------- | ------ | ----------------------- |
| `close` | public | Closes the autocomplete. | | `void` | SbbOpenCloseBaseElement |
| `open` | public | Opens the autocomplete. | | `void` | SbbOpenCloseBaseElement |

## Events

| Name | Type | Description | Inherited From |
| ----------- | ------------------- | ------------------------------------------------------------------------------------- | -------------- |
| `didClose` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` is closed. | |
| `didOpen` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` is opened. | |
| `willClose` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` begins the closing transition. Can be canceled. | |
| `willOpen` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` starts the opening transition. Can be canceled. | |
| Name | Type | Description | Inherited From |
| ----------- | ------------------- | ------------------------------------------------------------------------------------- | ----------------------- |
| `didClose` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` is closed. | SbbOpenCloseBaseElement |
| `didOpen` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` is opened. | SbbOpenCloseBaseElement |
| `willClose` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` begins the closing transition. Can be canceled. | SbbOpenCloseBaseElement |
| `willOpen` | `CustomEvent<void>` | Emits whenever the `sbb-autocomplete` starts the opening transition. Can be canceled. | SbbOpenCloseBaseElement |

## CSS Properties

Expand Down
1 change: 1 addition & 0 deletions src/elements/core/base-elements.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './base-elements/action-base-element.js';
export * from './base-elements/button-base-element.js';
export * from './base-elements/link-base-element.js';
export * from './base-elements/open-close-base-element.js';
60 changes: 60 additions & 0 deletions src/elements/core/base-elements/open-close-base-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { LitElement } from 'lit';

import { EventEmitter } from '../eventing.js';
import type { SbbOpenedClosedState } from '../interfaces.js';

/**
* Base class for overlay components.
*
* @event willOpen - Emits whenever the component starts the opening transition. Can be canceled.
* @event didOpen - Emits whenever the component is opened.
* @event willClose - Emits whenever the component begins the closing transition. Can be canceled.
* @event didClose - Emits whenever the component is closed.
*/
export abstract class SbbOpenCloseBaseElement extends LitElement {
public static readonly events = {
willOpen: 'willOpen',
didOpen: 'didOpen',
willClose: 'willClose',
didClose: 'didClose',
} as const;

/** The state of the component. */
protected set state(state: SbbOpenedClosedState) {
this.setAttribute('data-state', state);
}
protected get state(): SbbOpenedClosedState {
return this.getAttribute('data-state') as SbbOpenedClosedState;
}

/** Emits whenever the component starts the opening transition. */
protected willOpen: EventEmitter = new EventEmitter(
this,
SbbOpenCloseBaseElement.events.willOpen,
);

/** Emits whenever the component is opened. */
protected didOpen: EventEmitter = new EventEmitter(this, SbbOpenCloseBaseElement.events.didOpen);

/** Emits whenever the component begins the closing transition. */
protected willClose: EventEmitter = new EventEmitter(
this,
SbbOpenCloseBaseElement.events.willClose,
);

/** Emits whenever the component is closed. */
protected didClose: EventEmitter = new EventEmitter(
this,
SbbOpenCloseBaseElement.events.didClose,
);

/** Opens the component. */
public abstract open(): void;
/** Closes the component. */
public abstract close(): void;

public override connectedCallback(): void {
super.connectedCallback();
this.state ||= 'closed';
}
}
1 change: 1 addition & 0 deletions src/elements/core/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './interfaces/overlay-close-details.js';
export * from './interfaces/types.js';
export * from './interfaces/validation-change.js';
4 changes: 4 additions & 0 deletions src/elements/core/interfaces/overlay-close-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SbbOverlayCloseEventDetails = {
returnValue?: any;
closeTarget?: HTMLElement;
};
14 changes: 9 additions & 5 deletions src/elements/core/mixins/update-scheduler-mixin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LitElement } from 'lit';

import type { Constructor } from './constructor.js';
import type { AbstractConstructor } from './constructor.js';

// Define the interface for the mixin
export declare class SbbUpdateSchedulerMixinType {
Expand All @@ -14,10 +14,13 @@ export declare class SbbUpdateSchedulerMixinType {
* @returns A class extended with the slot child observer functionality.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const SbbUpdateSchedulerMixin = <T extends Constructor<LitElement>>(
export const SbbUpdateSchedulerMixin = <T extends AbstractConstructor<LitElement>>(
base: T,
): Constructor<SbbUpdateSchedulerMixinType> & T => {
class SbbUpdateSchedulerElement extends base implements Partial<SbbUpdateSchedulerMixinType> {
): AbstractConstructor<SbbUpdateSchedulerMixinType> & T => {
abstract class SbbUpdateSchedulerElement
extends base
implements Partial<SbbUpdateSchedulerMixinType>
{
private _updatePromise = Promise.resolve();
private _updateResolve = (): void => {};

Expand All @@ -35,5 +38,6 @@ export const SbbUpdateSchedulerMixin = <T extends Constructor<LitElement>>(
return result;
}
}
return SbbUpdateSchedulerElement as unknown as Constructor<SbbUpdateSchedulerMixinType> & T;
return SbbUpdateSchedulerElement as unknown as AbstractConstructor<SbbUpdateSchedulerMixinType> &
T;
};
Loading

0 comments on commit 2059719

Please sign in to comment.