kempo-ui 0.3.11 → 0.3.12

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.
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"../lit-all.min.js";import{boolTrueFalse as i}from"../utils/propConverters.js";import"./FocusCapture.js";import"./Icon.js";export default class o extends e{static properties={state:{type:String,reflect:!0},side:{type:String,reflect:!0},main:{type:String,reflect:!0},overlayClose:{type:Boolean,reflect:!0,attribute:"overlay-close",converter:i},escClose:{type:Boolean,reflect:!0,attribute:"esc-close",converter:i},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this.state="offscreen",this.side="left",this.main="overlay",this.overlayClose=!0,this.escClose=!0,this.persistentId=null}overlayClick=()=>{this.overlayClose&&this.hide()};handleKeyDown=e=>{this.escClose&&"Escape"===e.key&&this.hide()};updated(e){if(super.updated(e),e.has("persistentId")&&this.persistentId&&window?.localStorage){const e=window.localStorage.getItem(`aside-persistent-id-${this.persistentId}`);e&&(this.state=e)}if(e.has("state")){const t=e.get("state");"offscreen"===this.state||"offscreen"!==t&&void 0!==t?"offscreen"===this.state&&void 0!==t&&"offscreen"!==t&&document.removeEventListener("keydown",this.handleKeyDown):document.addEventListener("keydown",this.handleKeyDown),"overlay"===this.main&&("expanded"===this.state?document.body.classList.add("no-scroll"):document.body.classList.remove("no-scroll"));const s="offscreen"===this.state?0:this.getTargetWidth(this.state),a={aside:this,state:this.state,main:this.main,width:s};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),this.inert="offscreen"===this.state,this.persistentId&&window?.localStorage&&window.localStorage.setItem(`aside-persistent-id-${this.persistentId}`,this.state)}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown),document.body.classList.remove("no-scroll");const e={aside:this,state:"offscreen",main:this.main,width:0};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:e})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:e}))}expand=()=>this.state="expanded";collapse=()=>this.state="collapsed";hide=()=>this.state="offscreen";toggle=()=>{this.state="expanded"===this.state?"offscreen":"expanded"};getTargetWidth(e){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),s="collapsed"===e?"--collapsed-width":"--width",a=getComputedStyle(this).getPropertyValue(s).trim();return(a?parseFloat(a):"collapsed"===e?3.5:20)*t}render(){const e=t`<aside><slot></slot></aside>`;return"overlay"===this.main?t`
1
+ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"../lit-all.min.js";import{boolTrueFalse as i}from"../utils/propConverters.js";import"./Dropdown.js";import"./FocusCapture.js";import"./Icon.js";export default class n extends e{static properties={state:{type:String,reflect:!0},side:{type:String,reflect:!0},main:{type:String,reflect:!0},overlayClose:{type:Boolean,reflect:!0,attribute:"overlay-close",converter:i},escClose:{type:Boolean,reflect:!0,attribute:"esc-close",converter:i},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this.state="offscreen",this.side="left",this.main="overlay",this.overlayClose=!0,this.escClose=!0,this.persistentId=null}overlayClick=()=>{this.overlayClose&&this.hide()};handleKeyDown=e=>{this.escClose&&"Escape"===e.key&&this.hide()};updated(e){if(super.updated(e),e.has("persistentId")&&this.persistentId&&window?.localStorage){const e=window.localStorage.getItem(`aside-persistent-id-${this.persistentId}`);e&&(this.state=e)}if(e.has("state")){const t=e.get("state");"offscreen"===this.state||"offscreen"!==t&&void 0!==t?"offscreen"===this.state&&void 0!==t&&"offscreen"!==t&&document.removeEventListener("keydown",this.handleKeyDown):document.addEventListener("keydown",this.handleKeyDown),"overlay"===this.main&&("expanded"===this.state?document.body.classList.add("no-scroll"):document.body.classList.remove("no-scroll"));const s="offscreen"===this.state?0:this.getTargetWidth(this.state),a={aside:this,state:this.state,main:this.main,width:s};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),this.inert="offscreen"===this.state,this.persistentId&&window?.localStorage&&window.localStorage.setItem(`aside-persistent-id-${this.persistentId}`,this.state)}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown),document.body.classList.remove("no-scroll");const e={aside:this,state:"offscreen",main:this.main,width:0};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:e})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:e}))}expand=()=>this.state="expanded";collapse=()=>this.state="collapsed";hide=()=>this.state="offscreen";toggle=()=>{this.state="expanded"===this.state?"offscreen":"expanded"};getTargetWidth(e){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),s="collapsed"===e?"--collapsed-width":"--width",a=getComputedStyle(this).getPropertyValue(s).trim();return(a?parseFloat(a):"collapsed"===e?3.5:20)*t}render(){const e=t`<aside><slot></slot></aside>`;return"overlay"===this.main?t`
2
2
  <k-focus-capture>
3
3
  <div id="container">
4
4
  <button id="overlay-btn" @click=${this.overlayClick}>
@@ -121,10 +121,10 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
121
121
  }
122
122
  :host([main="push"][state="collapsed"]) aside {
123
123
  width: var(--collapsed-width);
124
- transform: translateX(0);
124
+ transform: none;
125
125
  }
126
126
  :host([main="push"][state="expanded"]) aside {
127
- transform: translateX(0);
127
+ transform: none;
128
128
  }
129
129
  :host([main="overlay"]) aside {
130
130
  width: var(--width);
@@ -133,15 +133,15 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
133
133
  transform: translateX(-100%);
134
134
  }
135
135
  :host([main="overlay"][state="expanded"]) aside {
136
- transform: translateX(0);
136
+ transform: none;
137
137
  }
138
138
  :host([main="overlay"][side="right"]) aside {
139
139
  transform: translateX(100%);
140
140
  }
141
141
  :host([main="overlay"][side="right"][state="expanded"]) aside {
142
- transform: translateX(0);
142
+ transform: none;
143
143
  }
144
- `}customElements.define("k-aside",o);class n extends e{static properties={icon:{type:String},href:{type:String},active:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.href="#",this.active=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};handleClick=e=>{this.collapsed&&!this["no-expand"]&&this.aside&&(e.preventDefault(),this.aside.expand())};render(){return t`
144
+ `}customElements.define("k-aside",n);class o extends e{static properties={icon:{type:String},href:{type:String},active:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.href="#",this.active=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.inMenu=!!this.closest("k-aside-menu"),this.aside&&!this.inMenu&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};handleClick=e=>{this.collapsed&&!this["no-expand"]&&this.aside&&(e.preventDefault(),this.aside.expand())};render(){return t`
145
145
  <a href="${this.href}" class="item ${this.active?"active bg-primary":""}" @click=${this.handleClick}>
146
146
  ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:this.collapsed?t`<k-icon name="dot"></k-icon>`:a}
147
147
  ${this.collapsed?a:t`<span class="label"><slot></slot></span>`}
@@ -183,7 +183,7 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
183
183
  overflow: hidden;
184
184
  text-overflow: ellipsis;
185
185
  }
186
- `}class d extends e{static properties={collapsed:{type:Boolean,reflect:!0}};constructor(){super(),this.collapsed=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};render(){return this.collapsed?t`<hr>`:t`<div class="label"><slot></slot></div>`}static styles=s`
186
+ `}class r extends e{static properties={collapsed:{type:Boolean,reflect:!0}};constructor(){super(),this.collapsed=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};render(){return this.collapsed?t`<hr>`:t`<div class="label"><slot></slot></div>`}static styles=s`
187
187
  :host {
188
188
  display: block;
189
189
  margin: var(--spacer_h) 0;
@@ -208,14 +208,21 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
208
208
  :host([collapsed]) hr {
209
209
  margin: var(--spacer_h) 0;
210
210
  }
211
- `}class r extends e{static properties={icon:{type:String},label:{type:String},open:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.label="",this.open=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?(this.collapsed=!0,this.open=!1):"expanded"===t&&(this.collapsed=!1)};toggleMenu=()=>{this.collapsed&&!this["no-expand"]&&this.aside?this.aside.expand():this.collapsed||(this.open=!this.open)};render(){return t`
211
+ `}class d extends e{static properties={icon:{type:String},label:{type:String},open:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},side:{type:String},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.label="",this.open=!1,this.collapsed=!1,this.side="left",this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.side=this.aside.side||"left",this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?(this.collapsed=!0,this.open=!1):"expanded"===t&&(this.collapsed=!1),this.side=this.aside?.side||"left"};toggleMenu=()=>{this.collapsed||(this.open=!this.open)};handleCollapsedClick=()=>{this.open=!0,this.aside?.expand()};render(){if(this.collapsed){const e="right"===this.side?"left down":"right down";return t`
212
+ <k-dropdown open-direction=${e} hover>
213
+ <button slot="trigger" class="no-btn collapsed-trigger" @click=${this.handleCollapsedClick}>
214
+ ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:t`<k-icon name="dot"></k-icon>`}
215
+ </button>
216
+ <slot></slot>
217
+ </k-dropdown>
218
+ `}return t`
212
219
  <div class="menu-container">
