From 13b0934c8f828d97b60fa2f4311b341b6148a345 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Wed, 2 Oct 2024 10:34:28 +0200 Subject: [PATCH 01/22] feat(sbb-paginator): move to new folder and create common files --- src/elements/paginator.ts | 1 + src/elements/paginator/common.ts | 1 + .../paginator/common/paginator-common.ts | 167 ++++++++ src/elements/paginator/index.ts | 1 - src/elements/paginator/paginator.ts | 384 +----------------- .../paginator.snapshot.spec.snap.js | 0 .../paginator/{ => paginator}/paginator.scss | 2 +- .../paginator.snapshot.spec.ts | 6 +- .../{ => paginator}/paginator.spec.ts | 12 +- .../{ => paginator}/paginator.ssr.spec.ts | 2 +- .../{ => paginator}/paginator.stories.ts | 2 +- src/elements/paginator/paginator/paginator.ts | 251 ++++++++++++ .../{ => paginator}/paginator.visual.spec.ts | 2 +- .../paginator/{ => paginator}/readme.md | 18 +- 14 files changed, 443 insertions(+), 406 deletions(-) create mode 100644 src/elements/paginator/common.ts create mode 100644 src/elements/paginator/common/paginator-common.ts delete mode 100644 src/elements/paginator/index.ts rename src/elements/paginator/{ => paginator}/__snapshots__/paginator.snapshot.spec.snap.js (100%) rename src/elements/paginator/{ => paginator}/paginator.scss (99%) rename src/elements/paginator/{ => paginator}/paginator.snapshot.spec.ts (94%) rename src/elements/paginator/{ => paginator}/paginator.spec.ts (96%) rename src/elements/paginator/{ => paginator}/paginator.ssr.spec.ts (89%) rename src/elements/paginator/{ => paginator}/paginator.stories.ts (98%) create mode 100644 src/elements/paginator/paginator/paginator.ts rename src/elements/paginator/{ => paginator}/paginator.visual.spec.ts (98%) rename src/elements/paginator/{ => paginator}/readme.md (79%) diff --git a/src/elements/paginator.ts b/src/elements/paginator.ts index 1373e2f42e..54f22e2d70 100644 --- a/src/elements/paginator.ts +++ b/src/elements/paginator.ts @@ -1 +1,2 @@ +export * from './paginator/common.js'; export * from './paginator/paginator.js'; diff --git a/src/elements/paginator/common.ts b/src/elements/paginator/common.ts new file mode 100644 index 0000000000..5a4e64e169 --- /dev/null +++ b/src/elements/paginator/common.ts @@ -0,0 +1 @@ +export * from './common/paginator-common.js'; diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts new file mode 100644 index 0000000000..283939b450 --- /dev/null +++ b/src/elements/paginator/common/paginator-common.ts @@ -0,0 +1,167 @@ +import { html, type LitElement, 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 } from '../../core/i18n.js'; +import { type AbstractConstructor, SbbNegativeMixin } from '../../core/mixins.js'; + +import '../../button/mini-button.js'; +import '../../button/mini-button-group.js'; +import '../../divider.js'; + +export type SbbPaginatorPageEventDetails = { + length: number; + pageSize: number; + pageIndex: number; + previousPageIndex: number; +}; + +export declare class SbbPaginatorCommonElementMixinType { + public negative: boolean; + public length: number; + public pageSize: number; + public pageIndex: number; + public pagerPosition: 'start' | 'end'; + public size: 'm' | 's'; + protected language: SbbLanguageController; + protected numberOfPages(): number; + protected pageIndexChanged(value: number): void; + protected emitPageEvent(previousPageIndex: number): void; + protected renderPrevNextButtons(): TemplateResult; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbPaginatorCommonElementMixin = >( + superClass: T, +): AbstractConstructor & T => { + @hostAttributes({ + role: 'group', + }) + abstract class SbbPaginatorCommonElement + extends SbbNegativeMixin(superClass) + implements Partial + { + public static readonly events: Record = { + 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: if `pageSizeOptions` is set, the sbb-select for the pageSize change + * will be positioned oppositely with the page numbers always in the center. + */ + @property({ attribute: 'pager-position', reflect: true }) public pagerPosition: + | 'start' + | 'end' = 'start'; + + /** Size variant, either m or s. */ + @property({ reflect: true }) public size: 'm' | 's' = 'm'; + + private _page: EventEmitter = new EventEmitter( + this, + SbbPaginatorCommonElement.events.page, + { composed: true, bubbles: true }, + ); + protected language = new SbbLanguageController(this); + + /** 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, + ); + } + + /** + * 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` + + this.pageIndexChanged(this._pageIndex - 1)} + > + + this.pageIndexChanged(this._pageIndex + 1)} + > + + `; + } + } + return SbbPaginatorCommonElement as unknown as AbstractConstructor & + T; +}; diff --git a/src/elements/paginator/index.ts b/src/elements/paginator/index.ts deleted file mode 100644 index c24409f273..0000000000 --- a/src/elements/paginator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './paginator.js'; diff --git a/src/elements/paginator/paginator.ts b/src/elements/paginator/paginator.ts index 49bc670581..1373e2f42e 100644 --- a/src/elements/paginator/paginator.ts +++ b/src/elements/paginator/paginator.ts @@ -1,383 +1 @@ -import { - type CSSResultGroup, - html, - LitElement, - nothing, - type PropertyValues, - type TemplateResult, -} from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import { repeat } from 'lit/directives/repeat.js'; - -import { sbbInputModalityDetector } from '../core/a11y.js'; -import { SbbLanguageController } from '../core/controllers.js'; -import { hostAttributes } from '../core/decorators.js'; -import { EventEmitter } from '../core/eventing.js'; -import { - i18nItemsPerPage, - i18nNextPage, - i18nPage, - i18nPreviousPage, - i18nSelectedPage, -} from '../core/i18n.js'; -import { SbbNegativeMixin } from '../core/mixins.js'; -import type { SbbSelectElement } from '../select.js'; - -import style from './paginator.scss?lit&inline'; - -import '../button/mini-button.js'; -import '../button/mini-button-group.js'; -import '../divider.js'; -import '../form-field.js'; -import '../select.js'; -import '../option.js'; -import '../screen-reader-only.js'; - -export type SbbPaginatorPageEventDetails = { - length: number; - pageSize: number; - pageIndex: number; - previousPageIndex: number; -}; - -const MAX_PAGE_NUMBERS_DISPLAYED = 3; - -let nextId = 0; - -/** - * It displays a paginator component. - * - * @event {CustomEvent} page - Emits when the pageIndex changes. - */ -@customElement('sbb-paginator') -@hostAttributes({ - role: 'group', -}) -export class SbbPaginatorElement extends SbbNegativeMixin(LitElement) { - public static override styles: CSSResultGroup = style; - public static readonly events: Record = { - 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; - - /** The available `pageSize` choices. */ - @property({ attribute: 'page-size-options', type: Array }) - public set pageSizeOptions(value: number[]) { - this._pageSizeOptions = value; - this._updateSelectAriaLabelledBy = true; - } - public get pageSizeOptions(): number[] | undefined { - return this._pageSizeOptions; - } - private _pageSizeOptions?: number[]; - - /** - * Position of the prev/next buttons: if `pageSizeOptions` is set, the sbb-select for the pageSize change - * will be positioned oppositely with the page numbers always in the center. - */ - @property({ attribute: 'pager-position', reflect: true }) public pagerPosition: 'start' | 'end' = - 'start'; - - /** Size variant, either m or s. */ - @property({ reflect: true }) public size: 'm' | 's' = 'm'; - - private _page: EventEmitter = new EventEmitter( - this, - SbbPaginatorElement.events.page, - { composed: true, bubbles: true }, - ); - - private _paginatorOptionsLabel = `sbb-paginator-options-label-${++nextId}`; - private _language = new SbbLanguageController(this); - private _markForFocus: number | null = null; - private _updateSelectAriaLabelledBy: boolean = false; - - protected override updated(changedProperties: PropertyValues): void { - super.updated(changedProperties); - - /** Tab navigation can force a rerender when ellipsis elements need to be displayed; the focus must stay on the correct element. */ - if (this._markForFocus && sbbInputModalityDetector.mostRecentModality === 'keyboard') { - const focusElement = this._getVisiblePages().find( - (e) => this.pageIndex === +e.getAttribute('data-index')!, - ); - if (focusElement) { - (focusElement as HTMLElement).focus(); - } - // Reset mark for focus - this._markForFocus = null; - } - - /** - * TODO: Accessibility fix required to correctly read the label; - * can be possibly removed after the merge of https://github.com/sbb-design-systems/lyne-components/issues/3062 - */ - const select = this.shadowRoot!.querySelector('sbb-select'); - if (select && this._updateSelectAriaLabelledBy) { - select.setAttribute('aria-labelledby', this._paginatorOptionsLabel); - this._updateSelectAriaLabelledBy = false; - } - - // 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(); - } - - /** - * 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). - */ - private _numberOfPages(): number { - return this.pageSize ? Math.ceil(this.length / this.pageSize) : 0; - } - - /** - * If the `pageSize` changes due to user interaction with the `pageSizeOptions` select, - * emit the `page` event and then update the `pageSize` value. - */ - private _pageSizeChanged(value: number): void { - const previousPageSize = this.pageSize; - const previousPageIndex = this.pageIndex; - this.pageSize = value; - - if (previousPageSize !== this.pageSize) { - this._emitPageEvent(previousPageIndex); - } - } - - /** - * If the `pageIndex` changes due to user interaction, - * emit the `page` event and then update the `pageIndex` value. - */ - private _pageIndexChanged(value: number): void { - const previousPageIndex = this.pageIndex; - this.pageIndex = value; - - if (previousPageIndex !== this.pageIndex) { - this._emitPageEvent(previousPageIndex); - } - } - - private _emitPageEvent(previousPageIndex: number): void { - this._page.emit({ - previousPageIndex, - pageIndex: this.pageIndex, - length: this.length, - pageSize: this.pageSize, - }); - } - - /** Returns the displayed page elements. */ - private _getVisiblePages(): Element[] { - return Array.from(this.shadowRoot!.querySelectorAll('.sbb-paginator__page--number-item')); - } - - /** 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, - ); - } - - /** - * Calculate the pages set based on the following rules: - * - the first page must always be visible; - * - the last page must always be visible; - * - if there are more than `MAX_PAGE_NUMBERS_DISPLAYED` other pages, ellipsis button must be used. - */ - private _getVisiblePagesIndex(): (number | 'ellipsis')[] { - const totalPages: number = this._numberOfPages(); - const currentPageIndex: number = this.pageIndex; - - if (totalPages <= MAX_PAGE_NUMBERS_DISPLAYED + 2) { - return this._range(totalPages); - } else if (currentPageIndex < MAX_PAGE_NUMBERS_DISPLAYED) { - return [...this._range(MAX_PAGE_NUMBERS_DISPLAYED + 1), 'ellipsis', totalPages - 1]; - } else if (currentPageIndex >= totalPages - MAX_PAGE_NUMBERS_DISPLAYED) { - return [ - 0, - 'ellipsis', - ...this._range(MAX_PAGE_NUMBERS_DISPLAYED + 1, totalPages - 1 - MAX_PAGE_NUMBERS_DISPLAYED), - ]; - } else { - return [ - 0, - 'ellipsis', - currentPageIndex - 1, - currentPageIndex, - currentPageIndex + 1, - 'ellipsis', - totalPages - 1, - ]; - } - } - - /** Creates an array of consecutive numbers given the length and the starting value. */ - private _range(length: number, offset: number = 0): number[] { - return Array.from({ length }, (_, k) => k + offset); - } - - private _handleKeyUp(event: KeyboardEvent): void { - if (event.key !== ' ' && event.key !== 'Enter') { - return; - } - - const current = this._getVisiblePages().find((e: Element) => e === event.target); - if (current) { - this._markForFocus = this.pageIndex; - } - } - - private _currentPageLabel(): string { - return i18nSelectedPage(this.pageIndex + 1)[this._language.current]; - } - - private _renderPrevNextButtons(): TemplateResult { - return html` - - this._pageIndexChanged(this._pageIndex - 1)} - > - - this._pageIndexChanged(this._pageIndex + 1)} - > - - `; - } - - private _renderItemPerPageTemplate(): TemplateResult | typeof nothing { - return this.pageSizeOptions && this.pageSizeOptions.length > 0 - ? html` -
- - - e === this.pageSize) ?? - this.pageSizeOptions![0]} - @change=${(e: CustomEvent) => - this._pageSizeChanged(+((e.target as SbbSelectElement).value as string))} - > - ${repeat( - this.pageSizeOptions!, - (element) => html`${element}`, - )} - - -
- ` - : nothing; - } - - private _renderPageNumbers(): TemplateResult { - return html` -
    - ${repeat( - this._getVisiblePagesIndex(), - (item: number | 'ellipsis'): TemplateResult => - item === 'ellipsis' - ? html` -
  • - -
  • - ` - : html` -
  • - -
  • - `, - )} -
- `; - } - - protected override render(): TemplateResult { - return html` -
- ${ - this.pagerPosition === 'start' - ? html` - ${this._renderPrevNextButtons()} ${this._renderPageNumbers()} - - ${this._renderItemPerPageTemplate()}` - : html`${this._renderItemPerPageTemplate()} - - ${this._renderPageNumbers()} ${this._renderPrevNextButtons()} - ` - } -
- - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'sbb-paginator': SbbPaginatorElement; - } - interface HTMLElementEventMap { - page: CustomEvent; - } -} +export * from './paginator/paginator.js'; diff --git a/src/elements/paginator/__snapshots__/paginator.snapshot.spec.snap.js b/src/elements/paginator/paginator/__snapshots__/paginator.snapshot.spec.snap.js similarity index 100% rename from src/elements/paginator/__snapshots__/paginator.snapshot.spec.snap.js rename to src/elements/paginator/paginator/__snapshots__/paginator.snapshot.spec.snap.js diff --git a/src/elements/paginator/paginator.scss b/src/elements/paginator/paginator/paginator.scss similarity index 99% rename from src/elements/paginator/paginator.scss rename to src/elements/paginator/paginator/paginator.scss index fdeb339f87..fe3d503a74 100644 --- a/src/elements/paginator/paginator.scss +++ b/src/elements/paginator/paginator/paginator.scss @@ -1,4 +1,4 @@ -@use '../core/styles' as sbb; +@use '../../core/styles/index' 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; diff --git a/src/elements/paginator/paginator.snapshot.spec.ts b/src/elements/paginator/paginator/paginator.snapshot.spec.ts similarity index 94% rename from src/elements/paginator/paginator.snapshot.spec.ts rename to src/elements/paginator/paginator/paginator.snapshot.spec.ts index 33e854273f..283d8ec36b 100644 --- a/src/elements/paginator/paginator.snapshot.spec.ts +++ b/src/elements/paginator/paginator/paginator.snapshot.spec.ts @@ -1,9 +1,9 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { isSafari } from '../core/dom.js'; -import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; -import { describeIf } from '../core/testing.js'; +import { isSafari } from '../../core/dom.js'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; +import { describeIf } from '../../core/testing.js'; import type { SbbPaginatorElement } from './paginator.js'; diff --git a/src/elements/paginator/paginator.spec.ts b/src/elements/paginator/paginator/paginator.spec.ts similarity index 96% rename from src/elements/paginator/paginator.spec.ts rename to src/elements/paginator/paginator/paginator.spec.ts index f0eb05f92f..013a9a2701 100644 --- a/src/elements/paginator/paginator.spec.ts +++ b/src/elements/paginator/paginator/paginator.spec.ts @@ -3,12 +3,12 @@ import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; import { spy } from 'sinon'; -import type { SbbMiniButtonElement } from '../button/mini-button.js'; -import { tabKey } from '../core/testing/private/keys.js'; -import { fixture } from '../core/testing/private.js'; -import { EventSpy, waitForCondition, waitForLitRender } from '../core/testing.js'; -import type { SbbOptionElement } from '../option.js'; -import { SbbSelectElement } from '../select.js'; +import type { SbbMiniButtonElement } from '../../button/mini-button.js'; +import { tabKey } from '../../core/testing/private/keys.js'; +import { fixture } from '../../core/testing/private.js'; +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js'; +import type { SbbOptionElement } from '../../option.js'; +import { SbbSelectElement } from '../../select.js'; import { SbbPaginatorElement } from './paginator.js'; diff --git a/src/elements/paginator/paginator.ssr.spec.ts b/src/elements/paginator/paginator/paginator.ssr.spec.ts similarity index 89% rename from src/elements/paginator/paginator.ssr.spec.ts rename to src/elements/paginator/paginator/paginator.ssr.spec.ts index a98be8acf9..fbe2c7991c 100644 --- a/src/elements/paginator/paginator.ssr.spec.ts +++ b/src/elements/paginator/paginator/paginator.ssr.spec.ts @@ -1,7 +1,7 @@ import { assert } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { ssrHydratedFixture } from '../core/testing/private.js'; +import { ssrHydratedFixture } from '../../core/testing/private.js'; import { SbbPaginatorElement } from './paginator.js'; diff --git a/src/elements/paginator/paginator.stories.ts b/src/elements/paginator/paginator/paginator.stories.ts similarity index 98% rename from src/elements/paginator/paginator.stories.ts rename to src/elements/paginator/paginator/paginator.stories.ts index ceda540de1..3f4c5f03af 100644 --- a/src/elements/paginator/paginator.stories.ts +++ b/src/elements/paginator/paginator/paginator.stories.ts @@ -11,7 +11,7 @@ import type { import type { TemplateResult } from 'lit'; import { html } from 'lit'; -import { sbbSpread } from '../../storybook/helpers/spread.js'; +import { sbbSpread } from '../../../storybook/helpers/spread.js'; import { SbbPaginatorElement } from './paginator.js'; import readme from './readme.md?raw'; diff --git a/src/elements/paginator/paginator/paginator.ts b/src/elements/paginator/paginator/paginator.ts new file mode 100644 index 0000000000..6ad3ca92d7 --- /dev/null +++ b/src/elements/paginator/paginator/paginator.ts @@ -0,0 +1,251 @@ +import { + type CSSResultGroup, + html, + LitElement, + nothing, + type PropertyValues, + type TemplateResult, +} from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; + +import { sbbInputModalityDetector } from '../../core/a11y.js'; +import { hostAttributes } from '../../core/decorators.js'; +import { i18nItemsPerPage, i18nPage, i18nSelectedPage } from '../../core/i18n.js'; +import type { SbbSelectElement } from '../../select.js'; +import { SbbPaginatorCommonElementMixin, type SbbPaginatorPageEventDetails } from '../common.js'; + +import style from './paginator.scss?lit&inline'; + +import '../../form-field.js'; +import '../../select.js'; +import '../../option.js'; +import '../../screen-reader-only.js'; + +const MAX_PAGE_NUMBERS_DISPLAYED = 3; + +let nextId = 0; + +/** + * It displays a paginator component. + * + * @event {CustomEvent} page - Emits when the pageIndex changes. + */ +@customElement('sbb-paginator') +@hostAttributes({ + role: 'group', +}) +export class SbbPaginatorElement extends SbbPaginatorCommonElementMixin(LitElement) { + public static override styles: CSSResultGroup = style; + public static readonly events: Record = { + page: 'page', + } as const; + + /** The available `pageSize` choices. */ + @property({ attribute: 'page-size-options', type: Array }) + public set pageSizeOptions(value: number[]) { + this._pageSizeOptions = value; + this._updateSelectAriaLabelledBy = true; + } + public get pageSizeOptions(): number[] | undefined { + return this._pageSizeOptions; + } + private _pageSizeOptions?: number[]; + + private _paginatorOptionsLabel = `sbb-paginator-options-label-${++nextId}`; + private _markForFocus: number | null = null; + private _updateSelectAriaLabelledBy: boolean = false; + + protected override updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + + /** Tab navigation can force a rerender when ellipsis elements need to be displayed; the focus must stay on the correct element. */ + if (this._markForFocus && sbbInputModalityDetector.mostRecentModality === 'keyboard') { + const focusElement = this._getVisiblePages().find( + (e) => this.pageIndex === +e.getAttribute('data-index')!, + ); + if (focusElement) { + (focusElement as HTMLElement).focus(); + } + // Reset mark for focus + this._markForFocus = null; + } + + /** + * TODO: Accessibility fix required to correctly read the label; + * can be possibly removed after the merge of https://github.com/sbb-design-systems/lyne-components/issues/3062 + */ + const select = this.shadowRoot!.querySelector('sbb-select'); + if (select && this._updateSelectAriaLabelledBy) { + select.setAttribute('aria-labelledby', this._paginatorOptionsLabel); + this._updateSelectAriaLabelledBy = false; + } + + // 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(); + } + + /** + * If the `pageSize` changes due to user interaction with the `pageSizeOptions` select, + * emit the `page` event and then update the `pageSize` value. + */ + private _pageSizeChanged(value: number): void { + const previousPageSize = this.pageSize; + const previousPageIndex = this.pageIndex; + this.pageSize = value; + + if (previousPageSize !== this.pageSize) { + this.emitPageEvent(previousPageIndex); + } + } + + /** Returns the displayed page elements. */ + private _getVisiblePages(): Element[] { + return Array.from(this.shadowRoot!.querySelectorAll('.sbb-paginator__page--number-item')); + } + + /** + * Calculate the pages set based on the following rules: + * - the first page must always be visible; + * - the last page must always be visible; + * - if there are more than `MAX_PAGE_NUMBERS_DISPLAYED` other pages, ellipsis button must be used. + */ + private _getVisiblePagesIndex(): (number | 'ellipsis')[] { + const totalPages: number = this.numberOfPages(); + const currentPageIndex: number = this.pageIndex; + + if (totalPages <= MAX_PAGE_NUMBERS_DISPLAYED + 2) { + return this._range(totalPages); + } else if (currentPageIndex < MAX_PAGE_NUMBERS_DISPLAYED) { + return [...this._range(MAX_PAGE_NUMBERS_DISPLAYED + 1), 'ellipsis', totalPages - 1]; + } else if (currentPageIndex >= totalPages - MAX_PAGE_NUMBERS_DISPLAYED) { + return [ + 0, + 'ellipsis', + ...this._range(MAX_PAGE_NUMBERS_DISPLAYED + 1, totalPages - 1 - MAX_PAGE_NUMBERS_DISPLAYED), + ]; + } else { + return [ + 0, + 'ellipsis', + currentPageIndex - 1, + currentPageIndex, + currentPageIndex + 1, + 'ellipsis', + totalPages - 1, + ]; + } + } + + /** Creates an array of consecutive numbers given the length and the starting value. */ + private _range(length: number, offset: number = 0): number[] { + return Array.from({ length }, (_, k) => k + offset); + } + + private _handleKeyUp(event: KeyboardEvent): void { + if (event.key !== ' ' && event.key !== 'Enter') { + return; + } + + const current = this._getVisiblePages().find((e: Element) => e === event.target); + if (current) { + this._markForFocus = this.pageIndex; + } + } + + private _currentPageLabel(): string { + return i18nSelectedPage(this.pageIndex + 1)[this.language.current]; + } + + private _renderItemPerPageTemplate(): TemplateResult | typeof nothing { + return this.pageSizeOptions && this.pageSizeOptions.length > 0 + ? html` +
+ + + e === this.pageSize) ?? + this.pageSizeOptions![0]} + @change=${(e: CustomEvent) => + this._pageSizeChanged(+((e.target as SbbSelectElement).value as string))} + > + ${repeat( + this.pageSizeOptions!, + (element) => html`${element}`, + )} + + +
+ ` + : nothing; + } + + private _renderPageNumbers(): TemplateResult { + return html` +
    + ${repeat( + this._getVisiblePagesIndex(), + (item: number | 'ellipsis'): TemplateResult => + item === 'ellipsis' + ? html` +
  • + +
  • + ` + : html` +
  • + +
  • + `, + )} +
+ `; + } + + protected override render(): TemplateResult { + return html` +
+ ${ + this.pagerPosition === 'start' + ? html` + ${this.renderPrevNextButtons()} ${this._renderPageNumbers()} + + ${this._renderItemPerPageTemplate()}` + : html`${this._renderItemPerPageTemplate()} + + ${this._renderPageNumbers()} ${this.renderPrevNextButtons()} + ` + } +
+ + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-paginator': SbbPaginatorElement; + } + interface HTMLElementEventMap { + page: CustomEvent; + } +} diff --git a/src/elements/paginator/paginator.visual.spec.ts b/src/elements/paginator/paginator/paginator.visual.spec.ts similarity index 98% rename from src/elements/paginator/paginator.visual.spec.ts rename to src/elements/paginator/paginator/paginator.visual.spec.ts index 3ce58b4032..a8098d87b6 100644 --- a/src/elements/paginator/paginator.visual.spec.ts +++ b/src/elements/paginator/paginator/paginator.visual.spec.ts @@ -6,7 +6,7 @@ import { visualDiffDefault, visualDiffFocus, visualDiffHover, -} from '../core/testing/private.js'; +} from '../../core/testing/private.js'; import './paginator.js'; diff --git a/src/elements/paginator/readme.md b/src/elements/paginator/paginator/readme.md similarity index 79% rename from src/elements/paginator/readme.md rename to src/elements/paginator/paginator/readme.md index 50a2941d2f..6d22e2760c 100644 --- a/src/elements/paginator/readme.md +++ b/src/elements/paginator/paginator/readme.md @@ -57,15 +57,15 @@ that describes the content controlled by the paginator. ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ----------------- | ------------------- | ------- | ----------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `length` | `length` | public | `number` | `0` | Total number of items. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `pageIndex` | `page-index` | public | `number` | `0` | Current page index. | -| `pagerPosition` | `pager-position` | public | `'start' \| 'end'` | `'start'` | Position of the prev/next buttons: if `pageSizeOptions` is set, the sbb-select for the pageSize change will be positioned oppositely with the page numbers always in the center. | -| `pageSize` | `page-size` | public | `number` | `10` | Number of items per page. | -| `pageSizeOptions` | `page-size-options` | public | `number[] \| undefined` | | The available `pageSize` choices. | -| `size` | `size` | public | `'m' \| 's'` | `'m'` | Size variant, either m or s. | +| Name | Attribute | Privacy | Type | Default | Description | +| ----------------- | ------------------- | ------- | --------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `length` | `length` | public | `number` | `0` | Total number of items. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `pageIndex` | `page-index` | public | `number` | `0` | Current page index. | +| `pagerPosition` | `pager-position` | public | `\| 'start' \| 'end'` | `'start'` | Position of the prev/next buttons: if `pageSizeOptions` is set, the sbb-select for the pageSize change will be positioned oppositely with the page numbers always in the center. | +| `pageSize` | `page-size` | public | `number` | `10` | Number of items per page. | +| `pageSizeOptions` | `page-size-options` | public | `number[] \| undefined` | | The available `pageSize` choices. | +| `size` | `size` | public | `'m' \| 's'` | `'m'` | Size variant, either m or s. | ## Events From 81c8dd317dbccfb591e91b5d3394eeff3381cdd5 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Wed, 2 Oct 2024 18:15:50 +0200 Subject: [PATCH 02/22] feat: add sbb-compact-paginator --- src/elements/paginator.ts | 1 + .../paginator/common/paginator-common.ts | 22 ++- src/elements/paginator/compact-paginator.ts | 1 + .../compact-paginator/compact-paginator.scss | 33 ++++ .../compact-paginator.snapshot.spec.ts | 29 ++++ .../compact-paginator.spec.ts | 145 ++++++++++++++++++ .../compact-paginator.ssr.spec.ts | 23 +++ .../compact-paginator.stories.ts | 127 +++++++++++++++ .../compact-paginator/compact-paginator.ts | 55 +++++++ .../compact-paginator.visual.spec.ts | 62 ++++++++ .../paginator/compact-paginator/readme.md | 61 ++++++++ .../paginator/paginator/paginator.stories.ts | 2 +- src/elements/paginator/paginator/paginator.ts | 15 +- 13 files changed, 560 insertions(+), 16 deletions(-) create mode 100644 src/elements/paginator/compact-paginator.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.scss create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.spec.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.ssr.spec.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.stories.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.ts create mode 100644 src/elements/paginator/compact-paginator/compact-paginator.visual.spec.ts create mode 100644 src/elements/paginator/compact-paginator/readme.md diff --git a/src/elements/paginator.ts b/src/elements/paginator.ts index 54f22e2d70..1bf8387e45 100644 --- a/src/elements/paginator.ts +++ b/src/elements/paginator.ts @@ -1,2 +1,3 @@ export * from './paginator/common.js'; +export * from './paginator/compact-paginator.js'; export * from './paginator/paginator.js'; diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts index 283939b450..b6bbe7cf39 100644 --- a/src/elements/paginator/common/paginator-common.ts +++ b/src/elements/paginator/common/paginator-common.ts @@ -1,10 +1,10 @@ -import { html, type LitElement, type TemplateResult } from 'lit'; +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 } from '../../core/i18n.js'; +import { i18nNextPage, i18nPreviousPage, i18nSelectedPage } from '../../core/i18n.js'; import { type AbstractConstructor, SbbNegativeMixin } from '../../core/mixins.js'; import '../../button/mini-button.js'; @@ -102,6 +102,18 @@ export const SbbPaginatorCommonElementMixin = ): 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(); + } + + private _currentPageLabel(): string { + return i18nSelectedPage(this.pageIndex + 1)[this.language.current]; + } + /** Evaluate `pageIndex` by excluding edge cases. */ private _coercePageIndexInRange(pageIndex: number): number { return Math.max( @@ -165,3 +177,9 @@ export const SbbPaginatorCommonElementMixin = & T; }; + +declare global { + interface HTMLElementEventMap { + page: CustomEvent; + } +} diff --git a/src/elements/paginator/compact-paginator.ts b/src/elements/paginator/compact-paginator.ts new file mode 100644 index 0000000000..b81712b867 --- /dev/null +++ b/src/elements/paginator/compact-paginator.ts @@ -0,0 +1 @@ +export * from './compact-paginator/compact-paginator.js'; diff --git a/src/elements/paginator/compact-paginator/compact-paginator.scss b/src/elements/paginator/compact-paginator/compact-paginator.scss new file mode 100644 index 0000000000..730031a1fa --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.scss @@ -0,0 +1,33 @@ +@use '../../core/styles/index' 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); +} + +:host([size='s']) { + --sbb-compact-paginator-height: var(--sbb-size-element-xs); +} + +.sbb-compact-paginator { + display: flex; + gap: var(--sbb-spacing-fixed-5x); +} + +.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-color-granite); +} + +sbb-divider { + height: var(--sbb-font-size-text-m); +} diff --git a/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts b/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts new file mode 100644 index 0000000000..4e0dc7cf32 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts @@ -0,0 +1,29 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; + +import type { SbbCompactPaginatorElement } from './compact-paginator.js'; +import './compact-paginator.js'; + +describe(`sbb-compact-paginator`, () => { + describe('renders', () => { + let element: SbbCompactPaginatorElement; + + beforeEach(async () => { + element = await fixture( + html``, + ); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); + }); +}); diff --git a/src/elements/paginator/compact-paginator/compact-paginator.spec.ts b/src/elements/paginator/compact-paginator/compact-paginator.spec.ts new file mode 100644 index 0000000000..6962818c64 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.spec.ts @@ -0,0 +1,145 @@ +import { assert, expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import { spy } from 'sinon'; + +import type { SbbMiniButtonElement } from '../../button/mini-button.js'; +import { fixture } from '../../core/testing/private.js'; +import { EventSpy, waitForLitRender } from '../../core/testing.js'; + +import { SbbCompactPaginatorElement } from './compact-paginator.js'; + +describe('sbb-compact-paginator', () => { + let element: SbbCompactPaginatorElement; + + beforeEach(async () => { + element = await fixture( + html``, + ); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbCompactPaginatorElement); + }); + + it('change pages via prev/next buttons and emits `page` event', async () => { + const pageEventSpy = spy(); + const goToPrev: SbbMiniButtonElement = element.shadowRoot!.querySelector( + '#sbb-paginator-prev-page', + )!; + const goToNext: SbbMiniButtonElement = element.shadowRoot!.querySelector( + '#sbb-paginator-next-page', + )!; + + element.addEventListener('page', (event) => { + expect(event.detail.pageIndex).to.be.equal(element.pageIndex); + pageEventSpy(); + }); + + expect(goToPrev).to.have.attribute('disabled'); + goToPrev.click(); + await waitForLitRender(element); + expect(pageEventSpy).not.to.have.been.called; + + expect(goToNext).not.to.have.attribute('disabled'); + goToNext.click(); + await waitForLitRender(element); + expect(pageEventSpy).to.have.been.calledOnce; + expect(element.pageIndex).to.be.equal(1); + expect(goToPrev).not.to.have.attribute('disabled'); + expect(goToNext).not.to.have.attribute('disabled'); + + goToPrev.click(); + await waitForLitRender(element); + expect(pageEventSpy).to.have.been.calledTwice; + expect(element.pageIndex).to.be.equal(0); + }); + + /* + * NOTE: when checking `selectedElement.textContent`, the sbb-divider is not considered, + * so the current page and the last page are joined in a single value. + */ + it('changes to the correct selected page when pageSize changes', async () => { + // go to page 5 / pageIndex=4, which includes items 21-25 + element.setAttribute('page-index', '4'); + await waitForLitRender(element); + let selectedElement = element.shadowRoot!.querySelector('.sbb-paginator__pages')!; + // lenght = 50 / pageSize = 5 / numberOfPages = 10 + expect(selectedElement.textContent).to.be.equal('510'); + + // switching to pageSize=10, item 21 should be on page 3 / pageIndex=2 + element.setAttribute('page-size', '10'); + await waitForLitRender(element); + selectedElement = element.shadowRoot!.querySelector('.sbb-paginator__pages')!; + // lenght = 50 / pageSize = 10 / numberOfPages = 5 + expect(selectedElement.textContent).to.be.equal('35'); + + // go to page 4 / pageIndex=3, which now includes items 31-40 + element.setAttribute('page-index', '3'); + await waitForLitRender(element); + selectedElement = element.shadowRoot!.querySelector('.sbb-paginator__pages')!; + // lenght = 50 / pageSize = 10 / numberOfPages = 5 + expect(selectedElement.textContent).to.be.equal('45'); + + // switching to pageSize=2, item 31 should be on page 16 / pageIndex=15 + element.setAttribute('page-size', '2'); + await waitForLitRender(element); + selectedElement = element.shadowRoot!.querySelector('.sbb-paginator__pages')!; + // lenght = 50 / pageSize = 2 / numberOfPages = 25 + expect(selectedElement.textContent).to.be.equal('1625'); + }); + + it('the `page` event is not emitted when pageSize and pageIndex change programmatically', async () => { + const pageEventSpy = new EventSpy(SbbCompactPaginatorElement.events.page); + element.setAttribute('page-index', '4'); + await waitForLitRender(element); + expect(element.pageIndex).to.be.equal(4); + expect(pageEventSpy.count).to.be.equal(0); + + element.setAttribute('page-size', '10'); + await waitForLitRender(element); + expect(element.pageSize).to.be.equal(10); + expect(pageEventSpy.count).to.be.equal(0); + }); + + it('handles length change', () => { + element.pageIndex = 9; + element.length = 100; + expect(element.pageIndex).to.be.equal(9); + + element.length = 10; + expect(element.pageIndex).to.be.equal(1); + + element.length = -1; + expect(element.length).to.be.equal(0); + expect(element.pageIndex).to.be.equal(0); + }); + + it('handles pageSize change', () => { + element.pageIndex = 9; + expect(element.pageIndex).to.be.equal(9); + + element.pageSize = 1; + expect(element.pageIndex).to.be.equal(45); + + element.pageSize = 10; + expect(element.pageIndex).to.be.equal(4); + + element.pageSize = -1; + expect(element.pageSize).to.be.equal(0); + expect(element.pageIndex).to.be.equal(0); + }); + + it('handles pageIndex change', () => { + element.pageIndex = 10; + expect(element.pageIndex).to.be.equal(9); + + element.pageIndex = -1; + expect(element.pageIndex).to.be.equal(0); + + element.pageIndex = 0; + expect(element.pageIndex).to.be.equal(0); + + element.pageIndex = 5; + expect(element.pageIndex).to.be.equal(5); + }); +}); diff --git a/src/elements/paginator/compact-paginator/compact-paginator.ssr.spec.ts b/src/elements/paginator/compact-paginator/compact-paginator.ssr.spec.ts new file mode 100644 index 0000000000..2fdfa81067 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.ssr.spec.ts @@ -0,0 +1,23 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { ssrHydratedFixture } from '../../core/testing/private.js'; + +import { SbbCompactPaginatorElement } from './compact-paginator.js'; + +describe(`sbb-compact-paginator ssr`, () => { + let root: SbbCompactPaginatorElement; + + beforeEach(async () => { + root = await ssrHydratedFixture( + html``, + { + modules: ['./compact-paginator.js'], + }, + ); + }); + + it('renders', () => { + assert.instanceOf(root, SbbCompactPaginatorElement); + }); +}); diff --git a/src/elements/paginator/compact-paginator/compact-paginator.stories.ts b/src/elements/paginator/compact-paginator/compact-paginator.stories.ts new file mode 100644 index 0000000000..12d426d6e4 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.stories.ts @@ -0,0 +1,127 @@ +import { withActions } from '@storybook/addon-actions/decorator'; +import type { InputType } from '@storybook/types'; +import type { + Args, + ArgTypes, + Decorator, + Meta, + StoryContext, + StoryObj, +} from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html } from 'lit'; + +import { sbbSpread } from '../../../storybook/helpers/spread.js'; + +import { SbbCompactPaginatorElement } from './compact-paginator.js'; +import readme from './readme.md?raw'; + +const length: InputType = { + control: { + type: 'number', + }, +}; + +const pageSize: InputType = { + control: { + type: 'number', + }, +}; + +const pageIndex: InputType = { + control: { + type: 'number', + }, +}; + +const pagerPosition: InputType = { + control: { + type: 'inline-radio', + }, + options: ['start', 'end'], +}; + +const size: InputType = { + control: { + type: 'inline-radio', + }, + options: ['m', 's'], +}; + +const negative: InputType = { + control: { + type: 'boolean', + }, +}; + +const defaultArgTypes: ArgTypes = { + length, + 'page-size': pageSize, + 'page-index': pageIndex, + 'pager-position': pagerPosition, + size, + negative, +}; + +const defaultArgs: Args = { + length: 100, + 'page-size': 10, + 'page-index': 0, + 'pager-position': pagerPosition.options![0], + size: size.options![0], + negative: false, +}; + +const Template = ({ ...args }: Args): TemplateResult => { + return html` `; +}; + +export const Default: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +export const Negative: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs, negative: true }, +}; + +export const SizeS: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs, size: size.options![1] }, +}; + +export const NegativeSizeS: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs, negative: true, size: size.options![1] }, +}; + +export const PagerPositionEnd: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs, 'pager-position': 'end' }, +}; + +const meta: Meta = { + decorators: [withActions as Decorator], + parameters: { + actions: { + handles: [SbbCompactPaginatorElement.events.page], + }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-paginator/sbb-compact-paginator', +}; + +export default meta; diff --git a/src/elements/paginator/compact-paginator/compact-paginator.ts b/src/elements/paginator/compact-paginator/compact-paginator.ts new file mode 100644 index 0000000000..2177a316a3 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.ts @@ -0,0 +1,55 @@ +import type { CSSResultGroup, TemplateResult } from 'lit'; +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import { hostAttributes } from '../../core/decorators.js'; +import { SbbPaginatorCommonElementMixin } from '../common.js'; +import '../../divider.js'; + +import style from './compact-paginator.scss?lit&inline'; + +/** + * It displays a paginator component in compact mode. + * + * @event {CustomEvent} page - Emits when the pageIndex changes. + */ +@customElement('sbb-compact-paginator') +@hostAttributes({ + role: 'group', +}) +export class SbbCompactPaginatorElement extends SbbPaginatorCommonElementMixin(LitElement) { + public static override styles: CSSResultGroup = style; + public static readonly events: Record = { + page: 'page', + } as const; + + private _renderPageNumbers(): TemplateResult { + return html` + ${this.pageIndex + 1}${this.numberOfPages()} + `; + } + + protected override render(): TemplateResult { + return html` +
+ ${this.pagerPosition === 'start' + ? html`${this.renderPrevNextButtons()} ${this._renderPageNumbers()}` + : html`${this._renderPageNumbers()} ${this.renderPrevNextButtons()}`} +
+ + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-compact-paginator': SbbCompactPaginatorElement; + } +} diff --git a/src/elements/paginator/compact-paginator/compact-paginator.visual.spec.ts b/src/elements/paginator/compact-paginator/compact-paginator.visual.spec.ts new file mode 100644 index 0000000000..a63ad234f7 --- /dev/null +++ b/src/elements/paginator/compact-paginator/compact-paginator.visual.spec.ts @@ -0,0 +1,62 @@ +import { html } from 'lit'; + +import { describeViewports, visualDiffDefault } from '../../core/testing/private.js'; + +import './compact-paginator.js'; + +describe('sbb-compact-paginator', () => { + describeViewports({ viewports: ['zero', 'medium'] }, () => { + for (const negative of [false, true]) { + describe(`negative=${negative}`, () => { + const wrapperStyle = { + backgroundColor: negative ? 'var(--sbb-color-black)' : undefined, + }; + + for (const pagerPosition of ['start', 'end']) { + describe(`pagerPosition=${pagerPosition}`, () => { + for (const forcedColors of [false, true]) { + describe(`forcedColors=${forcedColors}`, () => { + it( + visualDiffDefault.name, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` `, + { ...wrapperStyle, forcedColors }, + ); + }), + ); + }); + } + }); + } + + for (const size of ['s', 'm']) { + describe(`size=${size}`, () => { + for (const pageIndex of [0, 5, 9]) { + it( + `pageIndex=${pageIndex}`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` `, + wrapperStyle, + ); + }), + ); + } + }); + } + }); + } + }); +}); diff --git a/src/elements/paginator/compact-paginator/readme.md b/src/elements/paginator/compact-paginator/readme.md new file mode 100644 index 0000000000..03c7054775 --- /dev/null +++ b/src/elements/paginator/compact-paginator/readme.md @@ -0,0 +1,61 @@ +> Explain the use and the purpose of the component; add minor details if needed and provide a basic example.
+> If you reference other components, link their documentation at least once (the path must start from _/docs/..._ ).
+> For the examples, use triple backticks with file extension (` ```html ``` `).
+> The following list of paragraphs is only suggested; remove, create and adapt as needed. + +The `sbb-compact-paginator` is a component . . . + +```html + +``` + +## Slots + +> Describe slot naming and usage and provide an example of slotted content. + +## States + +> Describe the component states (`disabled`, `readonly`, etc.) and provide examples. + +## Style + +> Describe the properties which change the component visualization (`size`, `negative`, etc.) and provide examples. + +## Interactions + +> Describe how it's possible to interact with the component (open and close a `sbb-dialog`, dismiss a `sbb-alert`, etc.) and provide examples. + +## Events + +> Describe events triggered by the component and possibly how to get information from the payload. + +## Keyboard interaction + +> If the component has logic for keyboard navigation (as the `sbb-calendar` or the `sbb-select`) describe it. + +| Keyboard | Action | +| -------------- | ------------- | +| Key | What it does. | + +## Accessibility + +> Describe how accessibility is implemented and if there are issues or suggested best-practice for the consumers. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| --------------- | ---------------- | ------- | --------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `length` | `length` | public | `number` | `0` | Total number of items. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `pageIndex` | `page-index` | public | `number` | `0` | Current page index. | +| `pagerPosition` | `pager-position` | public | `\| 'start' \| 'end'` | `'start'` | Position of the prev/next buttons: if `pageSizeOptions` is set, the sbb-select for the pageSize change will be positioned oppositely with the page numbers always in the center. | +| `pageSize` | `page-size` | public | `number` | `10` | Number of items per page. | +| `size` | `size` | public | `'m' \| 's'` | `'m'` | Size variant, either m or s. | + +## Events + +| Name | Type | Description | Inherited From | +| ------ | ------------------------------------------- | --------------------------------- | -------------- | +| `page` | `CustomEvent` | Emits when the pageIndex changes. | | diff --git a/src/elements/paginator/paginator/paginator.stories.ts b/src/elements/paginator/paginator/paginator.stories.ts index 3f4c5f03af..a2587e5fb4 100644 --- a/src/elements/paginator/paginator/paginator.stories.ts +++ b/src/elements/paginator/paginator/paginator.stories.ts @@ -178,7 +178,7 @@ const meta: Meta = { extractComponentDescription: () => readme, }, }, - title: 'elements/sbb-paginator', + title: 'elements/sbb-paginator/sbb-paginator', }; export default meta; diff --git a/src/elements/paginator/paginator/paginator.ts b/src/elements/paginator/paginator/paginator.ts index 6ad3ca92d7..1a963ec662 100644 --- a/src/elements/paginator/paginator/paginator.ts +++ b/src/elements/paginator/paginator/paginator.ts @@ -11,9 +11,9 @@ import { repeat } from 'lit/directives/repeat.js'; import { sbbInputModalityDetector } from '../../core/a11y.js'; import { hostAttributes } from '../../core/decorators.js'; -import { i18nItemsPerPage, i18nPage, i18nSelectedPage } from '../../core/i18n.js'; +import { i18nItemsPerPage, i18nPage } from '../../core/i18n.js'; import type { SbbSelectElement } from '../../select.js'; -import { SbbPaginatorCommonElementMixin, type SbbPaginatorPageEventDetails } from '../common.js'; +import { SbbPaginatorCommonElementMixin } from '../common.js'; import style from './paginator.scss?lit&inline'; @@ -80,10 +80,6 @@ export class SbbPaginatorElement extends SbbPaginatorCommonElementMixin(LitEleme select.setAttribute('aria-labelledby', this._paginatorOptionsLabel); this._updateSelectAriaLabelledBy = false; } - - // 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(); } /** @@ -154,10 +150,6 @@ export class SbbPaginatorElement extends SbbPaginatorCommonElementMixin(LitEleme } } - private _currentPageLabel(): string { - return i18nSelectedPage(this.pageIndex + 1)[this.language.current]; - } - private _renderItemPerPageTemplate(): TemplateResult | typeof nothing { return this.pageSizeOptions && this.pageSizeOptions.length > 0 ? html` @@ -245,7 +237,4 @@ declare global { // eslint-disable-next-line @typescript-eslint/naming-convention 'sbb-paginator': SbbPaginatorElement; } - interface HTMLElementEventMap { - page: CustomEvent; - } } From e35cd5f9a606625cb3be01aca5f08d595426c722 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Thu, 3 Oct 2024 16:39:03 +0200 Subject: [PATCH 03/22] fix: build --- src/elements/core/interfaces.ts | 1 + src/elements/core/interfaces/paginator-page.ts | 6 ++++++ src/elements/paginator/common/paginator-common.ts | 8 +------- tools/vite/generate-react-wrappers.ts | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 src/elements/core/interfaces/paginator-page.ts diff --git a/src/elements/core/interfaces.ts b/src/elements/core/interfaces.ts index 4a7673d271..49cb62571d 100644 --- a/src/elements/core/interfaces.ts +++ b/src/elements/core/interfaces.ts @@ -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'; diff --git a/src/elements/core/interfaces/paginator-page.ts b/src/elements/core/interfaces/paginator-page.ts new file mode 100644 index 0000000000..9d045e0d4a --- /dev/null +++ b/src/elements/core/interfaces/paginator-page.ts @@ -0,0 +1,6 @@ +export type SbbPaginatorPageEventDetails = { + length: number; + pageSize: number; + pageIndex: number; + previousPageIndex: number; +}; diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts index b6bbe7cf39..2cc1ed30ba 100644 --- a/src/elements/paginator/common/paginator-common.ts +++ b/src/elements/paginator/common/paginator-common.ts @@ -5,19 +5,13 @@ 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, SbbNegativeMixin } from '../../core/mixins.js'; import '../../button/mini-button.js'; import '../../button/mini-button-group.js'; import '../../divider.js'; -export type SbbPaginatorPageEventDetails = { - length: number; - pageSize: number; - pageIndex: number; - previousPageIndex: number; -}; - export declare class SbbPaginatorCommonElementMixinType { public negative: boolean; public length: number; diff --git a/tools/vite/generate-react-wrappers.ts b/tools/vite/generate-react-wrappers.ts index 690a3e727a..8367fa047d 100644 --- a/tools/vite/generate-react-wrappers.ts +++ b/tools/vite/generate-react-wrappers.ts @@ -153,11 +153,11 @@ function renderTemplate( .map((e) => e.type.text.substring(12).slice(0, -1)) .sort() .filter((v, i, a) => a.indexOf(v) === i && v.length > 1) ?? []; - // If a type or interface needs to be imported, the custom elements analyzer will not - // detect/extract these and therefore we need to have a manual list of required - // types/interfaces. + // If a type or interface needs to be imported, the custom elements analyzer will not detect/extract these, + // and therefore we need to have a manual list of required types/interfaces. const interfaces = new Map() .set('SbbOverlayCloseEventDetails', 'core/interfaces.js') + .set('SbbPaginatorPageEventDetails', 'core/interfaces.js') .set('SbbValidationChangeEvent', 'core/interfaces.js'); for (const customEventType of customEventTypes) { const exportModule = exports.find((e) => e.name === customEventType); From 1c6df7c7a66ebbcb986fae1f3eeb009d97cce66e Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Fri, 4 Oct 2024 12:06:59 +0200 Subject: [PATCH 04/22] feat: a11y, add snapshot, minor fixes --- src/elements/core/i18n/i18n.ts | 10 ++ .../paginator/common/paginator-common.ts | 16 +-- .../compact-paginator.snapshot.spec.snap.js | 136 ++++++++++++++++++ .../compact-paginator.snapshot.spec.ts | 2 +- .../compact-paginator/compact-paginator.ts | 18 ++- .../paginator/compact-paginator/readme.md | 56 ++++---- src/elements/paginator/paginator/paginator.ts | 10 +- 7 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js diff --git a/src/elements/core/i18n/i18n.ts b/src/elements/core/i18n/i18n.ts index 5b3cadcf6b..f66dfdbc75 100644 --- a/src/elements/core/i18n/i18n.ts +++ b/src/elements/core/i18n/i18n.ts @@ -715,3 +715,13 @@ export const i18nSelectedPage = (pageNumber: number): Record => fr: `Page ${pageNumber} sélectionnée.`, it: `Pagina ${pageNumber} selezionata.`, }); + +export const i18nPageOnTotal = ( + pageNumber: number, + totalPages: number, +): Record => ({ + de: `Seite ${pageNumber} von ${totalPages}.`, + en: `Page ${pageNumber} of ${totalPages}.`, + fr: `Page ${pageNumber} de ${totalPages}.`, + it: `Pagina ${pageNumber} di ${totalPages}.`, +}); diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts index 2cc1ed30ba..1b7400e436 100644 --- a/src/elements/paginator/common/paginator-common.ts +++ b/src/elements/paginator/common/paginator-common.ts @@ -1,10 +1,10 @@ -import { html, type LitElement, type PropertyValues, type TemplateResult } from 'lit'; +import { html, type LitElement, 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 { i18nNextPage, i18nPreviousPage } from '../../core/i18n.js'; import type { SbbPaginatorPageEventDetails } from '../../core/interfaces.js'; import { type AbstractConstructor, SbbNegativeMixin } from '../../core/mixins.js'; @@ -96,18 +96,6 @@ export const SbbPaginatorCommonElementMixin = ): 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(); - } - - private _currentPageLabel(): string { - return i18nSelectedPage(this.pageIndex + 1)[this.language.current]; - } - /** Evaluate `pageIndex` by excluding edge cases. */ private _coercePageIndexInRange(pageIndex: number): number { return Math.max( diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js new file mode 100644 index 0000000000..4659d1f358 --- /dev/null +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -0,0 +1,136 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["sbb-compact-paginator renders DOM"] = +` + +`; +/* end snapshot sbb-compact-paginator renders DOM */ + +snapshots["sbb-compact-paginator renders Shadow DOM"] = +`
+ + + + + + + + + + 1 + + 10 + +
+ + Page 1 of 10. + +`; +/* end snapshot sbb-compact-paginator renders Shadow DOM */ + +snapshots["sbb-compact-paginator renders A11y tree Chrome"] = +`

+ { + "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 of 10." + } + ] +} +

