From cfcf45d553bc3ab8c3f5e8b8d59a9a2a22e4702e Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Tue, 18 Jul 2023 14:34:17 -0400
Subject: [PATCH 01/40] feat(core): added `ariaActivedescendant` to
`internals-controller`
---
core/pfe-core/controllers/internals-controller.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/core/pfe-core/controllers/internals-controller.ts b/core/pfe-core/controllers/internals-controller.ts
index a7be52c2d3..c46eeacb08 100644
--- a/core/pfe-core/controllers/internals-controller.ts
+++ b/core/pfe-core/controllers/internals-controller.ts
@@ -8,6 +8,7 @@ export class InternalsController implements ReactiveController, ARIAMixin {
declare role: ARIAMixin['role'];
declare ariaAtomic: ARIAMixin['ariaAtomic'];
declare ariaAutoComplete: ARIAMixin['ariaAutoComplete'];
+ declare ariaActivedescendant: string | null;
declare ariaBusy: ARIAMixin['ariaBusy'];
declare ariaChecked: ARIAMixin['ariaChecked'];
declare ariaColCount: ARIAMixin['ariaColCount'];
From 20be42c85993283c8bb1f5bb8bc5cda9bea571e9 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Tue, 18 Jul 2023 14:36:07 -0400
Subject: [PATCH 02/40] feat(listbox): init pf-listbox, pf-listbox-group, and
pf-listbox-option
---
elements/package.json | 3 ++
elements/pf-listbox/README.md | 11 +++++
elements/pf-listbox/demo/demo.css | 3 ++
elements/pf-listbox/demo/pf-listbox.html | 28 +++++++++++++
elements/pf-listbox/demo/pf-listbox.js | 1 +
elements/pf-listbox/docs/pf-listbox.md | 17 ++++++++
elements/pf-listbox/pf-listbox-group.css | 3 ++
elements/pf-listbox/pf-listbox-group.ts | 31 ++++++++++++++
elements/pf-listbox/pf-listbox-option.css | 3 ++
elements/pf-listbox/pf-listbox-option.ts | 41 +++++++++++++++++++
elements/pf-listbox/pf-listbox.css | 3 ++
elements/pf-listbox/pf-listbox.ts | 45 +++++++++++++++++++++
elements/pf-listbox/test/pf-listbox.e2e.ts | 12 ++++++
elements/pf-listbox/test/pf-listbox.spec.ts | 21 ++++++++++
14 files changed, 222 insertions(+)
create mode 100644 elements/pf-listbox/README.md
create mode 100644 elements/pf-listbox/demo/demo.css
create mode 100644 elements/pf-listbox/demo/pf-listbox.html
create mode 100644 elements/pf-listbox/demo/pf-listbox.js
create mode 100644 elements/pf-listbox/docs/pf-listbox.md
create mode 100644 elements/pf-listbox/pf-listbox-group.css
create mode 100644 elements/pf-listbox/pf-listbox-group.ts
create mode 100644 elements/pf-listbox/pf-listbox-option.css
create mode 100644 elements/pf-listbox/pf-listbox-option.ts
create mode 100644 elements/pf-listbox/pf-listbox.css
create mode 100644 elements/pf-listbox/pf-listbox.ts
create mode 100644 elements/pf-listbox/test/pf-listbox.e2e.ts
create mode 100644 elements/pf-listbox/test/pf-listbox.spec.ts
diff --git a/elements/package.json b/elements/package.json
index 6c6965fe06..68a7a1fca6 100644
--- a/elements/package.json
+++ b/elements/package.json
@@ -36,6 +36,9 @@
"./pf-jump-links/pf-jump-links.js": "./pf-jump-links/pf-jump-links.js",
"./pf-label/BaseLabel.js": "./pf-label/BaseLabel.js",
"./pf-label/pf-label.js": "./pf-label/pf-label.js",
+ "./pf-listbox/pf-listbox.js": "./pf-listbox/pf-listbox.js",
+ "./pf-listbox/pf-listbox-group.js": "./pf-listbox/pf-listbox-group.js",
+ "./pf-listbox/pf-listbox-option.js": "./pf-listbox/pf-listbox-option.js",
"./pf-modal/pf-modal.js": "./pf-modal/pf-modal.js",
"./pf-panel/pf-panel.js": "./pf-panel/pf-panel.js",
"./pf-progress-stepper/pf-progress-step.js": "./pf-progress-stepper/pf-progress-step.js",
diff --git a/elements/pf-listbox/README.md b/elements/pf-listbox/README.md
new file mode 100644
index 0000000000..b09287b7d3
--- /dev/null
+++ b/elements/pf-listbox/README.md
@@ -0,0 +1,11 @@
+# Listbox
+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-listbox/demo/demo.css b/elements/pf-listbox/demo/demo.css
new file mode 100644
index 0000000000..8e786b884f
--- /dev/null
+++ b/elements/pf-listbox/demo/demo.css
@@ -0,0 +1,3 @@
+pf-listbox {
+ /* insert demo styles */
+}
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
new file mode 100644
index 0000000000..18af296489
--- /dev/null
+++ b/elements/pf-listbox/demo/pf-listbox.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+
+
+ Breakfast
+ Eggs
+ Toast
+ Waffles
+
+
+ Lunch
+ Salad
+ Sandwich
+ Soup
+
+
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-listbox/demo/pf-listbox.js
new file mode 100644
index 0000000000..dbfb27a103
--- /dev/null
+++ b/elements/pf-listbox/demo/pf-listbox.js
@@ -0,0 +1 @@
+import '@patternfly/elements/pf-listbox/pf-listbox.js';
diff --git a/elements/pf-listbox/docs/pf-listbox.md b/elements/pf-listbox/docs/pf-listbox.md
new file mode 100644
index 0000000000..f3745b57be
--- /dev/null
+++ b/elements/pf-listbox/docs/pf-listbox.md
@@ -0,0 +1,17 @@
+{% renderOverview %}
+
+{% endrenderOverview %}
+
+{% band header="Usage" %}{% endband %}
+
+{% renderSlots %}{% endrenderSlots %}
+
+{% renderAttributes %}{% endrenderAttributes %}
+
+{% renderMethods %}{% endrenderMethods %}
+
+{% renderEvents %}{% endrenderEvents %}
+
+{% renderCssCustomProperties %}{% endrenderCssCustomProperties %}
+
+{% renderCssParts %}{% endrenderCssParts %}
diff --git a/elements/pf-listbox/pf-listbox-group.css b/elements/pf-listbox/pf-listbox-group.css
new file mode 100644
index 0000000000..5d4e87f30f
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox-group.css
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/elements/pf-listbox/pf-listbox-group.ts b/elements/pf-listbox/pf-listbox-group.ts
new file mode 100644
index 0000000000..14096e195e
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox-group.ts
@@ -0,0 +1,31 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+
+import styles from './pf-listbox-group.css';
+
+/**
+ * Group of options within a listbox
+ * @slot - Place element content here
+ */
+@customElement('pf-listbox-group')
+export class PfListboxGroup extends LitElement {
+ static readonly styles = [styles];
+
+ #internals = new InternalsController(this, {
+ role: 'group'
+ });
+
+ render() {
+ return html`
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-listbox-group': PfListboxGroup;
+ }
+}
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-listbox/pf-listbox-option.css
new file mode 100644
index 0000000000..5d4e87f30f
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox-option.css
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
new file mode 100644
index 0000000000..5dba3859d7
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -0,0 +1,41 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+
+import styles from './pf-listbox-option.css';
+
+/**
+ * Option within a listbox
+ * @slot - Place element content here
+ */
+@customElement('pf-listbox-option')
+export class PfListboxOption extends LitElement {
+ static readonly styles = [styles];
+
+ #internals = new InternalsController(this, {
+ role: 'option'
+ });
+
+ updateSet(setSize?: number | null, posInSet?: number | null) {
+ if (!setSize || !posInSet) {
+ this.#internals.ariaSetSize = null;
+ this.#internals.ariaPosInSet = null;
+ } else {
+ const parsedSize = Math.max(-1, setSize);
+ this.#internals.ariaSetSize = `${parsedSize}`;
+ this.#internals.ariaPosInSet = `${Math.min(Math.max(1, posInSet), parsedSize)}`;
+ }
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-listbox-option': PfListboxOption;
+ }
+}
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
new file mode 100644
index 0000000000..5d4e87f30f
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox.css
@@ -0,0 +1,3 @@
+:host {
+ display: block;
+}
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
new file mode 100644
index 0000000000..0bb7e24e18
--- /dev/null
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -0,0 +1,45 @@
+import { LitElement, html } from 'lit';
+import { customElement } from 'lit/decorators/custom-element.js';
+import { property } from 'lit/decorators/property.js';
+import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+import { PfListboxOption } from './pf-listbox-option.js';
+import { PfListboxGroup } from './pf-listbox-group.js';
+
+import styles from './pf-listbox.css';
+
+/**
+ * List of selectable items
+ * @slot - Place element content here
+ */
+@customElement('pf-listbox')
+export class PfListbox extends LitElement {
+ static readonly styles = [styles];
+
+ #internals = new InternalsController(this, {
+ role: 'listbox'
+ });
+
+ get isVertical(): boolean {
+ return this?.getAttribute('aria-orientatiom') === 'vertical';
+ }
+
+ get isMultiselectable(): boolean {
+ return this?.getAttribute('aria-multiselectable') === 'true';
+ }
+
+ #updateActiveDescendant(idRef: string | null) {
+ this.#internals.ariaActivedescendant = idRef;
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'pf-listbox': PfListbox;
+ }
+}
diff --git a/elements/pf-listbox/test/pf-listbox.e2e.ts b/elements/pf-listbox/test/pf-listbox.e2e.ts
new file mode 100644
index 0000000000..60afcec218
--- /dev/null
+++ b/elements/pf-listbox/test/pf-listbox.e2e.ts
@@ -0,0 +1,12 @@
+import { test } from '@playwright/test';
+import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js';
+
+const tagName = 'pf-listbox';
+
+test.describe(tagName, () => {
+ test('snapshot', async ({ page }) => {
+ const componentPage = new PfeDemoPage(page, tagName);
+ await componentPage.navigate();
+ await componentPage.snapshot();
+ });
+});
diff --git a/elements/pf-listbox/test/pf-listbox.spec.ts b/elements/pf-listbox/test/pf-listbox.spec.ts
new file mode 100644
index 0000000000..0601c9f1d8
--- /dev/null
+++ b/elements/pf-listbox/test/pf-listbox.spec.ts
@@ -0,0 +1,21 @@
+import { expect, html } from '@open-wc/testing';
+import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
+import { PfListbox } from '@patternfly/elements/pf-listbox/pf-listbox.js';
+
+describe('', function() {
+ describe('simply instantiating', function() {
+ let element: PfListbox;
+ it('imperatively instantiates', function() {
+ expect(document.createElement('pf-listbox')).to.be.an.instanceof(PfListbox);
+ });
+
+ it('should upgrade', async function() {
+ element = await createFixture(html` `);
+ const klass = customElements.get('pf-listbox');
+ expect(element)
+ .to.be.an.instanceOf(klass)
+ .and
+ .to.be.an.instanceOf(PfListbox);
+ });
+ });
+});
From 12f6c52f3b70752f6677453e7aa15e10a04c27a0 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Tue, 18 Jul 2023 15:17:52 -0400
Subject: [PATCH 03/40] feat(listbox): init pf-listbox-item
---
elements/pf-listbox/demo/pf-listbox.js | 2 ++
elements/pf-listbox/pf-listbox.ts | 23 ++++++++++++++++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-listbox/demo/pf-listbox.js
index dbfb27a103..462f34e98c 100644
--- a/elements/pf-listbox/demo/pf-listbox.js
+++ b/elements/pf-listbox/demo/pf-listbox.js
@@ -1 +1,3 @@
import '@patternfly/elements/pf-listbox/pf-listbox.js';
+import '@patternfly/elements/pf-listbox/pf-listbox-item.js';
+import '@patternfly/elements/pf-listbox/pf-listbox-group.js';
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 0bb7e24e18..34f1166f4c 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -1,6 +1,7 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
+import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
import { PfListboxOption } from './pf-listbox-option.js';
import { PfListboxGroup } from './pf-listbox-group.js';
@@ -15,10 +16,18 @@ import styles from './pf-listbox.css';
export class PfListbox extends LitElement {
static readonly styles = [styles];
+ static isOption(element: PfListboxOption): element is PfListboxOption {
+ return element instanceof PfListboxOption;
+ }
+
+ @queryAssignedElements() private options!: PfListboxOption[];
+
#internals = new InternalsController(this, {
role: 'listbox'
});
+ #_allOptions: PfListboxOption[] = [];
+
get isVertical(): boolean {
return this?.getAttribute('aria-orientatiom') === 'vertical';
}
@@ -27,13 +36,25 @@ export class PfListbox extends LitElement {
return this?.getAttribute('aria-multiselectable') === 'true';
}
+ get #allOptions() {
+ return this.#_allOptions;
+ }
+
+ set #allOptions(options: PfListboxOption[]) {
+ this.#_allOptions = options.filter(options => (this.constructor as typeof PfListbox).isOption(options));
+ }
+
+ #onSlotchange() {
+ this.#_allOptions = this.options;
+ }
+
#updateActiveDescendant(idRef: string | null) {
this.#internals.ariaActivedescendant = idRef;
}
render() {
return html`
-
+
`;
}
}
From 02e352bdd50a8c83ff792d9803ff7c9aa67e3e3c Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Wed, 19 Jul 2023 09:27:24 -0400
Subject: [PATCH 04/40] feat(core): added orientation to arrow key handling
---
.../controllers/roving-tabindex-controller.ts | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts
index e4a2ca53f1..f456b62256 100644
--- a/core/pfe-core/controllers/roving-tabindex-controller.ts
+++ b/core/pfe-core/controllers/roving-tabindex-controller.ts
@@ -101,20 +101,31 @@ export class RovingTabindexController<
this.#focusableItems.includes(x as ItemType))) {
return;
}
+
+ const role = this.host.getAttribute('role');
+ const orientation = this.host.getAttribute('aria-orientation');
+
const item = this.activeItem;
let shouldPreventDefault = false;
const horizontalOnly =
!item ? false
: item.tagName === 'SELECT' ||
- item.getAttribute('role') === 'spinbutton';
+ item.getAttribute('role') === 'spinbutton' || orientation === 'horizontal';
+ const verticalOnly = orientation === 'vertical';
switch (event.key) {
case 'ArrowLeft':
+ if (verticalOnly) {
+ return;
+ }
this.focusOnItem(this.prevItem);
shouldPreventDefault = true;
break;
case 'ArrowRight':
+ if (verticalOnly) {
+ return;
+ }
this.focusOnItem(this.nextItem);
shouldPreventDefault = true;
break;
From 9b8cdafd584bb84570145ee28ad78fe86eb06806 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Wed, 19 Jul 2023 09:37:25 -0400
Subject: [PATCH 05/40] fix(core): removed pageUp and pageDown key handlers
because default behavior is more accessible
---
.../controllers/roving-tabindex-controller.ts | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts
index f456b62256..112c2c475e 100644
--- a/core/pfe-core/controllers/roving-tabindex-controller.ts
+++ b/core/pfe-core/controllers/roving-tabindex-controller.ts
@@ -147,24 +147,10 @@ export class RovingTabindexController<
this.focusOnItem(this.firstItem);
shouldPreventDefault = true;
break;
- case 'PageUp':
- if (horizontalOnly) {
- return;
- }
- this.focusOnItem(this.firstItem);
- shouldPreventDefault = true;
- break;
case 'End':
this.focusOnItem(this.lastItem);
shouldPreventDefault = true;
break;
- case 'PageDown':
- if (horizontalOnly) {
- return;
- }
- this.focusOnItem(this.lastItem);
- shouldPreventDefault = true;
- break;
default:
break;
}
From d6a46cc5f4eb4d0401ccd76e8563d6279dfd7aa5 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:06:26 -0400
Subject: [PATCH 06/40] feat(core): fixed roving-tabindex-controller's
`updateActiveItem` so it only happens if item changes
---
core/pfe-core/controllers/roving-tabindex-controller.ts | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts
index 112c2c475e..5339f3a6fb 100644
--- a/core/pfe-core/controllers/roving-tabindex-controller.ts
+++ b/core/pfe-core/controllers/roving-tabindex-controller.ts
@@ -102,7 +102,6 @@ export class RovingTabindexController<
return;
}
- const role = this.host.getAttribute('role');
const orientation = this.host.getAttribute('aria-orientation');
const item = this.activeItem;
@@ -112,8 +111,6 @@ export class RovingTabindexController<
: item.tagName === 'SELECT' ||
item.getAttribute('role') === 'spinbutton' || orientation === 'horizontal';
const verticalOnly = orientation === 'vertical';
-
-
switch (event.key) {
case 'ArrowLeft':
if (verticalOnly) {
@@ -165,8 +162,8 @@ export class RovingTabindexController<
* sets tabindex of item based on whether or not it is active
*/
updateActiveItem(item?: ItemType): void {
- if (item) {
- if (!!this.#activeItem && item !== this.#activeItem) {
+ if (item && item !== this.#activeItem) {
+ if (this.#activeItem) {
this.#activeItem.tabIndex = -1;
}
item.tabIndex = 0;
From c647911184ebd06eeb9194dc316214d2c5440c0e Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:08:24 -0400
Subject: [PATCH 07/40] feat(elements): added listbox-optiontemporary styles
for filtered and selected items
---
elements/pf-listbox/pf-listbox-option.css | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-listbox/pf-listbox-option.css
index 5d4e87f30f..3357654601 100644
--- a/elements/pf-listbox/pf-listbox-option.css
+++ b/elements/pf-listbox/pf-listbox-option.css
@@ -1,3 +1,12 @@
:host {
display: block;
}
+
+:host([hidden]),
+:host([hidden-by-filter]) {
+ display: none !important;
+}
+
+:host([aria-selected="true"]) {
+ background-color: #ddd;
+}
\ No newline at end of file
From b95ba6787e9fcf3d381d92f3f40bed40e6305f71 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:09:03 -0400
Subject: [PATCH 08/40] feat(elements): added filtering to listbox-option
---
elements/pf-listbox/pf-listbox-option.ts | 29 ++++++++++++++++++++----
1 file changed, 25 insertions(+), 4 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index 5dba3859d7..97084be3ea 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -1,6 +1,7 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+import { getRandomId } from '@patternfly/pfe-core/functions/random.js';
import styles from './pf-listbox-option.css';
@@ -16,6 +17,27 @@ export class PfListboxOption extends LitElement {
role: 'option'
});
+ override connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener('focus', this.#onFocus);
+ this.id = this.id || getRandomId();
+ }
+
+ getUpdateByFilter(filter = '') {
+ if (filter === '' || (this.textContent || '').toLowerCase().match(filter)) {
+ this.removeAttribute('hidden-by-filter');
+ } else {
+ this.setAttribute('hidden-by-filter', 'hidden-by-filter');
+ }
+ return !this.hasAttribute('hidden-by-filter');
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+
updateSet(setSize?: number | null, posInSet?: number | null) {
if (!setSize || !posInSet) {
this.#internals.ariaSetSize = null;
@@ -27,10 +49,9 @@ export class PfListboxOption extends LitElement {
}
}
- render() {
- return html`
-
- `;
+ #onFocus() {
+ // console.log('focus');
+ // this.dispatchEvent(new Event('focus', { bubbles: true }));
}
}
From dc739c7fa56641b1e75afbf26fe3d46aad015672 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:09:33 -0400
Subject: [PATCH 09/40] feat(elements): added temporary orientation styles to
listbox
---
elements/pf-listbox/pf-listbox.css | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 5d4e87f30f..b4741c0e42 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -1,3 +1,16 @@
:host {
display: block;
}
+
+slot {
+ display: flex;
+ flex-direction: column;
+}
+
+:host([aria-orientation="horizontal"]) slot {
+ flex-direction: row;
+}
+
+::slotted(*) {
+ margin: 2px 10px;
+}
From 1c0cdd6ca5ec4ef333ea6a0f609b679a1cf13399 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:10:10 -0400
Subject: [PATCH 10/40] feat(elements): added more variations to demo
---
elements/pf-listbox/demo/pf-listbox.html | 65 +++++++++++++++++-------
elements/pf-listbox/demo/pf-listbox.js | 2 -
2 files changed, 46 insertions(+), 21 deletions(-)
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
index 18af296489..5fb1a76404 100644
--- a/elements/pf-listbox/demo/pf-listbox.html
+++ b/elements/pf-listbox/demo/pf-listbox.html
@@ -1,28 +1,55 @@
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
+aria-orientation="horizontal"
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+aria-orientation="vertical"
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+aria-multiselectable="true"
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+pf-listbox-group
-
+
Breakfast
- Eggs
- Toast
- Waffles
-
-
+ Eggs
+ Toast
+ Waffles
+
+
Lunch
- Salad
- Sandwich
- Soup
-
+ Salad
+ Sandwich
+ Soup
+
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-listbox/demo/pf-listbox.js
index 462f34e98c..dbfb27a103 100644
--- a/elements/pf-listbox/demo/pf-listbox.js
+++ b/elements/pf-listbox/demo/pf-listbox.js
@@ -1,3 +1 @@
import '@patternfly/elements/pf-listbox/pf-listbox.js';
-import '@patternfly/elements/pf-listbox/pf-listbox-item.js';
-import '@patternfly/elements/pf-listbox/pf-listbox-group.js';
From fc55e07f5ef638123c5715b43b726cd261bc1897 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 09:14:48 -0400
Subject: [PATCH 11/40] feat(elements): added roving-tabindex-controller and
filtering to listbox
---
elements/pf-listbox/pf-listbox.ts | 236 ++++++++++++++++++++++++++++--
1 file changed, 227 insertions(+), 9 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 34f1166f4c..c03327b082 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -1,15 +1,18 @@
import { LitElement, html } from 'lit';
+import type { PropertyValues } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js';
import { PfListboxOption } from './pf-listbox-option.js';
-import { PfListboxGroup } from './pf-listbox-group.js';
+import './pf-listbox-group.js';
import styles from './pf-listbox.css';
/**
* List of selectable items
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/listbox/|WAI-ARIA Listbox Pattern}
* @slot - Place element content here
*/
@customElement('pf-listbox')
@@ -20,43 +23,258 @@ export class PfListbox extends LitElement {
return element instanceof PfListboxOption;
}
+ @property() filter = '';
+ @property({ reflect: true, attribute: 'disable-filter' }) disableFilter = '';
+
@queryAssignedElements() private options!: PfListboxOption[];
#internals = new InternalsController(this, {
role: 'listbox'
});
+ #tabindex = new RovingTabindexController(this);
+
#_allOptions: PfListboxOption[] = [];
- get isVertical(): boolean {
- return this?.getAttribute('aria-orientatiom') === 'vertical';
+ #hasFocus = false;
+
+ get isHorizontal(): boolean {
+ return this?.getAttribute('aria-orientation') === 'horizontal';
}
get isMultiselectable(): boolean {
return this?.getAttribute('aria-multiselectable') === 'true';
}
+ get activeItem() {
+ const [active] = this.#allOptions.filter(option => option.getAttribute('id') === this.#internals.ariaActivedescendant);
+ return active || this.#tabindex.firstItem;
+ }
+
get #allOptions() {
return this.#_allOptions;
}
set #allOptions(options: PfListboxOption[]) {
- this.#_allOptions = options.filter(options => (this.constructor as typeof PfListbox).isOption(options));
+ this.#_allOptions = options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.disableFilter || option.getUpdateByFilter(this.filter)));
}
- #onSlotchange() {
- this.#_allOptions = this.options;
+ set value(items: string | null) {
+ const oldValue = this.value;
+ const selectedItems = items?.toLowerCase().split(',');
+ const [firstItem] = selectedItems || [null];
+ this.options.forEach(option => {
+ const textContent = (option.textContent || '').toLowerCase();
+ const selected = this.isMultiselectable ? selectedItems?.includes(textContent) : firstItem === textContent;
+ if (selected) {
+ option.setAttribute('aria-selected', 'true');
+ } else {
+ option.removeAttribute('aria-selected');
+ }
+ });
+ if (oldValue !== this.value) {
+ this.#fireInput();
+ }
+ }
+
+ get value() {
+ const selectedItems = this.options.filter(option=>option.hasAttribute('aria-selected')).map(option => option.textContent);
+ return selectedItems.join(',');
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener('focus', this.#onOptionFocus);
}
- #updateActiveDescendant(idRef: string | null) {
- this.#internals.ariaActivedescendant = idRef;
+ updated(changed: PropertyValues) {
+ if (changed.has('filter')) {
+ this.#onFilterChange();
+ }
}
render() {
return html`
-
+
+
`;
}
+
+ /**
+ * filters listbox by keboard event when slotted option has focus,
+ * or by external element such as a text field
+ * @param event
+ * @returns { void }
+ */
+ filterByKeyboardEvent(event: KeyboardEvent) {
+ const target = event.target as PfListboxOption;
+ let stopEvent = false;
+ if (event.ctrlKey ||
+ event.altKey ||
+ event.metaKey ||
+ !event.composedPath().some(x =>
+ this.options.includes(x as PfListboxOption))) {
+ this.filter = '';
+ return;
+ }
+
+ switch (event.key) {
+ case 'Backspace':
+ case 'Delete':
+ this.filter = this.filter.slice(0, this.filter.length - 2);
+ stopEvent = true;
+ break;
+ case event.key?.match(/^[\w]$/)?.input:
+ this.filter += event.key?.toLowerCase();
+ stopEvent = true;
+ break;
+ case 'Enter':
+ case ' ':
+ this.#updateMultiselect(target);
+ stopEvent = true;
+ break;
+ default:
+ break;
+ }
+ if (stopEvent) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ focus() {
+ this.#tabindex.focusOnItem(this.activeItem);
+ }
+
+ #updateActiveDescendant() {
+ const [desc] = this.#allOptions.filter(option => option === this.#tabindex.activeItem);
+ this.#internals.ariaActivedescendant = desc?.id;
+ this.#updateSingleselect();
+ // TODO: set this.#value and fire events
+ }
+
+ #updateSingleselect() {
+ if (!this.isMultiselectable) {
+ this.options.forEach(option => {
+ if (option === this.#tabindex.activeItem) {
+ option.setAttribute('aria-selected', 'true');
+ } else {
+ option.removeAttribute('aria-selected');
+ }
+ });
+ this.#fireChange();
+ }
+ }
+
+ #updateMultiselect(option: PfListboxOption) {
+ if (this.isMultiselectable) {
+ if (!option.hasAttribute('aria-selected')) {
+ option.setAttribute('aria-selected', 'true');
+ } else {
+ option.removeAttribute('aria-selected');
+ }
+ this.#fireChange();
+ }
+ }
+
+ #onFilterChange() {
+ const oldValue = this.value;
+ this.#allOptions = this.options;
+ const all = this.#allOptions;
+ this.#tabindex.updateItems(all);
+ this.#updateActiveDescendant();
+ if (oldValue !== this.value) {
+ this.#fireInput();
+ }
+ }
+
+ /**
+ * handles user user selection change similar to HTMLSelectElement events
+ * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
+ * @fires change
+ */
+ #fireChange() {
+ this.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+
+ /**
+ * handles element value change similar to HTMLSelectElement events
+ * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
+ * @fires change
+ */
+ #fireInput() {
+ this.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+
+ #onOptionBlur() {
+ this.#hasFocus = false;
+ }
+
+ #onOptionFocus(event: FocusEvent) {
+ this.#hasFocus = true;
+ const target = event.target as PfListboxOption;
+ if (target !== this.#tabindex.activeItem) {
+ this.#tabindex.updateActiveItem(target);
+ }
+ this.#updateActiveDescendant();
+ }
+
+ #onOptionClick(event: MouseEvent) {
+ const target = event.target as PfListboxOption;
+ const sel = (option: PfListboxOption, selected = false) => {
+ if (selected) {
+ option.setAttribute('aria-selected', 'true');
+ } else {
+ option.removeAttribute('aria-selected');
+ }
+ };
+ if (this.isMultiselectable) {
+ if (!event.shiftKey) {
+ // toggle target
+ this.#updateMultiselect(event.target);
+ } else {
+ // select all options between active descendant and target
+ const [start, end] = [this.options.indexOf(this.activeItem), this.options.indexOf(target)].sort();
+ const options = [...this.options].slice(start, end + 1);
+ options.forEach(option=>sel(option, true));
+ }
+ } else {
+ // select target and deselect all other options
+ this.options.forEach(option => sel(option, option === target));
+ }
+ if (target !== this.#tabindex.activeItem) {
+ this.#tabindex.focusOnItem(target);
+ this.#updateActiveDescendant();
+ this.#fireChange();
+ }
+ }
+
+ #onOptionKeydown(event: KeyboardEvent) {
+ const { filter } = this;
+ this.filterByKeyboardEvent(event);
+ // only change focus if keydown occurred when option has focus
+ // (as opposed to an external text input and if filter has changed
+ if (filter !== this.filter) {
+ this.#tabindex.focusOnItem(this.activeItem);
+ }
+ }
+
+ #onSlotchange() {
+ this.#allOptions = this.options;
+ const all = this.#allOptions;
+ const setSize = this.options.length;
+ this.options.forEach((option, i) => {
+ option.setAttribute('aria-setsize', `${setSize}`);
+ option.setAttribute('aria-posinset', `${i + 1}`);
+ });
+ this.#tabindex.initItems(all, this);
+ }
}
declare global {
From 13cc4f92933f4382a2f700143862bdae80da3569 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 10:07:00 -0400
Subject: [PATCH 12/40] feat(elements): added temporary aria-selected styles to
listbox-option
---
elements/pf-listbox/pf-listbox-option.css | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-listbox/pf-listbox-option.css
index 3357654601..047c545b09 100644
--- a/elements/pf-listbox/pf-listbox-option.css
+++ b/elements/pf-listbox/pf-listbox-option.css
@@ -7,6 +7,11 @@
display: none !important;
}
-:host([aria-selected="true"]) {
- background-color: #ddd;
+:host:before {
+ content: '\2713 ';
+ opacity: 0;
+}
+
+:host([aria-selected="true"]):before {
+ opacity: 1;
}
\ No newline at end of file
From 51944a012d0a446ea09eb64209ba0501ecab443e Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 10:07:43 -0400
Subject: [PATCH 13/40] feat(elements): added focus and blur event handlers for
listbox-option
---
elements/pf-listbox/pf-listbox-option.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index 97084be3ea..50b524ad89 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -20,6 +20,7 @@ export class PfListboxOption extends LitElement {
override connectedCallback() {
super.connectedCallback();
this.addEventListener('focus', this.#onFocus);
+ this.addEventListener('blur', this.#onBlur);
this.id = this.id || getRandomId();
}
@@ -50,8 +51,11 @@ export class PfListboxOption extends LitElement {
}
#onFocus() {
- // console.log('focus');
- // this.dispatchEvent(new Event('focus', { bubbles: true }));
+ this.dispatchEvent(new Event('optionfocus', { bubbles: true }));
+ }
+
+ #onBlur() {
+ this.dispatchEvent(new Event('optionblur', { bubbles: true }));
}
}
From d3530bdf57a9dcfee7f55186a2625f517af165c0 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 10:08:36 -0400
Subject: [PATCH 14/40] feat(elements): added temporary active descendant
syling when listbox has focus
---
elements/pf-listbox/pf-listbox.css | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index b4741c0e42..4d990a56b6 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -14,3 +14,7 @@ slot {
::slotted(*) {
margin: 2px 10px;
}
+
+:host(:focus-within) ::slotted([active-descendant]) {
+ background-color: #e8f7ff;
+}
From 471a6c9a08485d8fe818686c7c3de720b3abdd52 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 10:11:33 -0400
Subject: [PATCH 15/40] feat(elements): change and input events, and updated
filter and active descndant logic
---
elements/pf-listbox/pf-listbox.ts | 51 +++++++++++++++++--------------
1 file changed, 28 insertions(+), 23 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index c03327b082..c56a576a82 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -82,11 +82,6 @@ export class PfListbox extends LitElement {
return selectedItems.join(',');
}
- override connectedCallback() {
- super.connectedCallback();
- this.addEventListener('focus', this.#onOptionFocus);
- }
-
updated(changed: PropertyValues) {
if (changed.has('filter')) {
this.#onFilterChange();
@@ -98,8 +93,8 @@ export class PfListbox extends LitElement {
@@ -153,16 +148,26 @@ export class PfListbox extends LitElement {
}
#updateActiveDescendant() {
- const [desc] = this.#allOptions.filter(option => option === this.#tabindex.activeItem);
- this.#internals.ariaActivedescendant = desc?.id;
+ let found = false;
+ this.#allOptions.forEach(option => {
+ if (option === this.#tabindex.activeItem) {
+ this.#internals.ariaActivedescendant = option.id;
+ option.setAttribute('active-descendant', 'active-descendant');
+ found = true;
+ } else {
+ option.removeAttribute('active-descendant');
+ }
+ });
+ if (!found) {
+ this.#internals.ariaActivedescendant = null;
+ }
this.#updateSingleselect();
- // TODO: set this.#value and fire events
}
#updateSingleselect() {
if (!this.isMultiselectable) {
this.options.forEach(option => {
- if (option === this.#tabindex.activeItem) {
+ if (option.id === this.#internals.ariaActivedescendant) {
option.setAttribute('aria-selected', 'true');
} else {
option.removeAttribute('aria-selected');
@@ -183,17 +188,6 @@ export class PfListbox extends LitElement {
}
}
- #onFilterChange() {
- const oldValue = this.value;
- this.#allOptions = this.options;
- const all = this.#allOptions;
- this.#tabindex.updateItems(all);
- this.#updateActiveDescendant();
- if (oldValue !== this.value) {
- this.#fireInput();
- }
- }
-
/**
* handles user user selection change similar to HTMLSelectElement events
* (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
@@ -212,6 +206,17 @@ export class PfListbox extends LitElement {
this.dispatchEvent(new Event('input', { bubbles: true }));
}
+ #onFilterChange() {
+ const oldValue = this.value;
+ this.#allOptions = this.options;
+ const all = this.#allOptions;
+ this.#tabindex.updateItems(all);
+ this.#updateActiveDescendant();
+ if (oldValue !== this.value) {
+ this.#fireInput();
+ }
+ }
+
#onOptionBlur() {
this.#hasFocus = false;
}
@@ -237,7 +242,7 @@ export class PfListbox extends LitElement {
if (this.isMultiselectable) {
if (!event.shiftKey) {
// toggle target
- this.#updateMultiselect(event.target);
+ this.#updateMultiselect(target);
} else {
// select all options between active descendant and target
const [start, end] = [this.options.indexOf(this.activeItem), this.options.indexOf(target)].sort();
From c8732e5984f11d02a0108d29589064c310de0376 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 10:28:02 -0400
Subject: [PATCH 16/40] feat(elements): fixed listbox filterbyKeyword for
multiselect and external input
---
elements/pf-listbox/pf-listbox.ts | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index c56a576a82..c0f12dd282 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -112,10 +112,7 @@ export class PfListbox extends LitElement {
let stopEvent = false;
if (event.ctrlKey ||
event.altKey ||
- event.metaKey ||
- !event.composedPath().some(x =>
- this.options.includes(x as PfListboxOption))) {
- this.filter = '';
+ event.metaKey) {
return;
}
@@ -131,7 +128,9 @@ export class PfListbox extends LitElement {
break;
case 'Enter':
case ' ':
- this.#updateMultiselect(target);
+ if (target) {
+ this.#updateMultiselect(target);
+ }
stopEvent = true;
break;
default:
From d772a34936cb097c8265107b25473247efce32a9 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 14:02:31 -0400
Subject: [PATCH 17/40] feat(listbox): added keyboardmultiselect support
---
elements/pf-listbox/pf-listbox.css | 4 +-
elements/pf-listbox/pf-listbox.ts | 69 ++++++++++++++----------------
2 files changed, 36 insertions(+), 37 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 4d990a56b6..4ea58617cf 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -1,5 +1,7 @@
:host {
display: block;
+ border: 1px solid #ddd;
+ padding: 10px 0;
}
slot {
@@ -12,7 +14,7 @@ slot {
}
::slotted(*) {
- margin: 2px 10px;
+ padding: 2px 10px;
}
:host(:focus-within) ::slotted([active-descendant]) {
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index c0f12dd282..1457e51a2f 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -38,6 +38,8 @@ export class PfListbox extends LitElement {
#hasFocus = false;
+ #shiftStartingItem: PfListboxOption | null = null;
+
get isHorizontal(): boolean {
return this?.getAttribute('aria-orientation') === 'horizontal';
}
@@ -66,11 +68,7 @@ export class PfListbox extends LitElement {
this.options.forEach(option => {
const textContent = (option.textContent || '').toLowerCase();
const selected = this.isMultiselectable ? selectedItems?.includes(textContent) : firstItem === textContent;
- if (selected) {
- option.setAttribute('aria-selected', 'true');
- } else {
- option.removeAttribute('aria-selected');
- }
+ option.ariaSelected = `${selected}`;
});
if (oldValue !== this.value) {
this.#fireInput();
@@ -78,7 +76,7 @@ export class PfListbox extends LitElement {
}
get value() {
- const selectedItems = this.options.filter(option=>option.hasAttribute('aria-selected')).map(option => option.textContent);
+ const selectedItems = this.options.filter(option=>option.ariaSelected).map(option => option.textContent);
return selectedItems.join(',');
}
@@ -96,7 +94,8 @@ export class PfListbox extends LitElement {
@optionfocus="${this.#onOptionFocus}"
@optionblur="${this.#onOptionBlur}"
@click="${this.#onOptionClick}"
- @keydown="${this.#onOptionKeydown}">
+ @keydown="${this.#onOptionKeydown}"
+ @keyup="${this.#onOptionKeyup}">
`;
}
@@ -128,8 +127,10 @@ export class PfListbox extends LitElement {
break;
case 'Enter':
case ' ':
+ // enter and space are only applicable if a listbox option is clicked
+ // an external text input should not trigger multiselect
if (target) {
- this.#updateMultiselect(target);
+ this.#updateMultiselect(target, event.shiftKey);
}
stopEvent = true;
break;
@@ -165,23 +166,21 @@ export class PfListbox extends LitElement {
#updateSingleselect() {
if (!this.isMultiselectable) {
- this.options.forEach(option => {
- if (option.id === this.#internals.ariaActivedescendant) {
- option.setAttribute('aria-selected', 'true');
- } else {
- option.removeAttribute('aria-selected');
- }
- });
+ this.options.forEach(option => option.ariaSelected = `${option.id === this.#internals.ariaActivedescendant}`);
this.#fireChange();
}
}
- #updateMultiselect(option: PfListboxOption) {
+ #updateMultiselect(option: PfListboxOption, shiftKey = false, startingItem = this.activeItem) {
if (this.isMultiselectable) {
- if (!option.hasAttribute('aria-selected')) {
- option.setAttribute('aria-selected', 'true');
+ if (shiftKey) {
+ // toggle target
+ option.ariaSelected = `${!option.ariaSelected}`;
} else {
- option.removeAttribute('aria-selected');
+ // select all options between active descendant and target
+ const [start, end] = [this.options.indexOf(startingItem), this.options.indexOf(option)].sort();
+ const options = [...this.options].slice(start, end + 1);
+ options.forEach(option => option.ariaSelected = 'true');
}
this.#fireChange();
}
@@ -231,26 +230,11 @@ export class PfListbox extends LitElement {
#onOptionClick(event: MouseEvent) {
const target = event.target as PfListboxOption;
- const sel = (option: PfListboxOption, selected = false) => {
- if (selected) {
- option.setAttribute('aria-selected', 'true');
- } else {
- option.removeAttribute('aria-selected');
- }
- };
if (this.isMultiselectable) {
- if (!event.shiftKey) {
- // toggle target
- this.#updateMultiselect(target);
- } else {
- // select all options between active descendant and target
- const [start, end] = [this.options.indexOf(this.activeItem), this.options.indexOf(target)].sort();
- const options = [...this.options].slice(start, end + 1);
- options.forEach(option=>sel(option, true));
- }
+ this.#updateMultiselect(target, event.shiftKey);
} else {
// select target and deselect all other options
- this.options.forEach(option => sel(option, option === target));
+ this.options.forEach(option => option.ariaSelected = `${option === target}`);
}
if (target !== this.#tabindex.activeItem) {
this.#tabindex.focusOnItem(target);
@@ -259,8 +243,21 @@ export class PfListbox extends LitElement {
}
}
+ #onOptionKeyup(event: KeyboardEvent) {
+ if (event.shiftKey) {
+ if (this.#shiftStartingItem && this.activeItem) {
+ this.#updateMultiselect(this.activeItem, true, this.#shiftStartingItem);
+ }
+ this.#shiftStartingItem = null;
+ }
+ }
+
#onOptionKeydown(event: KeyboardEvent) {
const { filter } = this;
+ // need to set for keyboard support of multiselect
+ if (event.shiftKey) {
+ this.#shiftStartingItem = this.activeItem;
+ }
this.filterByKeyboardEvent(event);
// only change focus if keydown occurred when option has focus
// (as opposed to an external text input and if filter has changed
From ec5f9374c202a093d080ca2c585bbfcb540fbaa0 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 14:03:35 -0400
Subject: [PATCH 18/40] feat(listbox): added flex temporary style
---
elements/pf-listbox/pf-listbox.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 4ea58617cf..3ca2eb1f25 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -1,5 +1,5 @@
:host {
- display: block;
+ display: flex;
border: 1px solid #ddd;
padding: 10px 0;
}
From 43df77d8275500bc07fd54a0ae8d0c4e4b8121df Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 16:48:38 -0400
Subject: [PATCH 19/40] feat(listbox): added temporary listbox styles
---
elements/pf-listbox/pf-listbox.css | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 3ca2eb1f25..f00f659e93 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -1,7 +1,8 @@
:host {
- display: flex;
+ display: inline-flex;
border: 1px solid #ddd;
- padding: 10px 0;
+ border-radius: 3px;
+ padding: 10px;
}
slot {
From 159a37f27d7e470b829c9ee259c6bef653e8a2fa Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 16:49:58 -0400
Subject: [PATCH 20/40] feat(listbox): added multiselect with keyboard events
---
elements/pf-listbox/pf-listbox.ts | 111 +++++++++++++++++++-----------
1 file changed, 69 insertions(+), 42 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 1457e51a2f..4e174b5ac7 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -108,39 +108,55 @@ export class PfListbox extends LitElement {
*/
filterByKeyboardEvent(event: KeyboardEvent) {
const target = event.target as PfListboxOption;
+ const oldValue = this.value;
let stopEvent = false;
- if (event.ctrlKey ||
- event.altKey ||
+ if (event.altKey ||
event.metaKey) {
return;
- }
-
- switch (event.key) {
- case 'Backspace':
- case 'Delete':
- this.filter = this.filter.slice(0, this.filter.length - 2);
- stopEvent = true;
- break;
- case event.key?.match(/^[\w]$/)?.input:
- this.filter += event.key?.toLowerCase();
+ } else if (event.ctrlKey) {
+ if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
+ this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem);
stopEvent = true;
- break;
- case 'Enter':
- case ' ':
- // enter and space are only applicable if a listbox option is clicked
- // an external text input should not trigger multiselect
- if (target) {
- this.#updateMultiselect(target, event.shiftKey);
- }
- stopEvent = true;
- break;
- default:
- break;
+ } else {
+ return;
+ }
+ } else {
+ switch (event.key) {
+ case 'Backspace':
+ case 'Delete':
+ this.filter = this.filter.slice(0, this.filter.length - 2);
+ stopEvent = true;
+ break;
+ case event.key?.match(/^[\w]$/)?.input:
+ this.filter += event.key?.toLowerCase();
+ stopEvent = true;
+ break;
+ case 'Enter':
+ case ' ':
+ // enter and space are only applicable if a listbox option is clicked
+ // an external text input should not trigger multiselect
+ if (target) {
+ if (this.isMultiselectable) {
+ if (event.shiftKey) {
+ this.#updateMultiselect(target);
+ } else {
+ target.ariaSelected = `${target.ariaSelected !== 'true'}`;
+ }
+ stopEvent = true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
}
if (stopEvent) {
event.stopPropagation();
event.preventDefault();
}
+ if (oldValue !== this.value) {
+ this.#fireChange();
+ }
}
focus() {
@@ -171,18 +187,15 @@ export class PfListbox extends LitElement {
}
}
- #updateMultiselect(option: PfListboxOption, shiftKey = false, startingItem = this.activeItem) {
+ #updateMultiselect(currentItem: PfListboxOption, referenceitem = this.activeItem) {
if (this.isMultiselectable) {
- if (shiftKey) {
- // toggle target
- option.ariaSelected = `${!option.ariaSelected}`;
- } else {
- // select all options between active descendant and target
- const [start, end] = [this.options.indexOf(startingItem), this.options.indexOf(option)].sort();
- const options = [...this.options].slice(start, end + 1);
- options.forEach(option => option.ariaSelected = 'true');
- }
- this.#fireChange();
+ // select all options between active descendant and target
+ const [start, end] = [this.options.indexOf(referenceitem), this.options.indexOf(currentItem)].sort();
+ const options = [...this.options].slice(start, end + 1);
+ // if all items in range are toggled, remove toggle
+ const toggle = options.filter(option => option.ariaSelected !== 'true')?.length !== 0;
+ options.forEach(option => option.ariaSelected = `${toggle}`);
+ this.#shiftStartingItem = currentItem;
}
}
@@ -198,7 +211,7 @@ export class PfListbox extends LitElement {
/**
* handles element value change similar to HTMLSelectElement events
* (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
- * @fires change
+ * @fires input
*/
#fireInput() {
this.dispatchEvent(new Event('input', { bubbles: true }));
@@ -230,8 +243,16 @@ export class PfListbox extends LitElement {
#onOptionClick(event: MouseEvent) {
const target = event.target as PfListboxOption;
+ const oldValue = this.value;
if (this.isMultiselectable) {
- this.#updateMultiselect(target, event.shiftKey);
+ if (!event.shiftKey) {
+ target.ariaSelected = `${target.ariaSelected !== 'true'}`;
+ } else {
+ if (this.#shiftStartingItem && target) {
+ this.#updateMultiselect(target, this.#shiftStartingItem);
+ this.#fireChange();
+ }
+ }
} else {
// select target and deselect all other options
this.options.forEach(option => option.ariaSelected = `${option === target}`);
@@ -239,23 +260,29 @@ export class PfListbox extends LitElement {
if (target !== this.#tabindex.activeItem) {
this.#tabindex.focusOnItem(target);
this.#updateActiveDescendant();
+ }
+ if (oldValue !== this.value) {
this.#fireChange();
}
}
#onOptionKeyup(event: KeyboardEvent) {
- if (event.shiftKey) {
- if (this.#shiftStartingItem && this.activeItem) {
- this.#updateMultiselect(this.activeItem, true, this.#shiftStartingItem);
+ const target = event.target as PfListboxOption;
+ if (event.shiftKey && this.isMultiselectable) {
+ if (this.#shiftStartingItem && target) {
+ this.#updateMultiselect(target, this.#shiftStartingItem);
+ this.#fireChange();
+ }
+ if (event.key === 'Shift') {
+ this.#shiftStartingItem = null;
}
- this.#shiftStartingItem = null;
}
}
#onOptionKeydown(event: KeyboardEvent) {
const { filter } = this;
// need to set for keyboard support of multiselect
- if (event.shiftKey) {
+ if (event.key === 'Shift' && this.isMultiselectable) {
this.#shiftStartingItem = this.activeItem;
}
this.filterByKeyboardEvent(event);
From fe95a58b4df2b8f3ba8bde791dc7a4f1b7c997fd Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 17:04:25 -0400
Subject: [PATCH 21/40] feat(listbox): updated temporary styles and demo
---
elements/pf-listbox/demo/demo.css | 4 ++--
elements/pf-listbox/demo/pf-listbox.html | 22 +++++++++++++++++++---
elements/pf-listbox/pf-listbox-group.css | 20 ++++++++++++++++++++
elements/pf-listbox/pf-listbox-option.css | 1 +
elements/pf-listbox/pf-listbox.css | 2 +-
5 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/elements/pf-listbox/demo/demo.css b/elements/pf-listbox/demo/demo.css
index 8e786b884f..1ac9938737 100644
--- a/elements/pf-listbox/demo/demo.css
+++ b/elements/pf-listbox/demo/demo.css
@@ -1,3 +1,3 @@
-pf-listbox {
- /* insert demo styles */
+[data-demo="pf-listbox"] {
+ padding: 20px;
}
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
index 5fb1a76404..0a00d33689 100644
--- a/elements/pf-listbox/demo/pf-listbox.html
+++ b/elements/pf-listbox/demo/pf-listbox.html
@@ -1,7 +1,21 @@
+default
+Any arrow keys work. Only one option can be selected.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
aria-orientation="horizontal"
+Only horizontal arrow keys work. Only one option can be selected.
Blue
Green
@@ -12,9 +26,9 @@ aria-orientation="horizontal"
Red
Yellow
-
aria-orientation="vertical"
+Only vertical arrow keys work. Only one option can be selected.
Blue
Green
@@ -25,8 +39,9 @@ aria-orientation="vertical"
Red
Yellow
-
+
aria-multiselectable="true"
+Any arrow keys work. Multiple option can be selected.
Blue
Green
@@ -37,8 +52,9 @@ aria-multiselectable="true"
Red
Yellow
-
+
pf-listbox-group
+Any arrow keys work. Only one option can be selected. Headings are noe selectable.
Breakfast
diff --git a/elements/pf-listbox/pf-listbox-group.css b/elements/pf-listbox/pf-listbox-group.css
index 5d4e87f30f..ba41c4be4f 100644
--- a/elements/pf-listbox/pf-listbox-group.css
+++ b/elements/pf-listbox/pf-listbox-group.css
@@ -1,3 +1,23 @@
:host {
display: block;
+ margin: 0 -10px;
+}
+
+:host(:first-child) {
+ margin-top: -10px;
+}
+
+:host(:last-child) {
+ margin-bottom: -10px;
+}
+
+slot[name="group-heading"] {
+ background-color: #f0f0f0;
+ display: block;
+ padding: 0 10px;
+ font-weight: bold;
+}
+
+::slotted(pf-listobx-option) {
+ padding: 2px 10px;
}
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-listbox/pf-listbox-option.css
index 047c545b09..b6d245ea7c 100644
--- a/elements/pf-listbox/pf-listbox-option.css
+++ b/elements/pf-listbox/pf-listbox-option.css
@@ -7,6 +7,7 @@
display: none !important;
}
+:host:after,
:host:before {
content: '\2713 ';
opacity: 0;
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index f00f659e93..5736b59654 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -14,7 +14,7 @@ slot {
flex-direction: row;
}
-::slotted(*) {
+::slotted(pf-listobx-option) {
padding: 2px 10px;
}
From 0fcd9cb24b3311890dfa4443f77d1a757a7ddaef Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 17:44:23 -0400
Subject: [PATCH 22/40] feat(listbox): added support for listbox groups
---
elements/pf-listbox/pf-listbox-group.ts | 16 +++++++++++++++-
elements/pf-listbox/pf-listbox.ts | 19 +++++++++++++++++--
2 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-group.ts b/elements/pf-listbox/pf-listbox-group.ts
index 14096e195e..84168be465 100644
--- a/elements/pf-listbox/pf-listbox-group.ts
+++ b/elements/pf-listbox/pf-listbox-group.ts
@@ -1,9 +1,13 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
+import { PfListboxOption } from './pf-listbox-option.js';
import styles from './pf-listbox-group.css';
+export type PfListboxGroupOrOption = PfListboxGroup | PfListboxOption;
+
/**
* Group of options within a listbox
* @slot - Place element content here
@@ -16,12 +20,22 @@ export class PfListboxGroup extends LitElement {
role: 'group'
});
+ @queryAssignedElements() private _options!: PfListboxOption[];
+
render() {
return html`
-
+
`;
}
+
+ get options() {
+ return this._options;
+ }
+
+ #onSlotchange() {
+ this.dispatchEvent(new Event('slotchange', { bubbles: true }));
+ }
}
declare global {
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 4e174b5ac7..af7158127d 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -6,9 +6,10 @@ import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js';
import { PfListboxOption } from './pf-listbox-option.js';
-import './pf-listbox-group.js';
+import { PfListboxGroup, type PfListboxGroupOrOption } from './pf-listbox-group.js';
import styles from './pf-listbox.css';
+import type { PfJumpLinksItem } from '../pf-jump-links/pf-jump-links-item.js';
/**
* List of selectable items
@@ -26,7 +27,7 @@ export class PfListbox extends LitElement {
@property() filter = '';
@property({ reflect: true, attribute: 'disable-filter' }) disableFilter = '';
- @queryAssignedElements() private options!: PfListboxOption[];
+ @queryAssignedElements() private groupsOrOptions!: PfListboxGroupOrOption[];
#internals = new InternalsController(this, {
role: 'listbox'
@@ -293,6 +294,20 @@ export class PfListbox extends LitElement {
}
}
+ get options() {
+ const extractItems = (group: PfListboxGroupOrOption[]) => group.flatMap((item: PfListboxGroupOrOption) => {
+ let options: PfListboxOption[];
+ if (item instanceof PfListboxGroup) {
+ const group = item as PfListboxGroup;
+ options = extractItems(group.options);
+ } else {
+ options = [item];
+ }
+ return options;
+ });
+ return extractItems(this.groupsOrOptions);
+ }
+
#onSlotchange() {
this.#allOptions = this.options;
const all = this.#allOptions;
From fd4decf76e0f07abf701ebbf765115cb045ca0dc Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 17:46:41 -0400
Subject: [PATCH 23/40] feat(listbox): removed unused `#hasFocus` variable from
listbox
---
elements/pf-listbox/pf-listbox.ts | 8 --------
1 file changed, 8 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index af7158127d..d380fed48b 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -37,8 +37,6 @@ export class PfListbox extends LitElement {
#_allOptions: PfListboxOption[] = [];
- #hasFocus = false;
-
#shiftStartingItem: PfListboxOption | null = null;
get isHorizontal(): boolean {
@@ -93,7 +91,6 @@ export class PfListbox extends LitElement {
class="${this.isHorizontal ? 'horizontal' : 'vertical'}"
@slotchange="${this.#onSlotchange}"
@optionfocus="${this.#onOptionFocus}"
- @optionblur="${this.#onOptionBlur}"
@click="${this.#onOptionClick}"
@keydown="${this.#onOptionKeydown}"
@keyup="${this.#onOptionKeyup}">
@@ -229,12 +226,7 @@ export class PfListbox extends LitElement {
}
}
- #onOptionBlur() {
- this.#hasFocus = false;
- }
-
#onOptionFocus(event: FocusEvent) {
- this.#hasFocus = true;
const target = event.target as PfListboxOption;
if (target !== this.#tabindex.activeItem) {
this.#tabindex.updateActiveItem(target);
From d8b5d0c99426904e7de7288569c6fba4c85f4490 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Thu, 20 Jul 2023 17:57:40 -0400
Subject: [PATCH 24/40] feat(listbox): updated demo styles
---
elements/pf-listbox/pf-listbox-group.css | 17 ++++-------------
elements/pf-listbox/pf-listbox-option.css | 5 +++++
elements/pf-listbox/pf-listbox.css | 11 ++++-------
3 files changed, 13 insertions(+), 20 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-group.css b/elements/pf-listbox/pf-listbox-group.css
index ba41c4be4f..da5afe506f 100644
--- a/elements/pf-listbox/pf-listbox-group.css
+++ b/elements/pf-listbox/pf-listbox-group.css
@@ -1,23 +1,14 @@
:host {
display: block;
- margin: 0 -10px;
}
-:host(:first-child) {
- margin-top: -10px;
-}
-
-:host(:last-child) {
- margin-bottom: -10px;
+slot {
+ padding: 10px;
}
slot[name="group-heading"] {
background-color: #f0f0f0;
display: block;
- padding: 0 10px;
font-weight: bold;
-}
-
-::slotted(pf-listobx-option) {
- padding: 2px 10px;
-}
+ padding: 0 10px;
+}
\ No newline at end of file
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-listbox/pf-listbox-option.css
index b6d245ea7c..29396196a2 100644
--- a/elements/pf-listbox/pf-listbox-option.css
+++ b/elements/pf-listbox/pf-listbox-option.css
@@ -1,5 +1,6 @@
:host {
display: block;
+ padding: 2px 10px;
}
:host([hidden]),
@@ -15,4 +16,8 @@
:host([aria-selected="true"]):before {
opacity: 1;
+}
+
+:host([active-descendant]) {
+ background-color: var(--_active-descendant-color, transparent);
}
\ No newline at end of file
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 5736b59654..1b4c7b349c 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -2,7 +2,7 @@
display: inline-flex;
border: 1px solid #ddd;
border-radius: 3px;
- padding: 10px;
+ --_active-descendant-color: transparent;
}
slot {
@@ -14,10 +14,7 @@ slot {
flex-direction: row;
}
-::slotted(pf-listobx-option) {
- padding: 2px 10px;
-}
-
-:host(:focus-within) ::slotted([active-descendant]) {
- background-color: #e8f7ff;
+:host(:focus-within) {
+ border: 1px solid blue;
+ --_active-descendant-color: #e1f4fe;
}
From c464616dcf4ec20d47282ab91b6581dac0911eeb Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 21 Jul 2023 12:57:47 -0400
Subject: [PATCH 25/40] feat(listbox): fixed multiselect handling, added
internals for aria, and updated demo
---
elements/pf-listbox/demo/pf-listbox.html | 20 +++++-----
elements/pf-listbox/pf-listbox-option.ts | 5 +++
elements/pf-listbox/pf-listbox.ts | 49 ++++++++++++++++++------
3 files changed, 53 insertions(+), 21 deletions(-)
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
index 0a00d33689..77e7023623 100644
--- a/elements/pf-listbox/demo/pf-listbox.html
+++ b/elements/pf-listbox/demo/pf-listbox.html
@@ -2,7 +2,7 @@
default
-Any arrow keys work. Only one option can be selected.
+Any arrow keys work. Only one option can be selected. Filter by typing a letter.
Blue
Green
@@ -14,9 +14,9 @@ default
Yellow
-aria-orientation="horizontal"
-Only horizontal arrow keys work. Only one option can be selected.
-
+orientation="horizontal"
+Only horizontal arrow keys work.
+
Blue
Green
Magenta
@@ -27,9 +27,9 @@ aria-orientation="horizontal"
Yellow
-aria-orientation="vertical"
+orientation="vertical"
Only vertical arrow keys work. Only one option can be selected.
-
+
Blue
Green
Magenta
@@ -40,9 +40,9 @@ aria-orientation="vertical"
Yellow
-aria-multiselectable="true"
-Any arrow keys work. Multiple option can be selected.
-
+multiple
+Multiple option can be selected. Shift will toggling off multiple items. Ctrl+A will toggle selection on all items.
+
Blue
Green
Magenta
@@ -54,7 +54,7 @@ aria-multiselectable="true"
pf-listbox-group
-Any arrow keys work. Only one option can be selected. Headings are noe selectable.
+Headings are noe selectable.
Breakfast
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index 50b524ad89..fd782af47e 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -50,6 +50,11 @@ export class PfListboxOption extends LitElement {
}
}
+ updateSetSizeAndPosition(setSize: number | null, posInSet: number | null) {
+ this.#internals.ariaSetSize = setSize !== null ? `${setSize}` : null;
+ this.#internals.ariaPosInSet = posInSet !== null ? `${posInSet}` : null;
+ }
+
#onFocus() {
this.dispatchEvent(new Event('optionfocus', { bubbles: true }));
}
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index d380fed48b..2ca4036de4 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -9,7 +9,6 @@ import { PfListboxOption } from './pf-listbox-option.js';
import { PfListboxGroup, type PfListboxGroupOrOption } from './pf-listbox-group.js';
import styles from './pf-listbox.css';
-import type { PfJumpLinksItem } from '../pf-jump-links/pf-jump-links-item.js';
/**
* List of selectable items
@@ -24,9 +23,30 @@ export class PfListbox extends LitElement {
return element instanceof PfListboxOption;
}
+ /**
+ * filter options that start with a string (case-insensitive)
+ */
@property() filter = '';
+
+ /**
+ * disable filtering feature
+ */
@property({ reflect: true, attribute: 'disable-filter' }) disableFilter = '';
+ /**
+ * whether list items are arranged vertically or horizontally;
+ * limits arrow keys based on orientation
+ */
+ @property({ reflect: true, attribute: 'orientation', type: String }) orientation = '';
+
+ /**
+ * whether multiple items can be selected
+ */
+ @property({ reflect: true, attribute: 'multiple', type: Boolean }) multiple = false;
+
+ /**
+ * all slotted listbox options and/or groups of options
+ */
@queryAssignedElements() private groupsOrOptions!: PfListboxGroupOrOption[];
#internals = new InternalsController(this, {
@@ -35,16 +55,19 @@ export class PfListbox extends LitElement {
#tabindex = new RovingTabindexController(this);
+ /**
+ * all options that will not be hidden by a filter
+ * */
#_allOptions: PfListboxOption[] = [];
#shiftStartingItem: PfListboxOption | null = null;
get isHorizontal(): boolean {
- return this?.getAttribute('aria-orientation') === 'horizontal';
+ return this.#internals.ariaOrientation === 'horizontal';
}
get isMultiselectable(): boolean {
- return this?.getAttribute('aria-multiselectable') === 'true';
+ return this.#internals.ariaMultiSelectable === 'true';
}
get activeItem() {
@@ -83,6 +106,12 @@ export class PfListbox extends LitElement {
if (changed.has('filter')) {
this.#onFilterChange();
}
+ if (changed.has('orientation')) {
+ this.#internals.ariaOrientation = ['vertical', 'horizontal'].includes(this.orientation) ? this.orientation : null;
+ }
+ if (changed.has('multiple')) {
+ this.#internals.ariaMultiSelectable = this.multiple ? 'true' : 'false';
+ }
}
render() {
@@ -113,7 +142,7 @@ export class PfListbox extends LitElement {
return;
} else if (event.ctrlKey) {
if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
- this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem);
+ this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem, true);
stopEvent = true;
} else {
return;
@@ -185,13 +214,14 @@ export class PfListbox extends LitElement {
}
}
- #updateMultiselect(currentItem: PfListboxOption, referenceitem = this.activeItem) {
+ #updateMultiselect(currentItem: PfListboxOption, referenceItem = this.activeItem, ctrlKey = false) {
if (this.isMultiselectable) {
// select all options between active descendant and target
- const [start, end] = [this.options.indexOf(referenceitem), this.options.indexOf(currentItem)].sort();
+ const [start, end] = [this.options.indexOf(referenceItem), this.options.indexOf(currentItem)].sort();
const options = [...this.options].slice(start, end + 1);
// if all items in range are toggled, remove toggle
- const toggle = options.filter(option => option.ariaSelected !== 'true')?.length !== 0;
+ const allSelected = ctrlKey && options.filter(option => option.ariaSelected !== 'true')?.length === 0;
+ const toggle = ctrlKey && allSelected ? false : ctrlKey ? true : referenceItem.ariaSelected === 'true';
options.forEach(option => option.ariaSelected = `${toggle}`);
this.#shiftStartingItem = currentItem;
}
@@ -304,10 +334,7 @@ export class PfListbox extends LitElement {
this.#allOptions = this.options;
const all = this.#allOptions;
const setSize = this.options.length;
- this.options.forEach((option, i) => {
- option.setAttribute('aria-setsize', `${setSize}`);
- option.setAttribute('aria-posinset', `${i + 1}`);
- });
+ this.options.forEach((option, i) => option.updateSetSizeAndPosition(setSize, i));
this.#tabindex.initItems(all, this);
}
}
From 827ec5c354612cacdbb923cb3e6d27518a245110 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 21 Jul 2023 16:36:10 -0400
Subject: [PATCH 26/40] feat(listbox): updated docs with more examples
---
elements/pf-listbox/demo/demo.css | 6 ++
elements/pf-listbox/demo/grouped-options.html | 23 +++++++
.../pf-listbox/demo/multiple-selections.html | 19 ++++++
elements/pf-listbox/demo/orientation.html | 28 ++++++++
elements/pf-listbox/demo/pf-listbox.html | 64 ++-----------------
5 files changed, 82 insertions(+), 58 deletions(-)
create mode 100644 elements/pf-listbox/demo/grouped-options.html
create mode 100644 elements/pf-listbox/demo/multiple-selections.html
create mode 100644 elements/pf-listbox/demo/orientation.html
diff --git a/elements/pf-listbox/demo/demo.css b/elements/pf-listbox/demo/demo.css
index 1ac9938737..62f58fd7b6 100644
--- a/elements/pf-listbox/demo/demo.css
+++ b/elements/pf-listbox/demo/demo.css
@@ -1,3 +1,9 @@
[data-demo="pf-listbox"] {
padding: 20px;
}
+
+#combo {
+ display: inline-flex;
+ align-items: stretch;
+ flex-direction: column;
+}
diff --git a/elements/pf-listbox/demo/grouped-options.html b/elements/pf-listbox/demo/grouped-options.html
new file mode 100644
index 0000000000..62e0830ce7
--- /dev/null
+++ b/elements/pf-listbox/demo/grouped-options.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Filter by typing a letter.
+ Headings are not selectable.
+
+
+
+ Breakfast
+ Eggs
+ Toast
+ Waffles
+
+
+ Lunch
+ Salad
+ Sandwich
+ Soup
+
+
diff --git a/elements/pf-listbox/demo/multiple-selections.html b/elements/pf-listbox/demo/multiple-selections.html
new file mode 100644
index 0000000000..ba94169504
--- /dev/null
+++ b/elements/pf-listbox/demo/multiple-selections.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Multiple options can be selected. Any arrow keys work.
+ Shift will toggling off multiple items.
+ Ctrl+A will toggle selection on all items.
+ Filter by typing a letter.
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/orientation.html b/elements/pf-listbox/demo/orientation.html
new file mode 100644
index 0000000000..408ad9ebc7
--- /dev/null
+++ b/elements/pf-listbox/demo/orientation.html
@@ -0,0 +1,28 @@
+
+
+
+orientation="vertical"
+Only vertical arrow keys work. Filter by typing a letter.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+orientation="horizontal"
+Only horizontal arrow keys work. Filter by typing a letter.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
index 77e7023623..0a428c9b0e 100644
--- a/elements/pf-listbox/demo/pf-listbox.html
+++ b/elements/pf-listbox/demo/pf-listbox.html
@@ -2,7 +2,11 @@
default
-Any arrow keys work. Only one option can be selected. Filter by typing a letter.
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Filter by typing a letter.
+
Blue
Green
@@ -12,60 +16,4 @@ default
Pink
Red
Yellow
-
-
-orientation="horizontal"
-Only horizontal arrow keys work.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
-orientation="vertical"
-Only vertical arrow keys work. Only one option can be selected.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
-multiple
-Multiple option can be selected. Shift will toggling off multiple items. Ctrl+A will toggle selection on all items.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
-pf-listbox-group
-Headings are noe selectable.
-
-
- Breakfast
- Eggs
- Toast
- Waffles
-
-
- Lunch
- Salad
- Sandwich
- Soup
-
-
+
\ No newline at end of file
From ac10a6322449d57b15a4e77b51dc86e2d793b541 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 21 Jul 2023 17:00:18 -0400
Subject: [PATCH 27/40] feat(listbox): added support for combobox filtering
---
.../controllers/roving-tabindex-controller.ts | 1 +
elements/pf-listbox/demo/demo.css | 4 ++
elements/pf-listbox/demo/filter-modes.html | 43 +++++++++++++++++++
elements/pf-listbox/demo/pf-listbox.js | 28 ++++++++++++
elements/pf-listbox/pf-listbox.css | 7 ++-
elements/pf-listbox/pf-listbox.ts | 26 ++++++++---
6 files changed, 103 insertions(+), 6 deletions(-)
create mode 100644 elements/pf-listbox/demo/filter-modes.html
diff --git a/core/pfe-core/controllers/roving-tabindex-controller.ts b/core/pfe-core/controllers/roving-tabindex-controller.ts
index 5339f3a6fb..6bf6203eb9 100644
--- a/core/pfe-core/controllers/roving-tabindex-controller.ts
+++ b/core/pfe-core/controllers/roving-tabindex-controller.ts
@@ -186,6 +186,7 @@ export class RovingTabindexController<
updateItems(items: ItemType[]) {
const sequence = [...items.slice(this.#itemIndex), ...items.slice(0, this.#itemIndex)];
const first = sequence.find(item => this.#focusableItems.includes(item));
+ this.#items = items ?? [];
this.focusOnItem(first || this.firstItem);
}
diff --git a/elements/pf-listbox/demo/demo.css b/elements/pf-listbox/demo/demo.css
index 62f58fd7b6..7a63e2ef0e 100644
--- a/elements/pf-listbox/demo/demo.css
+++ b/elements/pf-listbox/demo/demo.css
@@ -7,3 +7,7 @@
align-items: stretch;
flex-direction: column;
}
+
+#listbox {
+ display: flex;
+}
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/filter-modes.html b/elements/pf-listbox/demo/filter-modes.html
new file mode 100644
index 0000000000..4fba5b2c40
--- /dev/null
+++ b/elements/pf-listbox/demo/filter-modes.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+filter-mode="required": combobox example
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Listbox wont appear until input has text.
+
+
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-listbox/demo/pf-listbox.js
index dbfb27a103..dfc8752c38 100644
--- a/elements/pf-listbox/demo/pf-listbox.js
+++ b/elements/pf-listbox/demo/pf-listbox.js
@@ -1 +1,29 @@
import '@patternfly/elements/pf-listbox/pf-listbox.js';
+
+window.addEventListener('load', () => {
+ /**
+ * pf-listbox in a combox box
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/|WAI ARIA Examples: Combobox with ARIAutocomplete}
+ */
+ const input = document.querySelector('#text');
+ const listbox = document.querySelector('#listbox');
+ if (listbox && input) {
+ listbox.filter = input.value;
+ input.addEventListener('input', () => {
+ if (input.value !== listbox.value) {
+ listbox.filter = input.value;
+ }
+ });
+ listbox.addEventListener('change', () => {
+ const val = (listbox.value || '');
+ if (val.length > 0) {
+ input.value = val;
+ input.focus();
+ input.setSelectionRange(
+ listbox.filter.length,
+ val.length
+ );
+ }
+ });
+ }
+});
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-listbox/pf-listbox.css
index 1b4c7b349c..5831311e31 100644
--- a/elements/pf-listbox/pf-listbox.css
+++ b/elements/pf-listbox/pf-listbox.css
@@ -8,9 +8,14 @@
slot {
display: flex;
flex-direction: column;
+ width: 100%;
}
-:host([aria-orientation="horizontal"]) slot {
+:host([orientation="horizontal"]) {
+ overflow: scroll;
+}
+
+:host([orientation="horizontal"]) slot {
flex-direction: row;
}
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 2ca4036de4..9a5f0c75e9 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -29,9 +29,12 @@ export class PfListbox extends LitElement {
@property() filter = '';
/**
- * disable filtering feature
+ * determines how filtering will be handled:
+ * - "" (default): will show all options until filter text is not ""
+ * - "required": will hide all options until filter text is not ""
+ * - "disabled": will not hide options at all, regardless of filtering
*/
- @property({ reflect: true, attribute: 'disable-filter' }) disableFilter = '';
+ @property({ reflect: true, attribute: 'filter-mode' }) filterMode: '' | 'required' | 'disabled' = '';
/**
* whether list items are arranged vertically or horizontally;
@@ -80,7 +83,7 @@ export class PfListbox extends LitElement {
}
set #allOptions(options: PfListboxOption[]) {
- this.#_allOptions = options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.disableFilter || option.getUpdateByFilter(this.filter)));
+ this.#_allOptions = this.filterMode === 'required' && this.filter === '' ? [] : options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.filterMode === 'disabled' || option.getUpdateByFilter(this.filter)));
}
set value(items: string | null) {
@@ -98,10 +101,22 @@ export class PfListbox extends LitElement {
}
get value() {
- const selectedItems = this.options.filter(option=>option.ariaSelected).map(option => option.textContent);
+ const selectedItems = this.options.filter(option=>option.ariaSelected === 'true').map(option => option.textContent);
return selectedItems.join(',');
}
+ get validOptions() {
+ return this.options;
+ }
+
+ isValid(val: string | null) {
+ const vals = val?.split(',') || [];
+ const options = this.options.map(option => option.textContent);
+ return vals.every(val => {
+ return options.includes(val);
+ });
+ }
+
updated(changed: PropertyValues) {
if (changed.has('filter')) {
this.#onFilterChange();
@@ -202,7 +217,8 @@ export class PfListbox extends LitElement {
}
});
if (!found) {
- this.#internals.ariaActivedescendant = null;
+ this.#tabindex.updateActiveItem(this.#tabindex.firstItem);
+ this.#internals.ariaActivedescendant = this.#tabindex.firstItem?.id || null;
}
this.#updateSingleselect();
}
From 9a965a46bae80b7da54e01b74073e673bb117dd0 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 21 Jul 2023 17:08:27 -0400
Subject: [PATCH 28/40] feat(listbox): updated demo for filter modes
---
elements/pf-listbox/demo/filter-modes.html | 40 +++++++++++-----------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/elements/pf-listbox/demo/filter-modes.html b/elements/pf-listbox/demo/filter-modes.html
index 4fba5b2c40..88a8ee6fa0 100644
--- a/elements/pf-listbox/demo/filter-modes.html
+++ b/elements/pf-listbox/demo/filter-modes.html
@@ -1,7 +1,7 @@
-
+
filter-mode="required": combobox example
@@ -23,21 +23,21 @@
filter-mode="required": combobox example
Only one option can be selected.
Listbox wont appear until input has text.
-
-
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
\ No newline at end of file
+
+ Pick a color:
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
\ No newline at end of file
From 7a63196aa734d5393084fdcb907b83c9ecb2eb4d Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Mon, 24 Jul 2023 15:14:16 -0400
Subject: [PATCH 29/40] fix(listbox): updated demo js
---
elements/pf-listbox/demo/pf-listbox.js | 52 +++++++++++++-------------
1 file changed, 25 insertions(+), 27 deletions(-)
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-listbox/demo/pf-listbox.js
index dfc8752c38..bd228bc7d8 100644
--- a/elements/pf-listbox/demo/pf-listbox.js
+++ b/elements/pf-listbox/demo/pf-listbox.js
@@ -1,29 +1,27 @@
import '@patternfly/elements/pf-listbox/pf-listbox.js';
-window.addEventListener('load', () => {
- /**
- * pf-listbox in a combox box
- * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/|WAI ARIA Examples: Combobox with ARIAutocomplete}
- */
- const input = document.querySelector('#text');
- const listbox = document.querySelector('#listbox');
- if (listbox && input) {
- listbox.filter = input.value;
- input.addEventListener('input', () => {
- if (input.value !== listbox.value) {
- listbox.filter = input.value;
- }
- });
- listbox.addEventListener('change', () => {
- const val = (listbox.value || '');
- if (val.length > 0) {
- input.value = val;
- input.focus();
- input.setSelectionRange(
- listbox.filter.length,
- val.length
- );
- }
- });
- }
-});
+/**
+ * pf-listbox in a combox box
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/|WAI ARIA Examples: Combobox with ARIAutocomplete}
+ */
+const input = document.querySelector('#text');
+const listbox = document.querySelector('#listbox');
+if (listbox && input) {
+ listbox.filter = input.value;
+ input.addEventListener('input', () => {
+ if (input.value !== listbox.value) {
+ listbox.filter = input.value;
+ }
+ });
+ listbox.addEventListener('change', () => {
+ const val = (listbox.value || '');
+ if (val.length > 0) {
+ input.value = val;
+ input.focus();
+ input.setSelectionRange(
+ listbox.filter.length,
+ val.length
+ );
+ }
+ });
+}
From 40c038e1261cd3d784324b70ba78f6dec2865046 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Mon, 24 Jul 2023 15:14:44 -0400
Subject: [PATCH 30/40] feat(listbox): added case sensitivity
---
elements/pf-listbox/pf-listbox-option.ts | 6 ++++--
elements/pf-listbox/pf-listbox.ts | 9 +++++++--
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index fd782af47e..cfb9635063 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -24,8 +24,10 @@ export class PfListboxOption extends LitElement {
this.id = this.id || getRandomId();
}
- getUpdateByFilter(filter = '') {
- if (filter === '' || (this.textContent || '').toLowerCase().match(filter)) {
+ getUpdateByFilter(filter = '', caseSensitve = false) {
+ const search = caseSensitve ? filter : filter.toLowerCase();
+ const text = caseSensitve ? (this.textContent || '') : (this.textContent || '');
+ if (search === '' || text.match(search)) {
this.removeAttribute('hidden-by-filter');
} else {
this.setAttribute('hidden-by-filter', 'hidden-by-filter');
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index 9a5f0c75e9..af547b9d6c 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -28,6 +28,11 @@ export class PfListbox extends LitElement {
*/
@property() filter = '';
+ /**
+ * whetehr filtering (if enabled) will be case-sensitive
+ */
+ @property({ reflect: true, attribute: 'case-sensitive' }) caseSensitive = false;
+
/**
* determines how filtering will be handled:
* - "" (default): will show all options until filter text is not ""
@@ -83,7 +88,7 @@ export class PfListbox extends LitElement {
}
set #allOptions(options: PfListboxOption[]) {
- this.#_allOptions = this.filterMode === 'required' && this.filter === '' ? [] : options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.filterMode === 'disabled' || option.getUpdateByFilter(this.filter)));
+ this.#_allOptions = this.filterMode === 'required' && this.filter === '' ? [] : options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.filterMode === 'disabled' || option.getUpdateByFilter(this.filter, this.caseSensitive)));
}
set value(items: string | null) {
@@ -118,7 +123,7 @@ export class PfListbox extends LitElement {
}
updated(changed: PropertyValues) {
- if (changed.has('filter')) {
+ if (changed.has('filter') || changed.has('caseSensitive') || changed.has('filterMode') || changed.has('filterMode')) {
this.#onFilterChange();
}
if (changed.has('orientation')) {
From b99edd1c6737dc9b87bcd48e2b09748876758c50 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Mon, 24 Jul 2023 15:45:22 -0400
Subject: [PATCH 31/40] fix(listbox): typo on option
---
elements/pf-listbox/pf-listbox-option.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index cfb9635063..b2a62f02a8 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -24,9 +24,9 @@ export class PfListboxOption extends LitElement {
this.id = this.id || getRandomId();
}
- getUpdateByFilter(filter = '', caseSensitve = false) {
- const search = caseSensitve ? filter : filter.toLowerCase();
- const text = caseSensitve ? (this.textContent || '') : (this.textContent || '');
+ getUpdateByFilter(filter = '', caseSensitive = false) {
+ const search = caseSensitive ? filter : filter.toLowerCase();
+ const text = caseSensitive ? (this.textContent || '') : (this.textContent || '');
if (search === '' || text.match(search)) {
this.removeAttribute('hidden-by-filter');
} else {
From f942a2d3a2a9b320eff30c309d0203c5a6008631 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 09:21:52 -0400
Subject: [PATCH 32/40] feat(core): init listbox-controller
---
.../controllers/listbox-controller.ts | 428 ++++++++++++++++++
1 file changed, 428 insertions(+)
create mode 100644 core/pfe-core/controllers/listbox-controller.ts
diff --git a/core/pfe-core/controllers/listbox-controller.ts b/core/pfe-core/controllers/listbox-controller.ts
new file mode 100644
index 0000000000..06b59e82c8
--- /dev/null
+++ b/core/pfe-core/controllers/listbox-controller.ts
@@ -0,0 +1,428 @@
+import type { ReactiveController, ReactiveControllerHost } from 'lit';
+import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
+import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js';
+
+/**
+ * how filtering will be handled:
+ * - "" (default): will show all options until filter text is not ""
+ * - "required": will hide all options until filter text is not ""
+ * - "disabled": will not hide options at all, regardless of filtering
+ */
+export type ListboxFilterMode = '' | 'required' | 'disabled';
+
+/**
+ * whether list items are arranged vertically or horizontally;
+ * limits arrow keys based on orientation
+ */
+export type ListboxOrientation = '' | 'horizontal' | 'vertical';
+
+export interface ListboxOptions {
+ caseSensitive?: boolean;
+ filterMode?: ListboxFilterMode;
+ matchAnywhere?: boolean;
+ multiSelectable?: boolean;
+ orientation?: ListboxOrientation;
+}
+
+/**
+ * Implements roving tabindex, as described in WAI-ARIA practices, [Managing Focus Within
+ * Components Using a Roving
+ * tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex)
+ */
+export class ListboxController<
+ ItemType extends HTMLElement = HTMLElement,
+> implements ReactiveController {
+ /**
+ * filter options that start with a string (case-insensitive)
+ */
+ #filter = '';
+
+ /**
+ * whether filtering (if enabled) will be case-sensitive
+ */
+ #caseSensitive = false;
+
+ /**
+ * determines how filtering will be handled:
+ * - "" (default): will show all options until filter text is not ""
+ * - "required": will hide all options until filter text is not ""
+ * - "disabled": will not hide options at all, regardless of filtering
+ */
+ #filterMode: ListboxFilterMode = '';
+
+ #internals: InternalsController;
+
+ /** event listeners for host element */
+ #listeners = {
+ 'keydown': this.#onOptionKeydown.bind(this),
+ 'keyup': this.#onOptionKeyup.bind(this),
+ 'optionfocus': this.#onOptionFocus.bind(this),
+ 'click': this.#onOptionClick.bind(this),
+ };
+
+ /**
+ * whether filtering (if enabled) will look for filter match anywhere in option text
+ * (by default it will only match if the option starts with filter)
+ */
+ #matchAnywhere = false;
+
+ #shiftStartingItem: HTMLElement | null = null;
+
+ #tabindex: RovingTabindexController;
+
+ /**
+ * all options that will not be hidden by a filter
+ * */
+ #options: HTMLElement[] = [];
+
+ /**
+ * all options that will not be hidden by a filter
+ * */
+ #visibleOptions: HTMLElement[] = [];
+
+ get activeItem() {
+ const [active] = this.options.filter(option => option.getAttribute('id') === this.#internals.ariaActivedescendant);
+ return active || this.#tabindex.firstItem;
+ }
+
+ set filter(str: string) {
+ this.filter = str;
+ this.#onFilterChange();
+ }
+
+ get filter() {
+ return this.#filter;
+ }
+
+ set caseSensitive(caseSensitive: boolean) {
+ this.#caseSensitive = caseSensitive;
+ this.#onFilterChange();
+ }
+
+ get caseSensitive() {
+ return this.#caseSensitive;
+ }
+
+ set filterMode(str: ListboxFilterMode) {
+ this.#filterMode = str;
+ this.#onFilterChange();
+ }
+
+ get filterMode(): ListboxFilterMode {
+ return this.#filterMode || '';
+ }
+
+ set multiSelectable(multiSelectable: boolean) {
+ this.#internals.ariaMultiSelectable = multiSelectable ? 'true' : 'false';
+ }
+
+ get multiSelectable(): boolean {
+ return this.#internals.ariaMultiSelectable === 'true';
+ }
+
+ set matchAnywhere(matchAnywhere: boolean) {
+ this.#matchAnywhere = matchAnywhere;
+ this.#onFilterChange();
+ }
+
+ get matchAnywhere() {
+ return this.#matchAnywhere;
+ }
+
+ set orientation(orientation: ListboxOrientation) {
+ this.#internals.ariaOrientation = orientation;
+ }
+
+ get orientation(): ListboxOrientation {
+ const orientation = this.#internals.ariaOrientation || '';
+ return orientation as ListboxOrientation;
+ }
+
+ get options() {
+ return this.#options;
+ }
+
+ set options(options: HTMLElement[]) {
+ const setSize = options.length;
+ const filterOptions = (option: HTMLElement, posInSet: number) => {
+ option.ariaSetSize = setSize !== null ? `${setSize}` : null;
+ option.ariaPosInSet = posInSet !== null ? `${posInSet}` : null;
+ if (this.#filterMode === 'required' && this.#filter === '') {
+ return false;
+ } else if (this.#filterMode === 'disabled') {
+ return true;
+ } else {
+ const search = this.#matchAnywhere ? '' : '^';
+ const text = option.textContent || '';
+ const regex = new RegExp(`${search}${this.#filter}`, this.#caseSensitive ? 'i' : '');
+ if (search === '' || text.match(regex)) {
+ option.removeAttribute('hidden-by-filter');
+ } else {
+ option.setAttribute('hidden-by-filter', 'hidden-by-filter');
+ }
+ return !option.hasAttribute('hidden-by-filter');
+ }
+ };
+ this.#tabindex.initItems(this.#visibleOptions, this.host);
+ this.#visibleOptions = options.filter((option, i) => filterOptions(option, i));
+ this.#options = options;
+ }
+
+ get value() {
+ const selectedItems = this.options.filter(option=>option.ariaSelected === 'true').map(option => option.textContent?.replace(',', '\\,'));
+ return selectedItems.join(',');
+ }
+
+ set value(items: string | null) {
+ const oldValue = this.value;
+ const selectedItems = items?.toLowerCase().split(',');
+ const [firstItem] = selectedItems || [null];
+ this.options.forEach(option => {
+ const textContent = (option.textContent || '').replace('\\,', ',').toLowerCase();
+ const selected = this.multiSelectable ? selectedItems?.includes(textContent) : firstItem === textContent;
+ option.ariaSelected = `${selected}`;
+ });
+ if (oldValue !== this.value) {
+ this.#fireInput();
+ }
+ }
+
+ get visibleOptions() {
+ return this.#visibleOptions;
+ }
+
+ constructor(public host: ReactiveControllerHost & HTMLElement, options: ListboxOptions) {
+ this.host.addController(this);
+ this.#internals = new InternalsController(this.host, {
+ role: 'listbox'
+ });
+ this.#tabindex = new RovingTabindexController(this.host);
+ this.#caseSensitive = options.caseSensitive || false;
+ this.#filterMode = options.filterMode || '';
+ }
+
+ /**
+ * adds event listeners to host
+ */
+ hostConnected() {
+ for (const [event, listener] of Object.entries(this.#listeners)) {
+ this.host?.addEventListener(event, listener as (event: Event | null) => void);
+ }
+ }
+
+ /**
+ * removes event listeners from host
+ */
+ hostDisconnected() {
+ for (const [event, listener] of Object.entries(this.#listeners)) {
+ this.host?.removeEventListener(event, listener as (event: Event | null) => void);
+ }
+ }
+
+ isValid(val: string | null) {
+ const vals = val?.split(',') || [];
+ const options = this.options.map(option => option.textContent);
+ return vals.every(val => {
+ return options.includes(val);
+ });
+ }
+
+ #updateOption(option: HTMLElement) {
+ const search = this.#caseSensitive ? this.#filter : this.#filter.toLowerCase();
+ const text = this.#caseSensitive ? (option.textContent || '') : (option.textContent || '');
+ if (search === '' || text.match(search)) {
+ option.removeAttribute('hidden-by-filter');
+ } else {
+ option.setAttribute('hidden-by-filter', 'hidden-by-filter');
+ }
+ return !option.hasAttribute('hidden-by-filter');
+ }
+
+ /**
+ * filters listbox by keboard event when slotted option has focus,
+ * or by external element such as a text field
+ * @param event
+ * @returns { void }
+ */
+ filterByKeyboardEvent(event: KeyboardEvent) {
+ const target = event.target as HTMLElement;
+ const oldValue = this.value;
+ let stopEvent = false;
+ if (event.altKey ||
+ event.metaKey) {
+ return;
+ } else if (event.ctrlKey) {
+ if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
+ this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem, true);
+ stopEvent = true;
+ } else {
+ return;
+ }
+ } else {
+ switch (event.key) {
+ case 'Backspace':
+ case 'Delete':
+ this.#filter = this.#filter.slice(0, this.#filter.length - 2);
+ stopEvent = true;
+ break;
+ case event.key?.match(/^[\w]$/)?.input:
+ this.#filter += event.key?.toLowerCase();
+ stopEvent = true;
+ break;
+ case 'Enter':
+ case ' ':
+ // enter and space are only applicable if a listbox option is clicked
+ // an external text input should not trigger multiselect
+ if (target) {
+ if (this.multiSelectable) {
+ if (event.shiftKey) {
+ this.#updateMultiselect(target);
+ } else {
+ target.ariaSelected = `${target.ariaSelected !== 'true'}`;
+ }
+ stopEvent = true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ if (stopEvent) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ if (oldValue !== this.value) {
+ this.#fireChange();
+ }
+ }
+
+ focus() {
+ this.#tabindex.focusOnItem(this.activeItem);
+ }
+
+ #updateActiveDescendant() {
+ let found = false;
+ this.options.forEach(option => {
+ if (option === this.#tabindex.activeItem) {
+ this.#internals.ariaActivedescendant = option.id;
+ option.setAttribute('active-descendant', 'active-descendant');
+ found = true;
+ } else {
+ option.removeAttribute('active-descendant');
+ }
+ });
+ if (!found) {
+ this.#tabindex.updateActiveItem(this.#tabindex.firstItem);
+ this.#internals.ariaActivedescendant = this.#tabindex.firstItem?.id || null;
+ }
+ this.#updateSingleselect();
+ }
+
+ #updateSingleselect() {
+ if (!this.multiSelectable) {
+ this.options.forEach(option => option.ariaSelected = `${option.id === this.#internals.ariaActivedescendant}`);
+ this.#fireChange();
+ }
+ }
+
+ #updateMultiselect(currentItem: HTMLElement, referenceItem = this.activeItem, ctrlKey = false) {
+ if (this.multiSelectable) {
+ // select all options between active descendant and target
+ const [start, end] = [this.options.indexOf(referenceItem), this.options.indexOf(currentItem)].sort();
+ const options = [...this.options].slice(start, end + 1);
+ // if all items in range are toggled, remove toggle
+ const allSelected = ctrlKey && options.filter(option => option.ariaSelected !== 'true')?.length === 0;
+ const toggle = ctrlKey && allSelected ? false : ctrlKey ? true : referenceItem.ariaSelected === 'true';
+ options.forEach(option => option.ariaSelected = `${toggle}`);
+ this.#shiftStartingItem = currentItem;
+ }
+ }
+
+ /**
+ * handles user user selection change similar to HTMLSelectElement events
+ * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
+ * @fires change
+ */
+ #fireChange() {
+ this.host.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+
+ /**
+ * handles element value change similar to HTMLSelectElement events
+ * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
+ * @fires input
+ */
+ #fireInput() {
+ this.host.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+
+ #onFilterChange() {
+ const oldValue = this.value;
+ this.#tabindex.updateItems(this.#visibleOptions);
+ this.#updateActiveDescendant();
+ if (oldValue !== this.value) {
+ this.#fireInput();
+ }
+ }
+
+ #onOptionFocus(event: FocusEvent) {
+ const target = event.target as HTMLElement;
+ if (target !== this.#tabindex.activeItem) {
+ this.#tabindex.updateActiveItem(target);
+ }
+ this.#updateActiveDescendant();
+ }
+
+ #onOptionClick(event: MouseEvent) {
+ const target = event.target as HTMLElement;
+ const oldValue = this.value;
+ if (this.multiSelectable) {
+ if (!event.shiftKey) {
+ target.ariaSelected = `${target.ariaSelected !== 'true'}`;
+ } else {
+ if (this.#shiftStartingItem && target) {
+ this.#updateMultiselect(target, this.#shiftStartingItem);
+ this.#fireChange();
+ }
+ }
+ } else {
+ // select target and deselect all other options
+ this.options.forEach(option => option.ariaSelected = `${option === target}`);
+ }
+ if (target !== this.#tabindex.activeItem) {
+ this.#tabindex.focusOnItem(target);
+ this.#updateActiveDescendant();
+ }
+ if (oldValue !== this.value) {
+ this.#fireChange();
+ }
+ }
+
+ #onOptionKeyup(event: KeyboardEvent) {
+ const target = event.target as HTMLElement;
+ if (event.shiftKey && this.multiSelectable) {
+ if (this.#shiftStartingItem && target) {
+ this.#updateMultiselect(target, this.#shiftStartingItem);
+ this.#fireChange();
+ }
+ if (event.key === 'Shift') {
+ this.#shiftStartingItem = null;
+ }
+ }
+ }
+
+ #onOptionKeydown(event: KeyboardEvent) {
+ const { filter } = this;
+ // need to set for keyboard support of multiselect
+ if (event.key === 'Shift' && this.multiSelectable) {
+ this.#shiftStartingItem = this.activeItem;
+ }
+ this.filterByKeyboardEvent(event);
+ // only change focus if keydown occurred when option has focus
+ // (as opposed to an external text input and if filter has changed
+ if (filter !== this.#filter) {
+ this.#tabindex.focusOnItem(this.activeItem);
+ }
+ }
+}
From 2220a4bc7727d9297bdb3d74199c0702c3bda8b8 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 09:24:32 -0400
Subject: [PATCH 33/40] feat(listbox): updated listbox to use
listbox-controller
---
elements/pf-listbox/pf-listbox-group.ts | 4 +
elements/pf-listbox/pf-listbox-option.ts | 28 +--
elements/pf-listbox/pf-listbox.ts | 308 +++--------------------
3 files changed, 35 insertions(+), 305 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-group.ts b/elements/pf-listbox/pf-listbox-group.ts
index 84168be465..d5109e1887 100644
--- a/elements/pf-listbox/pf-listbox-group.ts
+++ b/elements/pf-listbox/pf-listbox-group.ts
@@ -22,6 +22,10 @@ export class PfListboxGroup extends LitElement {
@queryAssignedElements() private _options!: PfListboxOption[];
+ override connectedCallback() {
+ this.#internals;
+ }
+
render() {
return html`
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-listbox/pf-listbox-option.ts
index b2a62f02a8..5fb6c6911a 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-listbox/pf-listbox-option.ts
@@ -22,17 +22,7 @@ export class PfListboxOption extends LitElement {
this.addEventListener('focus', this.#onFocus);
this.addEventListener('blur', this.#onBlur);
this.id = this.id || getRandomId();
- }
-
- getUpdateByFilter(filter = '', caseSensitive = false) {
- const search = caseSensitive ? filter : filter.toLowerCase();
- const text = caseSensitive ? (this.textContent || '') : (this.textContent || '');
- if (search === '' || text.match(search)) {
- this.removeAttribute('hidden-by-filter');
- } else {
- this.setAttribute('hidden-by-filter', 'hidden-by-filter');
- }
- return !this.hasAttribute('hidden-by-filter');
+ this.#internals;
}
render() {
@@ -41,22 +31,6 @@ export class PfListboxOption extends LitElement {
`;
}
- updateSet(setSize?: number | null, posInSet?: number | null) {
- if (!setSize || !posInSet) {
- this.#internals.ariaSetSize = null;
- this.#internals.ariaPosInSet = null;
- } else {
- const parsedSize = Math.max(-1, setSize);
- this.#internals.ariaSetSize = `${parsedSize}`;
- this.#internals.ariaPosInSet = `${Math.min(Math.max(1, posInSet), parsedSize)}`;
- }
- }
-
- updateSetSizeAndPosition(setSize: number | null, posInSet: number | null) {
- this.#internals.ariaSetSize = setSize !== null ? `${setSize}` : null;
- this.#internals.ariaPosInSet = posInSet !== null ? `${posInSet}` : null;
- }
-
#onFocus() {
this.dispatchEvent(new Event('optionfocus', { bubbles: true }));
}
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index af547b9d6c..cb4fe2ae83 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -3,8 +3,7 @@ import type { PropertyValues } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
-import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
-import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js';
+import { ListboxController, type ListboxFilterMode, type ListboxOrientation } from '@patternfly/pfe-core/controllers/listbox-controller.js';
import { PfListboxOption } from './pf-listbox-option.js';
import { PfListboxGroup, type PfListboxGroupOrOption } from './pf-listbox-group.js';
@@ -29,9 +28,9 @@ export class PfListbox extends LitElement {
@property() filter = '';
/**
- * whetehr filtering (if enabled) will be case-sensitive
+ * whether filtering (if enabled) will be case-sensitive
*/
- @property({ reflect: true, attribute: 'case-sensitive' }) caseSensitive = false;
+ @property({ reflect: true, attribute: 'case-sensitive', type: Boolean }) caseSensitive = false;
/**
* determines how filtering will be handled:
@@ -39,302 +38,58 @@ export class PfListbox extends LitElement {
* - "required": will hide all options until filter text is not ""
* - "disabled": will not hide options at all, regardless of filtering
*/
- @property({ reflect: true, attribute: 'filter-mode' }) filterMode: '' | 'required' | 'disabled' = '';
+ @property({ reflect: true, attribute: 'filter-mode', type: String }) filterMode: ListboxFilterMode = '';
+
+ /**
+ * whether filtering (if enabled) will look for filter match anywhere in option text
+ * (by default it will only match if the option starts with filter)
+ */
+ @property({ reflect: true, attribute: 'match-anywhere', type: Boolean }) matchAnywhere = false;
+
+ /**
+ * whether multiple items can be selected
+ */
+ @property({ reflect: true, attribute: 'multi-selectable', type: Boolean }) multiSelectable = false;
/**
* whether list items are arranged vertically or horizontally;
* limits arrow keys based on orientation
*/
- @property({ reflect: true, attribute: 'orientation', type: String }) orientation = '';
+ @property({ reflect: true, attribute: 'orientation', type: String }) orientation: ListboxOrientation = '';
/**
- * whether multiple items can be selected
+ * value of listbox as a string
*/
- @property({ reflect: true, attribute: 'multiple', type: Boolean }) multiple = false;
+ @property({ reflect: true, attribute: 'value' }) value?: string = undefined;
/**
* all slotted listbox options and/or groups of options
*/
@queryAssignedElements() private groupsOrOptions!: PfListboxGroupOrOption[];
- #internals = new InternalsController(this, {
- role: 'listbox'
+ #listbox = new ListboxController(this, {
+ caseSensitive: this.caseSensitive,
+ filterMode: this.filterMode,
+ matchAnywhere: this.matchAnywhere,
+ multiSelectable: this.multiSelectable,
+ orientation: this.orientation
});
- #tabindex = new RovingTabindexController(this);
-
- /**
- * all options that will not be hidden by a filter
- * */
- #_allOptions: PfListboxOption[] = [];
-
- #shiftStartingItem: PfListboxOption | null = null;
-
- get isHorizontal(): boolean {
- return this.#internals.ariaOrientation === 'horizontal';
- }
-
- get isMultiselectable(): boolean {
- return this.#internals.ariaMultiSelectable === 'true';
- }
-
- get activeItem() {
- const [active] = this.#allOptions.filter(option => option.getAttribute('id') === this.#internals.ariaActivedescendant);
- return active || this.#tabindex.firstItem;
- }
-
- get #allOptions() {
- return this.#_allOptions;
- }
-
- set #allOptions(options: PfListboxOption[]) {
- this.#_allOptions = this.filterMode === 'required' && this.filter === '' ? [] : options.filter(option => (this.constructor as typeof PfListbox).isOption(option) && (this.filterMode === 'disabled' || option.getUpdateByFilter(this.filter, this.caseSensitive)));
- }
-
- set value(items: string | null) {
- const oldValue = this.value;
- const selectedItems = items?.toLowerCase().split(',');
- const [firstItem] = selectedItems || [null];
- this.options.forEach(option => {
- const textContent = (option.textContent || '').toLowerCase();
- const selected = this.isMultiselectable ? selectedItems?.includes(textContent) : firstItem === textContent;
- option.ariaSelected = `${selected}`;
- });
- if (oldValue !== this.value) {
- this.#fireInput();
- }
- }
-
- get value() {
- const selectedItems = this.options.filter(option=>option.ariaSelected === 'true').map(option => option.textContent);
- return selectedItems.join(',');
- }
-
- get validOptions() {
- return this.options;
- }
-
- isValid(val: string | null) {
- const vals = val?.split(',') || [];
- const options = this.options.map(option => option.textContent);
- return vals.every(val => {
- return options.includes(val);
- });
- }
-
updated(changed: PropertyValues) {
- if (changed.has('filter') || changed.has('caseSensitive') || changed.has('filterMode') || changed.has('filterMode')) {
- this.#onFilterChange();
- }
- if (changed.has('orientation')) {
- this.#internals.ariaOrientation = ['vertical', 'horizontal'].includes(this.orientation) ? this.orientation : null;
- }
- if (changed.has('multiple')) {
- this.#internals.ariaMultiSelectable = this.multiple ? 'true' : 'false';
- }
+ // console.log(changed);
}
render() {
return html`
+ class="${this.orientation}"
+ @slotchange="${this.#onSlotchange}">
`;
}
- /**
- * filters listbox by keboard event when slotted option has focus,
- * or by external element such as a text field
- * @param event
- * @returns { void }
- */
- filterByKeyboardEvent(event: KeyboardEvent) {
- const target = event.target as PfListboxOption;
- const oldValue = this.value;
- let stopEvent = false;
- if (event.altKey ||
- event.metaKey) {
- return;
- } else if (event.ctrlKey) {
- if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
- this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem, true);
- stopEvent = true;
- } else {
- return;
- }
- } else {
- switch (event.key) {
- case 'Backspace':
- case 'Delete':
- this.filter = this.filter.slice(0, this.filter.length - 2);
- stopEvent = true;
- break;
- case event.key?.match(/^[\w]$/)?.input:
- this.filter += event.key?.toLowerCase();
- stopEvent = true;
- break;
- case 'Enter':
- case ' ':
- // enter and space are only applicable if a listbox option is clicked
- // an external text input should not trigger multiselect
- if (target) {
- if (this.isMultiselectable) {
- if (event.shiftKey) {
- this.#updateMultiselect(target);
- } else {
- target.ariaSelected = `${target.ariaSelected !== 'true'}`;
- }
- stopEvent = true;
- }
- }
- break;
- default:
- break;
- }
- }
- if (stopEvent) {
- event.stopPropagation();
- event.preventDefault();
- }
- if (oldValue !== this.value) {
- this.#fireChange();
- }
- }
-
focus() {
- this.#tabindex.focusOnItem(this.activeItem);
- }
-
- #updateActiveDescendant() {
- let found = false;
- this.#allOptions.forEach(option => {
- if (option === this.#tabindex.activeItem) {
- this.#internals.ariaActivedescendant = option.id;
- option.setAttribute('active-descendant', 'active-descendant');
- found = true;
- } else {
- option.removeAttribute('active-descendant');
- }
- });
- if (!found) {
- this.#tabindex.updateActiveItem(this.#tabindex.firstItem);
- this.#internals.ariaActivedescendant = this.#tabindex.firstItem?.id || null;
- }
- this.#updateSingleselect();
- }
-
- #updateSingleselect() {
- if (!this.isMultiselectable) {
- this.options.forEach(option => option.ariaSelected = `${option.id === this.#internals.ariaActivedescendant}`);
- this.#fireChange();
- }
- }
-
- #updateMultiselect(currentItem: PfListboxOption, referenceItem = this.activeItem, ctrlKey = false) {
- if (this.isMultiselectable) {
- // select all options between active descendant and target
- const [start, end] = [this.options.indexOf(referenceItem), this.options.indexOf(currentItem)].sort();
- const options = [...this.options].slice(start, end + 1);
- // if all items in range are toggled, remove toggle
- const allSelected = ctrlKey && options.filter(option => option.ariaSelected !== 'true')?.length === 0;
- const toggle = ctrlKey && allSelected ? false : ctrlKey ? true : referenceItem.ariaSelected === 'true';
- options.forEach(option => option.ariaSelected = `${toggle}`);
- this.#shiftStartingItem = currentItem;
- }
- }
-
- /**
- * handles user user selection change similar to HTMLSelectElement events
- * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
- * @fires change
- */
- #fireChange() {
- this.dispatchEvent(new Event('change', { bubbles: true }));
- }
-
- /**
- * handles element value change similar to HTMLSelectElement events
- * (@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement#events|MDN: HTMLSelectElement Events})
- * @fires input
- */
- #fireInput() {
- this.dispatchEvent(new Event('input', { bubbles: true }));
- }
-
- #onFilterChange() {
- const oldValue = this.value;
- this.#allOptions = this.options;
- const all = this.#allOptions;
- this.#tabindex.updateItems(all);
- this.#updateActiveDescendant();
- if (oldValue !== this.value) {
- this.#fireInput();
- }
- }
-
- #onOptionFocus(event: FocusEvent) {
- const target = event.target as PfListboxOption;
- if (target !== this.#tabindex.activeItem) {
- this.#tabindex.updateActiveItem(target);
- }
- this.#updateActiveDescendant();
- }
-
- #onOptionClick(event: MouseEvent) {
- const target = event.target as PfListboxOption;
- const oldValue = this.value;
- if (this.isMultiselectable) {
- if (!event.shiftKey) {
- target.ariaSelected = `${target.ariaSelected !== 'true'}`;
- } else {
- if (this.#shiftStartingItem && target) {
- this.#updateMultiselect(target, this.#shiftStartingItem);
- this.#fireChange();
- }
- }
- } else {
- // select target and deselect all other options
- this.options.forEach(option => option.ariaSelected = `${option === target}`);
- }
- if (target !== this.#tabindex.activeItem) {
- this.#tabindex.focusOnItem(target);
- this.#updateActiveDescendant();
- }
- if (oldValue !== this.value) {
- this.#fireChange();
- }
- }
-
- #onOptionKeyup(event: KeyboardEvent) {
- const target = event.target as PfListboxOption;
- if (event.shiftKey && this.isMultiselectable) {
- if (this.#shiftStartingItem && target) {
- this.#updateMultiselect(target, this.#shiftStartingItem);
- this.#fireChange();
- }
- if (event.key === 'Shift') {
- this.#shiftStartingItem = null;
- }
- }
- }
-
- #onOptionKeydown(event: KeyboardEvent) {
- const { filter } = this;
- // need to set for keyboard support of multiselect
- if (event.key === 'Shift' && this.isMultiselectable) {
- this.#shiftStartingItem = this.activeItem;
- }
- this.filterByKeyboardEvent(event);
- // only change focus if keydown occurred when option has focus
- // (as opposed to an external text input and if filter has changed
- if (filter !== this.filter) {
- this.#tabindex.focusOnItem(this.activeItem);
- }
+ this.#listbox.focus();
}
get options() {
@@ -348,15 +103,12 @@ export class PfListbox extends LitElement {
}
return options;
});
+ // console.log(this.querySelectorAll('pf-listbox-option'), extractItems(this.groupsOrOptions));
return extractItems(this.groupsOrOptions);
}
#onSlotchange() {
- this.#allOptions = this.options;
- const all = this.#allOptions;
- const setSize = this.options.length;
- this.options.forEach((option, i) => option.updateSetSizeAndPosition(setSize, i));
- this.#tabindex.initItems(all, this);
+ this.#listbox.options = this.options;
}
}
From f61771ce28d83eab198fb8a750dba331c0a60e5b Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 11:53:15 -0400
Subject: [PATCH 34/40] feat(core): updated listbox-controller api
---
.../controllers/listbox-controller.ts | 174 ++++++++++--------
1 file changed, 96 insertions(+), 78 deletions(-)
diff --git a/core/pfe-core/controllers/listbox-controller.ts b/core/pfe-core/controllers/listbox-controller.ts
index 06b59e82c8..19f5bd19d5 100644
--- a/core/pfe-core/controllers/listbox-controller.ts
+++ b/core/pfe-core/controllers/listbox-controller.ts
@@ -85,8 +85,8 @@ export class ListboxController<
return active || this.#tabindex.firstItem;
}
- set filter(str: string) {
- this.filter = str;
+ set filter(filtertext: string) {
+ this.filter = filtertext;
this.#onFilterChange();
}
@@ -103,8 +103,8 @@ export class ListboxController<
return this.#caseSensitive;
}
- set filterMode(str: ListboxFilterMode) {
- this.#filterMode = str;
+ set filterMode(filterMode: ListboxFilterMode) {
+ this.#filterMode = filterMode;
this.#onFilterChange();
}
@@ -163,7 +163,7 @@ export class ListboxController<
return !option.hasAttribute('hidden-by-filter');
}
};
- this.#tabindex.initItems(this.#visibleOptions, this.host);
+ this.#tabindex.initItems(this.#visibleOptions);
this.#visibleOptions = options.filter((option, i) => filterOptions(option, i));
this.#options = options;
}
@@ -173,9 +173,9 @@ export class ListboxController<
return selectedItems.join(',');
}
- set value(items: string | null) {
+ set value(optionsList: string | null) {
const oldValue = this.value;
- const selectedItems = items?.toLowerCase().split(',');
+ const selectedItems = optionsList?.toLowerCase().split(',');
const [firstItem] = selectedItems || [null];
this.options.forEach(option => {
const textContent = (option.textContent || '').replace('\\,', ',').toLowerCase();
@@ -227,76 +227,6 @@ export class ListboxController<
});
}
- #updateOption(option: HTMLElement) {
- const search = this.#caseSensitive ? this.#filter : this.#filter.toLowerCase();
- const text = this.#caseSensitive ? (option.textContent || '') : (option.textContent || '');
- if (search === '' || text.match(search)) {
- option.removeAttribute('hidden-by-filter');
- } else {
- option.setAttribute('hidden-by-filter', 'hidden-by-filter');
- }
- return !option.hasAttribute('hidden-by-filter');
- }
-
- /**
- * filters listbox by keboard event when slotted option has focus,
- * or by external element such as a text field
- * @param event
- * @returns { void }
- */
- filterByKeyboardEvent(event: KeyboardEvent) {
- const target = event.target as HTMLElement;
- const oldValue = this.value;
- let stopEvent = false;
- if (event.altKey ||
- event.metaKey) {
- return;
- } else if (event.ctrlKey) {
- if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
- this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem, true);
- stopEvent = true;
- } else {
- return;
- }
- } else {
- switch (event.key) {
- case 'Backspace':
- case 'Delete':
- this.#filter = this.#filter.slice(0, this.#filter.length - 2);
- stopEvent = true;
- break;
- case event.key?.match(/^[\w]$/)?.input:
- this.#filter += event.key?.toLowerCase();
- stopEvent = true;
- break;
- case 'Enter':
- case ' ':
- // enter and space are only applicable if a listbox option is clicked
- // an external text input should not trigger multiselect
- if (target) {
- if (this.multiSelectable) {
- if (event.shiftKey) {
- this.#updateMultiselect(target);
- } else {
- target.ariaSelected = `${target.ariaSelected !== 'true'}`;
- }
- stopEvent = true;
- }
- }
- break;
- default:
- break;
- }
- }
- if (stopEvent) {
- event.stopPropagation();
- event.preventDefault();
- }
- if (oldValue !== this.value) {
- this.#fireChange();
- }
- }
-
focus() {
this.#tabindex.focusOnItem(this.activeItem);
}
@@ -326,6 +256,13 @@ export class ListboxController<
}
}
+ /**
+ * for listboxes that are multiselectable, updates listbox option selections:
+ * toggles all options between active descendant and target
+ * @param currentItem
+ * @param referenceItem
+ * @param ctrlKey
+ */
#updateMultiselect(currentItem: HTMLElement, referenceItem = this.activeItem, ctrlKey = false) {
if (this.multiSelectable) {
// select all options between active descendant and target
@@ -357,6 +294,12 @@ export class ListboxController<
this.host.dispatchEvent(new Event('input', { bubbles: true }));
}
+ /**
+ * handles updates to filter text:
+ * hides options that do not match filter settings
+ * and updates active descendant based on which options are still visible
+ * @returns void
+ */
#onFilterChange() {
const oldValue = this.value;
this.#tabindex.updateItems(this.#visibleOptions);
@@ -366,6 +309,12 @@ export class ListboxController<
}
}
+ /**
+ * handles focusing on an option:
+ * updates roving tabindex and active descendant
+ * @param event {FocusEvent}
+ * @returns void
+ */
#onOptionFocus(event: FocusEvent) {
const target = event.target as HTMLElement;
if (target !== this.#tabindex.activeItem) {
@@ -374,6 +323,13 @@ export class ListboxController<
this.#updateActiveDescendant();
}
+ /**
+ * handles clicking on a listbox option:
+ * which selects an item by default
+ * or toggles selection if multiselectable
+ * @param event {MouseEvent}
+ * @returns void
+ */
#onOptionClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const oldValue = this.value;
@@ -399,6 +355,12 @@ export class ListboxController<
}
}
+ /**
+ * handles keyup:
+ * track whether shift key is being used for multiselectable listbox
+ * @param event {KeyboardEvent}
+ * @returns void
+ */
#onOptionKeyup(event: KeyboardEvent) {
const target = event.target as HTMLElement;
if (event.shiftKey && this.multiSelectable) {
@@ -412,13 +374,69 @@ export class ListboxController<
}
}
+ /**
+ * handles keydown:
+ * filters listbox by keboard event when slotted option has focus,
+ * or by external element such as a text field
+ * @param event {KeyboardEvent}
+ * @returns void
+ */
#onOptionKeydown(event: KeyboardEvent) {
const { filter } = this;
// need to set for keyboard support of multiselect
if (event.key === 'Shift' && this.multiSelectable) {
this.#shiftStartingItem = this.activeItem;
}
- this.filterByKeyboardEvent(event);
+ const target = event.target as HTMLElement;
+ const oldValue = this.value;
+ let stopEvent = false;
+ if (event.altKey ||
+ event.metaKey) {
+ return;
+ } else if (event.ctrlKey) {
+ if (event.key?.match(/^[aA]$/)?.input && this.#tabindex.firstItem) {
+ this.#updateMultiselect(this.#tabindex.firstItem, this.#tabindex.lastItem, true);
+ stopEvent = true;
+ } else {
+ return;
+ }
+ } else {
+ switch (event.key) {
+ case 'Backspace':
+ case 'Delete':
+ this.#filter = this.#filter.slice(0, this.#filter.length - 2);
+ stopEvent = true;
+ break;
+ case event.key?.match(/^[\w]$/)?.input:
+ this.#filter += event.key?.toLowerCase();
+ stopEvent = true;
+ break;
+ case 'Enter':
+ case ' ':
+ // enter and space are only applicable if a listbox option is clicked
+ // an external text input should not trigger multiselect
+ if (target) {
+ if (this.multiSelectable) {
+ if (event.shiftKey) {
+ this.#updateMultiselect(target);
+ } else {
+ target.ariaSelected = `${target.ariaSelected !== 'true'}`;
+ }
+ stopEvent = true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ if (stopEvent) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ if (oldValue !== this.value) {
+ this.#fireChange();
+ }
// only change focus if keydown occurred when option has focus
// (as opposed to an external text input and if filter has changed
if (filter !== this.#filter) {
From 764671dad2351b57cc677c5604975e3243bf2bc3 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 11:54:13 -0400
Subject: [PATCH 35/40] feat(listbox): updated listbox to use
listbox-controller
---
elements/pf-listbox/pf-listbox.ts | 69 ++++++++++++++++---------------
1 file changed, 35 insertions(+), 34 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-listbox/pf-listbox.ts
index cb4fe2ae83..b04c80f419 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-listbox/pf-listbox.ts
@@ -2,10 +2,9 @@ import { LitElement, html } from 'lit';
import type { PropertyValues } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
-import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
import { ListboxController, type ListboxFilterMode, type ListboxOrientation } from '@patternfly/pfe-core/controllers/listbox-controller.js';
-import { PfListboxOption } from './pf-listbox-option.js';
-import { PfListboxGroup, type PfListboxGroupOrOption } from './pf-listbox-group.js';
+import './pf-listbox-option.js';
+import './pf-listbox-group.js';
import styles from './pf-listbox.css';
@@ -18,10 +17,6 @@ import styles from './pf-listbox.css';
export class PfListbox extends LitElement {
static readonly styles = [styles];
- static isOption(element: PfListboxOption): element is PfListboxOption {
- return element instanceof PfListboxOption;
- }
-
/**
* filter options that start with a string (case-insensitive)
*/
@@ -57,16 +52,6 @@ export class PfListbox extends LitElement {
*/
@property({ reflect: true, attribute: 'orientation', type: String }) orientation: ListboxOrientation = '';
- /**
- * value of listbox as a string
- */
- @property({ reflect: true, attribute: 'value' }) value?: string = undefined;
-
- /**
- * all slotted listbox options and/or groups of options
- */
- @queryAssignedElements() private groupsOrOptions!: PfListboxGroupOrOption[];
-
#listbox = new ListboxController(this, {
caseSensitive: this.caseSensitive,
filterMode: this.filterMode,
@@ -75,8 +60,16 @@ export class PfListbox extends LitElement {
orientation: this.orientation
});
- updated(changed: PropertyValues) {
- // console.log(changed);
+ get options() {
+ return [...this.querySelectorAll('pf-listbox-option')];
+ }
+
+ set value(optionsList: string | null) {
+ this.#listbox.value = optionsList;
+ }
+
+ get value() {
+ return this.#listbox.value;
}
render() {
@@ -88,23 +81,31 @@ export class PfListbox extends LitElement {
`;
}
- focus() {
- this.#listbox.focus();
+ constructor() {
+ super();
+ this.#listbox.options = this.options;
}
- get options() {
- const extractItems = (group: PfListboxGroupOrOption[]) => group.flatMap((item: PfListboxGroupOrOption) => {
- let options: PfListboxOption[];
- if (item instanceof PfListboxGroup) {
- const group = item as PfListboxGroup;
- options = extractItems(group.options);
- } else {
- options = [item];
- }
- return options;
- });
- // console.log(this.querySelectorAll('pf-listbox-option'), extractItems(this.groupsOrOptions));
- return extractItems(this.groupsOrOptions);
+ updated(changed: PropertyValues) {
+ if (changed.has('caseSensitive')) {
+ this.#listbox.caseSensitive = this.caseSensitive;
+ }
+ if (changed.has('filterMode')) {
+ this.#listbox.filterMode = this.filterMode;
+ }
+ if (changed.has('matchAnywhere')) {
+ this.#listbox.matchAnywhere = this.matchAnywhere;
+ }
+ if (changed.has('multiSelectable')) {
+ this.#listbox.multiSelectable = this.multiSelectable;
+ }
+ if (changed.has('orientation')) {
+ this.#listbox.orientation = this.orientation;
+ }
+ }
+
+ focus() {
+ this.#listbox.focus();
}
#onSlotchange() {
From dae525866fc4a19766fb04d9fdbe8ea518dd451d Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 12:30:23 -0400
Subject: [PATCH 36/40] feat(listbox): updated listbox group based on listbox
changes
---
elements/pf-listbox/pf-listbox-group.ts | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/elements/pf-listbox/pf-listbox-group.ts b/elements/pf-listbox/pf-listbox-group.ts
index d5109e1887..04c4ea546c 100644
--- a/elements/pf-listbox/pf-listbox-group.ts
+++ b/elements/pf-listbox/pf-listbox-group.ts
@@ -1,13 +1,9 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
-import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js';
-import { PfListboxOption } from './pf-listbox-option.js';
import styles from './pf-listbox-group.css';
-export type PfListboxGroupOrOption = PfListboxGroup | PfListboxOption;
-
/**
* Group of options within a listbox
* @slot - Place element content here
@@ -20,8 +16,6 @@ export class PfListboxGroup extends LitElement {
role: 'group'
});
- @queryAssignedElements() private _options!: PfListboxOption[];
-
override connectedCallback() {
this.#internals;
}
@@ -29,17 +23,9 @@ export class PfListboxGroup extends LitElement {
render() {
return html`
-
+
`;
}
-
- get options() {
- return this._options;
- }
-
- #onSlotchange() {
- this.dispatchEvent(new Event('slotchange', { bubbles: true }));
- }
}
declare global {
From 7768d4bdcda53661448a7b2f79aeeb310ec9f697 Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 13:11:42 -0400
Subject: [PATCH 37/40] chore(simple-list): renamed pf-listbox to match
upstream patternfly simple-list
---
elements/package.json | 6 +--
elements/pf-listbox/README.md | 11 -----
elements/pf-listbox/demo/filter-modes.html | 43 -------------------
elements/pf-listbox/demo/grouped-options.html | 23 ----------
.../pf-listbox/demo/multiple-selections.html | 19 --------
elements/pf-listbox/demo/orientation.html | 28 ------------
elements/pf-listbox/demo/pf-listbox.html | 19 --------
elements/pf-listbox/test/pf-listbox.spec.ts | 21 ---------
elements/pf-simple-list/README.md | 18 ++++++++
.../demo/demo.css | 2 +-
.../pf-simple-list/demo/filter-modes.html | 43 +++++++++++++++++++
.../pf-simple-list/demo/grouped-options.html | 23 ++++++++++
.../demo/multiple-selections.html | 19 ++++++++
elements/pf-simple-list/demo/orientation.html | 28 ++++++++++++
.../pf-simple-list/demo/pf-simple-list.html | 19 ++++++++
.../demo/pf-simple-list.js} | 4 +-
.../docs/pf-simple-list.md} | 2 +-
.../pf-simple-list-group.css} | 0
.../pf-simple-list-group.ts} | 8 ++--
.../pf-simple-list-option.css} | 0
.../pf-simple-list-option.ts} | 8 ++--
.../pf-simple-list.css} | 0
.../pf-simple-list.ts} | 14 +++---
.../test/pf-listbox.e2e.ts | 2 +-
.../pf-simple-list/test/pf-listbox.spec.ts | 21 +++++++++
25 files changed, 194 insertions(+), 187 deletions(-)
delete mode 100644 elements/pf-listbox/README.md
delete mode 100644 elements/pf-listbox/demo/filter-modes.html
delete mode 100644 elements/pf-listbox/demo/grouped-options.html
delete mode 100644 elements/pf-listbox/demo/multiple-selections.html
delete mode 100644 elements/pf-listbox/demo/orientation.html
delete mode 100644 elements/pf-listbox/demo/pf-listbox.html
delete mode 100644 elements/pf-listbox/test/pf-listbox.spec.ts
create mode 100644 elements/pf-simple-list/README.md
rename elements/{pf-listbox => pf-simple-list}/demo/demo.css (81%)
create mode 100644 elements/pf-simple-list/demo/filter-modes.html
create mode 100644 elements/pf-simple-list/demo/grouped-options.html
create mode 100644 elements/pf-simple-list/demo/multiple-selections.html
create mode 100644 elements/pf-simple-list/demo/orientation.html
create mode 100644 elements/pf-simple-list/demo/pf-simple-list.html
rename elements/{pf-listbox/demo/pf-listbox.js => pf-simple-list/demo/pf-simple-list.js} (87%)
rename elements/{pf-listbox/docs/pf-listbox.md => pf-simple-list/docs/pf-simple-list.md} (91%)
rename elements/{pf-listbox/pf-listbox-group.css => pf-simple-list/pf-simple-list-group.css} (100%)
rename elements/{pf-listbox/pf-listbox-group.ts => pf-simple-list/pf-simple-list-group.ts} (77%)
rename elements/{pf-listbox/pf-listbox-option.css => pf-simple-list/pf-simple-list-option.css} (100%)
rename elements/{pf-listbox/pf-listbox-option.ts => pf-simple-list/pf-simple-list-option.ts} (83%)
rename elements/{pf-listbox/pf-listbox.css => pf-simple-list/pf-simple-list.css} (100%)
rename elements/{pf-listbox/pf-listbox.ts => pf-simple-list/pf-simple-list.ts} (91%)
rename elements/{pf-listbox => pf-simple-list}/test/pf-listbox.e2e.ts (90%)
create mode 100644 elements/pf-simple-list/test/pf-listbox.spec.ts
diff --git a/elements/package.json b/elements/package.json
index 68a7a1fca6..13ab1eeef0 100644
--- a/elements/package.json
+++ b/elements/package.json
@@ -36,9 +36,9 @@
"./pf-jump-links/pf-jump-links.js": "./pf-jump-links/pf-jump-links.js",
"./pf-label/BaseLabel.js": "./pf-label/BaseLabel.js",
"./pf-label/pf-label.js": "./pf-label/pf-label.js",
- "./pf-listbox/pf-listbox.js": "./pf-listbox/pf-listbox.js",
- "./pf-listbox/pf-listbox-group.js": "./pf-listbox/pf-listbox-group.js",
- "./pf-listbox/pf-listbox-option.js": "./pf-listbox/pf-listbox-option.js",
+ "./pf-simple-list/pf-simple-list.js": "./pf-simple-list/pf-simple-list.js",
+ "./pf-simple-list/pf-simple-list-group.js": "./pf-simple-list/pf-simple-list-group.js",
+ "./pf-simple-list/pf-simple-list-option.js": "./pf-simple-list/pf-simple-list-option.js",
"./pf-modal/pf-modal.js": "./pf-modal/pf-modal.js",
"./pf-panel/pf-panel.js": "./pf-panel/pf-panel.js",
"./pf-progress-stepper/pf-progress-step.js": "./pf-progress-stepper/pf-progress-step.js",
diff --git a/elements/pf-listbox/README.md b/elements/pf-listbox/README.md
deleted file mode 100644
index b09287b7d3..0000000000
--- a/elements/pf-listbox/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Listbox
-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-listbox/demo/filter-modes.html b/elements/pf-listbox/demo/filter-modes.html
deleted file mode 100644
index 88a8ee6fa0..0000000000
--- a/elements/pf-listbox/demo/filter-modes.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-filter-mode="disabled"
-
- Any arrow keys work.
- Only one option can be selected.
- Filter is disabled.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
-filter-mode="required": combobox example
-
- Any arrow keys work.
- Only one option can be selected.
- Listbox wont appear until input has text.
-
-
- Pick a color:
-
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/grouped-options.html b/elements/pf-listbox/demo/grouped-options.html
deleted file mode 100644
index 62e0830ce7..0000000000
--- a/elements/pf-listbox/demo/grouped-options.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- Any arrow keys work.
- Only one option can be selected.
- Filter by typing a letter.
- Headings are not selectable.
-
-
-
- Breakfast
- Eggs
- Toast
- Waffles
-
-
- Lunch
- Salad
- Sandwich
- Soup
-
-
diff --git a/elements/pf-listbox/demo/multiple-selections.html b/elements/pf-listbox/demo/multiple-selections.html
deleted file mode 100644
index ba94169504..0000000000
--- a/elements/pf-listbox/demo/multiple-selections.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- Multiple options can be selected. Any arrow keys work.
- Shift will toggling off multiple items.
- Ctrl+A will toggle selection on all items.
- Filter by typing a letter.
-
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/orientation.html b/elements/pf-listbox/demo/orientation.html
deleted file mode 100644
index 408ad9ebc7..0000000000
--- a/elements/pf-listbox/demo/orientation.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-orientation="vertical"
-Only vertical arrow keys work. Filter by typing a letter.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
-
-orientation="horizontal"
-Only horizontal arrow keys work. Filter by typing a letter.
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/pf-listbox.html b/elements/pf-listbox/demo/pf-listbox.html
deleted file mode 100644
index 0a428c9b0e..0000000000
--- a/elements/pf-listbox/demo/pf-listbox.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-default
-
- Any arrow keys work.
- Only one option can be selected.
- Filter by typing a letter.
-
-
- Blue
- Green
- Magenta
- Orange
- Purple
- Pink
- Red
- Yellow
-
\ No newline at end of file
diff --git a/elements/pf-listbox/test/pf-listbox.spec.ts b/elements/pf-listbox/test/pf-listbox.spec.ts
deleted file mode 100644
index 0601c9f1d8..0000000000
--- a/elements/pf-listbox/test/pf-listbox.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { expect, html } from '@open-wc/testing';
-import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
-import { PfListbox } from '@patternfly/elements/pf-listbox/pf-listbox.js';
-
-describe('', function() {
- describe('simply instantiating', function() {
- let element: PfListbox;
- it('imperatively instantiates', function() {
- expect(document.createElement('pf-listbox')).to.be.an.instanceof(PfListbox);
- });
-
- it('should upgrade', async function() {
- element = await createFixture(html` `);
- const klass = customElements.get('pf-listbox');
- expect(element)
- .to.be.an.instanceOf(klass)
- .and
- .to.be.an.instanceOf(PfListbox);
- });
- });
-});
diff --git a/elements/pf-simple-list/README.md b/elements/pf-simple-list/README.md
new file mode 100644
index 0000000000..01bbe81351
--- /dev/null
+++ b/elements/pf-simple-list/README.md
@@ -0,0 +1,18 @@
+# Simple List
+A simple list provides a list of selectable items that can be shown within a page. Each item is described by a text label. The list may be divided into logical sections by introducing group headers.
+
+## Usage
+A simple list is used when the information presented does not follow a specific pattern, or does not require bullets to differentiate one list item from another.
+
+```html
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+```
diff --git a/elements/pf-listbox/demo/demo.css b/elements/pf-simple-list/demo/demo.css
similarity index 81%
rename from elements/pf-listbox/demo/demo.css
rename to elements/pf-simple-list/demo/demo.css
index 7a63e2ef0e..6a5018b783 100644
--- a/elements/pf-listbox/demo/demo.css
+++ b/elements/pf-simple-list/demo/demo.css
@@ -1,4 +1,4 @@
-[data-demo="pf-listbox"] {
+[data-demo="pf-simple-list"] {
padding: 20px;
}
diff --git a/elements/pf-simple-list/demo/filter-modes.html b/elements/pf-simple-list/demo/filter-modes.html
new file mode 100644
index 0000000000..d924206499
--- /dev/null
+++ b/elements/pf-simple-list/demo/filter-modes.html
@@ -0,0 +1,43 @@
+
+
+
+filter-mode="disabled"
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Filter is disabled.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+filter-mode="required": combobox example
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Listbox wont appear until input has text.
+
+
+ Pick a color:
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
\ No newline at end of file
diff --git a/elements/pf-simple-list/demo/grouped-options.html b/elements/pf-simple-list/demo/grouped-options.html
new file mode 100644
index 0000000000..1a2cd842f2
--- /dev/null
+++ b/elements/pf-simple-list/demo/grouped-options.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Filter by typing a letter.
+ Headings are not selectable.
+
+
+
+ Breakfast
+ Eggs
+ Toast
+ Waffles
+
+
+ Lunch
+ Salad
+ Sandwich
+ Soup
+
+
diff --git a/elements/pf-simple-list/demo/multiple-selections.html b/elements/pf-simple-list/demo/multiple-selections.html
new file mode 100644
index 0000000000..ed8dff149e
--- /dev/null
+++ b/elements/pf-simple-list/demo/multiple-selections.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Multiple options can be selected. Any arrow keys work.
+ Shift will toggling off multiple items.
+ Ctrl+A will toggle selection on all items.
+ Filter by typing a letter.
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
\ No newline at end of file
diff --git a/elements/pf-simple-list/demo/orientation.html b/elements/pf-simple-list/demo/orientation.html
new file mode 100644
index 0000000000..160aa6d6cb
--- /dev/null
+++ b/elements/pf-simple-list/demo/orientation.html
@@ -0,0 +1,28 @@
+
+
+
+orientation="vertical"
+Only vertical arrow keys work. Filter by typing a letter.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
+
+orientation="horizontal"
+Only horizontal arrow keys work. Filter by typing a letter.
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
\ No newline at end of file
diff --git a/elements/pf-simple-list/demo/pf-simple-list.html b/elements/pf-simple-list/demo/pf-simple-list.html
new file mode 100644
index 0000000000..0701759da6
--- /dev/null
+++ b/elements/pf-simple-list/demo/pf-simple-list.html
@@ -0,0 +1,19 @@
+
+
+
+default
+
+ Any arrow keys work.
+ Only one option can be selected.
+ Filter by typing a letter.
+
+
+ Blue
+ Green
+ Magenta
+ Orange
+ Purple
+ Pink
+ Red
+ Yellow
+
\ No newline at end of file
diff --git a/elements/pf-listbox/demo/pf-listbox.js b/elements/pf-simple-list/demo/pf-simple-list.js
similarity index 87%
rename from elements/pf-listbox/demo/pf-listbox.js
rename to elements/pf-simple-list/demo/pf-simple-list.js
index bd228bc7d8..d80cd1e5c3 100644
--- a/elements/pf-listbox/demo/pf-listbox.js
+++ b/elements/pf-simple-list/demo/pf-simple-list.js
@@ -1,7 +1,7 @@
-import '@patternfly/elements/pf-listbox/pf-listbox.js';
+import '@patternfly/elements/pf-simple-list/pf-simple-list.js';
/**
- * pf-listbox in a combox box
+ * pf-simple-list in a combox box
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-both/|WAI ARIA Examples: Combobox with ARIAutocomplete}
*/
const input = document.querySelector('#text');
diff --git a/elements/pf-listbox/docs/pf-listbox.md b/elements/pf-simple-list/docs/pf-simple-list.md
similarity index 91%
rename from elements/pf-listbox/docs/pf-listbox.md
rename to elements/pf-simple-list/docs/pf-simple-list.md
index f3745b57be..e1de71c8b6 100644
--- a/elements/pf-listbox/docs/pf-listbox.md
+++ b/elements/pf-simple-list/docs/pf-simple-list.md
@@ -1,5 +1,5 @@
{% renderOverview %}
-
+
{% endrenderOverview %}
{% band header="Usage" %}{% endband %}
diff --git a/elements/pf-listbox/pf-listbox-group.css b/elements/pf-simple-list/pf-simple-list-group.css
similarity index 100%
rename from elements/pf-listbox/pf-listbox-group.css
rename to elements/pf-simple-list/pf-simple-list-group.css
diff --git a/elements/pf-listbox/pf-listbox-group.ts b/elements/pf-simple-list/pf-simple-list-group.ts
similarity index 77%
rename from elements/pf-listbox/pf-listbox-group.ts
rename to elements/pf-simple-list/pf-simple-list-group.ts
index 04c4ea546c..0e4c8893b6 100644
--- a/elements/pf-listbox/pf-listbox-group.ts
+++ b/elements/pf-simple-list/pf-simple-list-group.ts
@@ -2,14 +2,14 @@ import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
-import styles from './pf-listbox-group.css';
+import styles from './pf-simple-list-group.css';
/**
* Group of options within a listbox
* @slot - Place element content here
*/
-@customElement('pf-listbox-group')
-export class PfListboxGroup extends LitElement {
+@customElement('pf-simple-list-group')
+export class PfSimpleListGroup extends LitElement {
static readonly styles = [styles];
#internals = new InternalsController(this, {
@@ -30,6 +30,6 @@ export class PfListboxGroup extends LitElement {
declare global {
interface HTMLElementTagNameMap {
- 'pf-listbox-group': PfListboxGroup;
+ 'pf-simple-list-group': PfSimpleListGroup;
}
}
diff --git a/elements/pf-listbox/pf-listbox-option.css b/elements/pf-simple-list/pf-simple-list-option.css
similarity index 100%
rename from elements/pf-listbox/pf-listbox-option.css
rename to elements/pf-simple-list/pf-simple-list-option.css
diff --git a/elements/pf-listbox/pf-listbox-option.ts b/elements/pf-simple-list/pf-simple-list-option.ts
similarity index 83%
rename from elements/pf-listbox/pf-listbox-option.ts
rename to elements/pf-simple-list/pf-simple-list-option.ts
index 5fb6c6911a..8441078ada 100644
--- a/elements/pf-listbox/pf-listbox-option.ts
+++ b/elements/pf-simple-list/pf-simple-list-option.ts
@@ -3,14 +3,14 @@ import { customElement } from 'lit/decorators/custom-element.js';
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
import { getRandomId } from '@patternfly/pfe-core/functions/random.js';
-import styles from './pf-listbox-option.css';
+import styles from './pf-simple-list-option.css';
/**
* Option within a listbox
* @slot - Place element content here
*/
-@customElement('pf-listbox-option')
-export class PfListboxOption extends LitElement {
+@customElement('pf-simple-list-option')
+export class PfSimpleListOption extends LitElement {
static readonly styles = [styles];
#internals = new InternalsController(this, {
@@ -42,6 +42,6 @@ export class PfListboxOption extends LitElement {
declare global {
interface HTMLElementTagNameMap {
- 'pf-listbox-option': PfListboxOption;
+ 'pf-simple-list-option': PfSimpleListOption;
}
}
diff --git a/elements/pf-listbox/pf-listbox.css b/elements/pf-simple-list/pf-simple-list.css
similarity index 100%
rename from elements/pf-listbox/pf-listbox.css
rename to elements/pf-simple-list/pf-simple-list.css
diff --git a/elements/pf-listbox/pf-listbox.ts b/elements/pf-simple-list/pf-simple-list.ts
similarity index 91%
rename from elements/pf-listbox/pf-listbox.ts
rename to elements/pf-simple-list/pf-simple-list.ts
index b04c80f419..c774c7ec05 100644
--- a/elements/pf-listbox/pf-listbox.ts
+++ b/elements/pf-simple-list/pf-simple-list.ts
@@ -3,18 +3,18 @@ import type { PropertyValues } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { ListboxController, type ListboxFilterMode, type ListboxOrientation } from '@patternfly/pfe-core/controllers/listbox-controller.js';
-import './pf-listbox-option.js';
-import './pf-listbox-group.js';
+import './pf-simple-list-option.js';
+import './pf-simple-list-group.js';
-import styles from './pf-listbox.css';
+import styles from './pf-simple-list.css';
/**
* List of selectable items
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/listbox/|WAI-ARIA Listbox Pattern}
* @slot - Place element content here
*/
-@customElement('pf-listbox')
-export class PfListbox extends LitElement {
+@customElement('pf-simple-list')
+export class PfSimpleList extends LitElement {
static readonly styles = [styles];
/**
@@ -61,7 +61,7 @@ export class PfListbox extends LitElement {
});
get options() {
- return [...this.querySelectorAll('pf-listbox-option')];
+ return [...this.querySelectorAll('pf-simple-list-option')];
}
set value(optionsList: string | null) {
@@ -115,6 +115,6 @@ export class PfListbox extends LitElement {
declare global {
interface HTMLElementTagNameMap {
- 'pf-listbox': PfListbox;
+ 'pf-simple-list': PfSimpleList;
}
}
diff --git a/elements/pf-listbox/test/pf-listbox.e2e.ts b/elements/pf-simple-list/test/pf-listbox.e2e.ts
similarity index 90%
rename from elements/pf-listbox/test/pf-listbox.e2e.ts
rename to elements/pf-simple-list/test/pf-listbox.e2e.ts
index 60afcec218..faa48da84c 100644
--- a/elements/pf-listbox/test/pf-listbox.e2e.ts
+++ b/elements/pf-simple-list/test/pf-listbox.e2e.ts
@@ -1,7 +1,7 @@
import { test } from '@playwright/test';
import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js';
-const tagName = 'pf-listbox';
+const tagName = 'pf-simple-list';
test.describe(tagName, () => {
test('snapshot', async ({ page }) => {
diff --git a/elements/pf-simple-list/test/pf-listbox.spec.ts b/elements/pf-simple-list/test/pf-listbox.spec.ts
new file mode 100644
index 0000000000..394e32750c
--- /dev/null
+++ b/elements/pf-simple-list/test/pf-listbox.spec.ts
@@ -0,0 +1,21 @@
+import { expect, html } from '@open-wc/testing';
+import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js';
+import { PfSimpleList } from '@patternfly/elements/pf-simple-list/pf-simple-list.js';
+
+describe('', function() {
+ describe('simply instantiating', function() {
+ let element: PfSimpleList;
+ it('imperatively instantiates', function() {
+ expect(document.createElement('pf-simple-list')).to.be.an.instanceof(PfSimpleList);
+ });
+
+ it('should upgrade', async function() {
+ element = await createFixture(html` `);
+ const klass = customElements.get('pf-simple-list');
+ expect(element)
+ .to.be.an.instanceOf(klass)
+ .and
+ .to.be.an.instanceOf(PfSimpleList);
+ });
+ });
+});
From 62d689e44d3766bc3d32d28394c3476806b04ecd Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 15:29:10 -0400
Subject: [PATCH 38/40] feat(core): fixed selection issue with
listbox-controller
---
.../controllers/listbox-controller.ts | 99 +++++++++++--------
1 file changed, 57 insertions(+), 42 deletions(-)
diff --git a/core/pfe-core/controllers/listbox-controller.ts b/core/pfe-core/controllers/listbox-controller.ts
index 19f5bd19d5..ca3935ac4c 100644
--- a/core/pfe-core/controllers/listbox-controller.ts
+++ b/core/pfe-core/controllers/listbox-controller.ts
@@ -44,9 +44,9 @@ export class ListboxController<
/**
* determines how filtering will be handled:
- * - "" (default): will show all options until filter text is not ""
- * - "required": will hide all options until filter text is not ""
- * - "disabled": will not hide options at all, regardless of filtering
+ * - "" (default): will show only options that match filter or in no options match will show all options
+ * - "required": all listbox options are hidden _until_ option matches filter
+ * - "disabled": all listbox options are visible; ignores filter
*/
#filterMode: ListboxFilterMode = '';
@@ -75,18 +75,13 @@ export class ListboxController<
* */
#options: HTMLElement[] = [];
- /**
- * all options that will not be hidden by a filter
- * */
- #visibleOptions: HTMLElement[] = [];
-
get activeItem() {
const [active] = this.options.filter(option => option.getAttribute('id') === this.#internals.ariaActivedescendant);
return active || this.#tabindex.firstItem;
}
- set filter(filtertext: string) {
- this.filter = filtertext;
+ set filter(filterText: string) {
+ this.#filter = filterText;
this.#onFilterChange();
}
@@ -144,27 +139,11 @@ export class ListboxController<
set options(options: HTMLElement[]) {
const setSize = options.length;
- const filterOptions = (option: HTMLElement, posInSet: number) => {
+ options.forEach((option, posInSet) => {
option.ariaSetSize = setSize !== null ? `${setSize}` : null;
option.ariaPosInSet = posInSet !== null ? `${posInSet}` : null;
- if (this.#filterMode === 'required' && this.#filter === '') {
- return false;
- } else if (this.#filterMode === 'disabled') {
- return true;
- } else {
- const search = this.#matchAnywhere ? '' : '^';
- const text = option.textContent || '';
- const regex = new RegExp(`${search}${this.#filter}`, this.#caseSensitive ? 'i' : '');
- if (search === '' || text.match(regex)) {
- option.removeAttribute('hidden-by-filter');
- } else {
- option.setAttribute('hidden-by-filter', 'hidden-by-filter');
- }
- return !option.hasAttribute('hidden-by-filter');
- }
- };
- this.#tabindex.initItems(this.#visibleOptions);
- this.#visibleOptions = options.filter((option, i) => filterOptions(option, i));
+ });
+ this.#tabindex.initItems(this.visibleOptions);
this.#options = options;
}
@@ -188,7 +167,42 @@ export class ListboxController<
}
get visibleOptions() {
- return this.#visibleOptions;
+ let matchedOptions = this.options;
+ if (this.filterMode !== 'disabled') {
+ if (this.#filterMode === 'required' && this.filter === '') {
+ matchedOptions = [];
+ } else {
+ matchedOptions = this.options.filter(option => {
+ if (this.filterMode === 'disabled' || this.filter === '*') {
+ return true;
+ } else {
+ const search = this.matchAnywhere ? '' : '^';
+ const text = option.textContent || '';
+ const regex = new RegExp(`${search}${this.filter}`, this.caseSensitive ? '' : 'i');
+ if (this.filter === '' || text.match(regex)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+ }
+
+ // unless filter mode is required,
+ // ensure there is at least one option showing,
+ // regardless of matches
+ if (this.#filterMode !== 'required' && matchedOptions.length < 1) {
+ matchedOptions = this.options;
+ }
+ this.options.forEach(option => {
+ if (matchedOptions.includes(option)) {
+ option.removeAttribute('hidden-by-filter');
+ } else {
+ option.setAttribute('hidden-by-filter', 'hidden-by-filter');
+ }
+ });
+ return matchedOptions;
}
constructor(public host: ReactiveControllerHost & HTMLElement, options: ListboxOptions) {
@@ -198,7 +212,7 @@ export class ListboxController<
});
this.#tabindex = new RovingTabindexController(this.host);
this.#caseSensitive = options.caseSensitive || false;
- this.#filterMode = options.filterMode || '';
+ this.filterMode = options.filterMode || '';
}
/**
@@ -232,20 +246,17 @@ export class ListboxController<
}
#updateActiveDescendant() {
- let found = false;
this.options.forEach(option => {
- if (option === this.#tabindex.activeItem) {
+ if (option === this.#tabindex.activeItem && this.visibleOptions.includes(option)) {
this.#internals.ariaActivedescendant = option.id;
option.setAttribute('active-descendant', 'active-descendant');
- found = true;
} else {
+ if (this.#internals.ariaActivedescendant === option.id) {
+ this.#internals.ariaActivedescendant = null;
+ }
option.removeAttribute('active-descendant');
}
});
- if (!found) {
- this.#tabindex.updateActiveItem(this.#tabindex.firstItem);
- this.#internals.ariaActivedescendant = this.#tabindex.firstItem?.id || null;
- }
this.#updateSingleselect();
}
@@ -302,7 +313,7 @@ export class ListboxController<
*/
#onFilterChange() {
const oldValue = this.value;
- this.#tabindex.updateItems(this.#visibleOptions);
+ this.#tabindex.initItems(this.visibleOptions);
this.#updateActiveDescendant();
if (oldValue !== this.value) {
this.#fireInput();
@@ -402,13 +413,17 @@ export class ListboxController<
}
} else {
switch (event.key) {
+ case '*':
+ this.filter = '*';
+ stopEvent = true;
+ break;
case 'Backspace':
case 'Delete':
- this.#filter = this.#filter.slice(0, this.#filter.length - 2);
+ this.filter = '';
stopEvent = true;
break;
case event.key?.match(/^[\w]$/)?.input:
- this.#filter += event.key?.toLowerCase();
+ this.filter = event.key;
stopEvent = true;
break;
case 'Enter':
@@ -439,7 +454,7 @@ export class ListboxController<
}
// only change focus if keydown occurred when option has focus
// (as opposed to an external text input and if filter has changed
- if (filter !== this.#filter) {
+ if (filter !== this.filter) {
this.#tabindex.focusOnItem(this.activeItem);
}
}
From c3b47e7bbfbb88e6f6fd6df8f935c63d76bf13ba Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 15:30:33 -0400
Subject: [PATCH 39/40] feat(simple-list): fixed required filterMode for
comboboxes and updated demo
---
.../{filter-modes.html => filtering.html} | 34 +++++++-
.../pf-simple-list/demo/pf-simple-list.js | 7 +-
elements/pf-simple-list/pf-simple-list.ts | 86 +++++++++++--------
3 files changed, 87 insertions(+), 40 deletions(-)
rename elements/pf-simple-list/demo/{filter-modes.html => filtering.html} (54%)
diff --git a/elements/pf-simple-list/demo/filter-modes.html b/elements/pf-simple-list/demo/filtering.html
similarity index 54%
rename from elements/pf-simple-list/demo/filter-modes.html
rename to elements/pf-simple-list/demo/filtering.html
index d924206499..199ad827e7 100644
--- a/elements/pf-simple-list/demo/filter-modes.html
+++ b/elements/pf-simple-list/demo/filtering.html
@@ -1,6 +1,38 @@
+
Any arrow keys work.
Only one option can be selected.
diff --git a/elements/pf-simple-list/demo/pf-simple-list.js b/elements/pf-simple-list/demo/pf-simple-list.js
index d80cd1e5c3..a6165ee008 100644
--- a/elements/pf-simple-list/demo/pf-simple-list.js
+++ b/elements/pf-simple-list/demo/pf-simple-list.js
@@ -6,16 +6,17 @@ import '@patternfly/elements/pf-simple-list/pf-simple-list.js';
*/
const input = document.querySelector('#text');
const listbox = document.querySelector('#listbox');
+let filter = '';
if (listbox && input) {
- listbox.filter = input.value;
input.addEventListener('input', () => {
- if (input.value !== listbox.value) {
+ if (input.value !== listbox.value && filter !== input.value) {
listbox.filter = input.value;
}
});
listbox.addEventListener('change', () => {
const val = (listbox.value || '');
- if (val.length > 0) {
+ if (val.length > 0 && input.value !== val) {
+ filter = val.slice(0, input.value.length);
input.value = val;
input.focus();
input.setSelectionRange(
diff --git a/elements/pf-simple-list/pf-simple-list.ts b/elements/pf-simple-list/pf-simple-list.ts
index c774c7ec05..3b80180f5d 100644
--- a/elements/pf-simple-list/pf-simple-list.ts
+++ b/elements/pf-simple-list/pf-simple-list.ts
@@ -1,5 +1,5 @@
import { LitElement, html } from 'lit';
-import type { PropertyValues } from 'lit';
+import type { PropertyValueMap, PropertyValues } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { ListboxController, type ListboxFilterMode, type ListboxOrientation } from '@patternfly/pfe-core/controllers/listbox-controller.js';
@@ -17,11 +17,6 @@ import styles from './pf-simple-list.css';
export class PfSimpleList extends LitElement {
static readonly styles = [styles];
- /**
- * filter options that start with a string (case-insensitive)
- */
- @property() filter = '';
-
/**
* whether filtering (if enabled) will be case-sensitive
*/
@@ -29,9 +24,9 @@ export class PfSimpleList extends LitElement {
/**
* determines how filtering will be handled:
- * - "" (default): will show all options until filter text is not ""
- * - "required": will hide all options until filter text is not ""
- * - "disabled": will not hide options at all, regardless of filtering
+ * - "" (default): will show only options that match filter or in no options match will show all options
+ * - "required": all listbox options are hidden _until_ option matches filter
+ * - "disabled": all listbox options are visible; ignores filter
*/
@property({ reflect: true, attribute: 'filter-mode', type: String }) filterMode: ListboxFilterMode = '';
@@ -52,24 +47,30 @@ export class PfSimpleList extends LitElement {
*/
@property({ reflect: true, attribute: 'orientation', type: String }) orientation: ListboxOrientation = '';
- #listbox = new ListboxController(this, {
- caseSensitive: this.caseSensitive,
- filterMode: this.filterMode,
- matchAnywhere: this.matchAnywhere,
- multiSelectable: this.multiSelectable,
- orientation: this.orientation
- });
+ #listbox?: ListboxController;
get options() {
return [...this.querySelectorAll('pf-simple-list-option')];
}
+ set filter(filterText: string) {
+ if (this.#listbox) {
+ this.#listbox.filter = filterText;
+ }
+ }
+
+ get filter() {
+ return this.#listbox?.filter || '';
+ }
+
set value(optionsList: string | null) {
- this.#listbox.value = optionsList;
+ if (this.#listbox) {
+ this.#listbox.value = optionsList;
+ }
}
get value() {
- return this.#listbox.value;
+ return this.#listbox?.value || null;
}
render() {
@@ -81,35 +82,48 @@ export class PfSimpleList extends LitElement {
`;
}
- constructor() {
- super();
+ firstUpdated(changed: PropertyValueMap | Map): void {
+ this.#listbox = new ListboxController(this, {
+ caseSensitive: this.caseSensitive,
+ filterMode: this.filterMode,
+ matchAnywhere: this.matchAnywhere,
+ multiSelectable: this.multiSelectable,
+ orientation: this.orientation
+ });
this.#listbox.options = this.options;
+ super.firstUpdated(changed);
}
updated(changed: PropertyValues) {
- if (changed.has('caseSensitive')) {
- this.#listbox.caseSensitive = this.caseSensitive;
- }
- if (changed.has('filterMode')) {
- this.#listbox.filterMode = this.filterMode;
- }
- if (changed.has('matchAnywhere')) {
- this.#listbox.matchAnywhere = this.matchAnywhere;
- }
- if (changed.has('multiSelectable')) {
- this.#listbox.multiSelectable = this.multiSelectable;
- }
- if (changed.has('orientation')) {
- this.#listbox.orientation = this.orientation;
+ if (this.#listbox) {
+ if (changed.has('caseSensitive')) {
+ this.#listbox.caseSensitive = this.caseSensitive;
+ }
+ if (changed.has('filterMode')) {
+ this.#listbox.filterMode = this.filterMode;
+ }
+ if (changed.has('matchAnywhere')) {
+ this.#listbox.matchAnywhere = this.matchAnywhere;
+ }
+ if (changed.has('multiSelectable')) {
+ this.#listbox.multiSelectable = this.multiSelectable;
+ }
+ if (changed.has('orientation')) {
+ this.#listbox.orientation = this.orientation;
+ }
}
}
focus() {
- this.#listbox.focus();
+ if (this.#listbox) {
+ this.#listbox.focus();
+ }
}
#onSlotchange() {
- this.#listbox.options = this.options;
+ if (this.#listbox) {
+ this.#listbox.options = this.options;
+ }
}
}
From c6fe97222f4728c877601edc134e9cfe02917eaa Mon Sep 17 00:00:00 2001
From: Nikki Massaro Kauffman
Date: Fri, 28 Jul 2023 15:33:22 -0400
Subject: [PATCH 40/40] feat(simple-list): updated demos
---
elements/pf-simple-list/demo/filtering.html | 7 ++++---
elements/pf-simple-list/demo/pf-simple-list.html | 3 ++-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/elements/pf-simple-list/demo/filtering.html b/elements/pf-simple-list/demo/filtering.html
index 199ad827e7..c543ddcfba 100644
--- a/elements/pf-simple-list/demo/filtering.html
+++ b/elements/pf-simple-list/demo/filtering.html
@@ -1,7 +1,7 @@
-
+filter-mode="required": combobox example
Any arrow keys work.
Only one option can be selected.
- Listbox wont appear until input has text.
+ Listbox wont appear until input has text.
+ Type * to show all options.
Pick a color:
diff --git a/elements/pf-simple-list/demo/pf-simple-list.html b/elements/pf-simple-list/demo/pf-simple-list.html
index 0701759da6..943aa3feb7 100644
--- a/elements/pf-simple-list/demo/pf-simple-list.html
+++ b/elements/pf-simple-list/demo/pf-simple-list.html
@@ -5,7 +5,8 @@ default
Any arrow keys work.
Only one option can be selected.
- Filter by typing a letter.
+ Filter by typing any letter. Type *
+ to remove filtering altogether.
Blue