From ecf261d2c1d2684f77a081eff84574ed83497f14 Mon Sep 17 00:00:00 2001
From: Ivana Rodriguez <ivrodrig@redhat.com>
Date: Tue, 28 Mar 2023 10:13:11 -0400
Subject: [PATCH 1/3] feat: pf-pagination bottom variant

---
 elements/package.json                         |   3 +-
 elements/pf-button/pf-button.css              |   2 +-
 elements/pf-pagination/README.md              |  11 +
 elements/pf-pagination/demo/demo.css          |   7 +
 .../pf-pagination/demo/pf-pagination.html     |   7 +
 elements/pf-pagination/demo/pf-pagination.js  |   1 +
 elements/pf-pagination/docs/pf-pagination.md  |  17 +
 elements/pf-pagination/pf-pagination.css      | 283 +++++++++++++++++
 elements/pf-pagination/pf-pagination.ts       | 296 ++++++++++++++++++
 .../pf-pagination/test/pf-pagination.e2e.ts   |  12 +
 .../pf-pagination/test/pf-pagination.spec.ts  |  18 ++
 11 files changed, 655 insertions(+), 2 deletions(-)
 create mode 100644 elements/pf-pagination/README.md
 create mode 100644 elements/pf-pagination/demo/demo.css
 create mode 100644 elements/pf-pagination/demo/pf-pagination.html
 create mode 100644 elements/pf-pagination/demo/pf-pagination.js
 create mode 100644 elements/pf-pagination/docs/pf-pagination.md
 create mode 100644 elements/pf-pagination/pf-pagination.css
 create mode 100644 elements/pf-pagination/pf-pagination.ts
 create mode 100644 elements/pf-pagination/test/pf-pagination.e2e.ts
 create mode 100644 elements/pf-pagination/test/pf-pagination.spec.ts

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-button/pf-button.css b/elements/pf-button/pf-button.css
index b45267f432..8db4157902 100644
--- a/elements/pf-button/pf-button.css
+++ b/elements/pf-button/pf-button.css
@@ -570,7 +570,7 @@ button:hover {
 :host([block]) button {
   display: flex;
   width: 100%;
-  justify-content: center;
+  justify-content: var(--pf-c-button--JustifyContent, center);
 }
 
 /******************************
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
+<pf-pagination>
+
+</pf-pagination>
+```
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 @@
+<link rel="stylesheet" href="demo.css" />
+<script type="module" src="pf-pagination.js"></script>
+
+<section>
+  <h2>Bottom</h2>
+  <pf-pagination count="100"></pf-pagination>
+</section>
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 %}
+  <pf-pagination></pf-pagination>
+{% 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..ad2ce80de2
--- /dev/null
+++ b/elements/pf-pagination/pf-pagination.css
@@ -0,0 +1,283 @@
+: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--JustifyContent: start;
+  --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..754d9304e9
--- /dev/null
+++ b/elements/pf-pagination/pf-pagination.ts
@@ -0,0 +1,296 @@
+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 { 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 '@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`<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 448 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M223.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L319.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L393.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34zm-192 34l136 136c9.4 9.4 24.6 9.4 33.9 0l22.6-22.6c9.4-9.4 9.4-24.6 0-33.9L127.9 256l96.4-96.4c9.4-9.4 9.4-24.6 0-33.9L201.7 103c-9.4-9.4-24.6-9.4-33.9 0l-136 136c-9.5 9.4-9.5 24.6-.1 34z"></path></svg>`,
+  [Action.Previous]: html`<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 256 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"></path></svg>`,
+  [Action.Next]: html`<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 256 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path></svg>`,
+  [Action.Last]: html`<svg fill="currentColor" height="1em" width="1em" viewBox="0 0 448 512" aria-hidden="true" role="img" style="vertical-align: -0.125em;"><path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34zm192-34l-136-136c-9.4-9.4-24.6-9.4-33.9 0l-22.6 22.6c-9.4 9.4-9.4 24.6 0 33.9l96.4 96.4-96.4 96.4c-9.4 9.4-9.4 24.6 0 33.9l22.6 22.6c9.4 9.4 24.6 9.4 33.9 0l136-136c9.4-9.2 9.4-24.4 0-33.8z"></path></svg>`
+};
+
+/**
+ * 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('#page-select-input') private input!: HTMLInputElement;
+
+  @state() _expanded = false;
+
+  connectedCallback() {
+    super.connectedCallback();
+    document.addEventListener('click', this._outsideClick);
+    this.addEventListener('click', this.#onClick);
+    this.addEventListener('keydown', this.#onKeydown);
+  }
+
+  render() {
+    return html`
+      <div id="container"
+           class="${classMap({ [this.variant]: !!this.variant })}"
+           part="container">
+        <div id="options-menu"
+             class="${classMap({ expanded: this._expanded })}">
+          <pf-button id="menu-toggle"
+                     aria-expanded=${this._expanded}
+                     aria-haspopup="listbox"
+                     icon="caret-${this._expanded ? 'up' : 'down'}" 
+                     icon-position="right"
+                     part="menu-toggle"
+                     plain
+                     type="button"
+                     @click=${this.#toggleExpanded}>
+              <span><b>${this.#firstOfPage()}-${this.#lastOfPage()}</b> ${TITLES.ofWord} <b>${this.count}</b></span>
+          </pf-button>
+          <ul id="menu-list"
+              part="menu-list">
+            ${PER_PAGE_OPTIONS.map(option => html`<li role="none">
+              <pf-button variant="tertiary"
+                         block
+                         class="${classMap({ ['menu-item']: true, selected: this.#selected(option) })}"
+                         data-action=${Action.PerPage}
+                         data-value=${option}
+                         icon=${ifDefined(this.#selected(option) ? 'check' : undefined)} 
+                         icon-position=${ifDefined(this.#selected(option) ? 'right' : undefined)}
+                         part="menu-item"
+                         role="menuitem">
+                <span>${option} ${TITLES.perPageSuffix}</span>
+              </pf-button>
+            </li>`)}
+          </ul>
+        </div>
+        <nav id="nav"
+             aria-label="Pagination"
+             part="nav">
+          <div class="nav-control">
+            <pf-button id="first-page-button"
+                       aria-label=${TITLES.toFirstPage}
+                       data-action=${Action.First}
+                       plain
+                       .disabled=${this.page === 0 || this.page === 1}>
+              ${SVG[Action.First]}
+            </pf-button>
+          </div>
+          <div class="nav-control">
+            <pf-button id="previous-page-button"
+                       aria-label=${TITLES.toPreviousPage}
+                       data-action=${Action.Previous}
+                       plain
+                       .disabled=${this.page === 0 || this.page === 1}>
+              ${SVG[Action.Previous]}
+            </pf-button>
+          </div>
+          <div id="page-select">
+            <input id="page-select-input"
+                   aria-label=${TITLES.currentPage}
+                   inputmode="numeric"
+                   max=${this.#lastPage()}
+                   min="1"
+                   required
+                   type="number"
+                   .value=${this.page.toString()}
+                   @change=${this.#onChange}/> 
+            <span aria-hidden="true" ?hidden=${!this.count}>${TITLES.ofWord} ${this.#lastPage()}</span>
+          </div>
+          <div class="nav-control">
+            <pf-button id="next-page-button"
+                       aria-label=${TITLES.toNextPage}
+                       data-action=${Action.Next} 
+                       plain
+                       .disabled=${this.page === this.#lastPage()}>
+              ${SVG[Action.Next]}
+            </pf-button>
+          </div>
+          <div class="nav-control">
+            <pf-button id="last-page-button"
+                       aria-label=${TITLES.toLastPage}
+                       data-action=${Action.Last}
+                       plain
+                       .disabled=${this.page === this.#lastPage()}>
+              ${SVG[Action.Last]}
+            </pf-button>
+          </div>
+        </nav>
+      </div>
+    `;
+  }
+
+  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;
+    }
+  }
+
+  #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`
+  <pf-pagination></pf-pagination>
+`;
+
+describe('<pf-pagination>', function() {
+  it('should upgrade', async function() {
+    const el = await createFixture <PfPagination> (element);
+    const klass = customElements.get('pf-pagination');
+    expect(el)
+      .to.be.an.instanceOf(klass)
+      .and
+      .to.be.an.instanceOf(PfPagination);
+  });
+});

From 1f3d6c5d4f9887b6baa75fefed7265d37c6ad271 Mon Sep 17 00:00:00 2001
From: Ivana Rodriguez <ivrodrig@redhat.com>
Date: Wed, 29 Mar 2023 14:33:43 -0400
Subject: [PATCH 2/3] feat: leverage controller for per-page menu a11y

---
 elements/pf-pagination/pf-pagination.ts | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/elements/pf-pagination/pf-pagination.ts b/elements/pf-pagination/pf-pagination.ts
index 754d9304e9..40e51b165a 100644
--- a/elements/pf-pagination/pf-pagination.ts
+++ b/elements/pf-pagination/pf-pagination.ts
@@ -3,10 +3,13 @@ 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';
@@ -70,15 +73,20 @@ export class PfPagination extends LitElement {
   @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() {
@@ -194,6 +202,11 @@ export class PfPagination extends LitElement {
     }
   }
 
+  async #init() {
+    await this.updateComplete;
+    this.#tabindex.initItems([...this.menuItems], this.menuList);
+  }
+
   #firstOfPage() {
     return (this.page - 1) * this.perPage + 1;
   }

From ad26eb27c084cba90c40d0842e2d63c6d07b7bbc Mon Sep 17 00:00:00 2001
From: Ivana Rodriguez <ivrodrig@redhat.com>
Date: Tue, 4 Apr 2023 09:25:19 -0400
Subject: [PATCH 3/3] chore: these changes should be on a separate PR

---
 elements/pf-button/pf-button.css         | 2 +-
 elements/pf-pagination/pf-pagination.css | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/elements/pf-button/pf-button.css b/elements/pf-button/pf-button.css
index 8db4157902..b45267f432 100644
--- a/elements/pf-button/pf-button.css
+++ b/elements/pf-button/pf-button.css
@@ -570,7 +570,7 @@ button:hover {
 :host([block]) button {
   display: flex;
   width: 100%;
-  justify-content: var(--pf-c-button--JustifyContent, center);
+  justify-content: center;
 }
 
 /******************************
diff --git a/elements/pf-pagination/pf-pagination.css b/elements/pf-pagination/pf-pagination.css
index ad2ce80de2..8abbafd752 100644
--- a/elements/pf-pagination/pf-pagination.css
+++ b/elements/pf-pagination/pf-pagination.css
@@ -112,7 +112,6 @@
 
 #container #options-menu #menu-list .menu-item {
   white-space: nowrap;
-  --pf-c-button--JustifyContent: start;
   --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));