+`; +/* end snapshot sbb-compact-paginator renders A11y tree Chrome */ + +snapshots["sbb-compact-paginator renders A11y tree Firefox"] = +`

+ { + "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 of 10." + } + ] +} +

+`; +/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ + diff --git a/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts b/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts index 4e0dc7cf32..1530605f80 100644 --- a/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts +++ b/src/elements/paginator/compact-paginator/compact-paginator.snapshot.spec.ts @@ -12,7 +12,7 @@ describe(`sbb-compact-paginator`, () => { beforeEach(async () => { element = await fixture( - html``, + html``, ); }); diff --git a/src/elements/paginator/compact-paginator/compact-paginator.ts b/src/elements/paginator/compact-paginator/compact-paginator.ts index 2177a316a3..caeed235a7 100644 --- a/src/elements/paginator/compact-paginator/compact-paginator.ts +++ b/src/elements/paginator/compact-paginator/compact-paginator.ts @@ -1,8 +1,9 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { hostAttributes } from '../../core/decorators.js'; +import { i18nPageOnTotal } from '../../core/i18n.js'; import { SbbPaginatorCommonElementMixin } from '../common.js'; import '../../divider.js'; @@ -23,10 +24,23 @@ export class SbbCompactPaginatorElement extends SbbPaginatorCommonElementMixin(L page: 'page', } as const; + protected override updated(changedProperties: PropertyValues): 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(); + } + + private _currentPageLabel(): string { + return i18nPageOnTotal(this.pageIndex + 1, this.numberOfPages())[this.language.current]; + } + private _renderPageNumbers(): TemplateResult { return html` - ${this.pageIndex + 1} Explain the use and the purpose of the component; add minor details if needed and provide a basic example.
-> If you reference other components, link their documentation at least once (the path must start from _/docs/..._ ).
-> For the examples, use triple backticks with file extension (` ```html ``` `).
-> The following list of paragraphs is only suggested; remove, create and adapt as needed. +The `sbb-compact-paginator` provides an alternative to the `sbb-paginator` +as a navigation for content split across multiple pages, e.g. a table with many rows. +Differently from the `sbb-paginator`, it displays only the current page and the total number of pages +together with the `sbb-mini-button-group` to move to previous or next page. -The `sbb-compact-paginator` is a component . . . +It can be controlled via the following properties: + +- `length`: the total number of items being paged; +- `pageSize`: the number of items per page (default: `10`); +- `pageIndex`: the index of the current displayed page (default: `0`). ```html - + ``` -## Slots - -> Describe slot naming and usage and provide an example of slotted content. +By default, a [sbb-mini-button-group](/docs/elements-sbb-button-sbb-mini-button-group--docs) with two buttons is displayed, +which allows moving to the previous/next pages. +The positioning of this element relative to the page numbers is set using the `pagerPosition` property (default: `start`): -## States - -> Describe the component states (`disabled`, `readonly`, etc.) and provide examples. +```html + +``` ## Style -> Describe the properties which change the component visualization (`size`, `negative`, etc.) and provide examples. - -## Interactions +The component has two `size`, named `s` and `m` (default). -> Describe how it's possible to interact with the component (open and close a `sbb-dialog`, dismiss a `sbb-alert`, etc.) and provide examples. +```html + +``` ## Events -> Describe events triggered by the component and possibly how to get information from the payload. - -## Keyboard interaction - -> If the component has logic for keyboard navigation (as the `sbb-calendar` or the `sbb-select`) describe it. - -| Keyboard | Action | -| -------------- | ------------- | -| Key | What it does. | +Consumers can listen to the `page` event on the `sbb-compact-paginator` component to intercept the page change event. +The `event.detail` contains both the information about the `pageIndex` and the `previousPageIndex`, +as well as the `length` and the `pageSize`. ## Accessibility -> Describe how accessibility is implemented and if there are issues or suggested best-practice for the consumers. +The component has `role="group"` to semantically group its child controls; +consumers should add an appropriate `aria-label` attribute with a text +that describes the content controlled by the paginator. + +```html + +``` diff --git a/src/elements/paginator/paginator/paginator.ts b/src/elements/paginator/paginator/paginator.ts index 1a963ec662..718ac74362 100644 --- a/src/elements/paginator/paginator/paginator.ts +++ b/src/elements/paginator/paginator/paginator.ts @@ -11,7 +11,7 @@ import { repeat } from 'lit/directives/repeat.js'; import { sbbInputModalityDetector } from '../../core/a11y.js'; import { hostAttributes } from '../../core/decorators.js'; -import { i18nItemsPerPage, i18nPage } from '../../core/i18n.js'; +import { i18nItemsPerPage, i18nPage, i18nSelectedPage } from '../../core/i18n.js'; import type { SbbSelectElement } from '../../select.js'; import { SbbPaginatorCommonElementMixin } from '../common.js'; @@ -80,6 +80,10 @@ export class SbbPaginatorElement extends SbbPaginatorCommonElementMixin(LitEleme select.setAttribute('aria-labelledby', this._paginatorOptionsLabel); this._updateSelectAriaLabelledBy = false; } + + // 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(); } /** @@ -150,6 +154,10 @@ export class SbbPaginatorElement extends SbbPaginatorCommonElementMixin(LitEleme } } + private _currentPageLabel(): string { + return i18nSelectedPage(this.pageIndex + 1)[this.language.current]; + } + private _renderItemPerPageTemplate(): TemplateResult | typeof nothing { return this.pageSizeOptions && this.pageSizeOptions.length > 0 ? html` From 317742428d7d9c1c66198156cb6db497ad782b82 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Fri, 4 Oct 2024 15:05:23 +0200 Subject: [PATCH 05/22] style: fix color --- src/elements/paginator/compact-paginator/compact-paginator.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/paginator/compact-paginator/compact-paginator.scss b/src/elements/paginator/compact-paginator/compact-paginator.scss index 730031a1fa..61a8476ec8 100644 --- a/src/elements/paginator/compact-paginator/compact-paginator.scss +++ b/src/elements/paginator/compact-paginator/compact-paginator.scss @@ -25,7 +25,7 @@ align-items: center; justify-content: center; gap: var(--sbb-spacing-fixed-2x); - color: var(--sbb-color-granite); + color: var(--sbb-color-storm); } sbb-divider { From bf784823fc5e840f608dad7d8e3026d04cf21e19 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Mon, 7 Oct 2024 15:02:18 +0200 Subject: [PATCH 06/22] fix: common logic --- .../paginator/common/paginator-common.ts | 28 +++++++++++++++---- .../compact-paginator/compact-paginator.ts | 17 +++-------- .../paginator/compact-paginator/readme.md | 16 +++++------ src/elements/paginator/paginator/paginator.ts | 17 ++++++----- src/elements/paginator/paginator/readme.md | 18 ++++++------ 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts index 1b7400e436..e52b11f056 100644 --- a/src/elements/paginator/common/paginator-common.ts +++ b/src/elements/paginator/common/paginator-common.ts @@ -1,4 +1,4 @@ -import { html, type LitElement, type TemplateResult } from 'lit'; +import { html, type LitElement, type PropertyValues, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; import { SbbLanguageController } from '../../core/controllers.js'; @@ -12,7 +12,7 @@ import '../../button/mini-button.js'; import '../../button/mini-button-group.js'; import '../../divider.js'; -export declare class SbbPaginatorCommonElementMixinType { +export declare abstract class SbbPaginatorCommonElementMixinType { public negative: boolean; public length: number; public pageSize: number; @@ -24,6 +24,8 @@ export declare class SbbPaginatorCommonElementMixinType { protected pageIndexChanged(value: number): void; protected emitPageEvent(previousPageIndex: number): void; protected renderPrevNextButtons(): TemplateResult; + protected abstract currentPageLabel(): string; + protected abstract renderPaginator(): TemplateResult; } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -78,10 +80,7 @@ export const SbbPaginatorCommonElementMixin = ): 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 { @@ -155,6 +164,13 @@ export const SbbPaginatorCommonElementMixin = `; } + + protected override render(): TemplateResult { + return html` + ${this.renderPaginator()} + + `; + } } return SbbPaginatorCommonElement as unknown as AbstractConstructor & T; diff --git a/src/elements/paginator/compact-paginator/compact-paginator.ts b/src/elements/paginator/compact-paginator/compact-paginator.ts index caeed235a7..d3547bdacb 100644 --- a/src/elements/paginator/compact-paginator/compact-paginator.ts +++ b/src/elements/paginator/compact-paginator/compact-paginator.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import type { CSSResultGroup, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; @@ -24,21 +24,13 @@ export class SbbCompactPaginatorElement extends SbbPaginatorCommonElementMixin(L page: 'page', } as const; - protected override updated(changedProperties: PropertyValues): 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(); - } - - private _currentPageLabel(): string { + protected currentPageLabel(): string { return i18nPageOnTotal(this.pageIndex + 1, this.numberOfPages())[this.language.current]; } private _renderPageNumbers(): TemplateResult { return html` - ${this.pageIndex + 1} Date: Fri, 18 Oct 2024 15:21:53 +0200 Subject: [PATCH 14/22] fix: divider height --- .../mini-button-group/mini-button-group.scss | 2 +- .../compact-paginator.snapshot.spec.snap.js | 34 +------------------ .../compact-paginator/compact-paginator.scss | 4 +++ .../compact-paginator/compact-paginator.ts | 1 + 4 files changed, 7 insertions(+), 34 deletions(-) diff --git a/src/elements/button/mini-button-group/mini-button-group.scss b/src/elements/button/mini-button-group/mini-button-group.scss index ee519be198..7f52e7bb3d 100644 --- a/src/elements/button/mini-button-group/mini-button-group.scss +++ b/src/elements/button/mini-button-group/mini-button-group.scss @@ -43,9 +43,9 @@ ::slotted(sbb-divider) { --sbb-divider-border-width: var(--sbb-border-width-1x); - height: #{sbb.px-to-rem-build(16)}; padding-block: var(--sbb-spacing-fixed-1x); padding-inline: var(--sbb-spacing-fixed-1x); + height: var(--sbb-size-icon-ui-small); } .sbb-mini-button-group { diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js index 256258df52..bc1ccac9d2 100644 --- a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -56,6 +56,7 @@ snapshots["sbb-compact-paginator renders Shadow DOM"] =
Date: Sat, 19 Oct 2024 19:25:10 +0200 Subject: [PATCH 15/22] fix: snapshot --- .../compact-paginator.snapshot.spec.snap.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js index bc1ccac9d2..e3cfc3a3ea 100644 --- a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -103,3 +103,36 @@ snapshots["sbb-compact-paginator renders A11y tree Chrome"] = `; /* end snapshot sbb-compact-paginator renders A11y tree Chrome */ +snapshots["sbb-compact-paginator renders A11y tree Firefox"] = +`