213
220
  <button class="no-btn menu-header ${this.open?"open":""}" @click=${this.toggleMenu}>
214
- ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:this.collapsed?t`<k-icon name="dot"></k-icon>`:a}
215
- ${this.collapsed?a:t`<span class="label">${this.label}</span>`}
216
- ${this.collapsed?a:t`<k-icon class="chevron" name="chevron" direction="${this.open?"down":"right"}"></k-icon>`}
221
+ ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:a}
222
+ <span class="label">${this.label}</span>
223
+ <k-icon class="chevron" name="chevron" direction="${this.open?"down":"right"}"></k-icon>
217
224
  </button>
218
- <div class="menu-content ${this.open&&!this.collapsed?"open":""}">
225
+ <div class="menu-content ${this.open?"open":""}">
219
226
  <slot></slot>
220
227
  </div>
221
228
  </div>
@@ -242,12 +249,29 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
242
249
  white-space: nowrap;
243
250
  transition: background var(--animation_ms);
244
251
  }
245
- :host([collapsed]) .menu-header {
246
- width: auto;
247
- }
248
252
  .menu-header:hover {
249
253
  background: var(--c_bg_hover);
250
254
  }
255
+ .collapsed-trigger {
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ padding: var(--spacer_h);
260
+ color: var(--tc);
261
+ background: transparent;
262
+ border: none;
263
+ appearance: none;
264
+ border-radius: var(--radius);
265
+ margin: 0 var(--spacer_h);
266
+ cursor: pointer;
267
+ transition: background var(--animation_ms);
268
+ }
269
+ .collapsed-trigger:hover {
270
+ background: var(--c_bg_hover);
271
+ }
272
+ k-dropdown {
273
+ display: block;
274
+ }
251
275
  k-icon {
252
276
  flex-shrink: 0;
253
277
  }
@@ -266,9 +290,6 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
266
290
  transition: max-height var(--animation_ms);
267
291
  padding-left: calc(var(--spacer_h) * 2);
268
292
  }
269
- :host([collapsed]) .menu-content {
270
- padding-left: 0;
271
- }
272
293
  .menu-content.open {
273
294
  max-height: 500px;
274
295
  }
@@ -313,6 +334,7 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
313
334
  width: 2rem;
314
335
  height: 2rem;
315
336
  border: none;
337
+ appearance: none;
316
338
  background: transparent;
317
339
  color: var(--c_text);
318
340
  cursor: pointer;
@@ -325,4 +347,4 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
325
347
  #toggle:hover {
326
348
  background: var(--c_bg_hover);
327
349
  }
328
- `}customElements.define("k-aside-item",n),customElements.define("k-aside-label",d),customElements.define("k-aside-menu",r),customElements.define("k-aside-spacer",l),customElements.define("k-aside-toggle",c);
350
+ `}customElements.define("k-aside-item",o),customElements.define("k-aside-label",r),customElements.define("k-aside-menu",d),customElements.define("k-aside-spacer",l),customElements.define("k-aside-toggle",c);
@@ -1,4 +1,4 @@
1
- import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponent.js";import{boolTrueFalse as n}from"../utils/propConverters.js";const i=new Set;export default class s extends o{static properties={opened:{type:Boolean,reflect:!0},openDirection:{type:String,reflect:!0,attribute:"open-direction"},closeOnSelect:{type:Boolean,reflect:!0,attribute:"close-on-select",converter:n},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:n},submenu:{type:Boolean,reflect:!0}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0,this.submenu=!1}connectedCallback(){super.connectedCallback(),"K-DROPDOWN"!==this.parentElement?.tagName||this.hasAttribute("slot")||(this.submenu=!0,this.hasAttribute("open-direction")||(this.openDirection="right down")),this.submenu?(this.addEventListener("mouseenter",this.handleSubmenuEnter),this.addEventListener("mouseleave",this.handleSubmenuLeave)):document.addEventListener("click",this.handleDocumentClick),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),this.submenu?(this.removeEventListener("mouseenter",this.handleSubmenuEnter),this.removeEventListener("mouseleave",this.handleSubmenuLeave)):document.removeEventListener("click",this.handleDocumentClick),document.removeEventListener("keydown",this.handleKeydown),i.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?i.add(this):i.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.composedPath(),o=t.find(e=>e.matches?.('[slot="trigger"]'));if(o){const e=o.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!t.includes(this))return void(this.closeOnClickOutside&&this.close());const n=t.find(e=>e.matches?.("a, button"));n&&!n.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{e.stopPropagation(),this.opened||i.forEach(e=>{e!==this&&e.close()}),this.toggle()};handleMenuClick=e=>{const t=e.target.closest("a, button");if(!t||t.hasAttribute("disabled"))return;if(t.closest("k-dropdown")!==this)return;const o=t.dataset?.value||t.textContent.trim();this.dispatchEvent(new CustomEvent("select",{detail:{value:o,item:t},bubbles:!0}))};handleKeydown=e=>{if(!this.opened)return;const t=document.activeElement;if(!this.contains(t))return;const o=this.querySelector(":scope > k-dropdown[opened]");if(!o?.contains(t))if("Escape"===e.key||this.submenu&&"ArrowLeft"===e.key)e.preventDefault(),this.close(),this.focusTrigger();else if("ArrowDown"===e.key)e.preventDefault(),this.focusNextItem();else if("ArrowUp"===e.key)e.preventDefault(),this.focusPreviousItem();else if("ArrowRight"===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this&&(e.preventDefault(),o.open(),o.focusFirstItem())}else if("Enter"===e.key||" "===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this?(e.preventDefault(),o.open(),o.focusFirstItem()):t&&!t.closest('[slot="trigger"]')&&(e.preventDefault(),t.click())}};open(){return this.submenu?[...this.parentElement.querySelectorAll(":scope > k-dropdown[opened]")].forEach(e=>{e!==this&&e.close()}):i.forEach(e=>{e!==this&&e.close()}),this.opened=!0,this.submenu||requestAnimationFrame(()=>this.focusFirstItem()),this}close(){return this.querySelectorAll(":scope > k-dropdown[opened]").forEach(e=>e.close()),this.opened=!1,this}toggle(){return this.opened?this.close():this.open()}focusTrigger(){const e=this.querySelector('[slot="trigger"]');e&&e.focus()}getMenuItems(){return[...this.children].reduce((e,t)=>{if(t.matches('[slot="trigger"]')||t.hasAttribute("disabled"))return e;if("K-DROPDOWN"===t.tagName){const o=t.querySelector('[slot="trigger"]');o&&e.push(o)}else t.matches("a, button")&&e.push(t);return e},[])}focusFirstItem(){const e=this.getMenuItems();e.length>0&&e[0].focus()}focusNextItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o+1]||e[0];n&&n.focus()}focusPreviousItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o-1]||e[e.length-1];n&&n.focus()}static styles=t`
1
+ import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponent.js";import{boolTrueFalse as n}from"../utils/propConverters.js";const i=new Set;export default class s extends o{static properties={opened:{type:Boolean,reflect:!0},openDirection:{type:String,reflect:!0,attribute:"open-direction"},closeOnSelect:{type:Boolean,reflect:!0,attribute:"close-on-select",converter:n},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:n},submenu:{type:Boolean,reflect:!0},hover:{type:Boolean,reflect:!0}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0,this.submenu=!1,this.hover=!1}connectedCallback(){super.connectedCallback(),"K-DROPDOWN"!==this.parentElement?.tagName||this.hasAttribute("slot")||(this.submenu=!0,this.hasAttribute("open-direction")||(this.openDirection="right down")),this.submenu?(this.addEventListener("mouseenter",this.handleSubmenuEnter),this.addEventListener("mouseleave",this.handleSubmenuLeave)):(document.addEventListener("click",this.handleDocumentClick),this.hover&&(this.addEventListener("mouseenter",this.handleHoverEnter),this.addEventListener("mouseleave",this.handleHoverLeave))),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),this.submenu?(this.removeEventListener("mouseenter",this.handleSubmenuEnter),this.removeEventListener("mouseleave",this.handleSubmenuLeave)):(document.removeEventListener("click",this.handleDocumentClick),this.removeEventListener("mouseenter",this.handleHoverEnter),this.removeEventListener("mouseleave",this.handleHoverLeave)),document.removeEventListener("keydown",this.handleKeydown),i.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?i.add(this):i.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.composedPath(),o=t.find(e=>e.matches?.('[slot="trigger"]'));if(o){const e=o.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!t.includes(this))return void(this.closeOnClickOutside&&this.close());const n=t.find(e=>e.matches?.("a, button"));n&&!n.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{this.hover||(e.stopPropagation(),this.opened||i.forEach(e=>{e!==this&&e.close()}),this.toggle())};handleMenuClick=e=>{const t=e.target.closest("a, button");if(!t||t.hasAttribute("disabled"))return;if(t.closest("k-dropdown")!==this)return;const o=t.dataset?.value||t.textContent.trim();this.dispatchEvent(new CustomEvent("select",{detail:{value:o,item:t},bubbles:!0}))};handleKeydown=e=>{if(!this.opened)return;const t=document.activeElement;if(!this.contains(t))return;const o=this.querySelector(":scope > k-dropdown[opened]");if(!o?.contains(t))if("Escape"===e.key||this.submenu&&"ArrowLeft"===e.key)e.preventDefault(),this.close(),this.focusTrigger();else if("ArrowDown"===e.key)e.preventDefault(),this.focusNextItem();else if("ArrowUp"===e.key)e.preventDefault(),this.focusPreviousItem();else if("ArrowRight"===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this&&(e.preventDefault(),o.open(),o.focusFirstItem())}else if("Enter"===e.key||" "===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this?(e.preventDefault(),o.open(),o.focusFirstItem()):t&&!t.closest('[slot="trigger"]')&&(e.preventDefault(),t.click())}};open(){return this.submenu?[...this.parentElement.querySelectorAll(":scope > k-dropdown[opened]")].forEach(e=>{e!==this&&e.close()}):i.forEach(e=>{e!==this&&e.close()}),this.opened=!0,this.submenu||requestAnimationFrame(()=>this.focusFirstItem()),this}close(){return this.querySelectorAll(":scope > k-dropdown[opened]").forEach(e=>e.close()),this.opened=!1,this}toggle(){return this.opened?this.close():this.open()}focusTrigger(){const e=this.querySelector('[slot="trigger"]');e&&e.focus()}getMenuItems(){return[...this.children].reduce((e,t)=>{if(t.matches('[slot="trigger"]')||t.hasAttribute("disabled"))return e;if("K-DROPDOWN"===t.tagName){const o=t.querySelector('[slot="trigger"]');o&&e.push(o)}else t.matches("a, button")&&e.push(t);return e},[])}focusFirstItem(){const e=this.getMenuItems();e.length>0&&e[0].focus()}focusNextItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o+1]||e[0];n&&n.focus()}focusPreviousItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o-1]||e[e.length-1];n&&n.focus()}static styles=t`
2
2
  :host {
3
3
  display: inline-block;
4
4
  position: relative;
@@ -160,7 +160,7 @@ import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponen
160
160
  font-size: 0.75em;
161
161
  opacity: 0.6;
162
162
  }
