Skip to content

Commit d567bf5

Browse files
committed
chore: search input option added
1 parent 03d0269 commit d567bf5

File tree

5 files changed

+205
-30
lines changed

5 files changed

+205
-30
lines changed

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-stepper.js": "./pf-progress-stepper/pf-progress-stepper.js",
4747
"./pf-progress/pf-progress.js": "./pf-progress/pf-progress.js",
4848
"./pf-search-input/pf-search-input.js": "./pf-search-input/pf-search-input.js",
49+
"./pf-search-input/pf-search-input-option.js":"./pf-search-input/pf-search-input-option.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/pf-table.js": "./pf-table/pf-table.js",

elements/pf-search-input/demo/pf-search-input.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
<pf-search-input></pf-search-input>
1+
<pf-search-input>
2+
<pf-search-input-option value="New Jersey">New Jersey</pf-search-input-option>
3+
<pf-search-input-option value="New York">New York</pf-search-input-option>
4+
<pf-search-input-option value="New Mexico">New Mexico</pf-search-input-option>
5+
<pf-search-input-option value="North Carolina">North Carolina</pf-search-input-option>
6+
</pf-search-input>
27

38
<script type="module">
49
import '@patternfly/elements/pf-search-input/pf-search-input.js';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host {
2+
display: block;
3+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { LitElement, html, type TemplateResult } from 'lit';
2+
import { customElement } from 'lit/decorators/custom-element.js';
3+
import { queryAssignedNodes } from 'lit/decorators/query-assigned-nodes.js';
4+
import { property } from 'lit/decorators/property.js';
5+
import { classMap } from 'lit/directives/class-map.js';
6+
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
7+
import { observes } from '@patternfly/pfe-core/decorators/observes.js';
8+
import styles from './pf-search-input-option.css';
9+
10+
/**
11+
* Search Input Pf Search Input Option
12+
* @slot - Place element content here
13+
*/
14+
@customElement('pf-search-input-option')
15+
export class PfSearchInputOption extends LitElement {
16+
static readonly styles: CSSStyleSheet[] = [styles];
17+
18+
/** form value for this option */
19+
@property({ reflect: true })
20+
get value() {
21+
return (this.#value ?? this.textContent ?? '').trim();
22+
}
23+
24+
set value(v: string) {
25+
this.#value = v;
26+
}
27+
28+
/** whether option is selected */
29+
@property({ type: Boolean, reflect: true }) selected = false;
30+
31+
/** whether option is active descendant */
32+
@property({ type: Boolean, reflect: true }) active = false;
33+
34+
@queryAssignedNodes({ slot: '', flatten: true })
35+
private _slottedText!: Node[];
36+
37+
/**
38+
* this option's position relative to the other options
39+
*/
40+
set posInSet(posInSet: number | null) {
41+
this.#internals.ariaPosInSet = `${Math.max(0, posInSet ?? 0)}`;
42+
}
43+
44+
get posInSet() {
45+
const parsed = parseInt(this.#internals.ariaPosInSet ?? '0');
46+
return Number.isNaN(parsed) ? null : parsed;
47+
}
48+
49+
/**
50+
* total number of options
51+
*/
52+
set setSize(setSize: number | null) {
53+
this.#internals.ariaSetSize = `${Math.max(0, setSize ?? 0)}`;
54+
}
55+
56+
get setSize() {
57+
try {
58+
const int = parseInt(this.#internals.ariaSetSize ?? '0');
59+
if (Number.isNaN(int)) {
60+
return 0;
61+
} else {
62+
return int;
63+
}
64+
} catch {
65+
return 0;
66+
}
67+
}
68+
69+
#value?: string;
70+
71+
#internals = InternalsController.of(this, { role: 'option' });
72+
73+
74+
render(): TemplateResult<1> {
75+
const { active, selected } = this;
76+
return html`
77+
<div id="outer" class="${classMap({ active, selected })}">
78+
<input type="checkbox"
79+
inert
80+
role="presentation"
81+
tabindex="-1"
82+
?checked="${this.selected}">
83+
<slot name="icon"></slot>
84+
<span>
85+
<slot name="create"></slot>
86+
<slot>${this.value}</slot>
87+
</span>
88+
<svg ?hidden="${!this.selected}"
89+
viewBox="0 0 512 512"
90+
fill="currentColor"
91+
aria-hidden="true">
92+
<path d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path>
93+
</svg>
94+
</div>
95+
`;
96+
}
97+
98+
@observes('selected')
99+
private selectedChanged() {
100+
this.#internals.ariaSelected = String(!!this.selected);
101+
}
102+
103+
/**
104+
* text content within option (used for filtering)
105+
*/
106+
get optionText(): string {
107+
return this._slottedText.map(node => node.textContent).join('').trim();
108+
}
109+
}
110+
111+
declare global {
112+
interface HTMLElementTagNameMap {
113+
'pf-search-input-option': PfSearchInputOption;
114+
}
115+
}

elements/pf-search-input/pf-search-input.ts

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
import { LitElement, html, type TemplateResult } from 'lit';
1+
import { LitElement, html, isServer, type TemplateResult } from 'lit';
22
import { customElement } from 'lit/decorators/custom-element.js';
33
import { ComboboxController } from '@patternfly/pfe-core/controllers/combobox-controller.js';
44
import { query } from 'lit/decorators/query.js';
55
import { property } from 'lit/decorators/property.js';
66
import { observes } from '@patternfly/pfe-core/decorators/observes.js';
7+
import { styleMap } from 'lit/directives/style-map.js';
78
import {
89
FloatingDOMController,
910
type Placement,
1011
} from '@patternfly/pfe-core/controllers/floating-dom-controller.js';
12+
import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js';
13+
import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js';
1114
import '@patternfly/elements/pf-text-input/pf-text-input.js';
1215
import { arraysAreEquivalent } from '@patternfly/pfe-core/functions/arraysAreEquivalent.js';
1316
import '@patternfly/elements/pf-icon/pf-icon.js';
17+
import { PfSearchInputOption } from './pf-search-input-option.js';
18+
import { ifDefined } from 'lit/directives/if-defined.js';
1419
import styles from './pf-search-input.css';
1520

21+
export class PfSelectChangeEvent extends Event {
22+
constructor() {
23+
super('change', { bubbles: true });
24+
}
25+
}
26+
1627
/**
1728
* Search Input
1829
* @slot - Place element content here
@@ -24,24 +35,33 @@ export class PfSearchInput extends LitElement {
2435
@property({ type: Boolean, reflect: true }) expanded = false;
2536
@property({ reflect: true }) position: Placement = 'bottom';
2637
@property({ attribute: 'enable-flip', type: Boolean }) enableFlip = false;
27-
38+
/** Current form value */
39+
@property() value?: string;
40+
@property() variant: 'single' | 'checkbox' | 'typeahead' | 'typeaheadmulti' = 'single';
2841
@query('#listbox') listbox!: HTMLElement;
2942
@query('#button') button!: HTMLButtonElement;
3043
@query('#combobox') combobox!: HTMLInputElement;
31-
@query('#placeholder') placeholder!: HTMLOptionElement;
44+
@query('#placeholder') placeholder!: PfSearchInputOption;
3245
@query('#listbox-container') private _listboxContainer?: HTMLElement;
3346

47+
#isNotPlaceholderOption = (option: PfSearchInputOption) => option !== this.placeholder;
48+
3449

3550
#float = new FloatingDOMController(this, { content: () => this._listboxContainer });
3651

52+
#internals = InternalsController.of(this);
53+
54+
#slots = new SlotController(this, null, 'placeholder');
55+
56+
3757

3858
// static template: TemplateResult<1> = html`
3959
// <pf-text-input-autocomplete></pf-text-input-autocomplete>`;
4060

41-
#combobox: ComboboxController<HTMLOptionElement> = ComboboxController.of(this, {
42-
multi: false,
61+
#combobox = ComboboxController.of(this, {
62+
multi: this.variant === 'typeaheadmulti' || this.variant === 'checkbox',
4363
getItems: () => this.options,
44-
isItem: item => item instanceof HTMLOptionElement,
64+
isItem: item => item instanceof PfSearchInputOption,
4565
getFallbackLabel: () => 'options',
4666
getListboxElement: () => this.listbox ?? null,
4767
getToggleButton: () => this.combobox ?? null,
@@ -51,6 +71,7 @@ export class PfSearchInput extends LitElement {
5171
requestHideListbox: () => void (this.expanded &&= false),
5272
setItemActive: (item, active) => item.classList.toggle('active', active),
5373
setItemSelected: (item, selected) => item.selected = selected,
74+
setItemHidden: (item, hidden) => (item.id !== 'placeholder') && void (item.hidden = hidden)
5475
});
5576

5677
/**
@@ -63,27 +84,39 @@ export class PfSearchInput extends LitElement {
6384
this.#combobox.selected = list;
6485
}
6586

87+
88+
6689
/** List of options */
67-
get options(): HTMLOptionElement[] {
68-
return [
69-
...new Set([
90+
get options(): PfSearchInputOption[] {
91+
if (isServer) {
92+
return []; // TODO: expose a DOM property to allow setting options in SSR scenarios
93+
} else {
94+
return [
7095
this.placeholder,
71-
...this.querySelectorAll('option'),
72-
...this.renderRoot.querySelectorAll('option'),
73-
]),
74-
].filter(x => !!x);
96+
...Array.from(this.querySelectorAll('pf-search-input-option')),
97+
].filter((x): x is PfSearchInputOption => !!x && !x.hidden);
98+
}
7599
}
76100

77-
get selected(): HTMLOptionElement[] {
78-
return this.options.filter(x => x.selected);
101+
get selected(): PfSearchInputOption[] {
102+
return this.#combobox.selected;
79103
}
80104

81-
get activeOption(): HTMLOptionElement | undefined {
82-
return this.options.find(x => x.classList.contains('active'));
83-
}
105+
// get activeOption(): HTMLOptionElement | undefined {
106+
// return this.options.find(x => x.classList.contains('active'));
107+
// }
84108

85109
//<input id="combobox">
86110
render(): TemplateResult<1> {
111+
112+
const { expanded, placeholder, variant } = this;
113+
const { anchor = 'bottom', alignment = 'start', styles = {} } = this.#float;
114+
const { height, width } = this.getBoundingClientRect?.() || {};
115+
const placeholderIsInert = !placeholder && this.#slots.isEmpty('placeholder');
116+
const hasSelection = !!(Array.isArray(this.selected) ? this.selected.length : this.selected);
117+
const typeahead = variant.startsWith('typeahead');
118+
const hideLightDomItems = typeahead && !ComboboxController.supportsCrossRootActiveDescendant;
119+
87120
return html`
88121
<pf-text-input id="combobox" type="text" placeholder="Placeholder"></pf-text-input>
89122
<pf-button disabled plain label="Close">
@@ -113,22 +146,34 @@ export class PfSearchInput extends LitElement {
113146
</path>
114147
</svg>
115148
</pf-button>
116-
<div id="listbox-container">
117-
<div id="listbox" ?hidden="${!this.expanded}">
118-
<option id="placeholder" aria-disabled="true">Select an Option</option>
119-
<optgroup label="Swedish Cars">
120-
<option value="volvo">Volvo</option>
121-
<option value="saab">Saab</option>
122-
</optgroup>
123-
<optgroup label="German Cars">
124-
<option value="mercedes">Mercedes</option>
125-
<option value="audi">Audi</option>
126-
</optgroup>
149+
<div id="listbox-container" ?hidden="${!expanded}"
150+
style="${styleMap({
151+
marginTop: `${height || 0}px`,
152+
width: width ? `${width}px` : 'auto',
153+
})}">
154+
<div id="listbox">
155+
<pf-search-input-option id="placeholder"
156+
disabled
157+
?inert="${placeholderIsInert}"
158+
aria-hidden="${ifDefined(placeholderIsInert ? undefined : String(!!hasSelection))}"
159+
?hidden="${!placeholder && this.#slots.isEmpty('placeholder')}"
160+
><slot name="placeholder">${placeholder ?? ''}</slot></pf-search-input-option>
161+
${this.#combobox.renderItemsToShadowRoot()}
162+
<slot ?hidden="${hideLightDomItems}"></slot>
127163
</div>
128164
</div>
129165
`;
130166
}
131167

168+
@observes('selected')
169+
private async selectedChanged(_: PfSearchInputOption[], selected: PfSearchInputOption[]) {
170+
this.value = selected.map(x => x.value).join();
171+
await this.updateComplete;
172+
this.hide();
173+
// this._toggleButton?.focus();
174+
175+
}
176+
132177
@observes('expanded')
133178
private async expandedChanged(old: boolean, expanded: boolean) {
134179
if (this.dispatchEvent(new Event(this.expanded ? 'close' : 'open'))) {
@@ -184,6 +229,12 @@ export class PfSearchInput extends LitElement {
184229
await this.show();
185230
}
186231
}
232+
233+
@observes('value')
234+
private valueChanged() {
235+
// this.#internals.setFormValue(this.value ?? '');
236+
this.dispatchEvent(new PfSelectChangeEvent());
237+
}
187238
}
188239

189240
declare global {

0 commit comments

Comments
 (0)