@neovici/cosmoz-dropdown 7.1.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,48 +1,106 @@
1
- # cosmoz-dropdown
1
+ # @neovici/cosmoz-dropdown
2
2
 
3
- A dropdown web component built with [pionjs](https://github.com/pionjs/pion) and lit-html.
3
+ Dropdown components for Neovici applications.
4
4
 
5
- ## Install
5
+ ## Installation
6
6
 
7
- ```sh
7
+ ```bash
8
8
  npm install @neovici/cosmoz-dropdown
9
9
  ```
10
10
 
11
11
  ## Components
12
12
 
13
- ### cosmoz-dropdown
13
+ ### cosmoz-dropdown-next
14
14
 
15
- Classic dropdown with floating-ui positioning.
15
+ Modern dropdown using the Popover API and CSS Anchor Positioning.
16
16
 
17
- ```js
18
- import '@neovici/cosmoz-dropdown';
19
- ```
17
+ #### Usage
20
18
 
21
19
  ```html
22
- <cosmoz-dropdown-menu>
23
- <span slot="button">Menu</span>
24
- <div>Item 1</div>
25
- <div>Item 2</div>
26
- </cosmoz-dropdown-menu>
20
+ <script type="module">
21
+ import '@neovici/cosmoz-dropdown';
22
+ </script>
23
+
24
+ <cosmoz-dropdown-next placement="bottom span-right">
25
+ <button slot="button">Open Menu</button>
26
+ <div>Dropdown content</div>
27
+ </cosmoz-dropdown-next>
27
28
  ```
28
29
 
29
- ### cosmoz-dropdown-next
30
+ #### Properties
30
31
 
31
- Next-gen dropdown using the Popover API and CSS Anchor Positioning.
32
+ | Property | Type | Default | Description |
33
+ | --------------- | --------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
34
+ | `placement` | `string` | `'bottom span-right'` | CSS anchor `position-area` value. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/position-area) for options. |
35
+ | `open-on-hover` | `boolean` | `false` | Open on pointer hover. |
36
+ | `open-on-focus` | `boolean` | `false` | Open when the trigger receives focus. |
32
37
 
33
- ```js
34
- import '@neovici/cosmoz-dropdown/cosmoz-dropdown-next';
35
- ```
38
+ #### Auto-open Modes
39
+
40
+ The `open-on-hover` and `open-on-focus` attributes can be used independently or together:
36
41
 
37
42
  ```html
38
- <cosmoz-dropdown-next placement="bottom span-right">
39
- <button slot="button">Open</button>
40
- <div>Popover content</div>
43
+ <!-- Open on hover only -->
44
+ <cosmoz-dropdown-next open-on-hover>
45
+ <button slot="button">Hover me</button>
46
+ <div>Content appears on hover</div>
47
+ </cosmoz-dropdown-next>
48
+
49
+ <!-- Open on focus only -->
50
+ <cosmoz-dropdown-next open-on-focus>
51
+ <button slot="button">Focus me</button>
52
+ <div>Content appears on focus</div>
53
+ </cosmoz-dropdown-next>
54
+
55
+ <!-- Open on hover or focus -->
56
+ <cosmoz-dropdown-next open-on-hover open-on-focus>
57
+ <button slot="button">Hover or focus</button>
58
+ <div>Content appears on either</div>
41
59
  </cosmoz-dropdown-next>
42
60
  ```
43
61
 
44
- The `placement` prop accepts any CSS [`position-area`](https://developer.mozilla.org/en-US/docs/Web/CSS/position-area) value.
62
+ When auto-open is enabled:
63
+
64
+ - The dropdown closes with a 100ms delay to allow moving between trigger and content
65
+ - When `open-on-focus` is active, clicking the button only opens (does not toggle)
66
+ - Otherwise, click works as a toggle
67
+
68
+ #### Slots
69
+
70
+ | Slot | Description |
71
+ | --------- | ------------------------------------------- |
72
+ | `button` | The trigger element that opens the dropdown |
73
+ | (default) | The dropdown content |
74
+
75
+ #### Events
76
+
77
+ The dropdown listens for a `select` event on its content and automatically closes when triggered. This allows menu items to close the dropdown when selected:
78
+
79
+ ```javascript
80
+ menuItem.dispatchEvent(new Event('select', { bubbles: true }));
81
+ ```
82
+
83
+ The dropdown dispatches a `dropdown-toggle` event (a [`ToggleEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ToggleEvent)) when the popover opens or closes, allowing parent components across shadow boundaries to observe state changes:
84
+
85
+ ```javascript
86
+ dropdown.addEventListener('dropdown-toggle', (e) => {
87
+ console.log(e.oldState, '->', e.newState); // 'closed' -> 'open' or 'open' -> 'closed'
88
+ });
89
+ ```
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ npm install
95
+ npm run storybook:start
96
+ ```
97
+
98
+ ## Testing
99
+
100
+ ```bash
101
+ npm test
102
+ ```
45
103
 
46
- ## Storybook
104
+ ## License
47
105
 
48
- https://neovici.github.io/cosmoz-dropdown/
106
+ Apache-2.0
@@ -1,365 +1,5 @@
1
- declare global {
2
- interface HTMLElement {
3
- connectedCallback(): void;
4
- disconnectedCallback(): void;
5
- }
6
- }
7
- export declare const connectable: (base?: {
8
- new (): HTMLElement;
9
- prototype: HTMLElement;
10
- }) => {
11
- new (): {
12
- connectedCallback(): void;
13
- disconnectedCallback(): void;
14
- accessKey: string;
15
- readonly accessKeyLabel: string;
16
- autocapitalize: string;
17
- autocorrect: boolean;
18
- dir: string;
19
- draggable: boolean;
20
- hidden: boolean;
21
- inert: boolean;
22
- innerText: string;
23
- lang: string;
24
- readonly offsetHeight: number;
25
- readonly offsetLeft: number;
26
- readonly offsetParent: Element | null;
27
- readonly offsetTop: number;
28
- readonly offsetWidth: number;
29
- outerText: string;
30
- popover: string | null;
31
- spellcheck: boolean;
32
- title: string;
33
- translate: boolean;
34
- writingSuggestions: string;
35
- attachInternals(): ElementInternals;
36
- click(): void;
37
- hidePopover(): void;
38
- showPopover(): void;
39
- togglePopover(options?: boolean): boolean;
40
- addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
41
- addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
42
- removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
43
- removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
44
- readonly attributes: NamedNodeMap;
45
- get classList(): DOMTokenList;
46
- set classList(value: string);
47
- className: string;
48
- readonly clientHeight: number;
49
- readonly clientLeft: number;
50
- readonly clientTop: number;
51
- readonly clientWidth: number;
52
- readonly currentCSSZoom: number;
53
- id: string;
54
- innerHTML: string;
55
- readonly localName: string;
56
- readonly namespaceURI: string | null;
57
- onfullscreenchange: ((this: Element, ev: Event) => any) | null;
58
- onfullscreenerror: ((this: Element, ev: Event) => any) | null;
59
- outerHTML: string;
60
- readonly ownerDocument: Document;
61
- get part(): DOMTokenList;
62
- set part(value: string);
63
- readonly prefix: string | null;
64
- readonly scrollHeight: number;
65
- scrollLeft: number;
66
- scrollTop: number;
67
- readonly scrollWidth: number;
68
- readonly shadowRoot: ShadowRoot | null;
69
- slot: string;
70
- readonly tagName: string;
71
- attachShadow(init: ShadowRootInit): ShadowRoot;
72
- checkVisibility(options?: CheckVisibilityOptions): boolean;
73
- closest<K extends keyof HTMLElementTagNameMap>(selector: K): HTMLElementTagNameMap[K] | null;
74
- closest<K extends keyof SVGElementTagNameMap>(selector: K): SVGElementTagNameMap[K] | null;
75
- closest<K extends keyof MathMLElementTagNameMap>(selector: K): MathMLElementTagNameMap[K] | null;
76
- closest<E extends Element = Element>(selectors: string): E | null;
77
- computedStyleMap(): StylePropertyMapReadOnly;
78
- getAttribute(qualifiedName: string): string | null;
79
- getAttributeNS(namespace: string | null, localName: string): string | null;
80
- getAttributeNames(): string[];
81
- getAttributeNode(qualifiedName: string): Attr | null;
82
- getAttributeNodeNS(namespace: string | null, localName: string): Attr | null;
83
- getBoundingClientRect(): DOMRect;
84
- getClientRects(): DOMRectList;
85
- getElementsByClassName(classNames: string): HTMLCollectionOf<Element>;
86
- getElementsByTagName<K extends keyof HTMLElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<HTMLElementTagNameMap[K]>;
87
- getElementsByTagName<K extends keyof SVGElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<SVGElementTagNameMap[K]>;
88
- getElementsByTagName<K extends keyof MathMLElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<MathMLElementTagNameMap[K]>;
89
- getElementsByTagName<K extends keyof HTMLElementDeprecatedTagNameMap>(qualifiedName: K): HTMLCollectionOf<HTMLElementDeprecatedTagNameMap[K]>;
90
- getElementsByTagName(qualifiedName: string): HTMLCollectionOf<Element>;
91
- getElementsByTagNameNS(namespaceURI: "http://www.w3.org/1999/xhtml", localName: string): HTMLCollectionOf<HTMLElement>;
92
- getElementsByTagNameNS(namespaceURI: "http://www.w3.org/2000/svg", localName: string): HTMLCollectionOf<SVGElement>;
93
- getElementsByTagNameNS(namespaceURI: "http://www.w3.org/1998/Math/MathML", localName: string): HTMLCollectionOf<MathMLElement>;
94
- getElementsByTagNameNS(namespace: string | null, localName: string): HTMLCollectionOf<Element>;
95
- getHTML(options?: GetHTMLOptions): string;
96
- hasAttribute(qualifiedName: string): boolean;
97
- hasAttributeNS(namespace: string | null, localName: string): boolean;
98
- hasAttributes(): boolean;
99
- hasPointerCapture(pointerId: number): boolean;
100
- insertAdjacentElement(where: InsertPosition, element: Element): Element | null;
101
- insertAdjacentHTML(position: InsertPosition, string: string): void;
102
- insertAdjacentText(where: InsertPosition, data: string): void;
103
- matches(selectors: string): boolean;
104
- releasePointerCapture(pointerId: number): void;
105
- removeAttribute(qualifiedName: string): void;
106
- removeAttributeNS(namespace: string | null, localName: string): void;
107
- removeAttributeNode(attr: Attr): Attr;
108
- requestFullscreen(options?: FullscreenOptions): Promise<void>;
109
- requestPointerLock(options?: PointerLockOptions): Promise<void>;
110
- scroll(options?: ScrollToOptions): void;
111
- scroll(x: number, y: number): void;
112
- scrollBy(options?: ScrollToOptions): void;
113
- scrollBy(x: number, y: number): void;
114
- scrollIntoView(arg?: boolean | ScrollIntoViewOptions): void;
115
- scrollTo(options?: ScrollToOptions): void;
116
- scrollTo(x: number, y: number): void;
117
- setAttribute(qualifiedName: string, value: string): void;
118
- setAttributeNS(namespace: string | null, qualifiedName: string, value: string): void;
119
- setAttributeNode(attr: Attr): Attr | null;
120
- setAttributeNodeNS(attr: Attr): Attr | null;
121
- setHTMLUnsafe(html: string): void;
122
- setPointerCapture(pointerId: number): void;
123
- toggleAttribute(qualifiedName: string, force?: boolean): boolean;
124
- webkitMatchesSelector(selectors: string): boolean;
125
- get textContent(): string;
126
- set textContent(value: string | null);
127
- readonly baseURI: string;
128
- readonly childNodes: NodeListOf<ChildNode>;
129
- readonly firstChild: ChildNode | null;
130
- readonly isConnected: boolean;
131
- readonly lastChild: ChildNode | null;
132
- readonly nextSibling: ChildNode | null;
133
- readonly nodeName: string;
134
- readonly nodeType: number;
135
- nodeValue: string | null;
136
- readonly parentElement: HTMLElement | null;
137
- readonly parentNode: ParentNode | null;
138
- readonly previousSibling: ChildNode | null;
139
- appendChild<T extends Node>(node: T): T;
140
- cloneNode(subtree?: boolean): Node;
141
- compareDocumentPosition(other: Node): number;
142
- contains(other: Node | null): boolean;
143
- getRootNode(options?: GetRootNodeOptions): Node;
144
- hasChildNodes(): boolean;
145
- insertBefore<T extends Node>(node: T, child: Node | null): T;
146
- isDefaultNamespace(namespace: string | null): boolean;
147
- isEqualNode(otherNode: Node | null): boolean;
148
- isSameNode(otherNode: Node | null): boolean;
149
- lookupNamespaceURI(prefix: string | null): string | null;
150
- lookupPrefix(namespace: string | null): string | null;
151
- normalize(): void;
152
- removeChild<T extends Node>(child: T): T;
153
- replaceChild<T extends Node>(node: Node, child: T): T;
154
- readonly ELEMENT_NODE: 1;
155
- readonly ATTRIBUTE_NODE: 2;
156
- readonly TEXT_NODE: 3;
157
- readonly CDATA_SECTION_NODE: 4;
158
- readonly ENTITY_REFERENCE_NODE: 5;
159
- readonly ENTITY_NODE: 6;
160
- readonly PROCESSING_INSTRUCTION_NODE: 7;
161
- readonly COMMENT_NODE: 8;
162
- readonly DOCUMENT_NODE: 9;
163
- readonly DOCUMENT_TYPE_NODE: 10;
164
- readonly DOCUMENT_FRAGMENT_NODE: 11;
165
- readonly NOTATION_NODE: 12;
166
- readonly DOCUMENT_POSITION_DISCONNECTED: 1;
167
- readonly DOCUMENT_POSITION_PRECEDING: 2;
168
- readonly DOCUMENT_POSITION_FOLLOWING: 4;
169
- readonly DOCUMENT_POSITION_CONTAINS: 8;
170
- readonly DOCUMENT_POSITION_CONTAINED_BY: 16;
171
- readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32;
172
- dispatchEvent(event: Event): boolean;
173
- ariaActiveDescendantElement: Element | null;
174
- ariaAtomic: string | null;
175
- ariaAutoComplete: string | null;
176
- ariaBrailleLabel: string | null;
177
- ariaBrailleRoleDescription: string | null;
178
- ariaBusy: string | null;
179
- ariaChecked: string | null;
180
- ariaColCount: string | null;
181
- ariaColIndex: string | null;
182
- ariaColIndexText: string | null;
183
- ariaColSpan: string | null;
184
- ariaControlsElements: ReadonlyArray<Element> | null;
185
- ariaCurrent: string | null;
186
- ariaDescribedByElements: ReadonlyArray<Element> | null;
187
- ariaDescription: string | null;
188
- ariaDetailsElements: ReadonlyArray<Element> | null;
189
- ariaDisabled: string | null;
190
- ariaErrorMessageElements: ReadonlyArray<Element> | null;
191
- ariaExpanded: string | null;
192
- ariaFlowToElements: ReadonlyArray<Element> | null;
193
- ariaHasPopup: string | null;
194
- ariaHidden: string | null;
195
- ariaInvalid: string | null;
196
- ariaKeyShortcuts: string | null;
197
- ariaLabel: string | null;
198
- ariaLabelledByElements: ReadonlyArray<Element> | null;
199
- ariaLevel: string | null;
200
- ariaLive: string | null;
201
- ariaModal: string | null;
202
- ariaMultiLine: string | null;
203
- ariaMultiSelectable: string | null;
204
- ariaOrientation: string | null;
205
- ariaOwnsElements: ReadonlyArray<Element> | null;
206
- ariaPlaceholder: string | null;
207
- ariaPosInSet: string | null;
208
- ariaPressed: string | null;
209
- ariaReadOnly: string | null;
210
- ariaRelevant: string | null;
211
- ariaRequired: string | null;
212
- ariaRoleDescription: string | null;
213
- ariaRowCount: string | null;
214
- ariaRowIndex: string | null;
215
- ariaRowIndexText: string | null;
216
- ariaRowSpan: string | null;
217
- ariaSelected: string | null;
218
- ariaSetSize: string | null;
219
- ariaSort: string | null;
220
- ariaValueMax: string | null;
221
- ariaValueMin: string | null;
222
- ariaValueNow: string | null;
223
- ariaValueText: string | null;
224
- role: string | null;
225
- animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions): Animation;
226
- getAnimations(options?: GetAnimationsOptions): Animation[];
227
- after(...nodes: (Node | string)[]): void;
228
- before(...nodes: (Node | string)[]): void;
229
- remove(): void;
230
- replaceWith(...nodes: (Node | string)[]): void;
231
- readonly nextElementSibling: Element | null;
232
- readonly previousElementSibling: Element | null;
233
- readonly childElementCount: number;
234
- readonly children: HTMLCollection;
235
- readonly firstElementChild: Element | null;
236
- readonly lastElementChild: Element | null;
237
- append(...nodes: (Node | string)[]): void;
238
- prepend(...nodes: (Node | string)[]): void;
239
- querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
240
- querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
241
- querySelector<K extends keyof MathMLElementTagNameMap>(selectors: K): MathMLElementTagNameMap[K] | null;
242
- querySelector<K extends keyof HTMLElementDeprecatedTagNameMap>(selectors: K): HTMLElementDeprecatedTagNameMap[K] | null;
243
- querySelector<E extends Element = Element>(selectors: string): E | null;
244
- querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
245
- querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
246
- querySelectorAll<K extends keyof MathMLElementTagNameMap>(selectors: K): NodeListOf<MathMLElementTagNameMap[K]>;
247
- querySelectorAll<K extends keyof HTMLElementDeprecatedTagNameMap>(selectors: K): NodeListOf<HTMLElementDeprecatedTagNameMap[K]>;
248
- querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
249
- replaceChildren(...nodes: (Node | string)[]): void;
250
- readonly assignedSlot: HTMLSlotElement | null;
251
- readonly attributeStyleMap: StylePropertyMap;
252
- get style(): CSSStyleDeclaration;
253
- set style(cssText: string);
254
- contentEditable: string;
255
- enterKeyHint: string;
256
- inputMode: string;
257
- readonly isContentEditable: boolean;
258
- onabort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
259
- onanimationcancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
260
- onanimationend: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
261
- onanimationiteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
262
- onanimationstart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
263
- onauxclick: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
264
- onbeforeinput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
265
- onbeforematch: ((this: GlobalEventHandlers, ev: Event) => any) | null;
266
- onbeforetoggle: ((this: GlobalEventHandlers, ev: ToggleEvent) => any) | null;
267
- onblur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
268
- oncancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
269
- oncanplay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
270
- oncanplaythrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
271
- onchange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
272
- onclick: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
273
- onclose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
274
- oncontextlost: ((this: GlobalEventHandlers, ev: Event) => any) | null;
275
- oncontextmenu: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
276
- oncontextrestored: ((this: GlobalEventHandlers, ev: Event) => any) | null;
277
- oncopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
278
- oncuechange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
279
- oncut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
280
- ondblclick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
281
- ondrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
282
- ondragend: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
283
- ondragenter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
284
- ondragleave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
285
- ondragover: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
286
- ondragstart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
287
- ondrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
288
- ondurationchange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
289
- onemptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
290
- onended: ((this: GlobalEventHandlers, ev: Event) => any) | null;
291
- onerror: OnErrorEventHandler;
292
- onfocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
293
- onformdata: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
294
- ongotpointercapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
295
- oninput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
296
- oninvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
297
- onkeydown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
298
- onkeypress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
299
- onkeyup: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
300
- onload: ((this: GlobalEventHandlers, ev: Event) => any) | null;
301
- onloadeddata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
302
- onloadedmetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
303
- onloadstart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
304
- onlostpointercapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
305
- onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
306
- onmouseenter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
307
- onmouseleave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
308
- onmousemove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
309
- onmouseout: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
310
- onmouseover: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
311
- onmouseup: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
312
- onpaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
313
- onpause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
314
- onplay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
315
- onplaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
316
- onpointercancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
317
- onpointerdown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
318
- onpointerenter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
319
- onpointerleave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
320
- onpointermove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
321
- onpointerout: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
322
- onpointerover: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
323
- onpointerrawupdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
324
- onpointerup: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
325
- onprogress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
326
- onratechange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
327
- onreset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
328
- onresize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
329
- onscroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
330
- onscrollend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
331
- onsecuritypolicyviolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
332
- onseeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
333
- onseeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
334
- onselect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
335
- onselectionchange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
336
- onselectstart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
337
- onslotchange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
338
- onstalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
339
- onsubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
340
- onsuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
341
- ontimeupdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
342
- ontoggle: ((this: GlobalEventHandlers, ev: ToggleEvent) => any) | null;
343
- ontouchcancel?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
344
- ontouchend?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
345
- ontouchmove?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
346
- ontouchstart?: ((this: GlobalEventHandlers, ev: TouchEvent) => any) | null | undefined;
347
- ontransitioncancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
348
- ontransitionend: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
349
- ontransitionrun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
350
- ontransitionstart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
351
- onvolumechange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
352
- onwaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
353
- onwebkitanimationend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
354
- onwebkitanimationiteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
355
- onwebkitanimationstart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
356
- onwebkittransitionend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
357
- onwheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
358
- autofocus: boolean;
359
- readonly dataset: DOMStringMap;
360
- nonce?: string;
361
- tabIndex: number;
362
- blur(): void;
363
- focus(options?: FocusOptions): void;
364
- };
365
- };
1
+ /**
2
+ * @deprecated Import from '@neovici/cosmoz-utils/connectable' instead.
3
+ * This export will be removed in the next major version.
4
+ */
5
+ export { connectable } from '@neovici/cosmoz-utils/connectable';
@@ -1,10 +1,5 @@
1
- export const connectable = (base = HTMLElement) => class extends base {
2
- connectedCallback() {
3
- super.connectedCallback?.();
4
- this.dispatchEvent(new CustomEvent('connected'));
5
- }
6
- disconnectedCallback() {
7
- super.disconnectedCallback?.();
8
- this.dispatchEvent(new CustomEvent('disconnected'));
9
- }
10
- };
1
+ /**
2
+ * @deprecated Import from '@neovici/cosmoz-utils/connectable' instead.
3
+ * This export will be removed in the next major version.
4
+ */
5
+ export { connectable } from '@neovici/cosmoz-utils/connectable';
@@ -1,6 +1,7 @@
1
1
  import { component, css, useRef } from '@pionjs/pion';
2
2
  import { html } from 'lit-html';
3
3
  import { ref } from 'lit-html/directives/ref.js';
4
+ import { useAutoOpen } from './use-auto-open.js';
4
5
  /**
5
6
  * Autofocus polyfill for slotted content.
6
7
  *
@@ -49,6 +50,7 @@ const style = css `
49
50
  padding: 0;
