Skip to content

Commit

Permalink
refactor: changed NamedSlotListElement to a mixin, minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
DavideMininni-Fincons committed Feb 16, 2024
1 parent ac79bb9 commit dc807ac
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 192 deletions.
17 changes: 13 additions & 4 deletions src/components/breadcrumb/breadcrumb-group/breadcrumb-group.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type { CSSResultGroup, PropertyValueMap, PropertyValues, TemplateResult } from 'lit';
import {
type CSSResultGroup,
LitElement,
type PropertyValueMap,
type PropertyValues,
type TemplateResult,
} from 'lit';
import { html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';

import { getNextElementIndex, isArrowKeyPressed, sbbInputModalityDetector } from '../../core/a11y';
import type { WithListChildren } from '../../core/common-behaviors';
import { LanguageController, NamedSlotListElement } from '../../core/common-behaviors';
import { SbbNamedSlotListElementMixin, type WithListChildren } from '../../core/common-behaviors';
import { LanguageController } from '../../core/common-behaviors';
import { setAttribute } from '../../core/dom';
import { ConnectedAbortController } from '../../core/eventing';
import { i18nBreadcrumbEllipsisButtonLabel } from '../../core/i18n';
Expand All @@ -21,7 +27,10 @@ import '../../icon';
* @slot - Use the unnamed slot to add `sbb-breadcrumb` elements.
*/
@customElement('sbb-breadcrumb-group')
export class SbbBreadcrumbGroupElement extends NamedSlotListElement<SbbBreadcrumbElement> {
export class SbbBreadcrumbGroupElement extends SbbNamedSlotListElementMixin<
SbbBreadcrumbElement,
typeof LitElement
>(LitElement) {
public static override styles: CSSResultGroup = style;
protected override readonly listChildTagNames = ['SBB-BREADCRUMB'];

Expand Down
211 changes: 119 additions & 92 deletions src/components/core/common-behaviors/named-slot-list-element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { TemplateResult } from 'lit';
import { LitElement, html, nothing } from 'lit';
import { type LitElement, html, nothing, type TemplateResult } from 'lit';
import { state } from 'lit/decorators.js';

import type { AbstractConstructor } from './constructor';
import { SlotChildObserver } from './slot-child-observer';
import '../../screenreader-only';

Expand All @@ -20,109 +20,136 @@ const SLOTNAME_PREFIX = 'li';
* }
*/
export type WithListChildren<
T extends NamedSlotListElement<C>,
T extends NamedSlotListElementMixinType<C>,
C extends HTMLElement = HTMLElement,
> = T & { listChildren: C[] };

/**
* This base class provides named slot list observer functionality.
* This allows using the pattern of rendering a named slot for each child, which allows
* wrapping children in a ul/li list.
*/
export abstract class NamedSlotListElement<
C extends HTMLElement = HTMLElement,
> extends SlotChildObserver(LitElement) {
/** A list of upper-cased tag names to match against. (e.g. SBB-LINK) */
export declare abstract class NamedSlotListElementMixinType<C extends HTMLElement> {
protected abstract readonly listChildTagNames: string[];
@state() protected listChildren: C[];
protected checkChildren(): void;
protected renderList(attributes?: {
class?: string;
ariaLabel?: string;
ariaLabelledby?: string;
}): TemplateResult;
protected listSlotNames(): string[];
protected renderHiddenSlot(): TemplateResult;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const SbbNamedSlotListElementMixin = <
C extends HTMLElement,
T extends AbstractConstructor<LitElement>,
>(
superClass: T,
): AbstractConstructor<NamedSlotListElementMixinType<C>> & T => {
/**
* A list of children with the defined tag names.
* This array is only updated, if there is an actual change
* to the child elements.
* This base class provides named slot list observer functionality.
* This allows using the pattern of rendering a named slot for each child, which allows
* wrapping children in a ul/li list.
*/
@state() protected listChildren: C[] = [];
abstract class NamedSlotListElement<C extends HTMLElement = HTMLElement>
extends SlotChildObserver(superClass)
implements Partial<NamedSlotListElementMixinType<C>>
{
/** A list of upper-cased tag names to match against. (e.g. SBB-LINK) */
protected abstract readonly listChildTagNames: string[];

protected override checkChildren(): void {
const listChildren = Array.from(this.children ?? []).filter((e): e is C =>
this.listChildTagNames.includes(e.tagName),
);
// If the slotted child instances have not changed, we can skip syncing and updating
// the link reference list.
if (
listChildren.length === this.listChildren.length &&
this.listChildren.every((e, i) => listChildren[i] === e)
) {
return;
}
/**
* A list of children with the defined tag names.
* This array is only updated, if there is an actual change
* to the child elements.
*/
@state() protected listChildren: C[] = [];

this.listChildren
.filter((c) => !listChildren.includes(c))
.forEach((c) => c.removeAttribute('slot'));
this.listChildren = listChildren;
this.listChildren.forEach((c, index) => c.setAttribute('slot', `${SLOTNAME_PREFIX}-${index}`));
protected override checkChildren(): void {
const listChildren = Array.from(this.children ?? []).filter((e): e is C =>
this.listChildTagNames.includes(e.tagName),
);
// If the slotted child instances have not changed, we can skip syncing and updating
// the link reference list.
if (
listChildren.length === this.listChildren.length &&
this.listChildren.every((e, i) => listChildren[i] === e)
) {
return;
}

// Remove the ssr attribute, once we have actually initialized the children elements.
this.removeAttribute(SSR_CHILD_COUNT_ATTRIBUTE);
}
this.listChildren
.filter((c) => !listChildren.includes(c))
.forEach((c) => c.removeAttribute('slot'));
this.listChildren = listChildren;
this.listChildren.forEach((c, index) =>
c.setAttribute('slot', `${SLOTNAME_PREFIX}-${index}`),
);

/**
* Renders list and list slots for slotted children or an amount of list slots
* corresponding to the `data-ssr-child-count` attribute value.
*
* This is a possible optimization for SSR, as in an SSR Lit environment
* other elements are not available, but might be available in the meta
* framework wrapper (like e.g. React). This allows to provide the amount of
* children to be passed via the `data-ssr-child-count` attribute value.
*/
protected renderList(
attributes: { class?: string; ariaLabel?: string; ariaLabelledby?: string } = {},
): TemplateResult {
const listSlotNames = this.listSlotNames();
// Remove the ssr attribute, once we have actually initialized the children elements.
this.removeAttribute(SSR_CHILD_COUNT_ATTRIBUTE);
}

/**
* Renders list and list slots for slotted children or an amount of list slots
* corresponding to the `data-ssr-child-count` attribute value.
*
* This is a possible optimization for SSR, as in an SSR Lit environment
* other elements are not available, but might be available in the meta
* framework wrapper (like e.g. React). This allows to provide the amount of
* children to be passed via the `data-ssr-child-count` attribute value.
*/
protected renderList(
attributes: { class?: string; ariaLabel?: string; ariaLabelledby?: string } = {},
): TemplateResult {
const listSlotNames = this.listSlotNames();

if (listSlotNames.length >= 2) {
return html`
<ul
class=${attributes.class || this.tagName.toLowerCase()}
aria-label=${attributes.ariaLabel || nothing}
aria-labelledby=${attributes.ariaLabelledby || nothing}
>
${listSlotNames.map((name) => html`<li><slot name=${name}></slot></li>`)}
</ul>
${this.renderHiddenSlot()}
`;
} else if (listSlotNames.length === 1) {
return html`<sbb-screenreader-only>${attributes.ariaLabel}</sbb-screenreader-only>
<span class=${attributes.class || this.tagName.toLowerCase()}>
<span><slot name=${listSlotNames[0]}></slot></span>
</span>
${this.renderHiddenSlot()} `;
} else {
return this.renderHiddenSlot();
if (listSlotNames.length >= 2) {
return html`
<ul
class=${attributes.class || this.tagName.toLowerCase()}
aria-label=${attributes.ariaLabel || nothing}
aria-labelledby=${attributes.ariaLabelledby || nothing}
>
${listSlotNames.map((name) => html`<li><slot name=${name}></slot></li>`)}
</ul>
${this.renderHiddenSlot()}
`;
} else if (listSlotNames.length === 1) {
return html`<sbb-screenreader-only>${attributes.ariaLabel}</sbb-screenreader-only>
<span class=${attributes.class || this.tagName.toLowerCase()}>
<span><slot name=${listSlotNames[0]}></slot></span>
</span>
${this.renderHiddenSlot()} `;
} else {
return this.renderHiddenSlot();
}
}
}

/**
* Returns an array of list slot names with the length corresponding to the amount of matched
* children or the `data-ssr-child-count` attribute value.
*
* This is a possible optimization for SSR, as in an SSR Lit environment
* other elements are not available, but might be available in the meta
* framework wrapper (like e.g. React). This allows to provide the amount of
* children to be passed via the `data-ssr-child-count` attribute value.
*/
protected listSlotNames(): string[] {
const listChildren = this.listChildren.length
? this.listChildren
: Array.from({ length: +(this.getAttribute(SSR_CHILD_COUNT_ATTRIBUTE) ?? 0) });
return listChildren.map((_, i) => `${SLOTNAME_PREFIX}-${i}`);
}
/**
* Returns an array of list slot names with the length corresponding to the amount of matched
* children or the `data-ssr-child-count` attribute value.
*
* This is a possible optimization for SSR, as in an SSR Lit environment
* other elements are not available, but might be available in the meta
* framework wrapper (like e.g. React). This allows to provide the amount of
* children to be passed via the `data-ssr-child-count` attribute value.
*/
protected listSlotNames(): string[] {
const listChildren = this.listChildren.length
? this.listChildren
: Array.from({ length: +(this.getAttribute(SSR_CHILD_COUNT_ATTRIBUTE) ?? 0) });
return listChildren.map((_, i) => `${SLOTNAME_PREFIX}-${i}`);
}

/**
* Returns a hidden slot, which is intended as the children change detection.
* When an element without a slot attribute is slotted to the element, it triggers
* the slotchange event, which can be used to assign it to the appropriate named slot.
*/
protected renderHiddenSlot(): TemplateResult {
return html`<span hidden><slot></slot></span>`;
/**
* Returns a hidden slot, which is intended as the children change detection.
* When an element without a slot attribute is slotted to the element, it triggers
* the slotchange event, which can be used to assign it to the appropriate named slot.
*/
protected renderHiddenSlot(): TemplateResult {
return html`<span hidden><slot></slot></span>`;
}
}
}

return NamedSlotListElement as unknown as AbstractConstructor<NamedSlotListElementMixinType<C>> &
T;
};
22 changes: 11 additions & 11 deletions src/components/link-list/link-list.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { CSSResultGroup, TemplateResult, PropertyValues } from 'lit';
import { html, nothing } from 'lit';
import { html, nothing, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

import type { WithListChildren } from '../core/common-behaviors';
import { NamedSlotListElement, NamedSlotStateController } from '../core/common-behaviors';
import {
SbbNamedSlotListElementMixin,
SbbNegativeMixin,
NamedSlotStateController,
type WithListChildren,
} from '../core/common-behaviors';
import type { SbbHorizontalFrom, SbbOrientation } from '../core/interfaces';
import type { SbbLinkElement, SbbLinkSize } from '../link';
import type { TitleLevel } from '../title';
Expand All @@ -19,9 +23,11 @@ import '../title';
* @slot title - Use this slot to provide a title.
*/
@customElement('sbb-link-list')
export class SbbLinkListElement extends NamedSlotListElement<SbbLinkElement> {
export class SbbLinkListElement extends SbbNegativeMixin(
SbbNamedSlotListElementMixin<SbbLinkElement, typeof LitElement>(LitElement),
) {
public static override styles: CSSResultGroup = style;
protected override readonly listChildTagNames = ['SBB-LINK'];
protected override readonly listChildTagNames = ['SBB-LINK', 'SBB-LINK-BUTTON'];

/** The title text we want to show before the list. */
@property({ attribute: 'title-content', reflect: true }) public titleContent?: string;
Expand All @@ -35,12 +41,6 @@ export class SbbLinkListElement extends NamedSlotListElement<SbbLinkElement> {
*/
@property({ reflect: true }) public size: SbbLinkSize = 's';

/**
* Whether to render the link list and nested sbb-link instances as negative. This will overwrite
* the negative attribute of nested sbb-link instances.
*/
@property({ reflect: true, type: Boolean }) public negative: boolean = false;

/** Selected breakpoint from which the list is rendered horizontally. */
@property({ attribute: 'horizontal-from', reflect: true })
public horizontalFrom?: SbbHorizontalFrom;
Expand Down
16 changes: 8 additions & 8 deletions src/components/link-list/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ The title will not be displayed in the horizontal orientation.

## Properties

| Name | Attribute | Privacy | Type | Default | Description |
| ---------------- | ----------------- | ------- | -------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `titleContent` | `title-content` | public | `string \| undefined` | | The title text we want to show before the list. |
| `titleLevel` | `title-level` | public | `TitleLevel \| undefined` | `'2'` | The semantic level of the title, e.g. 2 = h2. |
| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size of the nested sbb-link instances. This will overwrite the size attribute of nested sbb-link instances. |
| `negative` | `negative` | public | `boolean` | `false` | Whether to render the link list and nested sbb-link instances as negative. This will overwrite the negative attribute of nested sbb-link instances. |
| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| undefined` | | Selected breakpoint from which the list is rendered horizontally. |
| `orientation` | `orientation` | public | `SbbOrientation` | `'vertical'` | The orientation in which the list will be shown vertical or horizontal. |
| Name | Attribute | Privacy | Type | Default | Description |
| ---------------- | ----------------- | ------- | -------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `titleContent` | `title-content` | public | `string \| undefined` | | The title text we want to show before the list. |
| `titleLevel` | `title-level` | public | `TitleLevel \| undefined` | `'2'` | The semantic level of the title, e.g. 2 = h2. |
| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size of the nested sbb-link instances. This will overwrite the size attribute of nested sbb-link instances. |
| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| undefined` | | Selected breakpoint from which the list is rendered horizontally. |
| `orientation` | `orientation` | public | `SbbOrientation` | `'vertical'` | The orientation in which the list will be shown vertical or horizontal. |
| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. |

## Slots

Expand Down
12 changes: 6 additions & 6 deletions src/components/menu/menu/menu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { CSSResultGroup, TemplateResult } from 'lit';
import { html } from 'lit';
import { type CSSResultGroup, html, LitElement, type TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ref } from 'lit/directives/ref.js';

Expand All @@ -11,7 +10,7 @@ import {
isArrowKeyPressed,
setModalityOnNextFocus,
} from '../../core/a11y';
import { NamedSlotListElement } from '../../core/common-behaviors';
import { SbbNamedSlotListElementMixin } from '../../core/common-behaviors';
import {
findReferencedElement,
isBreakpoint,
Expand Down Expand Up @@ -55,9 +54,10 @@ let nextId = 0;
* @event {CustomEvent<void>} didClose - Emits whenever the `sbb-menu` is closed.
*/
@customElement('sbb-menu')
export class SbbMenuElement extends NamedSlotListElement<
SbbMenuButtonElement | SbbMenuLinkElement
> {
export class SbbMenuElement extends SbbNamedSlotListElementMixin<
SbbMenuButtonElement | SbbMenuLinkElement,
typeof LitElement
>(LitElement) {
public static override styles: CSSResultGroup = style;
public static readonly events = {
willOpen: 'willOpen',
Expand Down
18 changes: 12 additions & 6 deletions src/components/navigation/navigation-list/navigation-list.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
import { html } from 'lit';
import {
type CSSResultGroup,
html,
LitElement,
type PropertyValues,
type TemplateResult,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';

import {
NamedSlotListElement,
NamedSlotStateController,
SbbNamedSlotListElementMixin,
type WithListChildren,
} from '../../core/common-behaviors';
import type { SbbNavigationButtonElement, SbbNavigationLinkElement } from '../index';
Expand All @@ -18,9 +23,10 @@ import style from './navigation-list.scss?lit&inline';
* @slot label - Use this to provide a label element.
*/
@customElement('sbb-navigation-list')
export class SbbNavigationListElement extends NamedSlotListElement<
SbbNavigationButtonElement | SbbNavigationLinkElement
> {
export class SbbNavigationListElement extends SbbNamedSlotListElementMixin<
SbbNavigationButtonElement | SbbNavigationLinkElement,
typeof LitElement
>(LitElement) {
public static override styles: CSSResultGroup = style;
protected override readonly listChildTagNames = ['SBB-NAVIGATION-BUTTON', 'SBB-NAVIGATION-LINK'];

Expand Down
Loading

0 comments on commit dc807ac

Please sign in to comment.