Skip to content

Commit 616bd75

Browse files
authored
Merge branch 'main' into fix/scrollspy/noncongiguous
2 parents 008b6c8 + bd40a73 commit 616bd75

22 files changed

+2245
-57
lines changed

.changeset/famous-parts-add.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@patternfly/elements": minor
3+
---
4+
5+
✨ Added `<pf-search-input>`.
6+
7+
A search input consists of a text field where users can type to find specific content or items. Unlike selects or dropdowns, which offer predefined options, a search input lets users enter their own keywords to filter or locate results. It includes a clear (×) button to easily remove the current input, allowing users to start a new search quickly.
8+
9+
Use this when users need to search freely using their own terms — ideal for large or frequently changing sets of content.
10+
Do not use when the options are limited and known ahead of time — consider a dropdown or select instead
11+
12+
```html
13+
<pf-search-input>
14+
<pf-option value="Alabama"> Alabama </pf-option>
15+
<pf-option value="New Jersey"> New Jersey </pf-option>
16+
<pf-option value="New York"> New York </pf-option>
17+
<pf-option value="New Mexico"> New Mexico </pf-option>
18+
<pf-option value="North Carolina"> North Carolina </pf-option>
19+
</pf-search-input>
20+
```

.changeset/small-trees-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@patternfly/elements": patch
3+
---
4+
5+
`<pf-tooltip>`: hide content when copy/pasting tooltip elements

core/pfe-core/controllers/combobox-controller.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { nothing, type ReactiveController, type ReactiveControllerHost } from 'lit';
1+
import { isServer, nothing, type ReactiveController, type ReactiveControllerHost } from 'lit';
22
import type { ActivedescendantControllerOptions } from './activedescendant-controller.js';
33
import type { RovingTabindexControllerOptions } from './roving-tabindex-controller.js';
44
import type { ATFocusController } from './at-focus-controller';
@@ -188,6 +188,10 @@ export class ComboboxController<
188188

189189
private static langsRE = new RegExp(ComboboxController.langs.join('|'));
190190

