diff --git a/elements/package.json b/elements/package.json index a857b373a3..42e50fc69d 100644 --- a/elements/package.json +++ b/elements/package.json @@ -54,7 +54,8 @@ "./pf-tile/pf-tile.js": "./pf-tile/pf-tile.js", "./pf-timestamp/pf-timestamp.js": "./pf-timestamp/pf-timestamp.js", "./pf-tooltip/BaseTooltip.js": "./pf-tooltip/BaseTooltip.js", - "./pf-tooltip/pf-tooltip.js": "./pf-tooltip/pf-tooltip.js" + "./pf-tooltip/pf-tooltip.js": "./pf-tooltip/pf-tooltip.js", + "./pf-pagination/pf-pagination.js": "./pf-pagination/pf-pagination.js" }, "publishConfig": { "access": "public", diff --git a/elements/pf-pagination/README.md b/elements/pf-pagination/README.md new file mode 100644 index 0000000000..af14d7739c --- /dev/null +++ b/elements/pf-pagination/README.md @@ -0,0 +1,11 @@ +# Pagination +Add a description of the component here. + +## Usage +Describe how best to use this web component along with best practices. + +```html + + + +``` diff --git a/elements/pf-pagination/demo/demo.css b/elements/pf-pagination/demo/demo.css new file mode 100644 index 0000000000..4e3cc8524d --- /dev/null +++ b/elements/pf-pagination/demo/demo.css @@ -0,0 +1,7 @@ +body { + background-color: #f0f0f0; +} + +section { + padding: 6rem 1rem; +} \ No newline at end of file diff --git a/elements/pf-pagination/demo/pf-pagination.html b/elements/pf-pagination/demo/pf-pagination.html new file mode 100644 index 0000000000..00d170c649 --- /dev/null +++ b/elements/pf-pagination/demo/pf-pagination.html @@ -0,0 +1,7 @@ + + + +
+

Bottom

+ +
diff --git a/elements/pf-pagination/demo/pf-pagination.js b/elements/pf-pagination/demo/pf-pagination.js new file mode 100644 index 0000000000..5d7f9fcf47 --- /dev/null +++ b/elements/pf-pagination/demo/pf-pagination.js @@ -0,0 +1 @@ +import '@patternfly/elements/pf-pagination/pf-pagination.js'; diff --git a/elements/pf-pagination/docs/pf-pagination.md b/elements/pf-pagination/docs/pf-pagination.md new file mode 100644 index 0000000000..d12c05f384 --- /dev/null +++ b/elements/pf-pagination/docs/pf-pagination.md @@ -0,0 +1,17 @@ +{% renderOverview %} + +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} \ No newline at end of file diff --git a/elements/pf-pagination/pf-pagination.css b/elements/pf-pagination/pf-pagination.css new file mode 100644 index 0000000000..8abbafd752 --- /dev/null +++ b/elements/pf-pagination/pf-pagination.css @@ -0,0 +1,282 @@ +:host { + display: block; + /* todo: this is set programmatically */ + --pf-c-pagination__nav-page-select--c-form-control--width-chars: 2; +} + +#container { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; +} + +#container > *:not(:last-child) { + margin-right: var(--pf-c-pagination--child--MarginRight); +} + +@media (min-width: 768px) { + #container { + --pf-c-pagination--m-bottom__nav-control--c-button--PaddingTop: var(--pf-c-pagination--m-bottom__nav-control--c-button--md--PaddingTop, var(--pf-global--spacer--form-element, .375rem)); + --pf-c-pagination--m-bottom__nav-control--c-button--PaddingRight: var(--pf-c-pagination--m-bottom__nav-control--c-button--md--PaddingRight, var(--pf-global--spacer--sm, .5rem)); + --pf-c-pagination--m-bottom__nav-control--c-button--PaddingBottom: var(--pf-c-pagination--m-bottom__nav-control--c-button--md--PaddingBottom, var(--pf-global--spacer--form-element, .375rem)); + --pf-c-pagination--m-bottom__nav-control--c-button--PaddingLeft: var(--pf-c-pagination--m-bottom__nav-control--c-button--md--PaddingLeft, var(--pf-global--spacer--sm, .5rem)); + --pf-c-pagination--m-bottom--child--MarginRight: var(--pf-c-pagination--m-bottom--child--md--MarginRight, var(--pf-global--spacer--lg, 1.5rem)); + --pf-c-pagination--m-bottom__nav-control--c-button--OutlineOffset: 0; + --pf-c-pagination--m-bottom--BoxShadow: none; + --pf-c-pagination--c-options-menu--Display: inline-flex; + --pf-c-pagination--c-options-menu--Visibility: visible; + --pf-c-pagination__nav--Display: inline-flex; + --pf-c-pagination__nav--Visibility: visible; + --pf-c-pagination__total-items--Display: none; + --pf-c-pagination__total-items--Visibility: hidden; + } +} + +@media (min-width: 1200px) { + #container { + --pf-c-pagination--m-bottom--md--PaddingRight: var(--pf-c-pagination--m-bottom--xl--PaddingRight, var(--pf-global--spacer--lg, 1.5rem)); + --pf-c-pagination--m-bottom--md--PaddingLeft: var(--pf-c-pagination--m-bottom--xl--PaddingLeft, var(--pf-global--spacer--lg, 1.5rem)); + /* todo: find correct fallback */ + --pf-c-pagination__scroll-button--Width: var(--pf-c-pagination__scroll-button--xl--Width); + --pf-c-pagination--m-page-insets--inset: var(--pf-c-pagination--m-page-insets--xl--inset, var(--pf-global--spacer--lg, 1.5rem)); + } +} + +/* PER PAGE SELECT */ +#container #options-menu { + position: absolute; + display: block; + visibility: visible; +} + +@media (min-width: 768px) { + #container.bottom #options-menu { + position: relative; + } +} + +/* PER PAGE SELECT: TOGGLE */ +#container #options-menu #menu-toggle { + position: relative; + --pf-c-button--FontSize: var(--pf-c-pagination--c-options-menu__toggle--FontSize, var(--pf-global--FontSize--sm, .875rem)); + --pf-c-button--LineHeight: var(--pf-c-options-menu__toggle--LineHeight, var(--pf-global--LineHeight--md, 1.5)); + --pf-c-button--PaddingTop: var(--pf-c-options-menu__toggle--PaddingTop, var(--pf-global--spacer--form-element, 0.375rem)); + --pf-c-button--PaddingRight: var(--pf-c-options-menu__toggle--PaddingRight, var(--pf-global--spacer--sm, .5rem)); + --pf-c-button--PaddingBottom: var(--pf-c-options-menu__toggle--PaddingBottom, var(--pf-global--spacer--form-element, 0.375rem)); + --pf-c-button--PaddingLeft: var(--pf-c-options-menu__toggle--PaddingLeft, var(--pf-global--spacer--sm, .5rem)); + --pf-c-button--BorderRadius: 0; + --pf-c-button--m-plain--Color : var(--pf-c-options-menu__toggle--Color, var(--pf-global--Color--100, #151515)); + --pf-c-button--m-plain--BackgroundColor: var(--pf-c-options-menu__toggle--BackgroundColor, transparent); + --pf-c-button__icon--m-start--MarginLeft: var(--pf-c-options-menu__toggle-icon--MarginLeft, var(--pf-global--spacer--sm, .5rem)); +} + +#container #options-menu #menu-toggle:hover, +#container #options-menu #menu-toggle:active, +#container #options-menu #menu-toggle:focus { + --pf-c-options-menu__toggle--m-plain--Color: var(--pf-c-options-menu__toggle--m-plain--hover--Color, var(--pf-global--Color--100, #151515)); + --pf-c-options-menu--m-plain__toggle-icon--Color: var(--pf-c-options-menu--m-plain--hover__toggle-icon--Color, var(--pf-global--Color--100, #151515)); +} + +#container #options-menu #menu-toggle::part(icon) { + --_icon-color: var(--pf-c-options-menu__toggle-icon--Color, var(--pf-c-options-menu--m-plain__toggle-icon--Color, var(--pf-global--Color--200, #6a6e73))); + margin-right: var(--pf-c-options-menu__toggle-icon--MarginRight, var(--pf-global--spacer--sm, .5rem)); + color: var(--_icon-color, inherit); + /* todo: icon style + size not 100% aligned with pf */ + --pf-icon--size: 12px; + width: 12px; + left: 0; +} + +/* PER PAGE SELECT: MENU */ +#container #options-menu:not(.expanded) #menu-list { + display: none; +} + +#container #options-menu #menu-list { + position: absolute; + list-style: none; + margin: 0; + top: var(--pf-c-options-menu--m-top__menu--Top, 0); + transform: translateY(var(--pf-c-options-menu--m-top__menu--TranslateY, calc(-100% - var(--pf-global--spacer--xs, 0.25rem)))); + z-index: var(--pf-c-options-menu__menu--ZIndex, var(--pf-global--ZIndex--sm, 200)); + min-width: 100%; + padding-top: var(--pf-c-options-menu__menu--PaddingTop, var(--pf-global--spacer--sm, .5rem)); + padding-right: 0; + padding-bottom: var(--pf-c-options-menu__menu--PaddingBottom, var(--pf-global--spacer--sm, .5rem)); + padding-left: 0; + background-color: var(--pf-c-options-menu__menu--BackgroundColor, var(--pf-global--BackgroundColor--light-100, #fff)); + background-clip: padding-box; + box-shadow: var(--pf-c-options-menu__menu--BoxShadow, var(--pf-global--BoxShadow--md, 0 0.25rem 0.5rem 0rem rgba(3, 3, 3, 0.12), 0 0 0.25rem 0 rgba(3, 3, 3, 0.06))); +} + +#container #options-menu #menu-list .menu-item { + white-space: nowrap; + --pf-c-button--PaddingTop: var(--pf-c-options-menu__menu-item--PaddingTop, var(--pf-global--spacer--sm, .5rem)); + --pf-c-button--PaddingRight: var(--pf-c-options-menu__menu-item--PaddingRight, var(--pf-global--spacer--md, 1rem)); + --pf-c-button--PaddingBottom: var(--pf-c-options-menu__menu-item--PaddingBottom, var(--pf-global--spacer--sm, .5rem)); + --pf-c-button--PaddingLeft: var(--pf-c-options-menu__menu-item--PaddingLeft, var(--pf-global--spacer--md, 1rem)); + --pf-c-button--FontSize: var(--pf-c-options-menu__menu-item--FontSize, var(--pf-global--FontSize--md, 1rem)); + --pf-c-button--m-tertiary--Color: var(--pf-c-options-menu__menu-item--Color, var(--pf-global--Color--100, #151515)); + --pf-c-button--m-tertiary--BackgroundColor: var(--pf-c-options-menu__menu-item--BackgroundColor, transparent); + --pf-c-button--BorderRadius: 0; + --pf-c-button--m-tertiary--after--BorderColor: transparent; +} + +#container #options-menu #menu-list .menu-item.selected { + --pf-c-button__icon--m-start--MarginLeft: calc(var(--pf-c-options-menu__menu-item-icon--PaddingLeft, var(--pf-global--spacer--lg, 1.5rem)) / 2); + align-self: center; + width: auto; + margin-left: auto; +} + +#container #options-menu #menu-list .menu-item.selected::part(icon) { + /* todo: icon size doesn't seem to match */ + --pf-icon--size: var(--pf-c-options-menu__menu-item-icon--FontSize, var(--pf-global--icon--FontSize--sm, 0.625rem)); + width: var(--pf-c-options-menu__menu-item-icon--FontSize, var(--pf-global--icon--FontSize--sm, 0.625rem)); + color: var(--pf-c-options-menu__menu-item-icon--Color, var(--pf-global--active-color--100, #06c)); +} + +#container #options-menu #menu-list .menu-item:hover { + --pf-c-button--m-tertiary--hover--after--BorderColor: transparent; + --pf-c-button--m-tertiary--hover--Color: var(--pf-c-options-menu__menu-item--Color, var(--pf-global--Color--100, #151515)); + --pf-c-button--m-tertiary--hover--BackgroundColor: var(--pf-c-options-menu__menu-item--hover--BackgroundColor, var(--pf-global--BackgroundColor--light-300, #f0f0f0)); +} + +#container #options-menu #menu-list .menu-item:active { + --pf-c-button--m-tertiary--active--after--BorderColor: transparent; + --pf-c-button--m-tertiary--active--Color: var(--pf-c-options-menu__menu-item--Color, var(--pf-global--Color--100, #151515)); + --pf-c-button--m-tertiary--active--BackgroundColor: var(--pf-c-options-menu__menu-item--hover--BackgroundColor, var(--pf-global--BackgroundColor--light-300, #f0f0f0)); +} + +#container #options-menu #menu-list .menu-item:focus { + --pf-c-button--m-tertiary--focus--after--BorderColor: transparent; + --pf-c-button--m-tertiary--focus--Color: var(--pf-c-options-menu__menu-item--Color, var(--pf-global--Color--100, #151515)); + --pf-c-button--m-tertiary--focus--BackgroundColor: var(--pf-c-options-menu__menu-item--hover--BackgroundColor, var(--pf-global--BackgroundColor--light-300, #f0f0f0)); +} + +#container #nav #page-select > * { + font-size: var(--pf-c-pagination__nav-page-select--FontSize, var(--pf-global--FontSize--sm, .875rem)); + white-space: nowrap; +} + +#container #nav #page-select > *:not(:last-child) { + margin-right: var(--pf-c-pagination__nav-page-select--child--MarginRight, var(--pf-global--spacer--xs, .25rem)); +} + +#container #nav #page-select #page-select-input { + /* todo: long complicated calc */ + width: var(--pf-c-pagination__nav-page-select--c-form-control--Width, 24px); + appearance: textfield; + /* pf-form-control ? */ + font-family: inherit; + color: var(--pf-c-form-control--Color, var(--pf-global--Color--100, #151515)); + --_padding-top: var(--pf-c-form-control--PaddingTop, calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-global--BorderWidth--sm, 1px))); + --_padding-right: var(--pf-c-form-control--PaddingRight, var(--pf-c-form-control--inset--base, var(--pf-global--spacer--sm, 0.5rem))); + --_padding-bottom: var(--pf-c-form-control--PaddingTop, calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--pf-global--BorderWidth--sm, 1px))); + --_padding-left: var(--pf-c-form-control--PaddingRight, var(--pf-c-form-control--inset--base, var(--pf-global--spacer--sm, 0.5rem))); + padding: var(--_padding-top) var(--_padding-right) var(--_padding-bottom) var(--_padding-left); + line-height: var(--pf-c-form-control--LineHeight, 1.5); + background-color: var(--pf-c-form-control--BackgroundColor, var(--pf-global--BackgroundColor--100, var(--pf-global--BackgroundColor--light-100, #fff))); + background-repeat: no-repeat; + border: var(--pf-c-form-control--BorderWidth, var(--pf-global--BorderWidth--sm, 1px)) solid; + --_border-top-color: var(--pf-c-form-control--BorderTopColor, var(--pf-global--BorderColor--300, #f0f0f0)); + --_border-right-color: var(--pf-c-form-control--BorderRightColor, var(--pf-global--BorderColor--300, #f0f0f0)); + --_border-bottom-color: var(--pf-c-form-control--BorderBottomColor, var(--pf-global--BorderColor--200, #8a8d90)); + --_border-left-color: var(--pf-c-form-control--BorderLeftColor, var(--pf-global--BorderColor--300, #f0f0f0)); + border-color: var(--_border-top-color) var(--_border-right-color) var(--_border-bottom-color) var(--_border-left-color); + border-radius: var(--pf-c-form-control--BorderRadius, 0); +} + +#container #nav #page-select #page-select-input::-webkit-inner-spin-button, +#container #nav #page-select #page-select-input::-webkit-outer-spin-button { + appearance: none; + margin: 0; +} + +#container #nav #page-select #page-select-input:focus { + --pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--focus--BorderBottomColor, var(--pf-global--primary-color--100, var(--pf-global--primary-color--dark-100, #06c))); + --_border-bottom-width: var(--pf-c-form-control--focus--BorderBottomWidth, var(--pf-global--BorderWidth--md, 2px)); + padding-bottom: var(--pf-c-form-control--focus--PaddingBottom, calc(var(--pf-global--spacer--form-element, 0.375rem) - var(--_border-bottom-width))); + border-bottom-width: var(--_border-bottom-width); +} + +#container #nav #page-select #page-select-input:hover { + --pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--hover--BorderBottomColor, var(--pf-global--primary-color--100, var(--pf-global--primary-color--dark-100, #06c))); +} + +/* This should be on the input component */ +#container #nav #page-select input:not(textarea) { + height: var(--pf-c-form-control--Height); + text-overflow: ellipsis; +} + +#container #nav .nav-control pf-button { + --pf-c-button--PaddingRight: var(--pf-c-pagination__nav-control--c-button--PaddingRight); + --pf-c-button--PaddingLeft: var(--pf-c-pagination__nav-control--c-button--PaddingLeft); + --pf-c-button--FontSize: var(--pf-c-pagination__nav-control--c-button--FontSize, var(--pf-global--FontSize--md, 1rem)); +} + +/* Bottom variant */ +#container.bottom { + --pf-c-pagination--child--MarginRight: var(--pf-c-pagination--m-bottom--child--MarginRight, 0); + --pf-c-pagination__nav-control--c-button--PaddingRight: var(--pf-c-pagination--m-bottom__nav-control--c-button--PaddingRight, var(--pf-global--spacer--md, 1rem)); + --pf-c-pagination__nav-control--c-button--PaddingLeft: var(--pf-c-pagination--m-bottom__nav-control--c-button--PaddingRight, var(--pf-global--spacer--md, 1rem)); + position: sticky; + bottom: var(--pf-c-pagination--m-bottom--Bottom, 0); + justify-content: center; + background-color: var(--pf-c-pagination--m-bottom--BackgroundColor, var(--pf-global--BackgroundColor--100, #fff)); + box-shadow: var(--pf-c-pagination--m-bottom--BoxShadow, var(--pf-global--BoxShadow--sm-top, 0 -0.125rem 0.25rem -0.0625rem rgba(3, 3, 3, 0.16))); +} + +#container.bottom #nav { + display: flex; + flex-basis: 100%; + justify-content: space-between; + visibility: visible; +} + +#container.bottom #nav #page-select { + display: flex; + align-items: center; + padding-right: var(--pf-c-pagination__nav-page-select--PaddingRight, var(--pf-global--spacer--md, 1rem)); + padding-left: var(--pf-c-pagination__nav-page-select--PaddingLeft, var(--pf-global--spacer--md, 1rem)); +} + +#container.bottom #nav .nav-control:first-child, +#container.bottom #nav #page-select, +#container.bottom #nav .nav-control:last-child { + display: none; + visibility: hidden; +} + +#container.bottom #nav .nav-control pf-button { + --pf-c-button--PaddingTop: var(--pf-c-pagination--m-bottom__nav-control--c-button--PaddingTop); + --pf-c-button--PaddingBottom: var(--pf-c-pagination--m-bottom__nav-control--c-button--PaddingBottom); + /* Can't set on pf-button */ + outline-offset: var(--pf-c-pagination--m-bottom__nav-control--c-button--OutlineOffset, 0); +} + +@media (min-width: 768px) { + #container.bottom { + --pf-c-pagination--m-bottom--BorderTopWidth: 0; + --pf-c-pagination--m-bottom--MarginTop: 0; + --pf-c-pagination--m-bottom--Bottom: auto; + position: relative; + justify-content: flex-end; + padding: var(--pf-c-pagination--m-bottom--md--PaddingTop, var(--pf-global--spacer--md, 1rem)) var(--pf-c-pagination--m-bottom--md--PaddingRight, var(--pf-global--spacer--md, 1rem)) var(--pf-c-pagination--m-bottom--md--PaddingBottom, var(--pf-global--spacer--md, 1rem)) var(--pf-c-pagination--m-bottom--md--PaddingLeft, var(--pf-global--spacer--md, 1rem)); + } + + #container.bottom #nav { + display: inline-flex; + flex-basis: auto; + } + + /* Not using modifier class, verify that this works correctly */ + #container.bottom #nav .nav-control:first-child, + #container.bottom #nav #page-select, + #container.bottom #nav .nav-control:last-child { + display: block; + visibility: visible; + } +} \ No newline at end of file diff --git a/elements/pf-pagination/pf-pagination.ts b/elements/pf-pagination/pf-pagination.ts new file mode 100644 index 0000000000..40e51b165a --- /dev/null +++ b/elements/pf-pagination/pf-pagination.ts @@ -0,0 +1,309 @@ +import { LitElement, html } from 'lit'; +import { property } from 'lit/decorators/property.js'; +import { state } from 'lit/decorators/state.js'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { query } from 'lit/decorators/query.js'; +import { queryAll } from 'lit/decorators/query-all.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { ComposedEvent } from '@patternfly/pfe-core'; +import { bound, observed } from '@patternfly/pfe-core/decorators.js'; +import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; + +import '@patternfly/elements/pf-button/pf-button.js'; + +import styles from './pf-pagination.css'; + +export class PaginationEvent extends ComposedEvent { + constructor(public eventType: PaginationEventType, public newPage: number, public perPage: number, public startIndex: number, public endIndex: number) { + super('paginated'); + } +} + +type PaginationEventType = 'page' | 'per-page'; + +enum Action { + First = 'first', + Previous = 'previous', + Next = 'next', + Last = 'last', + PerPage = 'per-page' +} + +const TITLES = { + items: '', + page: '', + pages: '', + itemsPerPage: 'Items per page', + perPageSuffix: 'per page', + toFirstPage: 'Go to first page', + toPreviousPage: 'Go to previous page', + toLastPage: 'Go to last page', + toNextPage: 'Go to next page', + optionsToggle: '', + currentPage: 'Current page', + paginationTitle: 'Pagination', + ofWord: 'of' +}; + +const PER_PAGE_OPTIONS = ['10', '20', '50', '100']; + +const SVG = { + [Action.First]: html``, + [Action.Previous]: html``, + [Action.Next]: html``, + [Action.Last]: html`` +}; + +/** + * Pagination + * @slot - Place element content here + */ +@customElement('pf-pagination') +export class PfPagination extends LitElement { + static readonly styles = [styles]; + + @property() variant = 'bottom'; + @property({ type: Number }) count!: number; + + @observed + @property({ type: Number, reflect: true, attribute: 'per-page' }) perPage = 10; + + @observed + @property({ type: Number, reflect: true }) page = 1; + + @query('#menu-toggle') private menuToggle!: HTMLButtonElement; + @query('#menu-list') private menuList!: HTMLUListElement; + @queryAll('.menu-item') private menuItems!: HTMLButtonElement[]; + @query('#page-select-input') private input!: HTMLInputElement; + + @state() _expanded = false; + + #tabindex = new RovingTabindexController(this); + + connectedCallback() { + super.connectedCallback(); + document.addEventListener('click', this._outsideClick); + this.addEventListener('click', this.#onClick); + this.addEventListener('keydown', this.#onKeydown); + this.#init(); + } + + render() { + return html` +
+
+ + ${this.#firstOfPage()}-${this.#lastOfPage()} ${TITLES.ofWord} ${this.count} + + +
+ +
+ `; + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('click', this._outsideClick); + this.removeEventListener('click', this.#onClick); + this.removeEventListener('keydown', this.#onKeydown); + } + + protected _pageChanged() { + this.#paginate('page'); + } + + protected _perPageChanged() { + this.#paginate('per-page'); + } + + @bound private _outsideClick(event: MouseEvent) { + const path = event.composedPath(); + if (!path.includes(this.menuToggle) && this._expanded) { + this._expanded = false; + } + } + + async #init() { + await this.updateComplete; + this.#tabindex.initItems([...this.menuItems], this.menuList); + } + + #firstOfPage() { + return (this.page - 1) * this.perPage + 1; + } + + #lastOfPage() { + return this.page * this.perPage; + } + + #previousPage() { + return this.page - 1 >= 1 ? this.page - 1 : 1; + } + + #nextPage() { + const lastPage = this.#lastPage(); + return this.page + 1 <= lastPage ? this.page + 1 : lastPage; + } + + #lastPage() { + return this.count || this.count === 0 ? this.#totalPages() || 0 : this.page + 1; + } + + #totalPages() { + return Math.ceil(this.count / this.perPage); + } + + #toggleExpanded() { + this._expanded = !this._expanded; + } + + #selected(option: string) { + return this.perPage?.toString() === option; + } + + #parsePageInput(value: string) { + const page = parseInt(value, 10); + if (!isNaN(page)) { + const lastPage = this.#lastPage(); + return page > lastPage ? lastPage : page < 1 ? 1 : page; + } + return this.page; + } + + #onClick(event: Event) { + const path = event.composedPath(); + // @todo + // @ts-ignore + const { dataset } = path.find(target => target.dataset?.action) || {}; + const { action, value } = dataset; + + switch (action) { + case Action.First: + this.page = 1; + return; + case Action.Previous: + this.page = this.#previousPage(); + return; + case Action.Next: + this.page = this.#nextPage(); + return; + case Action.Last: + this.page = this.#lastPage(); + return; + case Action.PerPage: + if (value) { + this.perPage = parseInt(value, 10); + this.#toggleExpanded(); + } + return; + } + } + + #onKeydown(event: KeyboardEvent) { + switch (event.key) { + case 'Enter': + if (event.composedPath().includes(this.input)) { + this.page = parseInt(this.input.value, 10); + } else { + // @todo + this.#onClick(event); + } + } + } + + #onChange(event: Event) { + const input = event.target as HTMLInputElement; + input.value = this.#parsePageInput(input.value).toString(); + } + + #paginate(type: PaginationEventType) { + const startIndex = (this.page - 1) * this.perPage; + const endIndex = this.page * this.perPage; + this.dispatchEvent(new PaginationEvent(type, this.page, this.perPage, startIndex, endIndex)); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-pagination': PfPagination; + } +} diff --git a/elements/pf-pagination/test/pf-pagination.e2e.ts b/elements/pf-pagination/test/pf-pagination.e2e.ts new file mode 100644 index 0000000000..aea8296fdc --- /dev/null +++ b/elements/pf-pagination/test/pf-pagination.e2e.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; + +const tagName = 'pf-pagination'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); +}); diff --git a/elements/pf-pagination/test/pf-pagination.spec.ts b/elements/pf-pagination/test/pf-pagination.spec.ts new file mode 100644 index 0000000000..151ef7bf63 --- /dev/null +++ b/elements/pf-pagination/test/pf-pagination.spec.ts @@ -0,0 +1,18 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfPagination } from '@patternfly/elements/pf-pagination/pf-pagination.js'; + +const element = html` + +`; + +describe('', function() { + it('should upgrade', async function() { + const el = await createFixture (element); + const klass = customElements.get('pf-pagination'); + expect(el) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfPagination); + }); +});