+ { + "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 of 10." + } + ] +} +

+`; +/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ + From bcf631c22054b36a76e67066c4a0fe5f2b06e952 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Mon, 21 Oct 2024 10:26:13 +0200 Subject: [PATCH 16/22] fix: a11y part 1 --- src/elements/core/i18n/i18n.ts | 10 ---------- src/elements/paginator/common/paginator-common.ts | 9 ++++++--- .../paginator/compact-paginator/compact-paginator.ts | 5 ----- src/elements/paginator/paginator/paginator.ts | 6 +----- 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/elements/core/i18n/i18n.ts b/src/elements/core/i18n/i18n.ts index f66dfdbc75..5b3cadcf6b 100644 --- a/src/elements/core/i18n/i18n.ts +++ b/src/elements/core/i18n/i18n.ts @@ -715,13 +715,3 @@ export const i18nSelectedPage = (pageNumber: number): Record => fr: `Page ${pageNumber} sélectionnée.`, it: `Pagina ${pageNumber} selezionata.`, }); - -export const i18nPageOnTotal = ( - pageNumber: number, - totalPages: number, -): Record => ({ - de: `Seite ${pageNumber} von ${totalPages}.`, - en: `Page ${pageNumber} of ${totalPages}.`, - fr: `Page ${pageNumber} de ${totalPages}.`, - it: `Pagina ${pageNumber} di ${totalPages}.`, -}); diff --git a/src/elements/paginator/common/paginator-common.ts b/src/elements/paginator/common/paginator-common.ts index fb9ee667f7..edcefbea86 100644 --- a/src/elements/paginator/common/paginator-common.ts +++ b/src/elements/paginator/common/paginator-common.ts @@ -4,7 +4,7 @@ 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 } from '../../core/i18n.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'; @@ -25,7 +25,7 @@ export declare abstract class SbbPaginatorCommonElementMixinType { protected pageIndexChanged(value: number): void; protected emitPageEvent(previousPageIndex: number): void; protected renderPrevNextButtons(): TemplateResult; - protected abstract currentPageLabel(): string; + protected currentPageLabel(): string; protected abstract renderPaginator(): TemplateResult; } @@ -95,7 +95,6 @@ export const SbbPaginatorCommonElementMixin = ): void { @@ -114,6 +113,10 @@ export const SbbPaginatorCommonElementMixin = 0 ? html` From 41cc91a9f070a61f24181ce6d235e0170607fb38 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Mon, 21 Oct 2024 11:16:07 +0200 Subject: [PATCH 17/22] fix: tests --- .../compact-paginator.snapshot.spec.snap.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js index e3cfc3a3ea..86dca4c7ca 100644 --- a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["sbb-compact-paginator renders DOM"] = +snapshots["sbb-compact-paginator renders DOM"] = ` @@ -65,12 +65,12 @@ snapshots["sbb-compact-paginator renders Shadow DOM"] = - Page 1 of 10. + Page 1 selected. `; /* end snapshot sbb-compact-paginator renders Shadow DOM */ -snapshots["sbb-compact-paginator renders A11y tree Chrome"] = +snapshots["sbb-compact-paginator renders A11y tree Chrome"] = `

{ "role": "WebArea", @@ -95,7 +95,7 @@ snapshots["sbb-compact-paginator renders A11y tree Chrome"] = }, { "role": "text", - "name": "Page 1 of 10." + "name": "Page 1 selected." } ] } @@ -103,7 +103,7 @@ snapshots["sbb-compact-paginator renders A11y tree Chrome"] = `; /* end snapshot sbb-compact-paginator renders A11y tree Chrome */ -snapshots["sbb-compact-paginator renders A11y tree Firefox"] = +snapshots["sbb-compact-paginator renders A11y tree Firefox"] = `

{ "role": "document", From 249fb575bd68793fac2236ad83289e036ffd947c Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Mon, 21 Oct 2024 11:54:17 +0200 Subject: [PATCH 18/22] fix: regen snap --- .../compact-paginator.snapshot.spec.snap.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js index 86dca4c7ca..6f32cc5b64 100644 --- a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["sbb-compact-paginator renders DOM"] = +snapshots["sbb-compact-paginator renders DOM"] = ` { - "role": "WebArea", + "role": "document", "name": "", "children": [ { @@ -86,27 +86,27 @@ snapshots["sbb-compact-paginator renders A11y tree Chrome"] = "name": "Next page" }, { - "role": "text", + "role": "text leaf", "name": "1" }, { - "role": "text", + "role": "text leaf", "name": "10" }, { - "role": "text", + "role": "text leaf", "name": "Page 1 selected." } ] }

