@spectrum-web-components/combobox
Version:
Web component implementation of a Spectrum design Combobox
138 lines (137 loc) • 13.2 kB
JavaScript
"use strict";var u=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var a=(p,o,e,t)=>{for(var i=t>1?void 0:t?v(o,e):o,s=p.length-1,c;s>=0;s--)(c=p[s])&&(i=(t?c(o,e,i):c(i))||i);return t&&i&&u(o,e,i),i};import{html as l,nothing as b}from"@spectrum-web-components/base";import{property as r,query as h,state as d}from"@spectrum-web-components/base/src/decorators.js";import{ifDefined as n,live as f,repeat as m}from"@spectrum-web-components/base/src/directives.js";import"@spectrum-web-components/overlay/sp-overlay.js";import"@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js";import"@spectrum-web-components/popover/sp-popover.js";import"@spectrum-web-components/menu/sp-menu.js";import"@spectrum-web-components/menu/sp-menu-item.js";import{PendingStateController as g}from"@spectrum-web-components/reactive-controllers/src/PendingState.js";import"@spectrum-web-components/picker-button/sp-picker-button.js";import{Textfield as $}from"@spectrum-web-components/textfield";import y from"./combobox.css.js";import E from"@spectrum-web-components/icon/src/spectrum-icon-chevron.css.js";export class Combobox extends ${constructor(){super();this.autocomplete="none";this.availableOptions=[];this.open=!1;this.pending=!1;this.pendingLabel="Pending";this.overlayOpen=!1;this.itemValue="";this.optionEls=[];this.applyFocusElementLabel=e=>{this.appliedLabel=e};this._returnItems=()=>{};this.pendingStateController=new g(this)}static get styles(){return[...super.styles,y,E]}focus(){this.focusElement.focus()}click(){this.focus(),this.focusElement.click()}scrollToActiveDescendant(){if(!this.activeDescendant)return;const e=this.shadowRoot.getElementById(this.activeDescendant.value);e&&e.scrollIntoView({block:"nearest"})}handleComboboxKeydown(e){if(!(this.readonly||this.pending))if(e.altKey&&e.code==="ArrowDown")this.open=!0;else if(e.code==="ArrowDown")e.preventDefault(),this.open=!0,this.activateNextDescendant(),this.scrollToActiveDescendant();else if(e.code==="ArrowUp")e.preventDefault(),this.open=!0,this.activatePreviousDescendant(),this.scrollToActiveDescendant();else if(e.code==="Escape")this.open||(this.value=""),this.open=!1;else if(e.code==="Enter")this.selectDescendant(),this.open=!1;else if(e.code==="Home")this.focusElement.setSelectionRange(0,0),this.activeDescendant=void 0;else if(e.code==="End"){const{length:t}=this.value;this.focusElement.setSelectionRange(t,t),this.activeDescendant=void 0}else e.code==="ArrowLeft"?this.activeDescendant=void 0:e.code==="ArrowRight"&&(this.activeDescendant=void 0)}handleSlotchange(){this.setOptionsFromSlottedItems(),this.itemObserver.disconnect(),this.optionEls.map(e=>{this.itemObserver.observe(e,{attributes:!0,attributeFilter:["id"],childList:!0})})}handleTooltipSlotchange(e){this.tooltipEl=e.target.assignedElements()[0]}setOptionsFromSlottedItems(){const e=this.optionSlot.assignedElements({flatten:!0});this.optionEls=e}activateNextDescendant(){const e=this.activeDescendant?this.availableOptions.indexOf(this.activeDescendant):-1;let t=e;do if(t=(this.availableOptions.length+t+1)%this.availableOptions.length,t===e)break;while(this.availableOptions[t].disabled);this.availableOptions[t].disabled||(this.activeDescendant=this.availableOptions[t]),this.optionEls.forEach(i=>{var s;return i.setAttribute("aria-selected",i.value===((s=this.activeDescendant)==null?void 0:s.value)?"true":"false")})}activatePreviousDescendant(){const e=this.activeDescendant?this.availableOptions.indexOf(this.activeDescendant):0;let t=e;do if(t=(this.availableOptions.length+t-1)%this.availableOptions.length,t===e)break;while(this.availableOptions[t].disabled);this.availableOptions[t].disabled||(this.activeDescendant=this.availableOptions[t]),this.optionEls.forEach(i=>{var s;return i.setAttribute("aria-selected",i.value===((s=this.activeDescendant)==null?void 0:s.value)?"true":"false")})}selectDescendant(){if(!this.activeDescendant)return;const e=this.shadowRoot.getElementById(this.activeDescendant.value);e&&e.click()}filterAvailableOptions(){if(this.autocomplete==="none"||this.pending)return;const e=this.value.toLowerCase();this.availableOptions=(this.options||this.optionEls).filter(t=>t.itemText.toLowerCase().startsWith(e))}handleInput(e){super.handleInput(e),this.pending||(this.activeDescendant=void 0,this.open=!0)}handleMenuChange(e){const{target:t}=e,i=(this.options||this.optionEls).find(s=>s.value===(t==null?void 0:t.value));this.value=(i==null?void 0:i.itemText)||"",e.preventDefault(),this.open=!1,this._returnItems(),this.focus()}handleClosed(){this.open=!1,this.overlayOpen=!1}handleOpened(){}toggleOpen(){if(this.readonly||this.pending){this.open=!1;return}this.open=!this.open,this.inputElement.focus()}shouldUpdate(e){var t,i;return e.has("open")&&(this.open?this.overlayOpen=!0:this.activeDescendant=void 0),e.has("value")&&(this.filterAvailableOptions(),this.itemValue=(i=(t=this.availableOptions.find(s=>s.itemText===this.value))==null?void 0:t.value)!=null?i:""),super.shouldUpdate(e)}onBlur(e){e.relatedTarget&&(this.contains(e.relatedTarget)||this.shadowRoot.contains(e.relatedTarget))||super.onBlur(e)}renderAppliedLabel(){const e=this.label||this.appliedLabel;return l`
${this.pending?l`
<span
aria-hidden="true"
class="visually-hidden"
id="pending-label"
>
${this.pendingLabel}
</span>
`:b}
${this.value?l`
<span
aria-hidden="true"
class="visually-hidden"
id="applied-label"
>
${e}
</span>
<slot name="label" id="label">
<span class="visually-hidden" aria-hidden="true">
${this.value}
</span>
</slot>
`:l`
<span hidden id="applied-label">${e}</span>
`}
`}renderLoader(){return import("@spectrum-web-components/progress-circle/sp-progress-circle.js"),l`
<sp-progress-circle
size="s"
indeterminate
aria-hidden="true"
class="progress-circle"
></sp-progress-circle>
`}renderField(){return l`
${this.renderStateIcons()}
<input
aria-activedescendant=${n(this.activeDescendant?`${this.activeDescendant.value}`:void 0)}
aria-autocomplete=${n(this.autocomplete)}
aria-controls=${n(this.open?"listbox-menu":void 0)}
aria-describedby="${this.helpTextId} tooltip"
aria-expanded="${this.open?"true":"false"}"
aria-label=${n(this.label||this.appliedLabel)}
aria-labelledby="pending-label applied-label label"
aria-invalid=${n(this.invalid||void 0)}
autocomplete="off"
=${this.toggleOpen}
=${this.handleComboboxKeydown}
id="input"
class="input"
role="combobox"
type="text"
.value=${f(this.displayValue)}
tabindex="0"
-closed=${this.handleClosed}
-opened=${this.handleOpened}
maxlength=${n(this.maxlength>-1?this.maxlength:void 0)}
minlength=${n(this.minlength>-1?this.minlength:void 0)}
pattern=${n(this.pattern)}
=${this.handleChange}
=${this.handleInput}
=${this.onFocus}
=${this.onBlur}
?disabled=${this.disabled}
?required=${this.required}
?readonly=${this.readonly}
/>
${this.pendingStateController.renderPendingState()}
`}render(){const e=(this.input||this).offsetWidth;return this.tooltipEl&&(this.tooltipEl.disabled=this.open),l`
${super.render()}
<sp-picker-button
aria-controls="listbox-menu"
aria-describedby="${this.helpTextId} tooltip"
aria-expanded=${this.open?"true":"false"}
aria-label=${n(this.label||this.appliedLabel)}
aria-labelledby="applied-label label"
=${this.toggleOpen}
tabindex="-1"
class="button ${this.focused?"focus-visible is-keyboardFocused":""}"
?disabled=${this.disabled}
?focused=${this.focused}
?quiet=${this.quiet}
size=${this.size}
></sp-picker-button>
<sp-overlay
?open=${this.open}
.triggerElement=${this.input}
offset="0"
placement="bottom-start"
.receivesFocus=${"false"}
role="presentation"
>
<sp-popover
id="listbox"
?open=${this.open}
role="presentation"
?hidden=${this.availableOptions.length===0}
>
<sp-menu
=${this.handleMenuChange}
tabindex="-1"
aria-labelledby="label applied-label"
aria-label=${n(this.label||this.appliedLabel)}
id="listbox-menu"
role="listbox"
selects=${n(this.autocomplete==="none"?"single":void 0)}
.selected=${this.autocomplete==="none"&&this.itemValue?[this.itemValue]:[]}
style="min-width: ${e}px;"
size=${this.size}
>
${this.overlayOpen?m(this.availableOptions,t=>t.value,t=>{var i,s;return l`
<sp-menu-item
id="${t.value}"
?focused=${((i=this.activeDescendant)==null?void 0:i.value)===t.value}
aria-selected=${((s=this.activeDescendant)==null?void 0:s.value)===t.value?"true":"false"}
.value=${t.value}
.selected=${t.value===this.itemValue}
?disabled=${t.disabled}
>
${t.itemText}
</sp-menu-item>
`}):l``}
<slot
hidden
=${this.handleSlotchange}
></slot>
</sp-menu>
</sp-popover>
</sp-overlay>
${this.renderAppliedLabel()}
<slot
aria-hidden="true"
name="tooltip"
id="tooltip"
=${this.handleTooltipSlotchange}
></slot>
`}firstUpdated(e){super.firstUpdated(e),this.addEventListener("focusout",t=>{const i=t.relatedTarget&&this.contains(t.relatedTarget);t.target===this&&!i&&(this.focused=!1)})}async manageListOverlay(){this.open&&(this.focused=!0,this.focus())}updated(e){var t;if(e.has("open")&&!this.pending&&this.manageListOverlay(),!this.focused&&this.open&&(this.open=!1),e.has("pending")&&this.pending&&(this.open=!1),e.has("activeDescendant")){const i=e.get("activeDescendant");i&&(i.focused=!1),this.activeDescendant&&typeof this.activeDescendant.focused!="undefined"&&(this.activeDescendant.focused=!0)}(e.has("options")||e.has("optionEls"))&&((t=this.options)!=null&&t.every(i=>i.disabled)&&(this.disabled=!0),this.availableOptions=this.options||this.optionEls)}async getUpdateComplete(){const e=await super.getUpdateComplete(),t=this.shadowRoot.querySelector("#listbox");if(t){const i=[...t.children];await Promise.all(i.map(s=>s.updateComplete))}return e}connectedCallback(){super.connectedCallback(),this.itemObserver||(this.itemObserver=new MutationObserver(this.setOptionsFromSlottedItems.bind(this)))}disconnectedCallback(){this.itemObserver.disconnect(),this.open=!1,super.disconnectedCallback()}}a([d()],Combobox.prototype,"activeDescendant",2),a([r({type:String})],Combobox.prototype,"autocomplete",2),a([d()],Combobox.prototype,"availableOptions",2),a([r({type:Boolean,reflect:!0})],Combobox.prototype,"open",2),a([r({type:Boolean,reflect:!0})],Combobox.prototype,"pending",2),a([r({type:String,attribute:"pending-label"})],Combobox.prototype,"pendingLabel",2),a([h("slot:not([name])")],Combobox.prototype,"optionSlot",2),a([d()],Combobox.prototype,"overlayOpen",2),a([h("#input")],Combobox.prototype,"input",2),a([r({type:Array})],Combobox.prototype,"options",2),a([d()],Combobox.prototype,"optionEls",2);
//# sourceMappingURL=Combobox.js.map