@neovici/cosmoz-dropdown 7.0.2 → 7.2.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 +97 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/next/cosmoz-dropdown-next.d.ts +1 -0
- package/dist/next/cosmoz-dropdown-next.js +110 -0
- package/dist/next/use-auto-open.d.ts +12 -0
- package/dist/next/use-auto-open.js +48 -0
- package/package.json +112 -101
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @neovici/cosmoz-dropdown
|
|
2
|
+
|
|
3
|
+
Dropdown components for Neovici applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @neovici/cosmoz-dropdown
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Components
|
|
12
|
+
|
|
13
|
+
### cosmoz-dropdown-next
|
|
14
|
+
|
|
15
|
+
Modern dropdown using the Popover API and CSS Anchor Positioning.
|
|
16
|
+
|
|
17
|
+
#### Usage
|
|
18
|
+
|
|
19
|
+
```html
|
|
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>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### Properties
|
|
31
|
+
|
|
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. |
|
|
37
|
+
|
|
38
|
+
#### Auto-open Modes
|
|
39
|
+
|
|
40
|
+
The `open-on-hover` and `open-on-focus` attributes can be used independently or together:
|
|
41
|
+
|
|
42
|
+
```html
|
|
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>
|
|
59
|
+
</cosmoz-dropdown-next>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When auto-open is enabled:
|
|
63
|
+
|
|
64
|
+
- The dropdown closes with a 100ms delay to allow moving between trigger and content
|
|
65
|
+
- Click still works as a toggle regardless of these settings
|
|
66
|
+
|
|
67
|
+
#### Slots
|
|
68
|
+
|
|
69
|
+
| Slot | Description |
|
|
70
|
+
| --------- | ------------------------------------------- |
|
|
71
|
+
| `button` | The trigger element that opens the dropdown |
|
|
72
|
+
| (default) | The dropdown content |
|
|
73
|
+
|
|
74
|
+
#### Events
|
|
75
|
+
|
|
76
|
+
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:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
menuItem.dispatchEvent(new Event('select', { bubbles: true }));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install
|
|
86
|
+
npm run storybook:start
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Apache-2.0
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export * from './use-focus';
|
|
2
|
-
export * from './cosmoz-dropdown-menu';
|
|
3
1
|
export * from './cosmoz-dropdown';
|
|
2
|
+
export * from './cosmoz-dropdown-menu';
|
|
3
|
+
export * from './use-focus';
|
|
4
|
+
// Next generation dropdown using Popover API and CSS Anchor Positioning
|
|
5
|
+
import './next/cosmoz-dropdown-next';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { component, css, useRef } from '@pionjs/pion';
|
|
2
|
+
import { html } from 'lit-html';
|
|
3
|
+
import { ref } from 'lit-html/directives/ref.js';
|
|
4
|
+
import { useAutoOpen } from './use-auto-open.js';
|
|
5
|
+
/**
|
|
6
|
+
* Autofocus polyfill for slotted content.
|
|
7
|
+
*
|
|
8
|
+
* The HTML spec's autofocus delegate algorithm uses DOM tree traversal,
|
|
9
|
+
* not flat tree, so it doesn't find [autofocus] elements slotted into
|
|
10
|
+
* a popover/dialog. This is a known spec limitation being discussed at:
|
|
11
|
+
* https://github.com/whatwg/html/issues/9245
|
|
12
|
+
*
|
|
13
|
+
* This handler searches slotted content for [autofocus] and focuses it
|
|
14
|
+
* when the popover opens. Can be removed once browsers implement the
|
|
15
|
+
* spec fix (flat tree traversal for dialog/popover focus delegate).
|
|
16
|
+
*/
|
|
17
|
+
const autofocus = (e) => {
|
|
18
|
+
if (e.newState !== 'open')
|
|
19
|
+
return;
|
|
20
|
+
const popover = e.target;
|
|
21
|
+
const slot = popover.querySelector('slot:not([name])');
|
|
22
|
+
const elements = slot?.assignedElements({ flatten: true }) ?? [];
|
|
23
|
+
for (const el of elements) {
|
|
24
|
+
const autofocusEl = el.matches('[autofocus]')
|
|
25
|
+
? el
|
|
26
|
+
: el.querySelector('[autofocus]');
|
|
27
|
+
if (autofocusEl instanceof HTMLElement) {
|
|
28
|
+
autofocusEl.focus();
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const style = css `
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
anchor-name: --dropdown-anchor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[popover] {
|
|
40
|
+
position: fixed;
|
|
41
|
+
position-anchor: --dropdown-anchor;
|
|
42
|
+
inset: unset;
|
|
43
|
+
margin: var(--cz-spacing, 0.25rem);
|
|
44
|
+
position-try-fallbacks:
|
|
45
|
+
flip-block,
|
|
46
|
+
flip-inline,
|
|
47
|
+
flip-block flip-inline;
|
|
48
|
+
|
|
49
|
+
border: none;
|
|
50
|
+
padding: 0;
|
|
51
|
+
background: transparent;
|
|
52
|
+
overflow: visible;
|
|
53
|
+
|
|
54
|
+
/* Animation - open state */
|
|
55
|
+
opacity: 1;
|
|
56
|
+
transform: translateY(0) scale(1);
|
|
57
|
+
|
|
58
|
+
/* Transitions for smooth open/close animation */
|
|
59
|
+
transition:
|
|
60
|
+
opacity 150ms ease-out,
|
|
61
|
+
transform 150ms ease-out,
|
|
62
|
+
overlay 150ms ease-out allow-discrete,
|
|
63
|
+
display 150ms ease-out allow-discrete;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Starting state when popover opens */
|
|
67
|
+
@starting-style {
|
|
68
|
+
[popover]:popover-open {
|
|
69
|
+
opacity: 0;
|
|
70
|
+
transform: translateY(-4px) scale(0.96);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Closing state */
|
|
75
|
+
[popover]:not(:popover-open) {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: translateY(-4px) scale(0.96);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@media (prefers-reduced-motion: reduce) {
|
|
81
|
+
[popover] {
|
|
82
|
+
transition: none;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
const CosmozDropdownNext = (host) => {
|
|
87
|
+
const { placement = 'bottom span-right', openOnHover, openOnFocus } = host;
|
|
88
|
+
const popoverRef = useRef();
|
|
89
|
+
const open = () => popoverRef.current?.showPopover();
|
|
90
|
+
const close = () => popoverRef.current?.hidePopover();
|
|
91
|
+
const toggle = () => popoverRef.current?.togglePopover();
|
|
92
|
+
useAutoOpen({ host, popoverRef, openOnHover, openOnFocus, open, close });
|
|
93
|
+
return html `
|
|
94
|
+
<slot name="button" @click=${toggle}></slot>
|
|
95
|
+
<div
|
|
96
|
+
popover
|
|
97
|
+
style="position-area: ${placement}"
|
|
98
|
+
@toggle=${autofocus}
|
|
99
|
+
@select=${close}
|
|
100
|
+
${ref((el) => el && (popoverRef.current = el))}
|
|
101
|
+
>
|
|
102
|
+
<slot></slot>
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
};
|
|
106
|
+
customElements.define('cosmoz-dropdown-next', component(CosmozDropdownNext, {
|
|
107
|
+
styleSheets: [style],
|
|
108
|
+
observedAttributes: ['placement', 'open-on-hover', 'open-on-focus'],
|
|
109
|
+
shadowRootInit: { mode: 'open', delegatesFocus: true },
|
|
110
|
+
}));
|
|
@@ -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,103 +1,114 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
2
|
+
"name": "@neovici/cosmoz-dropdown",
|
|
3
|
+
"version": "7.2.0",
|
|
4
|
+
"description": "A simple dropdown web component",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lit-html",
|
|
7
|
+
"web-components"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/Neovici/cosmoz-dropdown#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Neovici/cosmoz-dropdown/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/Neovici/cosmoz-dropdown.git"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": "",
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"directories": {
|
|
21
|
+
"test": "test"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"lint": "tsc && eslint --cache .",
|
|
25
|
+
"check:duplicates": "check-duplicate-components",
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"start": "wds",
|
|
28
|
+
"test": "wtr --coverage && vitest --project=storybook --run",
|
|
29
|
+
"test:watch": "wtr --watch",
|
|
30
|
+
"test:storybook": "vitest --project=storybook --run",
|
|
31
|
+
"test:storybook:watch": "vitest --project=storybook",
|
|
32
|
+
"storybook:start": "storybook dev -p 8000",
|
|
33
|
+
"storybook:build": "storybook build",
|
|
34
|
+
"storybook:deploy": "storybook-to-ghpages",
|
|
35
|
+
"storybook:preview": "npm run storybook:build && http-server -d ./storybook-static/",
|
|
36
|
+
"prepare": "husky"
|
|
37
|
+
},
|
|
38
|
+
"release": {
|
|
39
|
+
"plugins": [
|
|
40
|
+
"@semantic-release/commit-analyzer",
|
|
41
|
+
"@semantic-release/release-notes-generator",
|
|
42
|
+
"@semantic-release/changelog",
|
|
43
|
+
"@semantic-release/github",
|
|
44
|
+
"@semantic-release/npm",
|
|
45
|
+
"@semantic-release/git"
|
|
46
|
+
],
|
|
47
|
+
"branch": "master",
|
|
48
|
+
"preset": "conventionalcommits"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist/",
|
|
55
|
+
"types"
|
|
56
|
+
],
|
|
57
|
+
"commitlint": {
|
|
58
|
+
"extends": [
|
|
59
|
+
"@commitlint/config-conventional"
|
|
60
|
+
],
|
|
61
|
+
"rules": {
|
|
62
|
+
"body-max-line-length": [
|
|
63
|
+
1,
|
|
64
|
+
"always",
|
|
65
|
+
600
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"exports": {
|
|
70
|
+
".": "./dist/index.js",
|
|
71
|
+
"./cosmoz-dropdown-next": "./dist/next/cosmoz-dropdown-next.js",
|
|
72
|
+
"./use-focus": "./dist/use-focus.js",
|
|
73
|
+
"./use-floating": "./dist/use-floating.js",
|
|
74
|
+
"./connectable": "./dist/connectable.js",
|
|
75
|
+
"./src/use-focus.js": "./dist/use-focus.js"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@floating-ui/dom": "^1.6.12",
|
|
79
|
+
"@neovici/cosmoz-utils": "^6.8.1",
|
|
80
|
+
"@pionjs/pion": "^2.5.2",
|
|
81
|
+
"lit-html": "^3.1.2"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@commitlint/cli": "^20.0.0",
|
|
85
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
86
|
+
"@neovici/cfg": "^2.8.0",
|
|
87
|
+
"@neovici/cosmoz-button": "^1.0.0",
|
|
88
|
+
"@neovici/cosmoz-tokens": "^3.2.1",
|
|
89
|
+
"@open-wc/testing": "^4.0.0",
|
|
90
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
91
|
+
"@semantic-release/git": "^10.0.0",
|
|
92
|
+
"@storybook/addon-docs": "^10.0.0",
|
|
93
|
+
"@storybook/addon-vitest": "^10.2.4",
|
|
94
|
+
"@storybook/web-components-vite": "^10.0.0",
|
|
95
|
+
"@types/mocha": "^10.0.6",
|
|
96
|
+
"@types/node": "^24.0.0",
|
|
97
|
+
"@vitest/browser": "^4.0.18",
|
|
98
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
99
|
+
"esbuild": "^0.25.0",
|
|
100
|
+
"http-server": "^14.1.1",
|
|
101
|
+
"husky": "^9.0.11",
|
|
102
|
+
"lint-staged": "^16.2.7",
|
|
103
|
+
"rollup-plugin-esbuild": "^6.1.1",
|
|
104
|
+
"semantic-release": "^25.0.0",
|
|
105
|
+
"shadow-dom-testing-library": "^1.13.1",
|
|
106
|
+
"sinon": "^21.0.0",
|
|
107
|
+
"storybook": "^10.0.0",
|
|
108
|
+
"typescript": "^5.4.3",
|
|
109
|
+
"vitest": "^4.0.18"
|
|
110
|
+
},
|
|
111
|
+
"overrides": {
|
|
112
|
+
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
113
|
+
}
|
|
103
114
|
}
|