`; -/* end snapshot sbb-compact-paginator renders A11y tree Chrome */ +/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ -snapshots["sbb-compact-paginator renders A11y tree Firefox"] = +snapshots["sbb-compact-paginator renders A11y tree Chrome"] = `

{ - "role": "document", + "role": "WebArea", "name": "", "children": [ { @@ -119,20 +119,20 @@ snapshots["sbb-compact-paginator renders A11y tree Firefox"] = "name": "Next page" }, { - "role": "text leaf", + "role": "text", "name": "1" }, { - "role": "text leaf", + "role": "text", "name": "10" }, { - "role": "text leaf", - "name": "Page 1 of 10." + "role": "text", + "name": "Page 1 selected." } ] }

`; -/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ +/* end snapshot sbb-compact-paginator renders A11y tree Chrome */ From dcb83477ed24468e7b4c8ab93b95d1ad74ce0f27 Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Wed, 23 Oct 2024 10:03:51 +0200 Subject: [PATCH 19/22] fix: review Mursel pt.2 --- .../compact-paginator.snapshot.spec.snap.js | 25 +++++++++---------- .../compact-paginator/compact-paginator.ts | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js index 6f32cc5b64..3a1eeffdc1 100644 --- a/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js +++ b/src/elements/paginator/compact-paginator/__snapshots__/compact-paginator.snapshot.spec.snap.js @@ -48,7 +48,6 @@ snapshots["sbb-compact-paginator renders Shadow DOM"] = @@ -70,10 +69,10 @@ snapshots["sbb-compact-paginator renders Shadow DOM"] = `; /* end snapshot sbb-compact-paginator renders Shadow DOM */ -snapshots["sbb-compact-paginator renders A11y tree Firefox"] = +snapshots["sbb-compact-paginator renders A11y tree Chrome"] = `

{ - "role": "document", + "role": "WebArea", "name": "", "children": [ { @@ -86,27 +85,27 @@ snapshots["sbb-compact-paginator renders A11y tree Firefox"] = "name": "Next page" }, { - "role": "text leaf", + "role": "text", "name": "1" }, { - "role": "text leaf", + "role": "text", "name": "10" }, { - "role": "text leaf", + "role": "text", "name": "Page 1 selected." } ] }

`; -/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ +/* end snapshot sbb-compact-paginator renders A11y tree Chrome */ -snapshots["sbb-compact-paginator renders A11y tree Chrome"] = +snapshots["sbb-compact-paginator renders A11y tree Firefox"] = `

{ - "role": "WebArea", + "role": "document", "name": "", "children": [ { @@ -119,20 +118,20 @@ snapshots["sbb-compact-paginator renders A11y tree Chrome"] = "name": "Next page" }, { - "role": "text", + "role": "text leaf", "name": "1" }, { - "role": "text", + "role": "text leaf", "name": "10" }, { - "role": "text", + "role": "text leaf", "name": "Page 1 selected." } ] }

`; -/* end snapshot sbb-compact-paginator renders A11y tree Chrome */ +/* end snapshot sbb-compact-paginator renders A11y tree Firefox */ diff --git a/src/elements/paginator/compact-paginator/compact-paginator.ts b/src/elements/paginator/compact-paginator/compact-paginator.ts index 54309a139e..d216b3295e 100644 --- a/src/elements/paginator/compact-paginator/compact-paginator.ts +++ b/src/elements/paginator/compact-paginator/compact-paginator.ts @@ -21,7 +21,7 @@ export class SbbCompactPaginatorElement extends SbbPaginatorCommonElementMixin(L private _renderPageNumbers(): TemplateResult { return html` - ${this.pageIndex + 1}