@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 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
@@ -1,3 +1,4 @@
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
+ import './next/cosmoz-dropdown-next';
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
- "name": "@neovici/cosmoz-dropdown",
3
- "version": "7.0.2",
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",
29
- "test:watch": "wtr --watch",
30
- "storybook:start": "storybook dev -p 8000",
31
- "storybook:build": "storybook build",
32
- "storybook:deploy": "storybook-to-ghpages",
33
- "storybook:preview": "npm run storybook:build && http-server -d ./storybook-static/",
34
- "prepare": "husky"
35
- },
36
- "release": {
37
- "plugins": [
38
- "@semantic-release/commit-analyzer",
39
- "@semantic-release/release-notes-generator",
40
- "@semantic-release/changelog",
41
- "@semantic-release/github",
42
- "@semantic-release/npm",
43
- "@semantic-release/git"
44
- ],
45
- "branch": "master",
46
- "preset": "conventionalcommits"
47
- },
48
- "publishConfig": {
49
- "access": "public"
50
- },
51
- "files": [
52
- "dist/",
53
- "types"
54
- ],
55
- "commitlint": {
56
- "extends": [
57
- "@commitlint/config-conventional"
58
- ],
59
- "rules": {
60
- "body-max-line-length": [
61
- 1,
62
- "always",
63
- 600
64
- ]
65
- }
66
- },
67
- "exports": {
68
- ".": "./dist/index.js",
69
- "./use-focus": "./dist/use-focus.js",
70
- "./use-floating": "./dist/use-floating.js",
71
- "./connectable": "./dist/connectable.js",
72
- "./src/use-focus.js": "./dist/use-focus.js"
73
- },
74
- "dependencies": {
75
- "@floating-ui/dom": "^1.6.12",
76
- "@neovici/cosmoz-utils": "^6.8.1",
77
- "@pionjs/pion": "^2.5.2",
78
- "lit-html": "^3.1.2"
79
- },
80
- "devDependencies": {
81
- "@commitlint/cli": "^20.0.0",
82
- "@commitlint/config-conventional": "^20.0.0",
83
- "@neovici/cfg": "^2.8.0",
84
- "@open-wc/testing": "^4.0.0",
85
- "@semantic-release/changelog": "^6.0.0",
86
- "@semantic-release/git": "^10.0.0",
87
- "@storybook/web-components-vite": "^9.1.5",
88
- "@types/mocha": "^10.0.6",
89
- "@types/node": "^24.0.0",
90
- "esbuild": "^0.25.0",
91
- "http-server": "^14.1.1",
92
- "husky": "^9.0.11",
93
- "lint-staged": "^16.2.7",
94
- "rollup-plugin-esbuild": "^6.1.1",
95
- "semantic-release": "^25.0.0",
96
- "sinon": "^21.0.0",
97
- "storybook": "^9.1.5",
98
- "typescript": "^5.4.3"
99
- },
100
- "overrides": {
101
- "conventional-changelog-conventionalcommits": ">= 8.0.0"
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
  }