50
51
  background: transparent;
51
52
  overflow: visible;
53
+ min-width: anchor-size(width);
52
54
 
53
55
  /* Animation - open state */
54
56
  opacity: 1;
@@ -75,25 +77,43 @@ const style = css `
75
77
  opacity: 0;
76
78
  transform: translateY(-4px) scale(0.96);
77
79
  }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ [popover] {
83
+ transition: none;
84
+ }
85
+ }
78
86
  `;
79
- const CosmozDropdownNext = ({ placement = 'bottom span-right', }) => {
80
- const popover = useRef();
81
- const toggle = () => {
82
- popover.current?.togglePopover();
83
- };
84
- const close = () => {
85
- popover.current?.hidePopover();
87
+ const CosmozDropdownNext = (host) => {
88
+ const { placement = 'bottom span-right', openOnHover, openOnFocus } = host;
89
+ const popoverRef = useRef();
90
+ const open = () => popoverRef.current?.showPopover();
91
+ const close = () => popoverRef.current?.hidePopover();
92
+ const toggle = () => popoverRef.current?.togglePopover();
93
+ useAutoOpen({ host, popoverRef, openOnHover, openOnFocus, open, close });
94
+ // When open-on-focus is active, clicking the button should only open
95
+ // (not toggle), since focusin already handles opening and toggle would
96
+ // race with the focusin handler (focusin opens, then click toggles closed).
97
+ const handleClick = openOnFocus ? open : toggle;
98
+ const onToggle = (e) => {
99
+ autofocus(e);
100
+ // Re-dispatch as a composed event so parent components across
101
+ // shadow boundaries can observe popover state changes.
102
+ // The native ToggleEvent is composed: false, bubbles: false.
103
+ host.dispatchEvent(new ToggleEvent('dropdown-toggle', {
104
+ newState: e.newState,
105
+ oldState: e.oldState,
106
+ composed: true,
107
+ }));
86
108
  };
87
109
  return html `
88
- <slot name="button" @click=${toggle}></slot>
110
+ <slot name="button" @click=${handleClick}></slot>
89
111
  <div
90
112
  popover
91
113
  style="position-area: ${placement}"
92
- @toggle=${autofocus}
114
+ @toggle=${onToggle}
93
115
  @select=${close}
94
- ${ref((el) => {
95
- popover.current = el;
96
- })}
116
+ ${ref((el) => el && (popoverRef.current = el))}
97
117
  >
98
118
  <slot></slot>
99
119
  </div>
@@ -101,6 +121,6 @@ const CosmozDropdownNext = ({ placement = 'bottom span-right', }) => {
101
121
  };
102
122
  customElements.define('cosmoz-dropdown-next', component(CosmozDropdownNext, {
103
123
  styleSheets: [style],
104
- observedAttributes: ['placement'],
124
+ observedAttributes: ['placement', 'open-on-hover', 'open-on-focus'],
105
125
  shadowRootInit: { mode: 'open', delegatesFocus: true },
106
126
  }));
@@ -0,0 +1,12 @@
1
+ interface UseAutoOpenOptions {
2
+ host: HTMLElement;
3
+ popoverRef: {
4
+ current?: HTMLElement;
5
+ };
6
+ openOnHover?: boolean;
7
+ openOnFocus?: boolean;
8
+ open: () => void;
9
+ close: () => void;
10
+ }
11
+ export declare const useAutoOpen: ({ host, popoverRef, openOnHover, openOnFocus, open, close, }: UseAutoOpenOptions) => void;
12
+ export {};
@@ -0,0 +1,48 @@
1
+ import { useEffect, useRef } from '@pionjs/pion';
2
+ export const useAutoOpen = ({ host, popoverRef, openOnHover, openOnFocus, open, close, }) => {
3
+ const closeTimeout = useRef();
4
+ const cancelClose = () => clearTimeout(closeTimeout.current);
5
+ const scheduleClose = () => {
6
+ clearTimeout(closeTimeout.current);
7
+ closeTimeout.current = setTimeout(() => {
8
+ const popover = popoverRef.current;
9
+ if (openOnHover &&
10
+ (host.matches(':hover') || popover?.matches(':hover'))) {
11
+ return;
12
+ }
13
+ if (openOnFocus &&
14
+ (host.matches(':focus-within') || popover?.matches(':focus-within'))) {
15
+ return;
16
+ }
17
+ close();
18
+ }, 100);
19
+ };
20
+ const handleEnter = () => {
21
+ cancelClose();
22
+ open();
23
+ };
24
+ // Auto-open on hover
25
+ useEffect(() => {
26
+ if (!openOnHover)
27
+ return;
28
+ host.addEventListener('pointerenter', handleEnter);
29
+ host.addEventListener('pointerleave', scheduleClose);
30
+ return () => {
31
+ cancelClose();
32
+ host.removeEventListener('pointerenter', handleEnter);
33
+ host.removeEventListener('pointerleave', scheduleClose);
34
+ };
35
+ }, [openOnHover, host]);
36
+ // Auto-open on focus
37
+ useEffect(() => {
38
+ if (!openOnFocus)
39
+ return;
40
+ host.addEventListener('focusin', handleEnter);
41
+ host.addEventListener('focusout', scheduleClose);
42
+ return () => {
43
+ cancelClose();
44
+ host.removeEventListener('focusin', handleEnter);
45
+ host.removeEventListener('focusout', scheduleClose);
46
+ };
47
+ }, [openOnFocus, host]);
48
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-dropdown",
3
- "version": "7.1.0",
3
+ "version": "7.3.0",
4
4
  "description": "A simple dropdown web component",
5
5
  "keywords": [
6
6
  "lit-html",
@@ -25,8 +25,10 @@
25
25
  "check:duplicates": "check-duplicate-components",
26
26
  "build": "tsc -p tsconfig.build.json",
27
27
  "start": "wds",
28
- "test": "wtr --coverage",
28
+ "test": "wtr --coverage && vitest --project=storybook --run",
29
29
  "test:watch": "wtr --watch",
30
+ "test:storybook": "vitest --project=storybook --run",
31
+ "test:storybook:watch": "vitest --project=storybook",
30
32
  "storybook:start": "storybook dev -p 8000",
31
33
  "storybook:build": "storybook build",
32
34
  "storybook:deploy": "storybook-to-ghpages",
@@ -74,7 +76,7 @@
74
76
  },
75
77
  "dependencies": {
76
78
  "@floating-ui/dom": "^1.6.12",
77
- "@neovici/cosmoz-utils": "^6.8.1",
79
+ "@neovici/cosmoz-utils": "^6.19.0",
78
80
  "@pionjs/pion": "^2.5.2",
79
81
  "lit-html": "^3.1.2"
80
82
  },
@@ -88,18 +90,23 @@
88
90
  "@semantic-release/changelog": "^6.0.0",
89
91
  "@semantic-release/git": "^10.0.0",
90
92
  "@storybook/addon-docs": "^10.0.0",
93
+ "@storybook/addon-vitest": "^10.2.4",
91
94
  "@storybook/web-components-vite": "^10.0.0",
92
95
  "@types/mocha": "^10.0.6",
93
96
  "@types/node": "^24.0.0",
97
+ "@vitest/browser": "^4.0.18",
98
+ "@vitest/browser-playwright": "^4.0.18",
94
99
  "esbuild": "^0.25.0",
95
100
  "http-server": "^14.1.1",
96
101
  "husky": "^9.0.11",
97
102
  "lint-staged": "^16.2.7",
98
103
  "rollup-plugin-esbuild": "^6.1.1",
99
104
  "semantic-release": "^25.0.0",
105
+ "shadow-dom-testing-library": "^1.13.1",
100
106
  "sinon": "^21.0.0",
101
107
  "storybook": "^10.0.0",
102
- "typescript": "^5.4.3"
108
+ "typescript": "^5.4.3",
109
+ "vitest": "^4.0.18"
103
110
  },
104
111
  "overrides": {
105
112
  "conventional-changelog-conventionalcommits": ">= 8.0.0"