191+
private static instances = new WeakMap<ReactiveControllerHost, ComboboxController<HTMLElement>>();
192+
193+
private static hosts = new Set<ReactiveControllerHost>();
194+
191195
static {
192196
// apply visually-hidden styles
193197
this.#alertTemplate.innerHTML = `
@@ -205,6 +209,21 @@ export class ComboboxController<
205209
`;
206210
}
207211

212+
// Hide listbox on focusout
213+
static {
214+
if (!isServer) {
215+
document.addEventListener('focusout', event => {
216+
const target = event.target as HTMLElement;
217+
for (const host of ComboboxController.hosts) {
218+
if (host instanceof Node && host.contains(target)) {
219+
const instance = ComboboxController.instances.get(host);
220+
instance?._onFocusoutElement();
221+
}
222+
}
223+
});
224+
}
225+
}
226+
208227
private options: RequireProps<ComboboxControllerOptions<Item>,
209228
| 'isItemDisabled'
210229
| 'isItem'
@@ -333,6 +352,8 @@ export class ComboboxController<
333352
isItemDisabled: this.options.isItemDisabled,
334353
setItemSelected: this.options.setItemSelected,
335354
});
355+
ComboboxController.instances.set(host, this);
356+
ComboboxController.hosts.add(host);
336357
}
337358

338359
async hostConnected(): Promise<void> {
@@ -347,18 +368,31 @@ export class ComboboxController<
347368
const expanded = this.options.isExpanded();
348369
this.#button?.setAttribute('aria-expanded', String(expanded));
349370
this.#input?.setAttribute('aria-expanded', String(expanded));
350-
if (this.#hasTextInput) {
351-
this.#button?.setAttribute('tabindex', '-1');
352-
} else {
353-
this.#button?.removeAttribute('tabindex');
354-
}
355371
this.#initLabels();
356372
}
357373

358374
hostDisconnected(): void {
359375
this.#fc?.hostDisconnected();
360376
}
361377

378+
disconnect(): void {
379+
ComboboxController.instances.delete(this.host);
380+
ComboboxController.hosts.delete(this.host);
381+
}
382+
383+
async _onFocusoutElement(): Promise<void> {
384+
if (this.#hasTextInput && this.options.isExpanded()) {
385+
const root = this.#element?.getRootNode();
386+
await new Promise(requestAnimationFrame);
387+
if (root instanceof ShadowRoot || root instanceof Document) {
388+
const { activeElement } = root;
389+
if (!this.#element?.contains(activeElement)) {
390+
this.#hide();
391+
}
392+
}
393+
}
394+
}
395+
362396
/**
363397
* Order of operations is important
364398
*/

core/pfe-core/controllers/test/combobox-controller.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,15 @@ abstract class TestCombobox extends ReactiveElement {
108108
expect(await a11ySnapshot()).axTreeFocusedNode.to.have.axRole('combobox');
109109
});
110110

111-
describe('Tab', function() {
112-
beforeEach(press('Tab'));
113-
beforeEach(updateComplete);
114-
beforeEach(nextFrame);
115-
116-
it('does not focus the toggle button', async function() {
117-
expect(await a11ySnapshot()).to.not.axContainQuery({ focused: true });
118-
});
119-
});
111+
// describe('Tab', function() {
112+
// beforeEach(press('Tab'));
113+
// beforeEach(updateComplete);
114+
// beforeEach(nextFrame);
115+
116+
// it('does not focus the toggle button', async function() {
117+
// expect(await a11ySnapshot()).to.not.axContainQuery({ focused: true });
118+
// });
119+
// });
120120

121121
describe('ArrowDown', function() {
122122
beforeEach(press('ArrowDown'));

elements/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"./pf-progress-stepper/pf-progress-step.js": "./pf-progress-stepper/pf-progress-step.js",
4747
"./pf-progress-stepper/pf-progress-stepper.js": "./pf-progress-stepper/pf-progress-stepper.js",
4848
"./pf-progress/pf-progress.js": "./pf-progress/pf-progress.js",
49+
"./pf-search-input/pf-search-input.js": "./pf-search-input/pf-search-input.js",
4950
"./pf-spinner/pf-spinner.js": "./pf-spinner/pf-spinner.js",
5051
"./pf-switch/pf-switch.js": "./pf-switch/pf-switch.js",
5152
"./pf-table/context.js": "./pf-table/context.js",

elements/pf-search-input/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Search Input
2+
A search input lets users type in words to find specific items or information. As they type, it can show matching results to help them quickly find what they are looking for.
3+
4+
## Usage
5+
A search input consists of a text field where users can type to find specific content or items. Unlike selects or dropdowns, which offer predefined options, a search input lets users enter their own keywords to filter or locate results. It includes a clear (×) button to easily remove the current input, allowing users to start a new search quickly.
6+
7+
```html
8+
<pf-search-input>
9+
<pf-option value="Alabama"> Alabama </pf-option>
10+
<pf-option value="New Jersey"> New Jersey </pf-option>
11+
<pf-option value="New York"> New York </pf-option>
12+
<pf-option value="New Mexico"> New Mexico </pf-option>
13+
<pf-option value="North Carolina"> North Carolina </pf-option>
14+
</pf-search-input>
15+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<form class="container">
2+
<div class="search-input-container">
3+
<pf-search-input disabled id="search-input" name="search" placeholder="Search">
4+
<pf-option>Blue</pf-option>
5+
<pf-option>Green</pf-option>
6+
<pf-option>Magenta</pf-option>
7+
<pf-option>Orange</pf-option>
8+
<pf-option>Purple</pf-option>
9+
<pf-option>Periwinkle</pf-option>
10+
<pf-option>Pink</pf-option>
11+
<pf-option>Red</pf-option>
12+
<pf-option>Yellow</pf-option>
13+
</pf-search-input>
14+
<pf-button> Search</pf-button>
15+
</div>
16+
</form>
17+
18+
<script type="module">
19+
import '@patternfly/elements/pf-search-input/pf-search-input.js';
20+
</script>
21+
22+
<style>
23+
.container {
24+
padding: 40px;
25+
.search-input-container {
26+
display: flex;
27+
align-items: center;
28+
justify-content: flex-start;
29+
#search-input {
30+
margin-right: 1rem;
31+
}
32+
}
33+
}
34+
</style>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<form class="container">
2+
<div class="search-input-container">
3+
<pf-search-input id="search-input" name="search" placeholder="Search">
4+
<pf-option value="Alabama"> Alabama </pf-option>
5+
<pf-option value="New Jersey"> New Jersey </pf-option>
6+
<pf-option value="New York"> New York </pf-option>
7+
<pf-option value="New Mexico"> New Mexico </pf-option>
8+
<pf-option value="North Carolina"> North Carolina </pf-option>
9+
<pf-option value="Alabama1"> Alabama 1 </pf-option>
10+
<pf-option value="New Jersey1"> New Jersey 1 </pf-option>
11+
<pf-option value="New York1"> New York 1 </pf-option>
12+
<pf-option value="New Mexico1"> New Mexico 1</pf-option>
13+
<pf-option value="North Carolina1"> North Carolina 1</pf-option>
14+
<pf-option value="Alabama2"> Alabama 2 </pf-option>
15+
<pf-option value="New Jersey2"> New Jersey 2 </pf-option>
16+
<pf-option value="New York2"> New York 2 </pf-option>
17+
<pf-option value="New Mexico2"> New Mexico 2 </pf-option>
18+
<pf-option value="North Carolina2"> North Carolina 2 </pf-option>
19+
<pf-option value="Alabama3"> Alabama 3 </pf-option>
20+
<pf-option value="New Jersey3"> New Jersey 3 </pf-option>
21+
<pf-option value="New York3"> New York 3 </pf-option>
22+
<pf-option value="New Mexico3"> New Mexico 3 </pf-option>
23+
<pf-option value="North Carolina3"> North Carolina 3 </pf-option>
24+
</pf-search-input>
25+
<pf-button> Search</pf-button>
26+
</div>
27+
</form>
28+
29+
30+
<script type="module">
31+
import '@patternfly/elements/pf-search-input/pf-search-input.js';
32+
33+
const searchInput = document.getElementById('search-input');
34+
35+
searchInput.addEventListener('change', (event) => {
36+
/* eslint-disable no-console */
37+
console.log('Selected:', event.target.value);
38+
/* eslint-disable no-console */
39+
});
40+
41+
const form = document.querySelector('form.container');
42+
form.addEventListener('submit', (event) =>{
43+
event.preventDefault();
44+
/* eslint-disable no-console */
45+
console.log("Value:", form.elements.search?.value);
46+
/* eslint-disable no-console */
47+
})
48+
</script>
49+
50+
<style>
51+
.container {
52+
padding: 40px;
53+
.search-input-container {
54+
display: flex;
55+
align-items: center;
56+
justify-content: flex-start;
57+
#search-input {
58+
margin-right: 1rem;
59+
}
60+
}
61+
}
62+
</style>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<div class="container">
2+
<pf-search-input id="search-input" placeholder="Search">
3+
<pf-option>What is Red Hat Enterprise Linux?</pf-option>
4+
<pf-option>How does Red Hat OpenShift work?</pf-option>
5+
<pf-option>Why use Red Hat Ansible for automation?</pf-option>
6+
<pf-option>Where can Red Hat OpenShift be deployed?</pf-option>
7+
<pf-option>When should you use Red Hat Enterprise Linux?</pf-option>
8+
<pf-option>What is Red Hat Satellite?</pf-option>
9+
<pf-option>How does Red Hat integrate with AWS and other clouds?</pf-option>
10+
<pf-option>Why choose Red Hat over other Linux vendors?</pf-option>
11+
<pf-option>Where can I learn Red Hat technologies?</pf-option>
12+
<pf-option>When does support end for RHEL versions?</pf-option>
13+
<pf-option>What are Red Hat certifications?</pf-option>
14+
<pf-option>How do you secure a RHEL server?</pf-option>
15+
<pf-option>Why use OpenShift instead of vanilla Kubernetes?</pf-option>
16+
<pf-option>Where is Red Hat headquartered?</pf-option>
17+
<pf-option>When should you use Red Hat CoreOS?</pf-option>
18+
<pf-option>What is Red Hat Insights?</pf-option>
19+
<pf-option>How do you manage Red Hat subscriptions?</pf-option>
20+
<pf-option>Why is RHEL considered enterprise-grade?</pf-option>
21+
<pf-option>Where can I download RHEL for testing?</pf-option>
22+
<pf-option>When was Red Hat founded?</pf-option>
23+
</pf-search-input>
24+
</div>
25+
26+
<script type="module">
27+
import '@patternfly/elements/pf-search-input/pf-search-input.js';
28+
29+
const searchInput = document.getElementById('search-input');
30+
31+
searchInput.addEventListener('change', (event) => {
32+
/* eslint-disable no-console */
33+
console.log('Selected:', event.target.value);
34+
/* eslint-disable no-console */
35+
});
36+
</script>
37+
38+
<style>
39+
.container {
40+
padding: 40px;
41+
}
42+
</style>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{% renderInstallation %} {% endrenderInstallation %}
2+
3+
<script type="module">
4+
import '@patternfly/elements/pf-search-input/pf-search-input.js';
5+
</script>
6+
7+
{% renderOverview %}
8+
<pf-search-input>
9+
<pf-option>Blue</pf-option>
10+
<pf-option>Black</pf-option>
11+
<pf-option>Brown</pf-option>
12+
<pf-option>Bronze</pf-option>
13+
<pf-option>Green</pf-option>
14+
<pf-option>Magenta</pf-option>
15+
<pf-option>Orange</pf-option>
16+
<pf-option>Purple</pf-option>
17+
<pf-option>Periwinkle</pf-option>
18+
<pf-option>Pink</pf-option>
19+
<pf-option>Red</pf-option>
20+
<pf-option>Yellow</pf-option>
21+
</pf-search-input>
22+
{% endrenderOverview %}
23+
24+
{% band header="Usage" %}
25+
26+
#### Search Input
27+
28+
{% htmlexample %}
29+
{% renderFile "./elements/pf-search-input/demo/pf-search-input.html" %}
30+
{% endhtmlexample %}
31+
32+
#### Search Input Form
33+
{% htmlexample %}
34+
{% renderFile "./elements/pf-search-input/demo/pf-search-input-with-submit.html" %}
35+
{% endhtmlexample %}
36+
37+
#### Disabled
38+
{% htmlexample %}
39+
{% renderFile "./elements/pf-search-input/demo/disabled.html" %}
40+
{% endhtmlexample %}
41+
42+
{% endband %}
43+
44+
{% band header="Accessibility" %}
45+
46+
The search input uses the [Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) recommendations from the WAI ARIA [Authoring Best Practices Guide (APG)](https://www.w3.org/WAI/ARIA/apg).
47+
48+
When the dropdown is disabled it follows [WAI ARIA focusability recommendations](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols) for composite widget elements, where dropdown items are still focusable even when the dropdown is disabled.
49+
50+
#### Toggle and typeahead input
51+
52+
When focus is on the toggle, the following keyboard interactions apply:
53+
54+
| Key | Function |
55+
| ---------------------- | -------------------------------------------------------------------------------------- |
56+
| <kbd>Down Arrow</kbd> | Opens the listbox and moves focus to the first listbox item. |
57+
| <kbd>Tab</kbd> | Moves focus to the close button if visible; otherwise, moves to the next focusable element, then closes the listbox.|
58+
| <kbd>Shift + Tab</kbd> | Moves focus out of element onto the previous focusable item and closes listbox. |
59+
60+
#### Listbox options
61+
62+
Listbox options use the [APG's Roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex) recommendation. When focus is on the listbox, the following keyboard interactions apply:
63+
64+
| Key | Function |
65+
| ---------------------- | ------------------------------------------------------------------------------------- |
66+
| <kbd>Enter</kbd> | Selects the options and closes the listbox. |
67+
| <kbd>Space</kbd> | Selects the options and closes the listbox. |
68+
| <kbd>Tab</kbd> | Moves focus out of element onto the next focusable options and closes listbox. |
69+
| <kbd>Shift + Tab</kbd> | Moves focus to the toggle button and closes listbox. |
70+
| <kbd>Up Arrow</kbd> | Moves focus to the previous option, optionally wrapping from the first to the last. |
71+
| <kbd>Down Arrow</kbd> | Moves focus to the next option, optionally wrapping from the last to the first. |
72+
| <kbd>Left Arrow</kbd> | Returns focus to the combobox without closing the popup and moves the input cursor one character to the left. If the input cursor is on the left-most character, the cursor does not move. |
73+
| <kbd>Right Arrow</kbd> | Returns focus to the combobox without closing the popup and moves the input cursor one character to the right. If the input cursor is on the right-most character, the cursor does not move. |
74+
| <kbd>Escape</kbd> | Close the listbox that contains focus and return focus to the input. |
75+
| <kbd>Any letter</kbd> | Navigates to the next option that starts with the letter. |
76+
77+
{% endband %}
78+
79+
{% renderSlots for="pf-search-input", header="Slots on `pf-search-input`" %}{% endrenderSlots %}
80+
{% renderAttributes for="pf-search-input", header="Attributes on `pf-search-input`" %}{% endrenderAttributes %}
81+
{% renderMethods for="pf-search-input", header="Methods on `pf-search-input`" %}{% endrenderMethods %}
82+
{% renderEvents for="pf-search-input", header="Events on `pf-search-input`" %}{% endrenderEvents %}
83+
{% renderCssCustomProperties for="pf-search-input", header="CSS Custom Properties on `pf-search-input`" %}{% endrenderCssCustomProperties %}
84+
{% renderCssParts for="pf-search-input", header="CSS Parts on `pf-search-input`" %}{% endrenderCssParts %}
85+
86+
{% renderSlots for="pf-option", header="Slots on `pf-option`" %}{% endrenderSlots %}
87+
{% renderAttributes for="pf-option", header="Attributes on `pf-option`" %}{% endrenderAttributes %}
88+
{% renderMethods for="pf-option", header="Methods on `pf-option`" %}{% endrenderMethods %}
89+
{% renderEvents for="pf-option", header="Events on `pf-option`" %}{% endrenderEvents %}
90+
{% renderCssCustomProperties for="pf-option", header="CSS Custom Properties on `pf-option`" %}{% endrenderCssCustomProperties %}
91+
{% renderCssParts for="pf-option", header="CSS Parts on `pf-option`" %}{% endrenderCssParts %}

0 commit comments

Comments
 (0)