163
- `;handleSubmenuEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleSubmenuLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSlotChange=e=>{e.target.assignedElements().filter(e=>e.matches("a, button, k-dropdown")).forEach((e,t)=>e.classList.toggle("k-dropdown-first",0===t))};render(){return e`
163
+ `;handleHoverEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleHoverLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSubmenuEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleSubmenuLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSlotChange=e=>{e.target.assignedElements().filter(e=>e.matches("a, button, k-dropdown")).forEach((e,t)=>e.classList.toggle("k-dropdown-first",0===t))};render(){return e`
164
164
  <div id="trigger" @click=${this.handleTriggerClick}>
165
165
  <slot name="trigger"></slot>
166
166
  ${this.submenu?e`<k-icon name="chevron"></k-icon>`:""}
@@ -263,6 +263,7 @@
263
263
  <a href="#disabledItems">Disabled Items</a><br />
264
264
  <a href="#links">Using Links</a><br />
265
265
  <a href="#events">Events</a><br />
266
+ <a href="#hover">Hover</a><br />
266
267
 
267
268
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
268
269
  <a href="#constructor">Constructor</a><br />
@@ -473,6 +474,26 @@
473
474
  </div>
474
475
  </div>
475
476
 
477
+ <h3 id="hover"><a href="#hover" class="no-link">Hover</a></h3>
478
+ <p>Add the <code>hover</code> attribute to open the menu when the trigger is hovered instead of clicked. The menu closes automatically 150ms after the cursor leaves the component.</p>
479
+ <div class="row -mx">
480
+ <div class="col m-span-12 px">
481
+ <k-card label="HTML">
482
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-dropdown</span> <span class="hljs-attr">hover</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">&#x27;trigger&#x27;</span>&gt;</span>Hover Me<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;edit&#x27;</span>&gt;</span>Edit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;copy&#x27;</span>&gt;</span>Copy<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;delete&#x27;</span>&gt;</span>Delete<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">k-dropdown</span>&gt;</span></code></pre>
483
+ </k-card>
484
+ </div>
485
+ <div class="col m-span-12 px">
486
+ <k-card label="Output">
487
+ <k-dropdown hover>
488
+ <button slot="trigger">Hover Me</button>
489
+ <button data-value="edit">Edit</button>
490
+ <button data-value="copy">Copy</button>
491
+ <button data-value="delete">Delete</button>
492
+ </k-dropdown>
493
+ </k-card>
494
+ </div>
495
+ </div>
496
+
476
497
  <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
477
498
 
478
499
  <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
@@ -501,6 +522,9 @@
501
522
  <h5><code>submenu<i>: boolean</i></code></h5>
502
523
  <p>Automatically set to <code>true</code> when a <code>k-dropdown</code> is nested inside another <code>k-dropdown</code>. Sub menus open on hover/focus, display a chevron icon, and default to <code>open-direction="right down"</code>. Supports <code>ArrowRight</code> to open and <code>ArrowLeft</code> to close via keyboard.</p>
503
524
 
525
+ <h5><code>hover<i>: boolean</i></code></h5>
526
+ <p>When present, the menu opens on <code>mouseenter</code> instead of trigger click. The menu closes 150ms after the cursor leaves the component. Default is <code>false</code>.</p>
527
+
504
528
  <h3 id="methods"><a href="#methods" class="no-link">Methods</a></h3>
505
529
  <h5><code>open()<i>: this</i></code></h5>
