Skip to content

Commit eb201e6

Browse files
Merge pull request #16 from Travelopia/refactor/multi-select-update
Update Multi-select component
2 parents 2fefff4 + 1bd2e67 commit eb201e6

File tree

8 files changed

+98
-82
lines changed

8 files changed

+98
-82
lines changed

src/multi-select/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ tp-multi-select-option {
3838
&[disabled="yes"] {
3939
opacity: 0.5;
4040
cursor: not-allowed;
41+
42+
&:active {
43+
pointer-events: none;
44+
}
4145
}
4246
}
4347

src/multi-select/tp-multi-select-field.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@ export class TPMultiSelectFieldElement extends HTMLElement {
1616

1717
/**
1818
* Toggle opening this component.
19-
*
20-
* @param {Event} e Click event.
2119
*/
22-
toggleOpen( e: Event ): void {
23-
if ( this !== e.target ) {
24-
return;
25-
}
26-
20+
toggleOpen(): void {
2721
const multiSelect: TPMultiSelectElement | null = this.closest( 'tp-multi-select' );
2822
if ( ! multiSelect ) {
2923
return;

src/multi-select/tp-multi-select-option.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ export class TPMultiSelectOptionElement extends HTMLElement {
2828

2929
if ( 'yes' !== this.getAttribute( 'selected' ) ) {
3030
multiSelect?.select( value );
31+
multiSelect?.dispatchEvent( new CustomEvent( 'select', { bubbles: true } ) );
3132
} else {
3233
multiSelect?.unSelect( value );
34+
multiSelect?.dispatchEvent( new CustomEvent( 'unselect', { bubbles: true } ) );
3335
}
36+
multiSelect?.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
3437
}
3538
}

src/multi-select/tp-multi-select-pill.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export class TPMultiSelectPillElement extends HTMLElement {
3232
const multiSelect: TPMultiSelectElement | null = this.closest( 'tp-multi-select' );
3333
if ( multiSelect && this.getAttribute( 'value' ) ) {
3434
multiSelect.unSelect( this.getAttribute( 'value' ) ?? '' );
35+
multiSelect.dispatchEvent( new CustomEvent( 'unselect', { bubbles: true } ) );
36+
multiSelect.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
3537
}
3638
}
3739
}

src/multi-select/tp-multi-select-pills.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class TPMultiSelectPillsElement extends HTMLElement {
1414
*/
1515
connectedCallback(): void {
1616
this.closest( 'tp-multi-select' )?.addEventListener( 'change', this.update.bind( this ) );
17+
this.update();
1718
}
1819

1920
/**

src/multi-select/tp-multi-select-search.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class TPMultiSelectSearchElement extends HTMLElement {
2020

2121
input.addEventListener( 'keydown', this.handleKeyboardInputs.bind( this ) );
2222
input.addEventListener( 'keyup', this.handleSearchChange.bind( this ) );
23-
input.addEventListener( 'change', this.handleSearchChange.bind( this ) );
23+
input.addEventListener( 'input', this.handleSearchChange.bind( this ) );
2424
this.addEventListener( 'click', this.handleClick.bind( this ) );
2525
this.closest( 'tp-multi-select' )?.addEventListener( 'open', this.focus.bind( this ) );
2626
}
@@ -64,10 +64,12 @@ export class TPMultiSelectSearchElement extends HTMLElement {
6464
return;
6565
}
6666

67+
let matchedOptionCount = 0;
6768
// Hide and show options based on search.
6869
options.forEach( ( option: TPMultiSelectOptionElement ): void => {
6970
if ( option.getAttribute( 'value' )?.match( new RegExp( `.*${ search.value }.*` ) ) ) {
7071
option.removeAttribute( 'hidden' );
72+
matchedOptionCount++;
7173
} else {
7274
option.setAttribute( 'hidden', 'yes' );
7375
}
@@ -80,6 +82,8 @@ export class TPMultiSelectSearchElement extends HTMLElement {
8082
search.style.width = `${ search.value.length + 2 }ch`;
8183
multiSelect.setAttribute( 'open', 'yes' );
8284
}
85+
86+
multiSelect.setAttribute( 'visible-options', matchedOptionCount.toString() );
8387
}
8488

8589
/**

src/multi-select/tp-multi-select-select-all.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ export class TPMultiSelectSelectAllElement extends HTMLElement {
4646

4747
if ( 'yes' !== this.getAttribute( 'selected' ) ) {
4848
multiSelect.selectAll();
49+
multiSelect.dispatchEvent( new CustomEvent( 'select-all', { bubbles: true } ) );
4950
} else {
5051
multiSelect.unSelectAll();
52+
multiSelect.dispatchEvent( new CustomEvent( 'unselect-all', { bubbles: true } ) );
5153
}
54+
multiSelect.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
5255
}
5356
}

src/multi-select/tp-multi-select.ts

Lines changed: 79 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ export class TPMultiSelectElement extends HTMLElement {
107107
const value: string[] = [];
108108

109109
const selectedOptions: NodeListOf<HTMLOptionElement> | null = this.querySelectorAll( 'select option[selected]' );
110-
selectedOptions?.forEach( ( option: HTMLOptionElement ) => value.push( option.value ) );
111-
110+
selectedOptions?.forEach( ( option: HTMLOptionElement ) => {
111+
const optionValue = option.getAttribute( 'value' );
112+
if ( optionValue ) {
113+
value.push( optionValue );
114+
}
115+
} );
112116
return value;
113117
}
114118

@@ -118,38 +122,37 @@ export class TPMultiSelectElement extends HTMLElement {
118122
protected updateFormFieldValue(): void {
119123
// Get options.
120124
const styledSelectedOptions: NodeListOf<TPMultiSelectOptionElement> | null = this.querySelectorAll( `tp-multi-select-option` );
121-
const selectFieldOptions: NodeListOf<HTMLOptionElement> | null = this.querySelectorAll( 'select option' );
125+
const selectField: HTMLSelectElement | null = this.querySelector( 'select' );
122126

123-
if ( ! styledSelectedOptions || ! selectFieldOptions ) {
127+
if ( ! styledSelectedOptions || ! selectField ) {
124128
return;
125129
}
126130

131+
const selectOptions: HTMLOptionElement[] = Array.from( selectField.options );
132+
127133
// Traverse options.
128134
styledSelectedOptions.forEach( ( option: TPMultiSelectOptionElement ): void => {
129-
// Get matching select field options.
130-
const matchingSelectOptions: HTMLOptionElement[] = [ ...selectFieldOptions ].filter( ( selectOption: HTMLOptionElement ): boolean =>
131-
selectOption.getAttribute( 'value' ) === option.getAttribute( 'value' ) );
132-
133-
if ( 0 === matchingSelectOptions.length ) {
134-
return;
135-
}
136-
137-
// Check whether to mark them as selected or not.
138-
if ( 'yes' === option.getAttribute( 'selected' ) ) {
139-
matchingSelectOptions.forEach( ( matchingSelectOption: HTMLOptionElement ): void => {
140-
matchingSelectOption.selected = true;
141-
matchingSelectOption.setAttribute( 'selected', 'selected' );
142-
} );
143-
} else {
144-
matchingSelectOptions.forEach( ( matchingSelectOption: HTMLOptionElement ): void => {
145-
matchingSelectOption.selected = false;
146-
matchingSelectOption.removeAttribute( 'selected' );
147-
} );
135+
const optionValue = option.getAttribute( 'value' ) ?? '';
136+
if ( optionValue ) {
137+
const matchingSelectOption: HTMLOptionElement | undefined = selectOptions.find( ( selectOption ) => selectOption.value === optionValue );
138+
139+
if ( 'yes' === option.getAttribute( 'selected' ) ) {
140+
if ( matchingSelectOption ) {
141+
matchingSelectOption.setAttribute( 'selected', 'selected' );
142+
} else {
143+
const newOption: HTMLOptionElement = document.createElement( 'option' );
144+
newOption.setAttribute( 'value', option.getAttribute( 'value' ) ?? '' );
145+
newOption.setAttribute( 'selected', 'selected' );
146+
selectField?.append( newOption );
147+
}
148+
} else {
149+
matchingSelectOption?.remove();
150+
}
148151
}
149152
} );
150153

151154
// Dispatch events.
152-
this.querySelector( 'select' )?.dispatchEvent( new Event( 'change' ) );
155+
selectField.dispatchEvent( new Event( 'change' ) );
153156
}
154157

155158
/**
@@ -195,12 +198,6 @@ export class TPMultiSelectElement extends HTMLElement {
195198
* Initialize component.
196199
*/
197200
initialize(): void {
198-
// Get options.
199-
const options: NodeListOf<HTMLOptionElement> | null = this.querySelectorAll( 'tp-multi-select-option' );
200-
if ( ! options ) {
201-
return;
202-
}
203-
204201
// Create select element (if it doesn't already exist).
205202
let selectElement: HTMLSelectElement | null = this.querySelector( 'select' );
206203
if ( ! selectElement ) {
@@ -216,15 +213,8 @@ export class TPMultiSelectElement extends HTMLElement {
216213
selectElement.innerHTML = '';
217214
}
218215

219-
// Append new options.
220-
options.forEach( ( option: HTMLOptionElement ): void => {
221-
const newOption: HTMLOptionElement = document.createElement( 'option' );
222-
newOption.setAttribute( 'value', option.getAttribute( 'value' ) ?? '' );
223-
if ( 'yes' === option.getAttribute( 'selected' ) ) {
224-
newOption.setAttribute( 'selected', 'selected' );
225-
}
226-
selectElement?.append( newOption );
227-
} );
216+
// Update components for selected options.
217+
this.update();
228218
}
229219

230220
/**
@@ -264,10 +254,7 @@ export class TPMultiSelectElement extends HTMLElement {
264254
if ( 'yes' === this.getAttribute( 'close-on-select' ) ) {
265255
this.removeAttribute( 'open' );
266256
}
267-
268-
// Trigger events.
269-
this.dispatchEvent( new CustomEvent( 'select', { bubbles: true } ) );
270-
this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
257+
this.update();
271258
}
272259

273260
/**
@@ -280,9 +267,7 @@ export class TPMultiSelectElement extends HTMLElement {
280267
option.setAttribute( 'selected', 'yes' );
281268
}
282269
} );
283-
284-
this.dispatchEvent( new CustomEvent( 'select-all', { bubbles: true } ) );
285-
this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
270+
this.update();
286271
}
287272

288273
/**
@@ -295,9 +280,7 @@ export class TPMultiSelectElement extends HTMLElement {
295280
styledSelectedOptions?.forEach( ( option: TPMultiSelectOptionElement ): void => {
296281
option.removeAttribute( 'selected' );
297282
} );
298-
299-
this.dispatchEvent( new CustomEvent( 'unselect', { bubbles: true } ) );
300-
this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
283+
this.update();
301284
}
302285

303286
/**
@@ -308,9 +291,7 @@ export class TPMultiSelectElement extends HTMLElement {
308291
styledSelectedOptions?.forEach( ( option: TPMultiSelectOptionElement ): void => {
309292
option.removeAttribute( 'selected' );
310293
} );
311-
312-
this.dispatchEvent( new CustomEvent( 'unselect-all', { bubbles: true } ) );
313-
this.dispatchEvent( new CustomEvent( 'change', { bubbles: true } ) );
294+
this.update();
314295
}
315296

316297
/**
@@ -321,9 +302,11 @@ export class TPMultiSelectElement extends HTMLElement {
321302
handleKeyboardInputs( e: KeyboardEvent ): void {
322303
switch ( e.key ) {
323304
case 'ArrowDown':
305+
e.preventDefault();
324306
this.highlightNextOption();
325307
break;
326308
case 'ArrowUp':
309+
e.preventDefault();
327310
this.highlightPreviousOption();
328311
break;
329312
case 'Enter':
@@ -347,21 +330,32 @@ export class TPMultiSelectElement extends HTMLElement {
347330
return;
348331
}
349332

350-
// Highlight next option.
351-
if ( this.currentlyHighlightedOption === options.length - 1 ) {
333+
// Find the next option to be highlighted. Assume next option is the favorable option.
334+
let nextToBeHighlighted = this.currentlyHighlightedOption + 1;
335+
336+
// Keep iterating to skip over disabled options until we find a suitable option.
337+
while ( nextToBeHighlighted < options.length && options[ nextToBeHighlighted ].getAttribute( 'disabled' ) === 'yes' ) {
338+
nextToBeHighlighted++;
339+
}
340+
341+
// If there are no more options to highlight, exit. Here, the last highlighted option keeps highlighted.
342+
if ( nextToBeHighlighted === options.length ) {
352343
return;
353344
}
354345

355-
this.currentlyHighlightedOption++;
346+
// Remove highlight from the current option, if any.
347+
if ( this.currentlyHighlightedOption !== -1 ) {
348+
options[ this.currentlyHighlightedOption ].removeAttribute( 'highlighted' );
349+
}
356350

357-
// Set option attributes based on highlight.
358-
options.forEach( ( option: TPMultiSelectOptionElement, index: number ): void => {
359-
if ( this.currentlyHighlightedOption === index ) {
360-
option.setAttribute( 'highlighted', 'yes' );
361-
} else {
362-
option.removeAttribute( 'highlighted' );
363-
}
364-
} );
351+
// Highlight the found option.
352+
options[ nextToBeHighlighted ].setAttribute( 'highlighted', 'yes' );
353+
354+
// Scroll the highlighted option into view with smooth behavior.
355+
options[ nextToBeHighlighted ].scrollIntoView( { behavior: 'smooth', block: 'nearest' } );
356+
357+
// Update the currentlyHighlightedOption for the next iteration.
358+
this.currentlyHighlightedOption = nextToBeHighlighted;
365359
}
366360

367361
/**
@@ -375,21 +369,32 @@ export class TPMultiSelectElement extends HTMLElement {
375369
return;
376370
}
377371

378-
// Highlight next option.
379-
if ( this.currentlyHighlightedOption === 0 ) {
372+
// Find the previous option to be highlighted. Assume previous option is the favorable option.
373+
let previousToBeHighlighted = this.currentlyHighlightedOption - 1;
374+
375+
// Keep iterating to skip over disabled options until we find a suitable option.
376+
while ( previousToBeHighlighted >= 0 && options[ previousToBeHighlighted ].getAttribute( 'disabled' ) === 'yes' ) {
377+
previousToBeHighlighted--;
378+
}
379+
380+
// If there are no more options to highlight, exit.
381+
if ( previousToBeHighlighted < 0 ) {
380382
return;
381383
}
382384

383-
this.currentlyHighlightedOption--;
385+
// Remove highlight from the current option, if any.
386+
if ( this.currentlyHighlightedOption !== 0 ) {
387+
options[ this.currentlyHighlightedOption ].removeAttribute( 'highlighted' );
388+
}
384389

385-
// Set option attributes based on highlight.
386-
options.forEach( ( option: TPMultiSelectOptionElement, index: number ): void => {
387-
if ( this.currentlyHighlightedOption === index ) {
388-
option.setAttribute( 'highlighted', 'yes' );
389-
} else {
390-
option.removeAttribute( 'highlighted' );
391-
}
392-
} );
390+
// Highlight the found option.
391+
options[ previousToBeHighlighted ].setAttribute( 'highlighted', 'yes' );
392+
393+
// Scroll the highlighted option into view with smooth behavior.
394+
options[ previousToBeHighlighted ].scrollIntoView( { behavior: 'smooth', block: 'nearest' } );
395+
396+
// Update the currentlyHighlightedOption for the next iteration.
397+
this.currentlyHighlightedOption = previousToBeHighlighted;
393398
}
394399

395400
/**

0 commit comments

Comments
 (0)