kempo-ui 0.1.1 → 0.1.2
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{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{boolTrueFalse}from"../utils/propConverters.js";const openDropdowns=new Set;export default class Dropdown extends ShadowComponent{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:boolTrueFalse},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:boolTrueFalse}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick),document.removeEventListener("keydown",this.handleKeydown),openDropdowns.delete(this)}updated(
|
|
1
|
+
import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{boolTrueFalse}from"../utils/propConverters.js";const openDropdowns=new Set;export default class Dropdown extends ShadowComponent{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:boolTrueFalse},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:boolTrueFalse},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),openDropdowns.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?openDropdowns.add(this):openDropdowns.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.target.closest('[slot="trigger"]');if(t){const e=t.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!this.contains(e.target))return void(this.closeOnClickOutside&&this.close());const o=e.target.closest("a, button");o&&!o.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{e.stopPropagation(),this.opened||openDropdowns.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()}):openDropdowns.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=css`
|
|
2
2
|
:host {
|
|
3
3
|
display: inline-block;
|
|
4
4
|
position: relative;
|
|
@@ -23,6 +23,9 @@ import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComp
|
|
|
23
23
|
:host([opened]) #menu {
|
|
24
24
|
display: block;
|
|
25
25
|
}
|
|
26
|
+
:host([submenu]) #menu {
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
26
29
|
/* Default: down left */
|
|
27
30
|
#menu {
|
|
28
31
|
position-area: bottom span-right;
|
|
@@ -115,9 +118,49 @@ import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComp
|
|
|
115
118
|
::slotted(hr) {
|
|
116
119
|
display: none !important;
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
/* Slotted submenu dropdowns */
|
|
122
|
+
::slotted(k-dropdown) {
|
|
123
|
+
display: block !important;
|
|
124
|
+
width: 100% !important;
|
|
125
|
+
border-top: 1px solid var(--c_border) !important;
|
|
126
|
+
}
|
|
127
|
+
::slotted(k-dropdown.k-dropdown-first) {
|
|
128
|
+
border-top: none !important;
|
|
129
|
+
}
|
|
130
|
+
/* Submenu host styles */
|
|
131
|
+
:host([submenu]) {
|
|
132
|
+
display: block;
|
|
133
|
+
position: relative;
|
|
134
|
+
}
|
|
135
|
+
:host([submenu]) #trigger {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 1rem;
|
|
140
|
+
padding: var(--spacer_h) var(--spacer);
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
transition: background var(--animation_ms);
|
|
144
|
+
}
|
|
145
|
+
:host([submenu]) #trigger:hover,
|
|
146
|
+
:host([submenu]) #trigger:focus-within {
|
|
147
|
+
background: var(--c_bg__alt);
|
|
148
|
+
}
|
|
149
|
+
:host([submenu]) #trigger ::slotted(button),
|
|
150
|
+
:host([submenu]) #trigger ::slotted(a) {
|
|
151
|
+
all: unset !important;
|
|
152
|
+
cursor: pointer !important;
|
|
153
|
+
font: inherit !important;
|
|
154
|
+
color: var(--tc) !important;
|
|
155
|
+
}
|
|
156
|
+
:host([submenu]) #trigger k-icon {
|
|
157
|
+
font-size: 0.75em;
|
|
158
|
+
opacity: 0.6;
|
|
159
|
+
}
|
|
160
|
+
`;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 html`
|
|
119
161
|
<div id="trigger" @click=${this.handleTriggerClick}>
|
|
120
162
|
<slot name="trigger"></slot>
|
|
163
|
+
${this.submenu?html`<k-icon name="chevron"></k-icon>`:""}
|
|
121
164
|
</div>
|
|
122
165
|
<div id="menu" role="menu" @click=${this.handleMenuClick}>
|
|
123
166
|
<slot @slotchange=${this.handleSlotChange}></slot>
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<h6>Examples</h6>
|
|
25
25
|
<a href="#basicUsage">Basic Usage</a><br />
|
|
26
26
|
<a href="#withIcons">With Icons</a><br />
|
|
27
|
+
<a href="#subMenus">Sub Menus</a><br />
|
|
27
28
|
<a href="#openDirection">Open Direction</a><br />
|
|
28
29
|
<a href="#disabledItems">Disabled Items</a><br />
|
|
29
30
|
<a href="#links">Using Links</a><br />
|
|
@@ -88,6 +89,30 @@
|
|
|
88
89
|
</div>
|
|
89
90
|
</div>
|
|
90
91
|
|
|
92
|
+
<h3 id="subMenus"><a href="#subMenus" class="no-link">Sub Menus</a></h3>
|
|
93
|
+
<p>Nest a <code>k-dropdown</code> inside another <code>k-dropdown</code> to create sub menus. The nested dropdown is automatically detected as a submenu: it opens on hover or focus, displays a chevron icon, and opens to the right by default. Keyboard navigation supports <code>ArrowRight</code> to open and <code>ArrowLeft</code> to close sub menus.</p>
|
|
94
|
+
<div class="row -mx">
|
|
95
|
+
<div class="col m-span-12 px">
|
|
96
|
+
<k-card label="HTML">
|
|
97
|
+
<pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">k-dropdown</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">"trigger"</span>></span>Actions<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">"edit"</span>></span>Edit<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">k-dropdown</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">"trigger"</span>></span>Share<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">"email"</span>></span>Email<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">"link"</span>></span>Copy Link<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br> <span class="hljs-tag"></<span class="hljs-name">k-dropdown</span>></span><br> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">data-value</span>=<span class="hljs-string">"delete"</span>></span>Delete<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br><span class="hljs-tag"></<span class="hljs-name">k-dropdown</span>></span></code></pre>
|
|
98
|
+
</k-card>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="col m-span-12 px">
|
|
101
|
+
<k-card label="Output">
|
|
102
|
+
<k-dropdown>
|
|
103
|
+
<button slot="trigger">Actions</button>
|
|
104
|
+
<button data-value="edit">Edit</button>
|
|
105
|
+
<k-dropdown>
|
|
106
|
+
<button slot="trigger">Share</button>
|
|
107
|
+
<button data-value="email">Email</button>
|
|
108
|
+
<button data-value="link">Copy Link</button>
|
|
109
|
+
</k-dropdown>
|
|
110
|
+
<button data-value="delete">Delete</button>
|
|
111
|
+
</k-dropdown>
|
|
112
|
+
</k-card>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
91
116
|
<h3 id="openDirection"><a href="#openDirection" class="no-link">Open Direction</a></h3>
|
|
92
117
|
<p>Control where the menu appears using the <code>open-direction</code> attribute. Specify a direction like <code>down left</code>, <code>up right</code>, <code>left up</code>, etc. The browser automatically handles fallback positioning if the menu would overflow the viewport using CSS Anchor Positioning.</p>
|
|
93
118
|
<div class="row -mx">
|
|
@@ -223,7 +248,7 @@
|
|
|
223
248
|
<h3 id="requirements"><a href="#requirements" class="no-link">Requirements</a></h3>
|
|
224
249
|
<ul>
|
|
225
250
|
<li><a href="./shadow-component.html">ShadowComponent</a></li>
|
|
226
|
-
<li><a href="./icon.html">Icon</a> (
|
|
251
|
+
<li><a href="./icon.html">Icon</a> (required for sub menus)</li>
|
|
227
252
|
</ul>
|
|
228
253
|
|
|
229
254
|
<h3 id="attributes"><a href="#attributes" class="no-link">Attributes</a></h3>
|
|
@@ -239,6 +264,9 @@
|
|
|
239
264
|
<h5><code>close-on-click-outside<i>: boolean</i></code></h5>
|
|
240
265
|
<p>Whether the dropdown closes when clicking outside of it. Default is <code>true</code>.</p>
|
|
241
266
|
|
|
267
|
+
<h5><code>submenu<i>: boolean</i></code></h5>
|
|
268
|
+
<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>
|
|
269
|
+
|
|
242
270
|
<h3 id="methods"><a href="#methods" class="no-link">Methods</a></h3>
|
|
243
271
|
<h5><code>open()<i>: this</i></code></h5>
|
|
244
272
|
<p>Opens the dropdown menu and focuses the first item.</p>
|
|
@@ -265,10 +293,12 @@
|
|
|
265
293
|
|
|
266
294
|
<h4>Keyboard Navigation</h4>
|
|
267
295
|
<ul>
|
|
268
|
-
<li><code>Enter</code> / <code>Space</code> - Select the focused item</li>
|
|
296
|
+
<li><code>Enter</code> / <code>Space</code> - Select the focused item (or open sub menu)</li>
|
|
269
297
|
<li><code>Arrow Down</code> - Move focus to the next item</li>
|
|
270
298
|
<li><code>Arrow Up</code> - Move focus to the previous item</li>
|
|
271
|
-
<li><code>
|
|
299
|
+
<li><code>Arrow Right</code> - Open focused sub menu</li>
|
|
300
|
+
<li><code>Arrow Left</code> - Close current sub menu</li>
|
|
301
|
+
<li><code>Escape</code> - Close the dropdown (or sub menu)</li>
|
|
272
302
|
</ul>
|
|
273
303
|
|
|
274
304
|
<h3 id="cssVars"><a href="#cssVars" class="no-link">CSS Custom Properties</a></h3>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{boolTrueFalse}from"../utils/propConverters.js";const openDropdowns=new Set;export default class Dropdown extends ShadowComponent{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:boolTrueFalse},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:boolTrueFalse}};constructor(){super(),this.opened=!1,this.openDirection="down left",this.closeOnSelect=!0,this.closeOnClickOutside=!0}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.handleDocumentClick),document.addEventListener("keydown",this.handleKeydown)}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.handleDocumentClick),document.removeEventListener("keydown",this.handleKeydown),openDropdowns.delete(this)}updated(
|
|
1
|
+
import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComponent.js";import{boolTrueFalse}from"../utils/propConverters.js";const openDropdowns=new Set;export default class Dropdown extends ShadowComponent{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:boolTrueFalse},closeOnClickOutside:{type:Boolean,reflect:!0,attribute:"close-on-click-outside",converter:boolTrueFalse},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),openDropdowns.delete(this)}updated(e){super.updated(e),e.has("opened")&&(this.submenu||(this.opened?openDropdowns.add(this):openDropdowns.delete(this)),this.dispatchEvent(new CustomEvent(this.opened?"opened":"closed",{bubbles:!0})))}handleDocumentClick=e=>{const t=e.target.closest('[slot="trigger"]');if(t){const e=t.closest("k-dropdown");if(e===this)return;if(this.contains(e))return;return void(this.opened&&this.close())}if(!this.opened)return;if(!this.contains(e.target))return void(this.closeOnClickOutside&&this.close());const o=e.target.closest("a, button");o&&!o.closest('[slot="trigger"]')&&this.closeOnSelect&&this.close()};handleTriggerClick=e=>{e.stopPropagation(),this.opened||openDropdowns.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()}):openDropdowns.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=css`
|
|
2
2
|
:host {
|
|
3
3
|
display: inline-block;
|
|
4
4
|
position: relative;
|
|
@@ -23,6 +23,9 @@ import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComp
|
|
|
23
23
|
:host([opened]) #menu {
|
|
24
24
|
display: block;
|
|
25
25
|
}
|
|
26
|
+
:host([submenu]) #menu {
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
26
29
|
/* Default: down left */
|
|
27
30
|
#menu {
|
|
28
31
|
position-area: bottom span-right;
|
|
@@ -115,9 +118,49 @@ import{html,css}from"../lit-all.min.js";import ShadowComponent from"./ShadowComp
|
|
|
115
118
|
::slotted(hr) {
|
|
116
119
|
display: none !important;
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
/* Slotted submenu dropdowns */
|
|
122
|
+
::slotted(k-dropdown) {
|
|
123
|
+
display: block !important;
|
|
124
|
+
width: 100% !important;
|
|
125
|
+
border-top: 1px solid var(--c_border) !important;
|
|
126
|
+
}
|
|
127
|
+
::slotted(k-dropdown.k-dropdown-first) {
|
|
128
|
+
border-top: none !important;
|
|
129
|
+
}
|
|
130
|
+
/* Submenu host styles */
|
|
131
|
+
:host([submenu]) {
|
|
132
|
+
display: block;
|
|
133
|
+
position: relative;
|
|
134
|
+
}
|
|
135
|
+
:host([submenu]) #trigger {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
gap: 1rem;
|
|
140
|
+
padding: var(--spacer_h) var(--spacer);
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
transition: background var(--animation_ms);
|
|
144
|
+
}
|
|
145
|
+
:host([submenu]) #trigger:hover,
|
|
146
|
+
:host([submenu]) #trigger:focus-within {
|
|
147
|
+
background: var(--c_bg__alt);
|
|
148
|
+
}
|
|
149
|
+
:host([submenu]) #trigger ::slotted(button),
|
|
150
|
+
:host([submenu]) #trigger ::slotted(a) {
|
|
151
|
+
all: unset !important;
|
|
152
|
+
cursor: pointer !important;
|
|
153
|
+
font: inherit !important;
|
|
154
|
+
color: var(--tc) !important;
|
|
155
|
+
}
|
|
156
|
+
:host([submenu]) #trigger k-icon {
|
|
157
|
+
font-size: 0.75em;
|
|
158
|
+
opacity: 0.6;
|
|
159
|
+
}
|
|
160
|
+
`;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 html`
|
|
119
161
|
<div id="trigger" @click=${this.handleTriggerClick}>
|
|
120
162
|
<slot name="trigger"></slot>
|
|
163
|
+
${this.submenu?html`<k-icon name="chevron"></k-icon>`:""}
|
|
121
164
|
</div>
|
|
122
165
|
<div id="menu" role="menu" @click=${this.handleMenuClick}>
|
|
123
166
|
<slot @slotchange=${this.handleSlotChange}></slot>
|
package/package.json
CHANGED
|
@@ -10,7 +10,8 @@ export default class Dropdown extends ShadowComponent {
|
|
|
10
10
|
opened: { type: Boolean, reflect: true },
|
|
11
11
|
openDirection: { type: String, reflect: true, attribute: 'open-direction' },
|
|
12
12
|
closeOnSelect: { type: Boolean, reflect: true, attribute: 'close-on-select', converter: boolTrueFalse },
|
|
13
|
-
closeOnClickOutside: { type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: boolTrueFalse }
|
|
13
|
+
closeOnClickOutside: { type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: boolTrueFalse },
|
|
14
|
+
submenu: { type: Boolean, reflect: true }
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
constructor() {
|
|
@@ -19,6 +20,7 @@ export default class Dropdown extends ShadowComponent {
|
|
|
19
20
|
this.openDirection = 'down left';
|
|
20
21
|
this.closeOnSelect = true;
|
|
21
22
|
this.closeOnClickOutside = true;
|
|
23
|
+
this.submenu = false;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/*
|
|
@@ -26,13 +28,27 @@ export default class Dropdown extends ShadowComponent {
|
|
|
26
28
|
*/
|
|
27
29
|
connectedCallback() {
|
|
28
30
|
super.connectedCallback();
|
|
29
|
-
|
|
31
|
+
if(this.parentElement?.tagName === 'K-DROPDOWN' && !this.hasAttribute('slot')) {
|
|
32
|
+
this.submenu = true;
|
|
33
|
+
if(!this.hasAttribute('open-direction')) this.openDirection = 'right down';
|
|
34
|
+
}
|
|
35
|
+
if(this.submenu) {
|
|
36
|
+
this.addEventListener('mouseenter', this.handleSubmenuEnter);
|
|
37
|
+
this.addEventListener('mouseleave', this.handleSubmenuLeave);
|
|
38
|
+
} else {
|
|
39
|
+
document.addEventListener('click', this.handleDocumentClick);
|
|
40
|
+
}
|
|
30
41
|
document.addEventListener('keydown', this.handleKeydown);
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
disconnectedCallback() {
|
|
34
45
|
super.disconnectedCallback();
|
|
35
|
-
|
|
46
|
+
if(this.submenu) {
|
|
47
|
+
this.removeEventListener('mouseenter', this.handleSubmenuEnter);
|
|
48
|
+
this.removeEventListener('mouseleave', this.handleSubmenuLeave);
|
|
49
|
+
} else {
|
|
50
|
+
document.removeEventListener('click', this.handleDocumentClick);
|
|
51
|
+
}
|
|
36
52
|
document.removeEventListener('keydown', this.handleKeydown);
|
|
37
53
|
openDropdowns.delete(this);
|
|
38
54
|
}
|
|
@@ -40,10 +56,12 @@ export default class Dropdown extends ShadowComponent {
|
|
|
40
56
|
updated(changedProperties) {
|
|
41
57
|
super.updated(changedProperties);
|
|
42
58
|
if(changedProperties.has('opened')) {
|
|
43
|
-
if(this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
if(!this.submenu) {
|
|
60
|
+
if(this.opened) {
|
|
61
|
+
openDropdowns.add(this);
|
|
62
|
+
} else {
|
|
63
|
+
openDropdowns.delete(this);
|
|
64
|
+
}
|
|
47
65
|
}
|
|
48
66
|
this.dispatchEvent(new CustomEvent(this.opened ? 'opened' : 'closed', {
|
|
49
67
|
bubbles: true
|
|
@@ -55,11 +73,13 @@ export default class Dropdown extends ShadowComponent {
|
|
|
55
73
|
Event Handlers
|
|
56
74
|
*/
|
|
57
75
|
handleDocumentClick = e => {
|
|
58
|
-
const clickedDropdown = e.target.closest('k-dropdown');
|
|
59
76
|
const isTrigger = e.target.closest('[slot="trigger"]');
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
if(isTrigger) {
|
|
79
|
+
const triggerDropdown = isTrigger.closest('k-dropdown');
|
|
80
|
+
if(triggerDropdown === this) return;
|
|
81
|
+
// Child submenu trigger — don't close
|
|
82
|
+
if(this.contains(triggerDropdown)) return;
|
|
63
83
|
if(this.opened) this.close();
|
|
64
84
|
return;
|
|
65
85
|
}
|
|
@@ -67,17 +87,14 @@ export default class Dropdown extends ShadowComponent {
|
|
|
67
87
|
if(!this.opened) return;
|
|
68
88
|
|
|
69
89
|
// Click is outside this dropdown entirely
|
|
70
|
-
if(
|
|
90
|
+
if(!this.contains(e.target)) {
|
|
71
91
|
if(this.closeOnClickOutside) this.close();
|
|
72
92
|
return;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
|
-
// Click is
|
|
76
|
-
if(isTrigger) return;
|
|
77
|
-
|
|
78
|
-
// Click is inside the menu on an interactive element
|
|
95
|
+
// Click is inside the menu on an interactive element (including submenu items)
|
|
79
96
|
const isInteractive = e.target.closest('a, button');
|
|
80
|
-
if(isInteractive && this.closeOnSelect) this.close();
|
|
97
|
+
if(isInteractive && !isInteractive.closest('[slot="trigger"]') && this.closeOnSelect) this.close();
|
|
81
98
|
};
|
|
82
99
|
|
|
83
100
|
handleTriggerClick = e => {
|
|
@@ -93,18 +110,25 @@ export default class Dropdown extends ShadowComponent {
|
|
|
93
110
|
|
|
94
111
|
handleMenuClick = e => {
|
|
95
112
|
const item = e.target.closest('a, button');
|
|
96
|
-
if(item
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
113
|
+
if(!item || item.hasAttribute('disabled')) return;
|
|
114
|
+
// Don't fire select for items inside child submenus
|
|
115
|
+
if(item.closest('k-dropdown') !== this) return;
|
|
116
|
+
const value = item.dataset?.value || item.textContent.trim();
|
|
117
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
118
|
+
detail: { value, item },
|
|
119
|
+
bubbles: true
|
|
120
|
+
}));
|
|
103
121
|
};
|
|
104
122
|
|
|
105
123
|
handleKeydown = e => {
|
|
106
124
|
if(!this.opened) return;
|
|
107
|
-
|
|
125
|
+
const focused = document.activeElement;
|
|
126
|
+
// Only handle if focus is within our direct scope
|
|
127
|
+
if(!this.contains(focused)) return;
|
|
128
|
+
const openChild = this.querySelector(':scope > k-dropdown[opened]');
|
|
129
|
+
if(openChild?.contains(focused)) return;
|
|
130
|
+
|
|
131
|
+
if(e.key === 'Escape' || (this.submenu && e.key === 'ArrowLeft')) {
|
|
108
132
|
e.preventDefault();
|
|
109
133
|
this.close();
|
|
110
134
|
this.focusTrigger();
|
|
@@ -114,9 +138,20 @@ export default class Dropdown extends ShadowComponent {
|
|
|
114
138
|
} else if(e.key === 'ArrowUp') {
|
|
115
139
|
e.preventDefault();
|
|
116
140
|
this.focusPreviousItem();
|
|
141
|
+
} else if(e.key === 'ArrowRight') {
|
|
142
|
+
const submenu = focused?.closest('k-dropdown[submenu]');
|
|
143
|
+
if(submenu?.parentElement === this) {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
submenu.open();
|
|
146
|
+
submenu.focusFirstItem();
|
|
147
|
+
}
|
|
117
148
|
} else if(e.key === 'Enter' || e.key === ' ') {
|
|
118
|
-
const
|
|
119
|
-
if(
|
|
149
|
+
const submenu = focused?.closest('k-dropdown[submenu]');
|
|
150
|
+
if(submenu?.parentElement === this) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
submenu.open();
|
|
153
|
+
submenu.focusFirstItem();
|
|
154
|
+
} else if(focused && !focused.closest('[slot="trigger"]')) {
|
|
120
155
|
e.preventDefault();
|
|
121
156
|
focused.click();
|
|
122
157
|
}
|
|
@@ -127,16 +162,22 @@ export default class Dropdown extends ShadowComponent {
|
|
|
127
162
|
Public Methods
|
|
128
163
|
*/
|
|
129
164
|
open() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
if(this.submenu) {
|
|
166
|
+
[...this.parentElement.querySelectorAll(':scope > k-dropdown[opened]')].forEach(sub => {
|
|
167
|
+
if(sub !== this) sub.close();
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
openDropdowns.forEach(dropdown => {
|
|
171
|
+
if(dropdown !== this) dropdown.close();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
134
174
|
this.opened = true;
|
|
135
|
-
requestAnimationFrame(() => this.focusFirstItem());
|
|
175
|
+
if(!this.submenu) requestAnimationFrame(() => this.focusFirstItem());
|
|
136
176
|
return this;
|
|
137
177
|
}
|
|
138
178
|
|
|
139
179
|
close() {
|
|
180
|
+
this.querySelectorAll(':scope > k-dropdown[opened]').forEach(sub => sub.close());
|
|
140
181
|
this.opened = false;
|
|
141
182
|
return this;
|
|
142
183
|
}
|
|
@@ -154,9 +195,16 @@ export default class Dropdown extends ShadowComponent {
|
|
|
154
195
|
}
|
|
155
196
|
|
|
156
197
|
getMenuItems() {
|
|
157
|
-
return [...this.
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
return [...this.children].reduce((items, child) => {
|
|
199
|
+
if(child.matches('[slot="trigger"]') || child.hasAttribute('disabled')) return items;
|
|
200
|
+
if(child.tagName === 'K-DROPDOWN') {
|
|
201
|
+
const trigger = child.querySelector('[slot="trigger"]');
|
|
202
|
+
if(trigger) items.push(trigger);
|
|
203
|
+
} else if(child.matches('a, button')) {
|
|
204
|
+
items.push(child);
|
|
205
|
+
}
|
|
206
|
+
return items;
|
|
207
|
+
}, []);
|
|
160
208
|
}
|
|
161
209
|
|
|
162
210
|
focusFirstItem() {
|
|
@@ -208,6 +256,9 @@ export default class Dropdown extends ShadowComponent {
|
|
|
208
256
|
:host([opened]) #menu {
|
|
209
257
|
display: block;
|
|
210
258
|
}
|
|
259
|
+
:host([submenu]) #menu {
|
|
260
|
+
margin: 0;
|
|
261
|
+
}
|
|
211
262
|
/* Default: down left */
|
|
212
263
|
#menu {
|
|
213
264
|
position-area: bottom span-right;
|
|
@@ -300,14 +351,62 @@ export default class Dropdown extends ShadowComponent {
|
|
|
300
351
|
::slotted(hr) {
|
|
301
352
|
display: none !important;
|
|
302
353
|
}
|
|
354
|
+
/* Slotted submenu dropdowns */
|
|
355
|
+
::slotted(k-dropdown) {
|
|
356
|
+
display: block !important;
|
|
357
|
+
width: 100% !important;
|
|
358
|
+
border-top: 1px solid var(--c_border) !important;
|
|
359
|
+
}
|
|
360
|
+
::slotted(k-dropdown.k-dropdown-first) {
|
|
361
|
+
border-top: none !important;
|
|
362
|
+
}
|
|
363
|
+
/* Submenu host styles */
|
|
364
|
+
:host([submenu]) {
|
|
365
|
+
display: block;
|
|
366
|
+
position: relative;
|
|
367
|
+
}
|
|
368
|
+
:host([submenu]) #trigger {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
justify-content: space-between;
|
|
372
|
+
gap: 1rem;
|
|
373
|
+
padding: var(--spacer_h) var(--spacer);
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
white-space: nowrap;
|
|
376
|
+
transition: background var(--animation_ms);
|
|
377
|
+
}
|
|
378
|
+
:host([submenu]) #trigger:hover,
|
|
379
|
+
:host([submenu]) #trigger:focus-within {
|
|
380
|
+
background: var(--c_bg__alt);
|
|
381
|
+
}
|
|
382
|
+
:host([submenu]) #trigger ::slotted(button),
|
|
383
|
+
:host([submenu]) #trigger ::slotted(a) {
|
|
384
|
+
all: unset !important;
|
|
385
|
+
cursor: pointer !important;
|
|
386
|
+
font: inherit !important;
|
|
387
|
+
color: var(--tc) !important;
|
|
388
|
+
}
|
|
389
|
+
:host([submenu]) #trigger k-icon {
|
|
390
|
+
font-size: 0.75em;
|
|
391
|
+
opacity: 0.6;
|
|
392
|
+
}
|
|
303
393
|
`;
|
|
304
394
|
|
|
305
395
|
/*
|
|
306
396
|
Slot Change Handler
|
|
307
397
|
*/
|
|
398
|
+
handleSubmenuEnter = () => {
|
|
399
|
+
clearTimeout(this.closeTimer);
|
|
400
|
+
if(!this.opened) this.open();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
handleSubmenuLeave = () => {
|
|
404
|
+
this.closeTimer = setTimeout(() => this.close(), 150);
|
|
405
|
+
};
|
|
406
|
+
|
|
308
407
|
handleSlotChange = e => {
|
|
309
408
|
const slot = e.target;
|
|
310
|
-
const items = slot.assignedElements().filter(el => el.matches('a, button'));
|
|
409
|
+
const items = slot.assignedElements().filter(el => el.matches('a, button, k-dropdown'));
|
|
311
410
|
items.forEach((item, i) => item.classList.toggle('k-dropdown-first', i === 0));
|
|
312
411
|
};
|
|
313
412
|
|
|
@@ -318,6 +417,7 @@ export default class Dropdown extends ShadowComponent {
|
|
|
318
417
|
return html`
|
|
319
418
|
<div id="trigger" @click=${this.handleTriggerClick}>
|
|
320
419
|
<slot name="trigger"></slot>
|
|
420
|
+
${this.submenu ? html`<k-icon name="chevron"></k-icon>` : ''}
|
|
321
421
|
</div>
|
|
322
422
|
<div id="menu" role="menu" @click=${this.handleMenuClick}>
|
|
323
423
|
<slot @slotchange=${this.handleSlotChange}></slot>
|