506
530
  <p>Opens the dropdown menu and focuses the first item.</p>
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"../lit-all.min.js";import{boolTrueFalse as i}from"../utils/propConverters.js";import"./FocusCapture.js";import"./Icon.js";export default class o extends e{static properties={state:{type:String,reflect:!0},side:{type:String,reflect:!0},main:{type:String,reflect:!0},overlayClose:{type:Boolean,reflect:!0,attribute:"overlay-close",converter:i},escClose:{type:Boolean,reflect:!0,attribute:"esc-close",converter:i},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this.state="offscreen",this.side="left",this.main="overlay",this.overlayClose=!0,this.escClose=!0,this.persistentId=null}overlayClick=()=>{this.overlayClose&&this.hide()};handleKeyDown=e=>{this.escClose&&"Escape"===e.key&&this.hide()};updated(e){if(super.updated(e),e.has("persistentId")&&this.persistentId&&window?.localStorage){const e=window.localStorage.getItem(`aside-persistent-id-${this.persistentId}`);e&&(this.state=e)}if(e.has("state")){const t=e.get("state");"offscreen"===this.state||"offscreen"!==t&&void 0!==t?"offscreen"===this.state&&void 0!==t&&"offscreen"!==t&&document.removeEventListener("keydown",this.handleKeyDown):document.addEventListener("keydown",this.handleKeyDown),"overlay"===this.main&&("expanded"===this.state?document.body.classList.add("no-scroll"):document.body.classList.remove("no-scroll"));const s="offscreen"===this.state?0:this.getTargetWidth(this.state),a={aside:this,state:this.state,main:this.main,width:s};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),this.inert="offscreen"===this.state,this.persistentId&&window?.localStorage&&window.localStorage.setItem(`aside-persistent-id-${this.persistentId}`,this.state)}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown),document.body.classList.remove("no-scroll");const e={aside:this,state:"offscreen",main:this.main,width:0};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:e})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:e}))}expand=()=>this.state="expanded";collapse=()=>this.state="collapsed";hide=()=>this.state="offscreen";toggle=()=>{this.state="expanded"===this.state?"offscreen":"expanded"};getTargetWidth(e){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),s="collapsed"===e?"--collapsed-width":"--width",a=getComputedStyle(this).getPropertyValue(s).trim();return(a?parseFloat(a):"collapsed"===e?3.5:20)*t}render(){const e=t`<aside><slot></slot></aside>`;return"overlay"===this.main?t`
1
+ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"../lit-all.min.js";import{boolTrueFalse as i}from"../utils/propConverters.js";import"./Dropdown.js";import"./FocusCapture.js";import"./Icon.js";export default class n extends e{static properties={state:{type:String,reflect:!0},side:{type:String,reflect:!0},main:{type:String,reflect:!0},overlayClose:{type:Boolean,reflect:!0,attribute:"overlay-close",converter:i},escClose:{type:Boolean,reflect:!0,attribute:"esc-close",converter:i},persistentId:{type:String,reflect:!0,attribute:"persistent-id"}};constructor(){super(),this.state="offscreen",this.side="left",this.main="overlay",this.overlayClose=!0,this.escClose=!0,this.persistentId=null}overlayClick=()=>{this.overlayClose&&this.hide()};handleKeyDown=e=>{this.escClose&&"Escape"===e.key&&this.hide()};updated(e){if(super.updated(e),e.has("persistentId")&&this.persistentId&&window?.localStorage){const e=window.localStorage.getItem(`aside-persistent-id-${this.persistentId}`);e&&(this.state=e)}if(e.has("state")){const t=e.get("state");"offscreen"===this.state||"offscreen"!==t&&void 0!==t?"offscreen"===this.state&&void 0!==t&&"offscreen"!==t&&document.removeEventListener("keydown",this.handleKeyDown):document.addEventListener("keydown",this.handleKeyDown),"overlay"===this.main&&("expanded"===this.state?document.body.classList.add("no-scroll"):document.body.classList.remove("no-scroll"));const s="offscreen"===this.state?0:this.getTargetWidth(this.state),a={aside:this,state:this.state,main:this.main,width:s};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:a})),this.inert="offscreen"===this.state,this.persistentId&&window?.localStorage&&window.localStorage.setItem(`aside-persistent-id-${this.persistentId}`,this.state)}}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("keydown",this.handleKeyDown),document.body.classList.remove("no-scroll");const e={aside:this,state:"offscreen",main:this.main,width:0};this.dispatchEvent(new CustomEvent("aside_state_change",{detail:e})),window.dispatchEvent(new CustomEvent("aside_state_change",{detail:e}))}expand=()=>this.state="expanded";collapse=()=>this.state="collapsed";hide=()=>this.state="offscreen";toggle=()=>{this.state="expanded"===this.state?"offscreen":"expanded"};getTargetWidth(e){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),s="collapsed"===e?"--collapsed-width":"--width",a=getComputedStyle(this).getPropertyValue(s).trim();return(a?parseFloat(a):"collapsed"===e?3.5:20)*t}render(){const e=t`<aside><slot></slot></aside>`;return"overlay"===this.main?t`
2
2
  <k-focus-capture>
3
3
  <div id="container">
4
4
  <button id="overlay-btn" @click=${this.overlayClick}>
@@ -121,10 +121,10 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
121
121
  }
122
122
  :host([main="push"][state="collapsed"]) aside {
123
123
  width: var(--collapsed-width);
124
- transform: translateX(0);
124
+ transform: none;
125
125
  }
126
126
  :host([main="push"][state="expanded"]) aside {
127
- transform: translateX(0);
127
+ transform: none;
128
128
  }
129
129
  :host([main="overlay"]) aside {
130
130
  width: var(--width);
@@ -133,15 +133,15 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
133
133
  transform: translateX(-100%);
134
134
  }
135
135
  :host([main="overlay"][state="expanded"]) aside {
136
- transform: translateX(0);
136
+ transform: none;
137
137
  }
138
138
  :host([main="overlay"][side="right"]) aside {
139
139
  transform: translateX(100%);
140
140
  }
141
141
  :host([main="overlay"][side="right"][state="expanded"]) aside {
142
- transform: translateX(0);
142
+ transform: none;
143
143
  }
144
- `}customElements.define("k-aside",o);class n extends e{static properties={icon:{type:String},href:{type:String},active:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.href="#",this.active=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};handleClick=e=>{this.collapsed&&!this["no-expand"]&&this.aside&&(e.preventDefault(),this.aside.expand())};render(){return t`
144
+ `}customElements.define("k-aside",n);class o extends e{static properties={icon:{type:String},href:{type:String},active:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.href="#",this.active=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.inMenu=!!this.closest("k-aside-menu"),this.aside&&!this.inMenu&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};handleClick=e=>{this.collapsed&&!this["no-expand"]&&this.aside&&(e.preventDefault(),this.aside.expand())};render(){return t`
145
145
  <a href="${this.href}" class="item ${this.active?"active bg-primary":""}" @click=${this.handleClick}>
146
146
  ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:this.collapsed?t`<k-icon name="dot"></k-icon>`:a}
147
147
  ${this.collapsed?a:t`<span class="label"><slot></slot></span>`}
@@ -183,7 +183,7 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
183
183
  overflow: hidden;
184
184
  text-overflow: ellipsis;
185
185
  }
186
- `}class d extends e{static properties={collapsed:{type:Boolean,reflect:!0}};constructor(){super(),this.collapsed=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};render(){return this.collapsed?t`<hr>`:t`<div class="label"><slot></slot></div>`}static styles=s`
186
+ `}class r extends e{static properties={collapsed:{type:Boolean,reflect:!0}};constructor(){super(),this.collapsed=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?this.collapsed=!0:"expanded"===t&&(this.collapsed=!1)};render(){return this.collapsed?t`<hr>`:t`<div class="label"><slot></slot></div>`}static styles=s`
187
187
  :host {
188
188
  display: block;
189
189
  margin: var(--spacer_h) 0;
@@ -208,14 +208,21 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
208
208
  :host([collapsed]) hr {
209
209
  margin: var(--spacer_h) 0;
210
210
  }
211
- `}class r extends e{static properties={icon:{type:String},label:{type:String},open:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.label="",this.open=!1,this.collapsed=!1,this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?(this.collapsed=!0,this.open=!1):"expanded"===t&&(this.collapsed=!1)};toggleMenu=()=>{this.collapsed&&!this["no-expand"]&&this.aside?this.aside.expand():this.collapsed||(this.open=!this.open)};render(){return t`
211
+ `}class d extends e{static properties={icon:{type:String},label:{type:String},open:{type:Boolean,reflect:!0},collapsed:{type:Boolean,reflect:!0},side:{type:String},"no-expand":{type:Boolean,attribute:"no-expand"},"hide-when-collapsed":{type:Boolean,attribute:"hide-when-collapsed"}};constructor(){super(),this.icon="",this.label="",this.open=!1,this.collapsed=!1,this.side="left",this["no-expand"]=!1,this["hide-when-collapsed"]=!1}connectedCallback(){super.connectedCallback(),this.aside=this.closest("k-aside"),this.aside&&(this.collapsed="collapsed"===this.aside.state,this.side=this.aside.side||"left",this.aside.addEventListener("aside_state_change",this.handleStateChange))}disconnectedCallback(){super.disconnectedCallback(),this.aside&&this.aside.removeEventListener("aside_state_change",this.handleStateChange)}handleStateChange=e=>{const{state:t}=e.detail;"collapsed"===t?(this.collapsed=!0,this.open=!1):"expanded"===t&&(this.collapsed=!1),this.side=this.aside?.side||"left"};toggleMenu=()=>{this.collapsed||(this.open=!this.open)};handleCollapsedClick=()=>{this.open=!0,this.aside?.expand()};render(){if(this.collapsed){const e="right"===this.side?"left down":"right down";return t`
212
+ <k-dropdown open-direction=${e} hover>
213
+ <button slot="trigger" class="no-btn collapsed-trigger" @click=${this.handleCollapsedClick}>
214
+ ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:t`<k-icon name="dot"></k-icon>`}
215
+ </button>
216
+ <slot></slot>
217
+ </k-dropdown>
218
+ `}return t`
212
219
  <div class="menu-container">
213
220
  <button class="no-btn menu-header ${this.open?"open":""}" @click=${this.toggleMenu}>
214
- ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:this.collapsed?t`<k-icon name="dot"></k-icon>`:a}
215
- ${this.collapsed?a:t`<span class="label">${this.label}</span>`}
216
- ${this.collapsed?a:t`<k-icon class="chevron" name="chevron" direction="${this.open?"down":"right"}"></k-icon>`}
221
+ ${this.icon?t`<k-icon name="${this.icon}"></k-icon>`:a}
222
+ <span class="label">${this.label}</span>
223
+ <k-icon class="chevron" name="chevron" direction="${this.open?"down":"right"}"></k-icon>
217
224
  </button>
218
- <div class="menu-content ${this.open&&!this.collapsed?"open":""}">
225
+ <div class="menu-content ${this.open?"open":""}">
219
226
  <slot></slot>
220
227
  </div>
221
228
  </div>
@@ -242,12 +249,29 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
242
249
  white-space: nowrap;
243
250
  transition: background var(--animation_ms);
244
251
  }
