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 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">No matches</div>
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 &amp; 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 &amp; 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">&lt;<span class="hljs-name">k-combobox</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">&#x27;Start typing to search...&#x27;</span> <span class="hljs-attr">no-results-message</span>=<span class="hljs-string">&#x27;No users found. Try a different search.&#x27;</span> <span class="hljs-attr">empty-message</span>=<span class="hljs-string">&#x27;Start typing to find a user...&#x27;</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">k-combobox</span>&gt;</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">No matches</div>
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 &amp; 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 &amp; 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">&lt;<span class="hljs-name">k-combobox</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">&#x27;Start typing to search...&#x27;</span> <span class="hljs-attr">no-results-message</span>=<span class="hljs-string">&#x27;No users found. Try a different search.&#x27;</span> <span class="hljs-attr">empty-message</span>=<span class="hljs-string">&#x27;Start typing to find a user...&#x27;</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">k-combobox</span>&gt;</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.17",
3
+ "version": "0.4.18",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -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">No matches</div>
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
  }