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 @@
+
+
+
+
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`
+
+
+
+
+
+
+
+ `;
+ }
+
+ 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);
+ });
+});