245
- :host([collapsed]) .menu-header {
246
- width: auto;
247
- }
248
252
  .menu-header:hover {
249
253
  background: var(--c_bg_hover);
250
254
  }
255
+ .collapsed-trigger {
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ padding: var(--spacer_h);
260
+ color: var(--tc);
261
+ background: transparent;
262
+ border: none;
263
+ appearance: none;
264
+ border-radius: var(--radius);
265
+ margin: 0 var(--spacer_h);
266
+ cursor: pointer;
267
+ transition: background var(--animation_ms);
268
+ }
269
+ .collapsed-trigger:hover {
270
+ background: var(--c_bg_hover);
271
+ }
272
+ k-dropdown {
273
+ display: block;
274
+ }
251
275
  k-icon {
252
276
  flex-shrink: 0;
253
277
  }
@@ -266,9 +290,6 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
266
290
  transition: max-height var(--animation_ms);
267
291
  padding-left: calc(var(--spacer_h) * 2);
268
292
  }
269
- :host([collapsed]) .menu-content {
270
- padding-left: 0;
271
- }
272
293
  .menu-content.open {
273
294
  max-height: 500px;
274
295
  }
@@ -313,6 +334,7 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
313
334
  width: 2rem;
314
335
  height: 2rem;
315
336
  border: none;
337
+ appearance: none;
316
338
  background: transparent;
317
339
  color: var(--c_text);
318
340
  cursor: pointer;
@@ -325,4 +347,4 @@ import e from"./ShadowComponent.js";import{html as t,css as s,nothing as a}from"
325
347
  #toggle:hover {
326
348
  background: var(--c_bg_hover);
327
349
  }
328
- `}customElements.define("k-aside-item",n),customElements.define("k-aside-label",d),customElements.define("k-aside-menu",r),customElements.define("k-aside-spacer",l),customElements.define("k-aside-toggle",c);
350
+ `}customElements.define("k-aside-item",o),customElements.define("k-aside-label",r),customElements.define("k-aside-menu",d),customElements.define("k-aside-spacer",l),customElements.define("k-aside-toggle",c);
@@ -1,4 +1,4 @@
1
- import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponent.js";import{boolTrueFalse as n}from"../utils/propConverters.js";const i=new Set;export default class s extends o{static properties={opened:{type:Boolean,reflect:!0},openDirection:{type:String,reflect:!0,attribute:"open-direction"},closeOnSelect:{type:Boolean,reflect:!0,attribute:"close-on-select",converter:n},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:n},submenu:{type:Boolean,reflect:!0}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0,this.submenu=!1}connectedCallback(){super.connectedCallback(),"K-DROPDOWN"!==this.parentElement?.tagName||this.hasAttribute("slot")||(this.submenu=!0,this.hasAttribute("open-direction")||(this.openDirection="right down")),this.submenu?(this.addEventListener("mouseenter",this.handleSubmenuEnter),this.addEventListener("mouseleave",this.handleSubmenuLeave)):document.addEventListener("click",this.handleDocumentClick),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),this.submenu?(this.removeEventListener("mouseenter",this.handleSubmenuEnter),this.removeEventListener("mouseleave",this.handleSubmenuLeave)):document.removeEventListener("click",this.handleDocumentClick),document.removeEventListener("keydown",this.handleKeydown),i.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?i.add(this):i.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.composedPath(),o=t.find(e=>e.matches?.('[slot="trigger"]'));if(o){const e=o.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!t.includes(this))return void(this.closeOnClickOutside&&this.close());const n=t.find(e=>e.matches?.("a, button"));n&&!n.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{e.stopPropagation(),this.opened||i.forEach(e=>{e!==this&&e.close()}),this.toggle()};handleMenuClick=e=>{const t=e.target.closest("a, button");if(!t||t.hasAttribute("disabled"))return;if(t.closest("k-dropdown")!==this)return;const o=t.dataset?.value||t.textContent.trim();this.dispatchEvent(new CustomEvent("select",{detail:{value:o,item:t},bubbles:!0}))};handleKeydown=e=>{if(!this.opened)return;const t=document.activeElement;if(!this.contains(t))return;const o=this.querySelector(":scope > k-dropdown[opened]");if(!o?.contains(t))if("Escape"===e.key||this.submenu&&"ArrowLeft"===e.key)e.preventDefault(),this.close(),this.focusTrigger();else if("ArrowDown"===e.key)e.preventDefault(),this.focusNextItem();else if("ArrowUp"===e.key)e.preventDefault(),this.focusPreviousItem();else if("ArrowRight"===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this&&(e.preventDefault(),o.open(),o.focusFirstItem())}else if("Enter"===e.key||" "===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this?(e.preventDefault(),o.open(),o.focusFirstItem()):t&&!t.closest('[slot="trigger"]')&&(e.preventDefault(),t.click())}};open(){return this.submenu?[...this.parentElement.querySelectorAll(":scope > k-dropdown[opened]")].forEach(e=>{e!==this&&e.close()}):i.forEach(e=>{e!==this&&e.close()}),this.opened=!0,this.submenu||requestAnimationFrame(()=>this.focusFirstItem()),this}close(){return this.querySelectorAll(":scope > k-dropdown[opened]").forEach(e=>e.close()),this.opened=!1,this}toggle(){return this.opened?this.close():this.open()}focusTrigger(){const e=this.querySelector('[slot="trigger"]');e&&e.focus()}getMenuItems(){return[...this.children].reduce((e,t)=>{if(t.matches('[slot="trigger"]')||t.hasAttribute("disabled"))return e;if("K-DROPDOWN"===t.tagName){const o=t.querySelector('[slot="trigger"]');o&&e.push(o)}else t.matches("a, button")&&e.push(t);return e},[])}focusFirstItem(){const e=this.getMenuItems();e.length>0&&e[0].focus()}focusNextItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o+1]||e[0];n&&n.focus()}focusPreviousItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o-1]||e[e.length-1];n&&n.focus()}static styles=t`
1
+ import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponent.js";import{boolTrueFalse as n}from"../utils/propConverters.js";const i=new Set;export default class s extends o{static properties={opened:{type:Boolean,reflect:!0},openDirection:{type:String,reflect:!0,attribute:"open-direction"},closeOnSelect:{type:Boolean,reflect:!0,attribute:"close-on-select",converter:n},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:n},submenu:{type:Boolean,reflect:!0},hover:{type:Boolean,reflect:!0}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0,this.submenu=!1,this.hover=!1}connectedCallback(){super.connectedCallback(),"K-DROPDOWN"!==this.parentElement?.tagName||this.hasAttribute("slot")||(this.submenu=!0,this.hasAttribute("open-direction")||(this.openDirection="right down")),this.submenu?(this.addEventListener("mouseenter",this.handleSubmenuEnter),this.addEventListener("mouseleave",this.handleSubmenuLeave)):(document.addEventListener("click",this.handleDocumentClick),this.hover&&(this.addEventListener("mouseenter",this.handleHoverEnter),this.addEventListener("mouseleave",this.handleHoverLeave))),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),this.submenu?(this.removeEventListener("mouseenter",this.handleSubmenuEnter),this.removeEventListener("mouseleave",this.handleSubmenuLeave)):(document.removeEventListener("click",this.handleDocumentClick),this.removeEventListener("mouseenter",this.handleHoverEnter),this.removeEventListener("mouseleave",this.handleHoverLeave)),document.removeEventListener("keydown",this.handleKeydown),i.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?i.add(this):i.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.composedPath(),o=t.find(e=>e.matches?.('[slot="trigger"]'));if(o){const e=o.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!t.includes(this))return void(this.closeOnClickOutside&&this.close());const n=t.find(e=>e.matches?.("a, button"));n&&!n.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{this.hover||(e.stopPropagation(),this.opened||i.forEach(e=>{e!==this&&e.close()}),this.toggle())};handleMenuClick=e=>{const t=e.target.closest("a, button");if(!t||t.hasAttribute("disabled"))return;if(t.closest("k-dropdown")!==this)return;const o=t.dataset?.value||t.textContent.trim();this.dispatchEvent(new CustomEvent("select",{detail:{value:o,item:t},bubbles:!0}))};handleKeydown=e=>{if(!this.opened)return;const t=document.activeElement;if(!this.contains(t))return;const o=this.querySelector(":scope > k-dropdown[opened]");if(!o?.contains(t))if("Escape"===e.key||this.submenu&&"ArrowLeft"===e.key)e.preventDefault(),this.close(),this.focusTrigger();else if("ArrowDown"===e.key)e.preventDefault(),this.focusNextItem();else if("ArrowUp"===e.key)e.preventDefault(),this.focusPreviousItem();else if("ArrowRight"===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this&&(e.preventDefault(),o.open(),o.focusFirstItem())}else if("Enter"===e.key||" "===e.key){const o=t?.closest("k-dropdown[submenu]");o?.parentElement===this?(e.preventDefault(),o.open(),o.focusFirstItem()):t&&!t.closest('[slot="trigger"]')&&(e.preventDefault(),t.click())}};open(){return this.submenu?[...this.parentElement.querySelectorAll(":scope > k-dropdown[opened]")].forEach(e=>{e!==this&&e.close()}):i.forEach(e=>{e!==this&&e.close()}),this.opened=!0,this.submenu||requestAnimationFrame(()=>this.focusFirstItem()),this}close(){return this.querySelectorAll(":scope > k-dropdown[opened]").forEach(e=>e.close()),this.opened=!1,this}toggle(){return this.opened?this.close():this.open()}focusTrigger(){const e=this.querySelector('[slot="trigger"]');e&&e.focus()}getMenuItems(){return[...this.children].reduce((e,t)=>{if(t.matches('[slot="trigger"]')||t.hasAttribute("disabled"))return e;if("K-DROPDOWN"===t.tagName){const o=t.querySelector('[slot="trigger"]');o&&e.push(o)}else t.matches("a, button")&&e.push(t);return e},[])}focusFirstItem(){const e=this.getMenuItems();e.length>0&&e[0].focus()}focusNextItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o+1]||e[0];n&&n.focus()}focusPreviousItem(){const e=this.getMenuItems(),t=document.activeElement,o=e.indexOf(t),n=e[o-1]||e[e.length-1];n&&n.focus()}static styles=t`
2
2
  :host {
3
3
  display: inline-block;
4
4
  position: relative;
@@ -160,7 +160,7 @@ import{html as e,css as t}from"../lit-all.min.js";import o from"./ShadowComponen
160
160
  font-size: 0.75em;
161
161
  opacity: 0.6;
162
162
  }
