kempo-ui 0.4.17 → 0.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/components/Combobox.js +2 -2
- package/docs/components/combobox.html +30 -0
- package/docs/src/components/Combobox.js +2 -2
- package/docs-src/components/combobox.page.html +30 -0
- package/package.json +1 -1
- package/src/components/Combobox.js +6 -2
- package/tests/components/Combobox.browser-test.js +69 -2
- package/tests/components/Tabs.browser-test.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.18] - 2026-05-04
|
|
10
|
+
### Added
|
|
11
|
+
- **Combobox component**: `no-results-message` and `empty-message` attributes
|
|
12
|
+
- `no-results-message` customizes the text shown when the user has typed something but no options match (default: `"No Matches"`)
|
|
13
|
+
- `empty-message` customizes the text shown when the input is empty and no options are loaded, e.g. before a dynamic search has fetched results (default: `"Type to search..."`)
|
|
14
|
+
- Both attributes map to `noResultsMessage` and `emptyMessage` properties respectively
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Tabs component**: Test for `persistent-id` localStorage restore had a logic inversion — `content3.active` was checked instead of `!content3.active`, causing the test to fail precisely when restoration was working correctly
|
|
18
|
+
|
|
9
19
|
## [0.4.17] - 2026-05-03
|
|
10
20
|
### Added
|
|
11
21
|
- **Markdown Editor component**: CSS size variables `--min-height` and `--max-height`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponent.js";import i from"../utils/debounce.js";import"./Option.js";export default class o extends s{static formAssociated=!0;static properties={value:{type:String,reflect:!0},name:{type:String,reflect:!0},placeholder:{type:String},opened:{type:Boolean,reflect:!0},searching:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},requireMatch:{type:Boolean,reflect:!0,attribute:"require-match"},disabled:{type:Boolean,reflect:!0},debounceMs:{type:Number,attribute:"debounce-ms"},maxVisible:{type:Number,attribute:"max-visible"}};#e=-1;#t=[];constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.opened=!1,this.searching=!1,this.required=!1,this.requireMatch=!1,this.disabled=!1,this.debounceMs=300,this.maxVisible=8,this.#s(300)}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),this.#i()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick)}childrenUpdated(){this.#i()}updated(e){super.updated(e),e.has("debounceMs")&&this.#s(this.debounceMs),(e.has("value")||e.has("required")||e.has("requireMatch"))&&this.#o()}formResetCallback(){this.value="",this.#o()}formStateRestoreCallback(e){this.value=e}handleInput=e=>{this.value=e.target.value,this.opened=!0,this.#e=-1,this.#a(this.value)};handleKeydown=e=>{const t=this.#n;if("ArrowDown"===e.key){if(e.preventDefault(),!this.opened)return void(this.opened=!0);this.#e=Math.min(this.#e+1,t.length-1),this.requestUpdate(),this.#r()}else"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.requestUpdate(),this.#r()):"Enter"===e.key?(e.preventDefault(),this.#e>=0&&this.#e<t.length&&this.#l(t[this.#e])):"Escape"===e.key?(e.preventDefault(),this.opened=!1,this.#e=-1):"Tab"===e.key&&(this.opened=!1,this.#e=-1)};handleDocumentClick=e=>{e.composedPath().includes(this)||(this.opened=!1,this.#e=-1)};handleFocus=()=>{this.opened=!0};handleOptionClick=e=>{const t=Number(e.currentTarget.dataset.index);this.#l(this.#n[t])};setOptions(e){return[...this.querySelectorAll("k-option")].forEach(e=>e.remove()),e.forEach(e=>{const t=document.createElement("k-option");"object"==typeof e?(t.setAttribute("value",e.value??e.label),t.textContent=e.label):t.textContent=e,this.appendChild(t)}),this}clear(){return this.value="",this.opened=!1,this.#e=-1,this.#o(),this}#s(e){this.#a=i(e=>{this.dispatchEvent(new CustomEvent("search",{detail:{value:e},bubbles:!0,composed:!0}))},e)}#a=()=>{};#i(){this.#t=[...this.querySelectorAll("k-option")].map(e=>({label:e.label,value:e.value})),this.requestUpdate()}#l(e){this.value=e.label,this.opened=!1,this.#e=-1,this.#o(),this.dispatchEvent(new CustomEvent("select",{detail:{value:e.value,label:e.label},bubbles:!0,composed:!0})),this.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}#o(){const e=this.#t.find(e=>e.label===this.value);this.internals.setFormValue(e?e.value:this.value),this.required&&!this.value?this.internals.setValidity({valueMissing:!0},"Please select a value.",this.shadowRoot?.querySelector("input")):this.requireMatch&&this.value&&!e?this.internals.setValidity({customError:!0},"Please select a valid option.",this.shadowRoot?.querySelector("input")):this.internals.setValidity({})}#r(){this.updateComplete.then(()=>{this.shadowRoot?.querySelector(".option.focused")?.scrollIntoView({block:"nearest"})})}get#d(){const e=(this.value||"").toLowerCase();return e?this.#t.filter(t=>t.label.toLowerCase().includes(e)):this.#t}get#n(){return this.#d.slice(0,this.maxVisible)}render(){const t=this.#n,s=this.#d.length>this.maxVisible;return e`
|
|
1
|
+
import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponent.js";import i from"../utils/debounce.js";import"./Option.js";export default class o extends s{static formAssociated=!0;static properties={value:{type:String,reflect:!0},name:{type:String,reflect:!0},placeholder:{type:String},opened:{type:Boolean,reflect:!0},searching:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},requireMatch:{type:Boolean,reflect:!0,attribute:"require-match"},disabled:{type:Boolean,reflect:!0},debounceMs:{type:Number,attribute:"debounce-ms"},maxVisible:{type:Number,attribute:"max-visible"},noResultsMessage:{type:String,attribute:"no-results-message"},emptyMessage:{type:String,attribute:"empty-message"}};#e=-1;#t=[];constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.opened=!1,this.searching=!1,this.required=!1,this.requireMatch=!1,this.disabled=!1,this.debounceMs=300,this.maxVisible=8,this.noResultsMessage="No Matches",this.emptyMessage="Type to search...",this.#s(300)}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),this.#i()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick)}childrenUpdated(){this.#i()}updated(e){super.updated(e),e.has("debounceMs")&&this.#s(this.debounceMs),(e.has("value")||e.has("required")||e.has("requireMatch"))&&this.#o()}formResetCallback(){this.value="",this.#o()}formStateRestoreCallback(e){this.value=e}handleInput=e=>{this.value=e.target.value,this.opened=!0,this.#e=-1,this.#a(this.value)};handleKeydown=e=>{const t=this.#n;if("ArrowDown"===e.key){if(e.preventDefault(),!this.opened)return void(this.opened=!0);this.#e=Math.min(this.#e+1,t.length-1),this.requestUpdate(),this.#r()}else"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.requestUpdate(),this.#r()):"Enter"===e.key?(e.preventDefault(),this.#e>=0&&this.#e<t.length&&this.#l(t[this.#e])):"Escape"===e.key?(e.preventDefault(),this.opened=!1,this.#e=-1):"Tab"===e.key&&(this.opened=!1,this.#e=-1)};handleDocumentClick=e=>{e.composedPath().includes(this)||(this.opened=!1,this.#e=-1)};handleFocus=()=>{this.opened=!0};handleOptionClick=e=>{const t=Number(e.currentTarget.dataset.index);this.#l(this.#n[t])};setOptions(e){return[...this.querySelectorAll("k-option")].forEach(e=>e.remove()),e.forEach(e=>{const t=document.createElement("k-option");"object"==typeof e?(t.setAttribute("value",e.value??e.label),t.textContent=e.label):t.textContent=e,this.appendChild(t)}),this}clear(){return this.value="",this.opened=!1,this.#e=-1,this.#o(),this}#s(e){this.#a=i(e=>{this.dispatchEvent(new CustomEvent("search",{detail:{value:e},bubbles:!0,composed:!0}))},e)}#a=()=>{};#i(){this.#t=[...this.querySelectorAll("k-option")].map(e=>({label:e.label,value:e.value})),this.requestUpdate()}#l(e){this.value=e.label,this.opened=!1,this.#e=-1,this.#o(),this.dispatchEvent(new CustomEvent("select",{detail:{value:e.value,label:e.label},bubbles:!0,composed:!0})),this.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}#o(){const e=this.#t.find(e=>e.label===this.value);this.internals.setFormValue(e?e.value:this.value),this.required&&!this.value?this.internals.setValidity({valueMissing:!0},"Please select a value.",this.shadowRoot?.querySelector("input")):this.requireMatch&&this.value&&!e?this.internals.setValidity({customError:!0},"Please select a valid option.",this.shadowRoot?.querySelector("input")):this.internals.setValidity({})}#r(){this.updateComplete.then(()=>{this.shadowRoot?.querySelector(".option.focused")?.scrollIntoView({block:"nearest"})})}get#d(){const e=(this.value||"").toLowerCase();return e?this.#t.filter(t=>t.label.toLowerCase().includes(e)):this.#t}get#n(){return this.#d.slice(0,this.maxVisible)}render(){const t=this.#n,s=this.#d.length>this.maxVisible;return e`
|
|
2
2
|
<slot style="display:none"></slot>
|
|
3
3
|
<input
|
|
4
4
|
type="text"
|
|
@@ -23,7 +23,7 @@ import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponen
|
|
|
23
23
|
<div class="more">${this.#d.length-this.maxVisible} more...</div>
|
|
24
24
|
`:""}
|
|
25
25
|
${0!==t.length||this.searching?"":e`
|
|
26
|
-
<div class="no-results"
|
|
26
|
+
<div class="no-results">${this.value?this.noResultsMessage:this.emptyMessage}</div>
|
|
27
27
|
`}
|
|
28
28
|
${this.searching?e`
|
|
29
29
|
<div class="searching">
|
|
@@ -498,6 +498,7 @@
|
|
|
498
498
|
<a href="#required">Required</a><br />
|
|
499
499
|
<a href="#requireMatch">Require Match</a><br />
|
|
500
500
|
<a href="#maxVisible">Max Visible</a><br />
|
|
501
|
+
<a href="#noResultsMessage">No Results Message & Empty Message</a><br />
|
|
501
502
|
|
|
502
503
|
<h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
|
|
503
504
|
<a href="#constructor">Constructor</a><br />
|
|
@@ -628,6 +629,21 @@
|
|
|
628
629
|
</div>
|
|
629
630
|
</div>
|
|
630
631
|
|
|
632
|
+
<h3 id="noResultsMessage"><a href="#noResultsMessage" class="no-link">No Results Message & Empty Message</a></h3>
|
|
633
|
+
<p>Use <code>no-results-message</code> to customize the text shown when the user has typed something but no options match. Use <code>empty-message</code> to customize the text shown when the input is empty and no options are loaded yet (e.g. before a dynamic search has fetched results). They default to <code>"No Matches"</code> and <code>"Type to search..."</code> respectively.</p>
|
|
634
|
+
<div class="row -mx">
|
|
635
|
+
<div class="col m-span-12 px">
|
|
636
|
+
<k-card label="HTML">
|
|
637
|
+
<pre><code class="hljs html"><span class="hljs-tag"><<span class="hljs-name">k-combobox</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Start typing to search...'</span> <span class="hljs-attr">no-results-message</span>=<span class="hljs-string">'No users found. Try a different search.'</span> <span class="hljs-attr">empty-message</span>=<span class="hljs-string">'Start typing to find a user...'</span>></span><br><span class="hljs-tag"></<span class="hljs-name">k-combobox</span>></span></code></pre>
|
|
638
|
+
</k-card>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="col m-span-12 px">
|
|
641
|
+
<k-card label="Output">
|
|
642
|
+
<k-combobox id="noResultsCb" placeholder="Start typing to search..." no-results-message="No users found. Try a different search." empty-message="Start typing to find a user..."></k-combobox>
|
|
643
|
+
</k-card>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
|
|
631
647
|
<h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
|
|
632
648
|
|
|
633
649
|
<h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
|
|
@@ -722,6 +738,20 @@
|
|
|
722
738
|
<td><code>8</code></td>
|
|
723
739
|
<td>Maximum number of options visible at once.</td>
|
|
724
740
|
</tr>
|
|
741
|
+
<tr>
|
|
742
|
+
<td><code>no-results-message</code></td>
|
|
743
|
+
<td><code>noResultsMessage</code></td>
|
|
744
|
+
<td><code>String</code></td>
|
|
745
|
+
<td><code>'No Matches'</code></td>
|
|
746
|
+
<td>Text displayed when the user has typed something but no options match.</td>
|
|
747
|
+
</tr>
|
|
748
|
+
<tr>
|
|
749
|
+
<td><code>empty-message</code></td>
|
|
750
|
+
<td><code>emptyMessage</code></td>
|
|
751
|
+
<td><code>String</code></td>
|
|
752
|
+
<td><code>'Type to search...'</code></td>
|
|
753
|
+
<td>Text displayed when the input is empty and no options are loaded (e.g. before an initial dynamic search fetch).</td>
|
|
754
|
+
</tr>
|
|
725
755
|
</tbody>
|
|
726
756
|
</table>
|
|
727
757
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponent.js";import i from"../utils/debounce.js";import"./Option.js";export default class o extends s{static formAssociated=!0;static properties={value:{type:String,reflect:!0},name:{type:String,reflect:!0},placeholder:{type:String},opened:{type:Boolean,reflect:!0},searching:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},requireMatch:{type:Boolean,reflect:!0,attribute:"require-match"},disabled:{type:Boolean,reflect:!0},debounceMs:{type:Number,attribute:"debounce-ms"},maxVisible:{type:Number,attribute:"max-visible"}};#e=-1;#t=[];constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.opened=!1,this.searching=!1,this.required=!1,this.requireMatch=!1,this.disabled=!1,this.debounceMs=300,this.maxVisible=8,this.#s(300)}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),this.#i()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick)}childrenUpdated(){this.#i()}updated(e){super.updated(e),e.has("debounceMs")&&this.#s(this.debounceMs),(e.has("value")||e.has("required")||e.has("requireMatch"))&&this.#o()}formResetCallback(){this.value="",this.#o()}formStateRestoreCallback(e){this.value=e}handleInput=e=>{this.value=e.target.value,this.opened=!0,this.#e=-1,this.#a(this.value)};handleKeydown=e=>{const t=this.#n;if("ArrowDown"===e.key){if(e.preventDefault(),!this.opened)return void(this.opened=!0);this.#e=Math.min(this.#e+1,t.length-1),this.requestUpdate(),this.#r()}else"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.requestUpdate(),this.#r()):"Enter"===e.key?(e.preventDefault(),this.#e>=0&&this.#e<t.length&&this.#l(t[this.#e])):"Escape"===e.key?(e.preventDefault(),this.opened=!1,this.#e=-1):"Tab"===e.key&&(this.opened=!1,this.#e=-1)};handleDocumentClick=e=>{e.composedPath().includes(this)||(this.opened=!1,this.#e=-1)};handleFocus=()=>{this.opened=!0};handleOptionClick=e=>{const t=Number(e.currentTarget.dataset.index);this.#l(this.#n[t])};setOptions(e){return[...this.querySelectorAll("k-option")].forEach(e=>e.remove()),e.forEach(e=>{const t=document.createElement("k-option");"object"==typeof e?(t.setAttribute("value",e.value??e.label),t.textContent=e.label):t.textContent=e,this.appendChild(t)}),this}clear(){return this.value="",this.opened=!1,this.#e=-1,this.#o(),this}#s(e){this.#a=i(e=>{this.dispatchEvent(new CustomEvent("search",{detail:{value:e},bubbles:!0,composed:!0}))},e)}#a=()=>{};#i(){this.#t=[...this.querySelectorAll("k-option")].map(e=>({label:e.label,value:e.value})),this.requestUpdate()}#l(e){this.value=e.label,this.opened=!1,this.#e=-1,this.#o(),this.dispatchEvent(new CustomEvent("select",{detail:{value:e.value,label:e.label},bubbles:!0,composed:!0})),this.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}#o(){const e=this.#t.find(e=>e.label===this.value);this.internals.setFormValue(e?e.value:this.value),this.required&&!this.value?this.internals.setValidity({valueMissing:!0},"Please select a value.",this.shadowRoot?.querySelector("input")):this.requireMatch&&this.value&&!e?this.internals.setValidity({customError:!0},"Please select a valid option.",this.shadowRoot?.querySelector("input")):this.internals.setValidity({})}#r(){this.updateComplete.then(()=>{this.shadowRoot?.querySelector(".option.focused")?.scrollIntoView({block:"nearest"})})}get#d(){const e=(this.value||"").toLowerCase();return e?this.#t.filter(t=>t.label.toLowerCase().includes(e)):this.#t}get#n(){return this.#d.slice(0,this.maxVisible)}render(){const t=this.#n,s=this.#d.length>this.maxVisible;return e`
|
|
1
|
+
import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponent.js";import i from"../utils/debounce.js";import"./Option.js";export default class o extends s{static formAssociated=!0;static properties={value:{type:String,reflect:!0},name:{type:String,reflect:!0},placeholder:{type:String},opened:{type:Boolean,reflect:!0},searching:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},requireMatch:{type:Boolean,reflect:!0,attribute:"require-match"},disabled:{type:Boolean,reflect:!0},debounceMs:{type:Number,attribute:"debounce-ms"},maxVisible:{type:Number,attribute:"max-visible"},noResultsMessage:{type:String,attribute:"no-results-message"},emptyMessage:{type:String,attribute:"empty-message"}};#e=-1;#t=[];constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.opened=!1,this.searching=!1,this.required=!1,this.requireMatch=!1,this.disabled=!1,this.debounceMs=300,this.maxVisible=8,this.noResultsMessage="No Matches",this.emptyMessage="Type to search...",this.#s(300)}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),this.#i()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick)}childrenUpdated(){this.#i()}updated(e){super.updated(e),e.has("debounceMs")&&this.#s(this.debounceMs),(e.has("value")||e.has("required")||e.has("requireMatch"))&&this.#o()}formResetCallback(){this.value="",this.#o()}formStateRestoreCallback(e){this.value=e}handleInput=e=>{this.value=e.target.value,this.opened=!0,this.#e=-1,this.#a(this.value)};handleKeydown=e=>{const t=this.#n;if("ArrowDown"===e.key){if(e.preventDefault(),!this.opened)return void(this.opened=!0);this.#e=Math.min(this.#e+1,t.length-1),this.requestUpdate(),this.#r()}else"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.requestUpdate(),this.#r()):"Enter"===e.key?(e.preventDefault(),this.#e>=0&&this.#e<t.length&&this.#l(t[this.#e])):"Escape"===e.key?(e.preventDefault(),this.opened=!1,this.#e=-1):"Tab"===e.key&&(this.opened=!1,this.#e=-1)};handleDocumentClick=e=>{e.composedPath().includes(this)||(this.opened=!1,this.#e=-1)};handleFocus=()=>{this.opened=!0};handleOptionClick=e=>{const t=Number(e.currentTarget.dataset.index);this.#l(this.#n[t])};setOptions(e){return[...this.querySelectorAll("k-option")].forEach(e=>e.remove()),e.forEach(e=>{const t=document.createElement("k-option");"object"==typeof e?(t.setAttribute("value",e.value??e.label),t.textContent=e.label):t.textContent=e,this.appendChild(t)}),this}clear(){return this.value="",this.opened=!1,this.#e=-1,this.#o(),this}#s(e){this.#a=i(e=>{this.dispatchEvent(new CustomEvent("search",{detail:{value:e},bubbles:!0,composed:!0}))},e)}#a=()=>{};#i(){this.#t=[...this.querySelectorAll("k-option")].map(e=>({label:e.label,value:e.value})),this.requestUpdate()}#l(e){this.value=e.label,this.opened=!1,this.#e=-1,this.#o(),this.dispatchEvent(new CustomEvent("select",{detail:{value:e.value,label:e.label},bubbles:!0,composed:!0})),this.dispatchEvent(new CustomEvent("change",{bubbles:!0}))}#o(){const e=this.#t.find(e=>e.label===this.value);this.internals.setFormValue(e?e.value:this.value),this.required&&!this.value?this.internals.setValidity({valueMissing:!0},"Please select a value.",this.shadowRoot?.querySelector("input")):this.requireMatch&&this.value&&!e?this.internals.setValidity({customError:!0},"Please select a valid option.",this.shadowRoot?.querySelector("input")):this.internals.setValidity({})}#r(){this.updateComplete.then(()=>{this.shadowRoot?.querySelector(".option.focused")?.scrollIntoView({block:"nearest"})})}get#d(){const e=(this.value||"").toLowerCase();return e?this.#t.filter(t=>t.label.toLowerCase().includes(e)):this.#t}get#n(){return this.#d.slice(0,this.maxVisible)}render(){const t=this.#n,s=this.#d.length>this.maxVisible;return e`
|
|
2
2
|
<slot style="display:none"></slot>
|
|
3
3
|
<input
|
|
4
4
|
type="text"
|
|
@@ -23,7 +23,7 @@ import{html as e,css as t}from"../lit-all.min.js";import s from"./ShadowComponen
|
|
|
23
23
|
<div class="more">${this.#d.length-this.maxVisible} more...</div>
|
|
24
24
|
`:""}
|
|
25
25
|
${0!==t.length||this.searching?"":e`
|
|
26
|
-
<div class="no-results"
|
|
26
|
+
<div class="no-results">${this.value?this.noResultsMessage:this.emptyMessage}</div>
|
|
27
27
|
`}
|
|
28
28
|
${this.searching?e`
|
|
29
29
|
<div class="searching">
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<a href="#required">Required</a><br />
|
|
14
14
|
<a href="#requireMatch">Require Match</a><br />
|
|
15
15
|
<a href="#maxVisible">Max Visible</a><br />
|
|
16
|
+
<a href="#noResultsMessage">No Results Message & Empty Message</a><br />
|
|
16
17
|
|
|
17
18
|
<h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
|
|
18
19
|
<a href="#constructor">Constructor</a><br />
|
|
@@ -143,6 +144,21 @@
|
|
|
143
144
|
</div>
|
|
144
145
|
</div>
|
|
145
146
|
|
|
147
|
+
<h3 id="noResultsMessage"><a href="#noResultsMessage" class="no-link">No Results Message & Empty Message</a></h3>
|
|
148
|
+
<p>Use <code>no-results-message</code> to customize the text shown when the user has typed something but no options match. Use <code>empty-message</code> to customize the text shown when the input is empty and no options are loaded yet (e.g. before a dynamic search has fetched results). They default to <code>"No Matches"</code> and <code>"Type to search..."</code> respectively.</p>
|
|
149
|
+
<div class="row -mx">
|
|
150
|
+
<div class="col m-span-12 px">
|
|
151
|
+
<k-card label="HTML">
|
|
152
|
+
<pre><code class="hljs html"><span class="hljs-tag"><<span class="hljs-name">k-combobox</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Start typing to search...'</span> <span class="hljs-attr">no-results-message</span>=<span class="hljs-string">'No users found. Try a different search.'</span> <span class="hljs-attr">empty-message</span>=<span class="hljs-string">'Start typing to find a user...'</span>></span><br><span class="hljs-tag"></<span class="hljs-name">k-combobox</span>></span></code></pre>
|
|
153
|
+
</k-card>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="col m-span-12 px">
|
|
156
|
+
<k-card label="Output">
|
|
157
|
+
<k-combobox id="noResultsCb" placeholder="Start typing to search..." no-results-message="No users found. Try a different search." empty-message="Start typing to find a user..."></k-combobox>
|
|
158
|
+
</k-card>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
146
162
|
<h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
|
|
147
163
|
|
|
148
164
|
<h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
|
|
@@ -237,6 +253,20 @@
|
|
|
237
253
|
<td><code>8</code></td>
|
|
238
254
|
<td>Maximum number of options visible at once.</td>
|
|
239
255
|
</tr>
|
|
256
|
+
<tr>
|
|
257
|
+
<td><code>no-results-message</code></td>
|
|
258
|
+
<td><code>noResultsMessage</code></td>
|
|
259
|
+
<td><code>String</code></td>
|
|
260
|
+
<td><code>'No Matches'</code></td>
|
|
261
|
+
<td>Text displayed when the user has typed something but no options match.</td>
|
|
262
|
+
</tr>
|
|
263
|
+
<tr>
|
|
264
|
+
<td><code>empty-message</code></td>
|
|
265
|
+
<td><code>emptyMessage</code></td>
|
|
266
|
+
<td><code>String</code></td>
|
|
267
|
+
<td><code>'Type to search...'</code></td>
|
|
268
|
+
<td>Text displayed when the input is empty and no options are loaded (e.g. before an initial dynamic search fetch).</td>
|
|
269
|
+
</tr>
|
|
240
270
|
</tbody>
|
|
241
271
|
</table>
|
|
242
272
|
|
package/package.json
CHANGED
|
@@ -16,7 +16,9 @@ export default class Combobox extends ShadowComponent {
|
|
|
16
16
|
requireMatch: { type: Boolean, reflect: true, attribute: 'require-match' },
|
|
17
17
|
disabled: { type: Boolean, reflect: true },
|
|
18
18
|
debounceMs: { type: Number, attribute: 'debounce-ms' },
|
|
19
|
-
maxVisible: { type: Number, attribute: 'max-visible' }
|
|
19
|
+
maxVisible: { type: Number, attribute: 'max-visible' },
|
|
20
|
+
noResultsMessage: { type: String, attribute: 'no-results-message' },
|
|
21
|
+
emptyMessage: { type: String, attribute: 'empty-message' }
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
#focusedIndex = -1;
|
|
@@ -38,6 +40,8 @@ export default class Combobox extends ShadowComponent {
|
|
|
38
40
|
this.disabled = false;
|
|
39
41
|
this.debounceMs = 300;
|
|
40
42
|
this.maxVisible = 8;
|
|
43
|
+
this.noResultsMessage = 'No Matches';
|
|
44
|
+
this.emptyMessage = 'Type to search...';
|
|
41
45
|
this.#setupDebounce(300);
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -255,7 +259,7 @@ export default class Combobox extends ShadowComponent {
|
|
|
255
259
|
<div class="more">${this.#filteredOptions.length - this.maxVisible} more...</div>
|
|
256
260
|
` : ''}
|
|
257
261
|
${visible.length === 0 && !this.searching ? html`
|
|
258
|
-
<div class="no-results"
|
|
262
|
+
<div class="no-results">${this.value ? this.noResultsMessage : this.emptyMessage}</div>
|
|
259
263
|
` : ''}
|
|
260
264
|
${this.searching ? html`
|
|
261
265
|
<div class="searching">
|
|
@@ -12,8 +12,8 @@ const createCombobox = async (attrs = {}, children = '') => {
|
|
|
12
12
|
${attrs.disabled ? 'disabled' : ''}
|
|
13
13
|
${attrs.name ? `name="${attrs.name}"` : ''}
|
|
14
14
|
${attrs.debounceMs ? `debounce-ms="${attrs.debounceMs}"` : ''}
|
|
15
|
-
${attrs.maxVisible ? `max-visible="${attrs.maxVisible}"` : ''}
|
|
16
|
-
>${children}</k-combobox>
|
|
15
|
+
${attrs.maxVisible ? `max-visible="${attrs.maxVisible}"` : ''} ${attrs.noResultsMessage ? `no-results-message="${attrs.noResultsMessage}"` : ''}
|
|
16
|
+
${attrs.emptyMessage ? `empty-message="${attrs.emptyMessage}"` : ''} >${children}</k-combobox>
|
|
17
17
|
`;
|
|
18
18
|
document.body.appendChild(container);
|
|
19
19
|
const el = container.querySelector('k-combobox');
|
|
@@ -119,6 +119,26 @@ export default {
|
|
|
119
119
|
pass();
|
|
120
120
|
},
|
|
121
121
|
|
|
122
|
+
'should have default noResultsMessage of No Matches': async ({pass, fail}) => {
|
|
123
|
+
const { container, el } = await createCombobox();
|
|
124
|
+
if(el.noResultsMessage !== 'No Matches') {
|
|
125
|
+
cleanup(container);
|
|
126
|
+
return fail(`Expected 'No Matches', got '${el.noResultsMessage}'`);
|
|
127
|
+
}
|
|
128
|
+
cleanup(container);
|
|
129
|
+
pass();
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
'should have default emptyMessage of Type to search...': async ({pass, fail}) => {
|
|
133
|
+
const { container, el } = await createCombobox();
|
|
134
|
+
if(el.emptyMessage !== 'Type to search...') {
|
|
135
|
+
cleanup(container);
|
|
136
|
+
return fail(`Expected 'Type to search...', got '${el.emptyMessage}'`);
|
|
137
|
+
}
|
|
138
|
+
cleanup(container);
|
|
139
|
+
pass();
|
|
140
|
+
},
|
|
141
|
+
|
|
122
142
|
'should not be required by default': async ({pass, fail}) => {
|
|
123
143
|
const { container, el } = await createCombobox();
|
|
124
144
|
if(el.required !== false) {
|
|
@@ -222,6 +242,26 @@ export default {
|
|
|
222
242
|
pass();
|
|
223
243
|
},
|
|
224
244
|
|
|
245
|
+
'should reflect no-results-message attribute': async ({pass, fail}) => {
|
|
246
|
+
const { container, el } = await createCombobox({ noResultsMessage: 'Nothing found' });
|
|
247
|
+
if(el.noResultsMessage !== 'Nothing found') {
|
|
248
|
+
cleanup(container);
|
|
249
|
+
return fail(`Expected 'Nothing found', got '${el.noResultsMessage}'`);
|
|
250
|
+
}
|
|
251
|
+
cleanup(container);
|
|
252
|
+
pass();
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
'should reflect empty-message attribute': async ({pass, fail}) => {
|
|
256
|
+
const { container, el } = await createCombobox({ emptyMessage: 'Start searching' });
|
|
257
|
+
if(el.emptyMessage !== 'Start searching') {
|
|
258
|
+
cleanup(container);
|
|
259
|
+
return fail(`Expected 'Start searching', got '${el.emptyMessage}'`);
|
|
260
|
+
}
|
|
261
|
+
cleanup(container);
|
|
262
|
+
pass();
|
|
263
|
+
},
|
|
264
|
+
|
|
225
265
|
/*
|
|
226
266
|
Children (k-option)
|
|
227
267
|
*/
|
|
@@ -443,6 +483,33 @@ export default {
|
|
|
443
483
|
pass();
|
|
444
484
|
},
|
|
445
485
|
|
|
486
|
+
'should show noResultsMessage text when value typed but no matches': async ({pass, fail}) => {
|
|
487
|
+
const { container, el } = await createCombobox({ noResultsMessage: 'Custom no results' }, fruitOptions);
|
|
488
|
+
el.value = 'zzz';
|
|
489
|
+
el.opened = true;
|
|
490
|
+
await el.updateComplete;
|
|
491
|
+
const msg = el.shadowRoot.querySelector('.no-results');
|
|
492
|
+
if(!msg || msg.textContent.trim() !== 'Custom no results') {
|
|
493
|
+
cleanup(container);
|
|
494
|
+
return fail(`Expected 'Custom no results', got '${msg?.textContent}'`);
|
|
495
|
+
}
|
|
496
|
+
cleanup(container);
|
|
497
|
+
pass();
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
'should show emptyMessage text when no value and no options': async ({pass, fail}) => {
|
|
501
|
+
const { container, el } = await createCombobox({ emptyMessage: 'Custom empty' });
|
|
502
|
+
el.opened = true;
|
|
503
|
+
await el.updateComplete;
|
|
504
|
+
const msg = el.shadowRoot.querySelector('.no-results');
|
|
505
|
+
if(!msg || msg.textContent.trim() !== 'Custom empty') {
|
|
506
|
+
cleanup(container);
|
|
507
|
+
return fail(`Expected 'Custom empty', got '${msg?.textContent}'`);
|
|
508
|
+
}
|
|
509
|
+
cleanup(container);
|
|
510
|
+
pass();
|
|
511
|
+
},
|
|
512
|
+
|
|
446
513
|
/*
|
|
447
514
|
Max Visible
|
|
448
515
|
*/
|
|
@@ -917,7 +917,7 @@ export default {
|
|
|
917
917
|
fail(`Expected active to be "tab3", got "${tabs.active}"`);
|
|
918
918
|
return;
|
|
919
919
|
}
|
|
920
|
-
if(!tab3.active || content3.active){
|
|
920
|
+
if(!tab3.active || !content3.active){
|
|
921
921
|
fail('tab3 should be active (restored from localStorage)');
|
|
922
922
|
return;
|
|
923
923
|
}
|