Skip to content

Commit

Permalink
feat(sbb-paginator): add sbb-compact-paginator component variant (#3142)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavideMininni-Fincons authored Oct 24, 2024
1 parent 07b8eba commit 2f3dc21
Show file tree
Hide file tree
Showing 26 changed files with 1,197 additions and 414 deletions.
1 change: 1 addition & 0 deletions src/elements/core/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './interfaces/overlay-close-details.js';
export * from './interfaces/paginator-page.js';
export * from './interfaces/types.js';
export * from './interfaces/validation-change.js';
6 changes: 6 additions & 0 deletions src/elements/core/interfaces/paginator-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type SbbPaginatorPageEventDetails = {
length: number;
pageSize: number;
pageIndex: number;
previousPageIndex: number;
};
2 changes: 2 additions & 0 deletions src/elements/paginator.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './paginator/common.js';
export * from './paginator/compact-paginator.js';
export * from './paginator/paginator.js';
1 change: 1 addition & 0 deletions src/elements/paginator/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './common/paginator-common.js';
186 changes: 186 additions & 0 deletions src/elements/paginator/common/paginator-common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { html, type LitElement, type PropertyValues, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';

import { SbbLanguageController } from '../../core/controllers.js';
import { hostAttributes } from '../../core/decorators.js';
import { EventEmitter } from '../../core/eventing.js';
import { i18nNextPage, i18nPreviousPage, i18nSelectedPage } from '../../core/i18n.js';
import type { SbbPaginatorPageEventDetails } from '../../core/interfaces.js';
import { type AbstractConstructor, SbbDisabledMixin, SbbNegativeMixin } from '../../core/mixins.js';

import '../../button/mini-button.js';
import '../../button/mini-button-group.js';
import '../../divider.js';

export declare abstract class SbbPaginatorCommonElementMixinType {
public accessor negative: boolean;
public accessor disabled: boolean;
public accessor length: number;
public accessor pageSize: number;
public accessor pageIndex: number;
public accessor pagerPosition: 'start' | 'end';
public accessor size: 'm' | 's';
protected language: SbbLanguageController;
protected numberOfPages(): number;
protected pageIndexChanged(value: number): void;
protected emitPageEvent(previousPageIndex: number): void;
protected renderPrevNextButtons(): TemplateResult;
protected abstract renderPaginator(): TemplateResult;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const SbbPaginatorCommonElementMixin = <T extends AbstractConstructor<LitElement>>(
superClass: T,
): AbstractConstructor<SbbPaginatorCommonElementMixinType> & T => {
@hostAttributes({
role: 'group',
})
abstract class SbbPaginatorCommonElement
extends SbbNegativeMixin(SbbDisabledMixin(superClass))
implements Partial<SbbPaginatorCommonElementMixinType>
{
public static readonly events: Record<string, string> = {
page: 'page',
} as const;

/** Total number of items. */
@property({ type: Number })
public set length(value: number) {
this._length = isNaN(value) || value < 0 ? 0 : value;
// Call setter of pageIndex to ensure bounds
// eslint-disable-next-line no-self-assign
this.pageIndex = this.pageIndex;
}
public get length(): number {
return this._length;
}
private _length: number = 0;

/** Number of items per page. */
@property({ attribute: 'page-size', type: Number })
public set pageSize(value: number) {
// Current page needs to be updated to reflect the new page size. Navigate to the page
// containing the previous page's first item.
const previousPageSize = this.pageSize;
this._pageSize = Math.max(value, 0);
this.pageIndex = Math.floor((this.pageIndex * previousPageSize) / this.pageSize) || 0;
}
public get pageSize(): number {
return this._pageSize;
}
private _pageSize: number = 10;

/** Current page index. */
@property({ attribute: 'page-index', type: Number })
public set pageIndex(value: number) {
this._pageIndex = this._coercePageIndexInRange(value);
}
public get pageIndex(): number {
return this._pageIndex;
}
private _pageIndex: number = 0;

/** Position of the prev/next buttons. */
@property({ attribute: 'pager-position', reflect: true }) public accessor pagerPosition:
| 'start'
| 'end' = 'start';

/** Size variant, either m or s. */
@property({ reflect: true }) public accessor size: 'm' | 's' = 'm';

private _page: EventEmitter<SbbPaginatorPageEventDetails> = new EventEmitter(
this,
SbbPaginatorCommonElement.events.page,
{ composed: true, bubbles: true },
);
protected language = new SbbLanguageController(this);
protected abstract renderPaginator(): string;

protected override updated(changedProperties: PropertyValues<this>): void {
super.updated(changedProperties);

// To reliably announce page change, we have to set the label in updated() (a tick later than the other changes).
this.shadowRoot!.querySelector('sbb-screen-reader-only')!.textContent =
this._currentPageLabel();
}

/** Evaluate `pageIndex` by excluding edge cases. */
private _coercePageIndexInRange(pageIndex: number): number {
return Math.max(
Math.min(Math.max(isNaN(pageIndex) ? 0 : pageIndex, 0), this.numberOfPages() - 1),
0,
);
}

private _currentPageLabel(): string {
return i18nSelectedPage(this.pageIndex + 1)[this.language.current];
}

/**
* Calculates the current number of pages based on the `length` and the `pageSize`;
* value must be rounded up (e.g. `length = 21` and `pageSize = 10` means 3 pages).
*/
protected numberOfPages(): number {
return this.pageSize ? Math.ceil(this.length / this.pageSize) : 0;
}

/**
* If the `pageIndex` changes due to user interaction,
* emit the `page` event and then update the `pageIndex` value.
*/
protected pageIndexChanged(value: number): void {
const previousPageIndex = this.pageIndex;
this.pageIndex = value;

if (previousPageIndex !== this.pageIndex) {
this.emitPageEvent(previousPageIndex);
}
}

protected emitPageEvent(previousPageIndex: number): void {
this._page.emit({
previousPageIndex,
pageIndex: this.pageIndex,
length: this.length,
pageSize: this.pageSize,
});
}

protected renderPrevNextButtons(): TemplateResult {
return html`
<sbb-mini-button-group ?negative=${this.negative} size=${this.size === 's' ? 's' : 'l'}>
<sbb-mini-button
id="sbb-paginator-prev-page"
aria-label=${i18nPreviousPage[this.language.current]}
icon-name="chevron-small-left-small"
?disabled=${this.disabled || this.pageIndex === 0}
@click=${() => this.pageIndexChanged(this._pageIndex - 1)}
></sbb-mini-button>
<sbb-divider orientation="vertical"></sbb-divider>
<sbb-mini-button
id="sbb-paginator-next-page"
aria-label=${i18nNextPage[this.language.current]}
icon-name="chevron-small-right-small"
?disabled=${this.disabled || this.pageIndex === this.numberOfPages() - 1}
@click=${() => this.pageIndexChanged(this._pageIndex + 1)}
></sbb-mini-button>
</sbb-mini-button-group>
`;
}

protected override render(): TemplateResult {
return html`
${this.renderPaginator()}
<sbb-screen-reader-only role="status"></sbb-screen-reader-only>
`;
}
}
return SbbPaginatorCommonElement as unknown as AbstractConstructor<SbbPaginatorCommonElementMixinType> &
T;
};

declare global {
interface HTMLElementEventMap {
page: CustomEvent<SbbPaginatorPageEventDetails>;
}
}
1 change: 1 addition & 0 deletions src/elements/paginator/compact-paginator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './compact-paginator/compact-paginator.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* @web/test-runner snapshot v1 */
export const snapshots = {};

snapshots["sbb-compact-paginator renders DOM"] =
`<sbb-compact-paginator
length="50"
page-size="5"
pager-position="start"
role="group"
size="m"
>
</sbb-compact-paginator>
`;
/* end snapshot sbb-compact-paginator renders DOM */

snapshots["sbb-compact-paginator renders Shadow DOM"] =
`<div class="sbb-compact-paginator">
<sbb-mini-button-group size="l">
<sbb-mini-button
aria-disabled="true"
aria-label="Previous page"
data-action=""
data-button=""
disabled=""
icon-name="chevron-small-left-small"
id="sbb-paginator-prev-page"
role="button"
slot="li-0"
>
</sbb-mini-button>
<sbb-divider
aria-orientation="vertical"
orientation="vertical"
role="separator"
slot="li-1"
>
</sbb-divider>
<sbb-mini-button
aria-label="Next page"
data-action=""
data-button=""
icon-name="chevron-small-right-small"
id="sbb-paginator-next-page"
role="button"
slot="li-2"
tabindex="0"
>
</sbb-mini-button>
</sbb-mini-button-group>
<span class="sbb-paginator__pages">
1
<sbb-divider
aria-hidden="true"
aria-orientation="vertical"
class="sbb-compact-paginator__divider"
orientation="vertical"
role="separator"
>
</sbb-divider>
10
</span>
</div>
<sbb-screen-reader-only role="status">
Page 1 selected.
</sbb-screen-reader-only>
`;
/* end snapshot sbb-compact-paginator renders Shadow DOM */

snapshots["sbb-compact-paginator renders A11y tree Firefox"] =
`<p>
{
"role": "document",
"name": "",
"children": [
{
"role": "button",
"name": "Previous page",
"disabled": true
},
{
"role": "button",
"name": "Next page"
},
{
"role": "text leaf",
"name": "1"
},
{
"role": "text leaf",
"name": "10"
},
{
"role": "text leaf",
"name": "Page 1 selected."
}
]
}
</p>
`;
/* end snapshot sbb-compact-paginator renders A11y tree Firefox */

snapshots["sbb-compact-paginator renders A11y tree Chrome"] =
`<p>
{
"role": "WebArea",
"name": "",
"children": [
{
"role": "button",
"name": "Previous page",
"disabled": true
},
{
"role": "button",
"name": "Next page"
},
{
"role": "text",
"name": "1"
},
{
"role": "text",
"name": "10"
},
{
"role": "text",
"name": "Page 1 selected."
}
]
}
</p>
`;
/* end snapshot sbb-compact-paginator renders A11y tree Chrome */

45 changes: 45 additions & 0 deletions src/elements/paginator/compact-paginator/compact-paginator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@use '../../core/styles' as sbb;

// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component.
@include sbb.box-sizing;

:host {
display: block;

--sbb-compact-paginator-height: var(--sbb-size-element-m);
--sbb-compact-paginator-color: var(--sbb-color-metal);
--sbb-paginator-compact-justify-content: start;
}

:host([size='s']) {
--sbb-compact-paginator-height: var(--sbb-size-element-xs);
}

:host([negative]) {
--sbb-compact-paginator-color: var(--sbb-color-storm);
}

:host([pager-position='end']) {
--sbb-paginator-compact-justify-content: end;
}

.sbb-compact-paginator {
display: flex;
gap: var(--sbb-spacing-fixed-5x);
justify-content: var(--sbb-paginator-compact-justify-content);
min-height: var(--sbb-compact-paginator-height);
}

.sbb-paginator__pages {
@include sbb.text-m--regular;

display: flex;
align-items: center;
justify-content: center;
gap: var(--sbb-spacing-fixed-2x);
color: var(--sbb-compact-paginator-color);
}

.sbb-compact-paginator__divider {
height: #{sbb.px-to-rem-build(16)};
}
Loading

0 comments on commit 2f3dc21

Please sign in to comment.