163
- `;handleSubmenuEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleSubmenuLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSlotChange=e=>{e.target.assignedElements().filter(e=>e.matches("a, button, k-dropdown")).forEach((e,t)=>e.classList.toggle("k-dropdown-first",0===t))};render(){return e`
163
+ `;handleHoverEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleHoverLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSubmenuEnter=()=>{clearTimeout(this.closeTimer),this.opened||this.open()};handleSubmenuLeave=()=>{this.closeTimer=setTimeout(()=>this.close(),150)};handleSlotChange=e=>{e.target.assignedElements().filter(e=>e.matches("a, button, k-dropdown")).forEach((e,t)=>e.classList.toggle("k-dropdown-first",0===t))};render(){return e`
164
164
  <div id="trigger" @click=${this.handleTriggerClick}>
165
165
  <slot name="trigger"></slot>
166
166
  ${this.submenu?e`<k-icon name="chevron"></k-icon>`:""}
@@ -12,5 +12,15 @@ export default {
12
12
  templating: {
13
13
  ssr: true,
14
14
  ssrPriority: true
15
+ },
16
+ "middleware": {
17
+ "security": {
18
+ "enabled": true,
19
+ "headers": {
20
+ "X-Content-Type-Options": "nosniff",
21
+ "X-Frame-Options": "SAMEORIGIN",
22
+ "X-XSS-Protection": "1; mode=block"
23
+ }
24
+ }
15
25
  }
16
26
  };
@@ -15,6 +15,7 @@
15
15
  <a href="#disabledItems">Disabled Items</a><br />
16
16
  <a href="#links">Using Links</a><br />
17
17
  <a href="#events">Events</a><br />
18
+ <a href="#hover">Hover</a><br />
18
19
 
19
20
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
20
21
  <a href="#constructor">Constructor</a><br />
@@ -225,6 +226,26 @@
225
226
  </div>
226
227
  </div>
227
228
 
229
+ <h3 id="hover"><a href="#hover" class="no-link">Hover</a></h3>
230
+ <p>Add the <code>hover</code> attribute to open the menu when the trigger is hovered instead of clicked. The menu closes automatically 150ms after the cursor leaves the component.</p>
231
+ <div class="row -mx">
232
+ <div class="col m-span-12 px">
233
+ <k-card label="HTML">
234
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">k-dropdown</span> <span class="hljs-attr">hover</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">&#x27;trigger&#x27;</span>&gt;</span>Hover Me<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;edit&#x27;</span>&gt;</span>Edit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;copy&#x27;</span>&gt;</span>Copy<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">&#x27;delete&#x27;</span>&gt;</span>Delete<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">k-dropdown</span>&gt;</span></code></pre>
235
+ </k-card>
236
+ </div>
237
+ <div class="col m-span-12 px">
238
+ <k-card label="Output">
239
+ <k-dropdown hover>
240
+ <button slot="trigger">Hover Me</button>
241
+ <button data-value="edit">Edit</button>
242
+ <button data-value="copy">Copy</button>
243
+ <button data-value="delete">Delete</button>
244
+ </k-dropdown>
245
+ </k-card>
246
+ </div>
247
+ </div>
248
+
228
249
  <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
229
250
 
230
251
  <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
@@ -253,6 +274,9 @@
253
274
  <h5><code>submenu<i>: boolean</i></code></h5>
254
275
  <p>Automatically set to <code>true</code> when a <code>k-dropdown</code> is nested inside another <code>k-dropdown</code>. Sub menus open on hover/focus, display a chevron icon, and default to <code>open-direction="right down"</code>. Supports <code>ArrowRight</code> to open and <code>ArrowLeft</code> to close via keyboard.</p>
255
276
 
277
+ <h5><code>hover<i>: boolean</i></code></h5>
278
+ <p>When present, the menu opens on <code>mouseenter</code> instead of trigger click. The menu closes 150ms after the cursor leaves the component. Default is <code>false</code>.</p>
279
+
256
280
  <h3 id="methods"><a href="#methods" class="no-link">Methods</a></h3>
257
281
  <h5><code>open()<i>: this</i></code></h5>
258
282
  <p>Opens the dropdown menu and focuses the first item.</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -1,6 +1,7 @@
1
1
  import ShadowComponent from './ShadowComponent.js';
2
2
  import { html, css, nothing } from '../lit-all.min.js';
3
3
  import { boolTrueFalse } from '../utils/propConverters.js';
4
+ import './Dropdown.js';
4
5
  import './FocusCapture.js';
5
6
  import './Icon.js';
6
7
 
@@ -240,10 +241,10 @@ export default class Aside extends ShadowComponent {
240
241
  }
241
242
  :host([main="push"][state="collapsed"]) aside {
242
243
  width: var(--collapsed-width);
243
- transform: translateX(0);
244
+ transform: none;
244
245
  }
245
246
  :host([main="push"][state="expanded"]) aside {
246
- transform: translateX(0);
247
+ transform: none;
247
248
  }
248
249
  :host([main="overlay"]) aside {
249
250
  width: var(--width);
@@ -252,13 +253,13 @@ export default class Aside extends ShadowComponent {
252
253
  transform: translateX(-100%);
253
254
  }
254
255
  :host([main="overlay"][state="expanded"]) aside {
255
- transform: translateX(0);
256
+ transform: none;
256
257
  }
257
258
  :host([main="overlay"][side="right"]) aside {
258
259
  transform: translateX(100%);
259
260
  }
260
261
  :host([main="overlay"][side="right"][state="expanded"]) aside {
261
- transform: translateX(0);
262
+ transform: none;
262
263
  }
263
264
  `;
264
265
  }
@@ -294,7 +295,8 @@ class AsideItem extends ShadowComponent {
294
295
  connectedCallback() {
295
296
  super.connectedCallback();
296
297
  this.aside = this.closest('k-aside');
297
- if(this.aside) {
298
+ this.inMenu = !!this.closest('k-aside-menu');
299
+ if(this.aside && !this.inMenu) {
298
300
  this.collapsed = this.aside.state === 'collapsed';
299
301
  this.aside.addEventListener('aside_state_change', this.handleStateChange);
300
302
  }
@@ -466,6 +468,7 @@ class AsideMenu extends ShadowComponent {
466
468
  label: { type: String },
467
469
  open: { type: Boolean, reflect: true },
468
470
  collapsed: { type: Boolean, reflect: true },
471
+ side: { type: String },
469
472
  'no-expand': { type: Boolean, attribute: 'no-expand' },
470
473
  'hide-when-collapsed': { type: Boolean, attribute: 'hide-when-collapsed' }
471
474
  };
@@ -476,6 +479,7 @@ class AsideMenu extends ShadowComponent {
476
479
  this.label = '';
477
480
  this.open = false;
478
481
  this.collapsed = false;
482
+ this.side = 'left';
479
483
  this['no-expand'] = false;
480
484
  this['hide-when-collapsed'] = false;
481
485
  }
@@ -488,6 +492,7 @@ class AsideMenu extends ShadowComponent {
488
492
  this.aside = this.closest('k-aside');
489
493
  if(this.aside) {
490
494
  this.collapsed = this.aside.state === 'collapsed';
495
+ this.side = this.aside.side || 'left';
491
496
  this.aside.addEventListener('aside_state_change', this.handleStateChange);
492
497
  }
493
498
  }
@@ -510,28 +515,41 @@ class AsideMenu extends ShadowComponent {
510
515
  } else if(state === 'expanded') {
511
516
  this.collapsed = false;
512
517
  }
518
+ this.side = this.aside?.side || 'left';
513
519
  };
514
520
 
515
521
  toggleMenu = () => {
516
- if(this.collapsed && !this['no-expand'] && this.aside) {
517
- this.aside.expand();
518
- } else if(!this.collapsed) {
519
- this.open = !this.open;
520
- }
522
+ if(!this.collapsed) this.open = !this.open;
523
+ };
524
+
525
+ handleCollapsedClick = () => {
526
+ this.open = true;
527
+ this.aside?.expand();
521
528
  };
522
529
 
523
530
  /*
524
531
  Rendering
525
532
  */
526
533
  render() {
534
+ if(this.collapsed) {
535
+ const openDirection = this.side === 'right' ? 'left down' : 'right down';
536
+ return html`
537
+ <k-dropdown open-direction=${openDirection} hover>
538
+ <button slot="trigger" class="no-btn collapsed-trigger" @click=${this.handleCollapsedClick}>
539
+ ${this.icon ? html`<k-icon name="${this.icon}"></k-icon>` : html`<k-icon name="dot"></k-icon>`}
540
+ </button>
541
+ <slot></slot>
542
+ </k-dropdown>
543
+ `;
544
+ }
527
545
  return html`
528
546
  <div class="menu-container">
529
547
  <button class="no-btn menu-header ${this.open ? 'open' : ''}" @click=${this.toggleMenu}>
530
- ${this.icon ? html`<k-icon name="${this.icon}"></k-icon>` : this.collapsed ? html`<k-icon name="dot"></k-icon>` : nothing}
531
- ${this.collapsed ? nothing : html`<span class="label">${this.label}</span>`}
532
- ${!this.collapsed ? html`<k-icon class="chevron" name="chevron" direction="${this.open ? 'down' : 'right'}"></k-icon>` : nothing}
548
+ ${this.icon ? html`<k-icon name="${this.icon}"></k-icon>` : nothing}
549
+ <span class="label">${this.label}</span>
550
+ <k-icon class="chevron" name="chevron" direction="${this.open ? 'down' : 'right'}"></k-icon>
533
551
  </button>
534
- <div class="menu-content ${this.open && !this.collapsed ? 'open' : ''}">
552
+ <div class="menu-content ${this.open ? 'open' : ''}">
535
553
  <slot></slot>
536
554
  </div>
537
555
  </div>
@@ -564,12 +582,29 @@ class AsideMenu extends ShadowComponent {
564
582
  white-space: nowrap;
565
583
  transition: background var(--animation_ms);
566
584
  }
567
- :host([collapsed]) .menu-header {
568
- width: auto;
569
- }
570
585
  .menu-header:hover {
571
586
  background: var(--c_bg_hover);
572
587
  }
588
+ .collapsed-trigger {
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ padding: var(--spacer_h);
593
+ color: var(--tc);
594
+ background: transparent;
595
+ border: none;
596
+ appearance: none;
597
+ border-radius: var(--radius);
598
+ margin: 0 var(--spacer_h);
599
+ cursor: pointer;
600
+ transition: background var(--animation_ms);
601
+ }
602
+ .collapsed-trigger:hover {
603
+ background: var(--c_bg_hover);
604
+ }
605
+ k-dropdown {
606
+ display: block;
607
+ }
573
608
  k-icon {
574
609
  flex-shrink: 0;
575
610
  }
@@ -588,9 +623,6 @@ class AsideMenu extends ShadowComponent {
588
623
  transition: max-height var(--animation_ms);
589
624
  padding-left: calc(var(--spacer_h) * 2);
590
625
  }
591
- :host([collapsed]) .menu-content {
592
- padding-left: 0;
593
- }
594
626
  .menu-content.open {
595
627
  max-height: 500px;
596
628
  }
@@ -717,6 +749,7 @@ class AsideToggle extends ShadowComponent {
717
749
  width: 2rem;
718
750
  height: 2rem;
719
751
  border: none;
752
+ appearance: none;
720
753
  background: transparent;
721
754
  color: var(--c_text);
722
755
  cursor: pointer;
@@ -11,7 +11,8 @@ export default class Dropdown extends ShadowComponent {
11
11
  openDirection: { type: String, reflect: true, attribute: 'open-direction' },
12
12
  closeOnSelect: { type: Boolean, reflect: true, attribute: 'close-on-select', converter: boolTrueFalse },
13
13
  closeOnClickOutside: { type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: boolTrueFalse },
14
- submenu: { type: Boolean, reflect: true }
14
+ submenu: { type: Boolean, reflect: true },
15
+ hover: { type: Boolean, reflect: true }
15
16
  };
16
17
 
17
18
  constructor() {
@@ -21,6 +22,7 @@ export default class Dropdown extends ShadowComponent {
21
22
  this.closeOnSelect = true;
22
23
  this.closeOnClickOutside = true;
23
24
  this.submenu = false;
25
+ this.hover = false;
24
26
  }
25
27
 
26
28
  /*
@@ -37,6 +39,10 @@ export default class Dropdown extends ShadowComponent {
37
39
  this.addEventListener('mouseleave', this.handleSubmenuLeave);
38
40
  } else {
39
41
  document.addEventListener('click', this.handleDocumentClick);
42
+ if(this.hover) {
43
+ this.addEventListener('mouseenter', this.handleHoverEnter);
44
+ this.addEventListener('mouseleave', this.handleHoverLeave);
45
+ }
40
46
  }
41
47
  document.addEventListener('keydown', this.handleKeydown);
42
48
  }
@@ -48,6 +54,8 @@ export default class Dropdown extends ShadowComponent {
48
54
  this.removeEventListener('mouseleave', this.handleSubmenuLeave);
49
55
  } else {
50
56
  document.removeEventListener('click', this.handleDocumentClick);
57
+ this.removeEventListener('mouseenter', this.handleHoverEnter);
58
+ this.removeEventListener('mouseleave', this.handleHoverLeave);
51
59
  }
52
60
  document.removeEventListener('keydown', this.handleKeydown);
53
61
  openDropdowns.delete(this);
@@ -99,6 +107,7 @@ export default class Dropdown extends ShadowComponent {
99
107
  };
100
108
 
101
109
  handleTriggerClick = e => {
110
+ if(this.hover) return;
102
111
  e.stopPropagation();
103
112
  // Close all other dropdowns before opening this one
104
113
  if(!this.opened) {
@@ -396,6 +405,15 @@ export default class Dropdown extends ShadowComponent {
396
405
  }
397
406
  `;
398
407
 
408
+ handleHoverEnter = () => {
409
+ clearTimeout(this.closeTimer);
410
+ if(!this.opened) this.open();
411
+ };
412
+
413
+ handleHoverLeave = () => {
414
+ this.closeTimer = setTimeout(() => this.close(), 150);
415
+ };
416
+
399
417
  /*
400
418
  Slot Change Handler
401
419
  */
@@ -814,6 +814,147 @@ export default {
814
814
  pass('Clicking collapsed menu expands aside');
815
815
  },
816
816
 
817
+ 'AsideMenu: clicking collapsed menu should also set open to true': async ({pass, fail}) => {
818
+ const aside = await createAside({ main: 'push', state: 'collapsed' });
819
+ const menu = document.createElement('k-aside-menu');
820
+ menu.icon = 'folder';
821
+ menu.label = 'Projects';
822
+ aside.appendChild(menu);
823
+ await menu.updateComplete;
824
+ const btn = menu.shadowRoot.querySelector('button');
825
+ btn.click();
826
+ await menu.updateComplete;
827
+ if(!menu.open) {
828
+ cleanup(aside);
829
+ return fail('Menu open should be true after clicking collapsed trigger');
830
+ }
831
+ cleanup(aside);
832
+ pass('Clicking collapsed trigger sets menu open to true');
833
+ },
834
+
835
+ 'AsideMenu: collapsed state should render k-dropdown': async ({pass, fail}) => {
836
+ const aside = await createAside({ main: 'push', state: 'collapsed' });
837
+ const menu = document.createElement('k-aside-menu');
838
+ menu.icon = 'folder';
839
+ menu.label = 'Projects';
840
+ aside.appendChild(menu);
841
+ await menu.updateComplete;
842
+ const dropdown = menu.shadowRoot.querySelector('k-dropdown');
843
+ if(!dropdown) {
844
+ cleanup(aside);
845
+ return fail('Collapsed menu should render k-dropdown');
846
+ }
847
+ cleanup(aside);
848
+ pass('Collapsed menu renders k-dropdown');
849
+ },
850
+
851
+ 'AsideMenu: collapsed k-dropdown should have hover attribute': async ({pass, fail}) => {
852
+ const aside = await createAside({ main: 'push', state: 'collapsed' });
853
+ const menu = document.createElement('k-aside-menu');
854
+ menu.icon = 'folder';
855
+ menu.label = 'Projects';
856
+ aside.appendChild(menu);
857
+ await menu.updateComplete;
858
+ const dropdown = menu.shadowRoot.querySelector('k-dropdown');
859
+ if(!dropdown || !dropdown.hasAttribute('hover')) {
860
+ cleanup(aside);
861
+ return fail('Collapsed k-dropdown should have hover attribute');
862
+ }
863
+ cleanup(aside);
864
+ pass('Collapsed k-dropdown has hover attribute');
865
+ },
866
+
867
+ 'AsideMenu: collapsed k-dropdown open-direction should be right down for left aside': async ({pass, fail}) => {
868
+ const aside = await createAside({ main: 'push', state: 'collapsed', side: 'left' });
869
+ const menu = document.createElement('k-aside-menu');
870
+ menu.icon = 'folder';
871
+ aside.appendChild(menu);
872
+ await menu.updateComplete;
873
+ const dropdown = menu.shadowRoot.querySelector('k-dropdown');
874
+ if(!dropdown || dropdown.getAttribute('open-direction') !== 'right down') {
875
+ cleanup(aside);
876
+ return fail(`Expected open-direction "right down", got "${dropdown?.getAttribute('open-direction')}"`);
877
+ }
878
+ cleanup(aside);
879
+ pass('Left aside collapsed menu opens right down');
880
+ },
881
+
882
+ 'AsideMenu: collapsed k-dropdown open-direction should be left down for right aside': async ({pass, fail}) => {
883
+ const aside = await createAside({ main: 'push', state: 'collapsed', side: 'right' });
884
+ const menu = document.createElement('k-aside-menu');
885
+ menu.icon = 'folder';
886
+ aside.appendChild(menu);
887
+ await menu.updateComplete;
888
+ const dropdown = menu.shadowRoot.querySelector('k-dropdown');
889
+ if(!dropdown || dropdown.getAttribute('open-direction') !== 'left down') {
890
+ cleanup(aside);
891
+ return fail(`Expected open-direction "left down", got "${dropdown?.getAttribute('open-direction')}"`);
892
+ }
893
+ cleanup(aside);
894
+ pass('Right aside collapsed menu opens left down');
895
+ },
896
+
897
+ 'AsideMenu: should not render k-dropdown when expanded': async ({pass, fail}) => {
898
+ const aside = await createAside({ main: 'push', state: 'expanded' });
899
+ const menu = document.createElement('k-aside-menu');
900
+ menu.icon = 'folder';
901
+ menu.label = 'Projects';
902
+ aside.appendChild(menu);
903
+ await menu.updateComplete;
904
+ const dropdown = menu.shadowRoot.querySelector('k-dropdown');
905
+ if(dropdown) {
906
+ cleanup(aside);
907
+ return fail('Expanded menu should not render k-dropdown');
908
+ }
909
+ cleanup(aside);
910
+ pass('Expanded menu does not render k-dropdown');
911
+ },
912
+
913
+ 'AsideItem: inside AsideMenu should not sync collapsed state from aside': async ({pass, fail}) => {
914
+ const aside = await createAside({ main: 'push', state: 'expanded' });
915
+ const menu = document.createElement('k-aside-menu');
916
+ menu.icon = 'folder';
917
+ menu.label = 'Projects';
918
+ const item = document.createElement('k-aside-item');
919
+ item.icon = 'cards';
920
+ item.textContent = 'Dashboard';
921
+ menu.appendChild(item);
922
+ aside.appendChild(menu);
923
+ await item.updateComplete;
924
+ aside.collapse();
925
+ await aside.updateComplete;
926
+ await item.updateComplete;
927
+ if(item.collapsed) {
928
+ cleanup(aside);
929
+ return fail('AsideItem inside AsideMenu should not become collapsed');
930
+ }
931
+ cleanup(aside);
932
+ pass('AsideItem inside AsideMenu ignores aside collapsed state');
933
+ },
934
+
935
+ 'AsideItem: inside AsideMenu should render label (not dot) when aside is collapsed': async ({pass, fail}) => {
936
+ const aside = await createAside({ main: 'push', state: 'expanded' });
937
+ const menu = document.createElement('k-aside-menu');
938
+ menu.icon = 'folder';
939
+ menu.label = 'Projects';
940
+ const item = document.createElement('k-aside-item');
941
+ item.icon = 'cards';
942
+ item.textContent = 'Dashboard';
943
+ menu.appendChild(item);
944
+ aside.appendChild(menu);
945
+ await item.updateComplete;
946
+ aside.collapse();
947
+ await aside.updateComplete;
948
+ await item.updateComplete;
949
+ const label = item.shadowRoot.querySelector('.label');
950
+ if(!label) {
951
+ cleanup(aside);
952
+ return fail('AsideItem inside AsideMenu should still render label when aside is collapsed');
953
+ }
954
+ cleanup(aside);
955
+ pass('AsideItem inside AsideMenu renders label regardless of aside state');
956
+ },
957
+
817
958
  /*
818
959
  AsideSpacer
819
960
  */
@@ -8,6 +8,7 @@ const createDropdown = async (options = {}) => {
8
8
  ${options.opened ? 'opened' : ''}
9
9
  ${options.closeOnSelect === false ? 'close-on-select="false"' : ''}
10
10
  ${options.closeOnClickOutside === false ? 'close-on-click-outside="false"' : ''}
11
+ ${options.hover ? 'hover' : ''}
11
12
  >
12
13
  <button slot="trigger">Trigger</button>
13
14
  <button data-value="item1">Item 1</button>
@@ -661,5 +662,65 @@ export default {
661
662
  }
662
663
  cleanup(container);
663
664
  pass('Dropdown does not inflate in pre-wrap context');
665
+ },
666
+
667
+ /*
668
+ Hover Mode
669
+ */
670
+ 'hover should be false by default': async ({pass, fail}) => {
671
+ const { container, dropdown } = await createDropdown();
672
+ if(dropdown.hover !== false) {
673
+ cleanup(container);
674
+ return fail(`Expected hover to be false, got ${dropdown.hover}`);
675
+ }
676
+ cleanup(container);
677
+ pass('hover is false by default');
678
+ },
679
+
680
+ 'hover attribute should reflect': async ({pass, fail}) => {
681
+ const { container, dropdown } = await createDropdown({ hover: true });
682
+ if(!dropdown.hasAttribute('hover')) {
683
+ cleanup(container);
684
+ return fail('hover attribute should be reflected');
685
+ }
686
+ cleanup(container);
687
+ pass('hover attribute reflects correctly');
688
+ },
689
+
690
+ 'should open on mouseenter when hover is true': async ({pass, fail}) => {
691
+ const { container, dropdown } = await createDropdown({ hover: true });
692
+ dropdown.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
693
+ await dropdown.updateComplete;
694
+ if(!dropdown.opened) {
695
+ cleanup(container);
696
+ return fail('Dropdown should open on mouseenter in hover mode');
697
+ }
698
+ cleanup(container);
699
+ pass('Dropdown opens on mouseenter in hover mode');
700
+ },
701
+
702
+ 'should close on mouseleave when hover is true': async ({pass, fail}) => {
703
+ const { container, dropdown } = await createDropdown({ hover: true, opened: true });
704
+ dropdown.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
705
+ await new Promise(r => setTimeout(r, 200));
706
+ if(dropdown.opened) {
707
+ cleanup(container);
708
+ return fail('Dropdown should close after mouseleave in hover mode');
709
+ }
710
+ cleanup(container);
711
+ pass('Dropdown closes on mouseleave in hover mode');
712
+ },
713
+
714
+ 'trigger click should not toggle in hover mode': async ({pass, fail}) => {
715
+ const { container, dropdown } = await createDropdown({ hover: true });
716
+ const trigger = dropdown.querySelector('[slot="trigger"]');
717
+ trigger.click();
718
+ await dropdown.updateComplete;
719
+ if(dropdown.opened) {
720
+ cleanup(container);
721
+ return fail('Trigger click should not open dropdown in hover mode');
722
+ }
723
+ cleanup(container);
724
+ pass('Trigger click does not toggle in hover mode');
664
725
  }
665
726
  };