@neovici/cosmoz-dropdown 3.0.2 → 3.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/dist/cosmoz-dropdown.d.ts +13 -0
- package/dist/cosmoz-dropdown.js +150 -0
- package/dist/index.js +3 -0
- package/dist/on-scrolled.d.ts +7 -0
- package/dist/on-scrolled.js +36 -0
- package/dist/use-focus.d.ts +16 -0
- package/dist/use-focus.js +54 -0
- package/dist/use-position.d.ts +16 -0
- package/dist/use-position.js +49 -0
- package/package.json +17 -12
- package/types/position.d.ts +49 -0
- package/src/cosmoz-dropdown.js +0 -163
- package/src/on-scrolled.js +0 -37
- package/src/use-focus.js +0 -71
- package/src/use-position.js +0 -51
- /package/{src/index.js → dist/index.d.ts} +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TemplateResult } from 'lit-html';
|
|
2
|
+
import { Placement } from './use-position';
|
|
3
|
+
import { UseFocusOpts } from './use-focus';
|
|
4
|
+
interface ContentProps {
|
|
5
|
+
anchor: HTMLElement;
|
|
6
|
+
placement?: Placement;
|
|
7
|
+
render?: () => TemplateResult;
|
|
8
|
+
}
|
|
9
|
+
declare const Content: (host: HTMLElement & ContentProps) => TemplateResult<1>;
|
|
10
|
+
interface DropdownProps extends UseFocusOpts, Pick<ContentProps, 'placement' | 'render'> {
|
|
11
|
+
}
|
|
12
|
+
declare const Dropdown: (host: HTMLElement & DropdownProps) => TemplateResult<1>;
|
|
13
|
+
export { Dropdown, Content };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { component, useCallback } from 'haunted';
|
|
2
|
+
import { html, nothing } from 'lit-html';
|
|
3
|
+
import { when } from 'lit-html/directives/when.js';
|
|
4
|
+
import { usePosition } from './use-position';
|
|
5
|
+
import { useHostFocus } from './use-focus';
|
|
6
|
+
const preventDefault = (e) => e.preventDefault();
|
|
7
|
+
const Content = (host) => {
|
|
8
|
+
const { anchor, placement, render } = host;
|
|
9
|
+
usePosition({ anchor, placement, host });
|
|
10
|
+
return html ` <style>
|
|
11
|
+
:host {
|
|
12
|
+
position: fixed;
|
|
13
|
+
left: -9999999999px;
|
|
14
|
+
min-width: 72px;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
padding: var(--cosmoz-dropdown-spacing, 0px);
|
|
17
|
+
z-index: var(--cosmoz-dropdown-z-index, 2);
|
|
18
|
+
}
|
|
19
|
+
.wrap {
|
|
20
|
+
background: var(--cosmoz-dropdown-bg-color, #fff);
|
|
21
|
+
box-shadow: var(
|
|
22
|
+
--cosmoz-dropdown-box-shadow,
|
|
23
|
+
0px 3px 4px 2px rgba(0, 0, 0, 0.1)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
::slotted(*) {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
<div class="wrap" part="wrap"><slot></slot>${render?.() || nothing}</div>`;
|
|
31
|
+
};
|
|
32
|
+
const Dropdown = (host) => {
|
|
33
|
+
const { placement, render } = host, anchor = useCallback(() => host.shadowRoot.querySelector('.anchor'), []), { active, onToggle } = useHostFocus(host);
|
|
34
|
+
return html ` <style>
|
|
35
|
+
.anchor {
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
padding: var(--cosmoz-dropdown-anchor-spacing);
|
|
38
|
+
}
|
|
39
|
+
button {
|
|
40
|
+
border: none;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
position: relative;
|
|
43
|
+
pointer-events: auto;
|
|
44
|
+
outline: none;
|
|
45
|
+
background: var(
|
|
46
|
+
--cosmoz-dropdown-button-bg-color,
|
|
47
|
+
var(--cosmoz-button-bg-color, #101010)
|
|
48
|
+
);
|
|
49
|
+
color: var(
|
|
50
|
+
--cosmoz-dropdown-button-color,
|
|
51
|
+
var(--cosmoz-button-color, #fff)
|
|
52
|
+
);
|
|
53
|
+
border-radius: var(--cosmoz-dropdown-button-radius, 50%);
|
|
54
|
+
width: var(
|
|
55
|
+
--cosmoz-dropdown-button-width,
|
|
56
|
+
var(--cosmoz-dropdown-button-size, 40px)
|
|
57
|
+
);
|
|
58
|
+
height: var(
|
|
59
|
+
--cosmoz-dropdown-button-height,
|
|
60
|
+
var(--cosmoz-dropdown-button-size, 40px)
|
|
61
|
+
);
|
|
62
|
+
padding: var(--cosmoz-dropdown-button-padding);
|
|
63
|
+
}
|
|
64
|
+
button:hover {
|
|
65
|
+
background: var(
|
|
66
|
+
--cosmoz-dropdown-button-hover-bg-color,
|
|
67
|
+
var(--cosmoz-button-hover-bg-color, #3a3f44)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
::slotted(svg) {
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
}
|
|
73
|
+
@-moz-document url-prefix() {
|
|
74
|
+
#content {
|
|
75
|
+
left: auto;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
79
|
+
<div class="anchor" part="anchor">
|
|
80
|
+
<button
|
|
81
|
+
@click=${onToggle}
|
|
82
|
+
@mousedown=${preventDefault}
|
|
83
|
+
part="button"
|
|
84
|
+
id="dropdownButton"
|
|
85
|
+
>
|
|
86
|
+
<slot name="button">...</slot>
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
${when(active, () => html ` <cosmoz-dropdown-content
|
|
90
|
+
id="content"
|
|
91
|
+
part="content"
|
|
92
|
+
exportparts="wrap, content"
|
|
93
|
+
.anchor=${anchor}
|
|
94
|
+
.placement=${placement}
|
|
95
|
+
.render=${render}
|
|
96
|
+
>
|
|
97
|
+
<slot></slot>
|
|
98
|
+
</cosmoz-dropdown-content>`)}`;
|
|
99
|
+
};
|
|
100
|
+
const List = () => html `
|
|
101
|
+
<style>
|
|
102
|
+
:host {
|
|
103
|
+
display: contents;
|
|
104
|
+
max-height: var(--cosmoz-dropdown-menu-max-height, calc(96vh - 64px));
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
}
|
|
107
|
+
::slotted(:not(slot)) {
|
|
108
|
+
display: block;
|
|
109
|
+
--paper-button_-_display: block;
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
padding: 10px 24px;
|
|
112
|
+
background: transparent;
|
|
113
|
+
color: var(--cosmoz-dropdown-menu-color, #101010);
|
|
114
|
+
transition: background 0.25s, color 0.25s;
|
|
115
|
+
border: none;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 14px;
|
|
118
|
+
line-height: 20px;
|
|
119
|
+
text-align: left;
|
|
120
|
+
margin: 0;
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
::slotted(:not(slot):hover) {
|
|
125
|
+
background: var(
|
|
126
|
+
--cosmoz-dropdown-menu-hover-color,
|
|
127
|
+
var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
::slotted(:not(slot)[disabled]) {
|
|
132
|
+
opacity: 0.5;
|
|
133
|
+
pointer-events: none;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
<slot></slot>
|
|
137
|
+
`;
|
|
138
|
+
const Menu = ({ placement }) => html ` <cosmoz-dropdown
|
|
139
|
+
.placement=${placement}
|
|
140
|
+
part="dropdown"
|
|
141
|
+
exportparts="anchor, button, content, wrap, dropdown"
|
|
142
|
+
>
|
|
143
|
+
<slot name="button" slot="button"></slot>
|
|
144
|
+
<cosmoz-dropdown-list><slot></slot></cosmoz-dropdown-list>
|
|
145
|
+
</cosmoz-dropdown>`;
|
|
146
|
+
customElements.define('cosmoz-dropdown-content', component(Content));
|
|
147
|
+
customElements.define('cosmoz-dropdown', component(Dropdown));
|
|
148
|
+
customElements.define('cosmoz-dropdown-list', component(List));
|
|
149
|
+
customElements.define('cosmoz-dropdown-menu', component(Menu));
|
|
150
|
+
export { Dropdown, Content };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listen to a scroll event in a ancestor root
|
|
3
|
+
* @param {HTMLElement} el The element
|
|
4
|
+
* @param {function} handler The event handler
|
|
5
|
+
* @returns {function} Function to cleanup the event listeners
|
|
6
|
+
*/
|
|
7
|
+
export declare const onScrolled: <T extends Element>(el: T, handler: (e: Event) => void) => () => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns all ancestor ShadowRoot elements
|
|
3
|
+
* @param {HTMLElement} el The element
|
|
4
|
+
* @param {HTMLElement} limit The limit to stop searching
|
|
5
|
+
* @returns {ShadowRoot[]} Array of ancestor roots
|
|
6
|
+
*/
|
|
7
|
+
const ancestorRoots = (el, limit = document.body) => {
|
|
8
|
+
const roots = [];
|
|
9
|
+
let ancestor = el;
|
|
10
|
+
while (ancestor && ancestor !== limit) {
|
|
11
|
+
if (ancestor instanceof Element && ancestor.assignedSlot) {
|
|
12
|
+
ancestor = ancestor.assignedSlot;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (ancestor instanceof ShadowRoot) {
|
|
16
|
+
roots.push(ancestor);
|
|
17
|
+
ancestor = ancestor.host;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
ancestor = ancestor.parentNode;
|
|
21
|
+
}
|
|
22
|
+
return roots;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Listen to a scroll event in a ancestor root
|
|
26
|
+
* @param {HTMLElement} el The element
|
|
27
|
+
* @param {function} handler The event handler
|
|
28
|
+
* @returns {function} Function to cleanup the event listeners
|
|
29
|
+
*/
|
|
30
|
+
export const onScrolled = (el, handler) => {
|
|
31
|
+
const roots = ancestorRoots(el);
|
|
32
|
+
roots.forEach((r) => r.addEventListener('scroll', handler, true));
|
|
33
|
+
return () => {
|
|
34
|
+
roots.forEach((r) => r.removeEventListener('scroll', handler, true));
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UseFocusOpts {
|
|
2
|
+
disabled?: boolean;
|
|
3
|
+
onFocus: (focused: boolean) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const useFocus: ({ disabled, onFocus }: UseFocusOpts) => {
|
|
6
|
+
active: boolean | undefined;
|
|
7
|
+
setClosed: (closed: boolean) => void;
|
|
8
|
+
onToggle: (e: Event) => void;
|
|
9
|
+
onFocus: (e: FocusEvent) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const useHostFocus: (host: HTMLElement & UseFocusOpts) => {
|
|
12
|
+
active: boolean | undefined;
|
|
13
|
+
setClosed: (closed: boolean) => void;
|
|
14
|
+
onToggle: (e: Event) => void;
|
|
15
|
+
onFocus: (e: FocusEvent) => void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'haunted';
|
|
2
|
+
import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
|
|
3
|
+
const isFocused = (t) => t.matches(':focus-within');
|
|
4
|
+
export const useFocus = ({ disabled, onFocus }) => {
|
|
5
|
+
const [focusState, setState] = useState(), { focused, closed } = focusState || {}, active = focused && !disabled, meta = useMeta({ closed, onFocus }), setClosed = useCallback((closed) => setState((p) => ({ ...p, closed })), []), onToggle = useCallback((e) => {
|
|
6
|
+
const target = e.currentTarget;
|
|
7
|
+
return isFocused(target)
|
|
8
|
+
? setState((p) => ({ focused: true, closed: !p?.closed }))
|
|
9
|
+
: target.focus();
|
|
10
|
+
}, []);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!active) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const handler = (e) => {
|
|
16
|
+
if (e.defaultPrevented) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const { closed } = meta;
|
|
20
|
+
if (e.key === 'Escape' && !closed) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setClosed(true);
|
|
23
|
+
}
|
|
24
|
+
else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setClosed(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
document.addEventListener('keydown', handler, true);
|
|
30
|
+
return () => document.removeEventListener('keydown', handler, true);
|
|
31
|
+
}, [active]);
|
|
32
|
+
return {
|
|
33
|
+
active: active && !closed,
|
|
34
|
+
setClosed,
|
|
35
|
+
onToggle,
|
|
36
|
+
onFocus: useCallback((e) => {
|
|
37
|
+
const focused = isFocused(e.currentTarget);
|
|
38
|
+
setState({ focused });
|
|
39
|
+
meta.onFocus?.(focused);
|
|
40
|
+
}, [meta]),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const fevs = ['focusin', 'focusout'];
|
|
44
|
+
export const useHostFocus = (host) => {
|
|
45
|
+
const thru = useFocus(host), { onFocus } = thru;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
host.setAttribute('tabindex', '-1');
|
|
48
|
+
fevs.forEach((ev) => host.addEventListener(ev, onFocus));
|
|
49
|
+
return () => {
|
|
50
|
+
fevs.forEach((ev) => host.removeEventListener(ev, onFocus));
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
return thru;
|
|
54
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference path="../types/position.d.ts" />
|
|
2
|
+
import type { Placement } from 'position.js';
|
|
3
|
+
export declare const defaultPlacement: readonly ["bottom-left", "bottom-right", "bottom", "top-left", "top-right", "top"];
|
|
4
|
+
export { Placement };
|
|
5
|
+
export interface PositionOpts {
|
|
6
|
+
host: HTMLElement;
|
|
7
|
+
anchor: HTMLElement;
|
|
8
|
+
placement?: Placement;
|
|
9
|
+
confinement?: HTMLElement;
|
|
10
|
+
limit?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const position: ({ host, anchor, placement, confinement, limit, }: PositionOpts) => void;
|
|
13
|
+
interface UsePositionOpts extends Omit<PositionOpts, 'anchor'> {
|
|
14
|
+
anchor?: (() => HTMLElement) | HTMLElement | null;
|
|
15
|
+
}
|
|
16
|
+
export declare const usePosition: ({ anchor: anchorage, host, ...thru }: UsePositionOpts) => void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// eslint-disable-next-line
|
|
2
|
+
/// <reference path="../types/position.d.ts" />
|
|
3
|
+
import { useEffect } from 'haunted';
|
|
4
|
+
import getPosition from 'position.js';
|
|
5
|
+
import { onScrolled } from './on-scrolled';
|
|
6
|
+
export const defaultPlacement = [
|
|
7
|
+
'bottom-left',
|
|
8
|
+
'bottom-right',
|
|
9
|
+
'bottom',
|
|
10
|
+
'top-left',
|
|
11
|
+
'top-right',
|
|
12
|
+
'top',
|
|
13
|
+
];
|
|
14
|
+
export const position = ({ host, anchor, placement = defaultPlacement, confinement, limit, }) => {
|
|
15
|
+
const anchorBounds = anchor.getBoundingClientRect(), hostBounds = host.getBoundingClientRect(), { popupOffset: offset } = getPosition(hostBounds, anchorBounds, placement, {
|
|
16
|
+
fixed: true,
|
|
17
|
+
adjustXY: 'both',
|
|
18
|
+
offsetParent: confinement,
|
|
19
|
+
}), { style } = host;
|
|
20
|
+
style.left = offset.left + 'px';
|
|
21
|
+
style.top = offset.top + 'px';
|
|
22
|
+
if (limit) {
|
|
23
|
+
style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const usePosition = ({ anchor: anchorage, host, ...thru }) => {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
|
|
29
|
+
if (anchor == null) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let rid;
|
|
33
|
+
const reposition = () => position({ host, anchor, ...thru }), ro = new ResizeObserver(reposition);
|
|
34
|
+
ro.observe(host);
|
|
35
|
+
ro.observe(anchor);
|
|
36
|
+
const onReposition = () => {
|
|
37
|
+
cancelAnimationFrame(rid);
|
|
38
|
+
rid = requestAnimationFrame(reposition);
|
|
39
|
+
}, offScroll = onScrolled(anchor, onReposition);
|
|
40
|
+
window.addEventListener('resize', onReposition, true);
|
|
41
|
+
return () => {
|
|
42
|
+
ro.unobserve(host);
|
|
43
|
+
ro.unobserve(anchor);
|
|
44
|
+
offScroll();
|
|
45
|
+
window.removeEventListener('resize', onReposition, true);
|
|
46
|
+
cancelAnimationFrame(rid);
|
|
47
|
+
};
|
|
48
|
+
}, [anchorage, ...Object.values(thru)]);
|
|
49
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-dropdown",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A simple dropdown web component",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lit-html",
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": "",
|
|
19
|
-
"main": "
|
|
19
|
+
"main": "dist/index.js",
|
|
20
20
|
"directories": {
|
|
21
21
|
"test": "test"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"lint": "eslint --cache
|
|
25
|
-
"
|
|
24
|
+
"lint": "tsc && eslint --cache .",
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
26
|
"start": "wds",
|
|
27
27
|
"test": "wtr --coverage",
|
|
28
28
|
"test:watch": "wtr --watch",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
|
-
"
|
|
49
|
+
"dist/",
|
|
50
|
+
"types"
|
|
50
51
|
],
|
|
51
52
|
"commitlint": {
|
|
52
53
|
"extends": [
|
|
@@ -60,6 +61,13 @@
|
|
|
60
61
|
]
|
|
61
62
|
}
|
|
62
63
|
},
|
|
64
|
+
"exports": {
|
|
65
|
+
".": "./dist/index.js",
|
|
66
|
+
"./use-focus": "./dist/use-focus.js",
|
|
67
|
+
"./use-position": "./dist/use-position.js",
|
|
68
|
+
"./src/use-focus.js": "./dist/use-focus.js",
|
|
69
|
+
"./src/use-position.js": "./dist/use-position.js"
|
|
70
|
+
},
|
|
63
71
|
"dependencies": {
|
|
64
72
|
"@neovici/cosmoz-utils": "^5.0.0",
|
|
65
73
|
"haunted": "^5.0.0",
|
|
@@ -74,13 +82,10 @@
|
|
|
74
82
|
"@semantic-release/changelog": "^6.0.0",
|
|
75
83
|
"@semantic-release/git": "^10.0.0",
|
|
76
84
|
"@storybook/storybook-deployer": "^2.8.5",
|
|
77
|
-
"@web/dev-server": "^0.
|
|
78
|
-
"@web/dev-server-storybook": "^0.5.1",
|
|
79
|
-
"@web/test-runner": "^0.13.0",
|
|
85
|
+
"@web/dev-server-storybook": "^0.6.0",
|
|
80
86
|
"husky": "^8.0.0",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"typescript": "^4.0.0"
|
|
87
|
+
"semantic-release": "^21.0.0",
|
|
88
|
+
"sinon": "^15.0.0",
|
|
89
|
+
"typescript": "^5.0.0"
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare module 'position.js' {
|
|
2
|
+
export type PlacementDirection =
|
|
3
|
+
| 'bottom'
|
|
4
|
+
| 'top'
|
|
5
|
+
| 'right'
|
|
6
|
+
| 'left'
|
|
7
|
+
| 'center'
|
|
8
|
+
| 'left-center'
|
|
9
|
+
| 'top-center'
|
|
10
|
+
| 'bottom-center'
|
|
11
|
+
| 'bottom-left'
|
|
12
|
+
| 'bottom-right'
|
|
13
|
+
| 'bottom'
|
|
14
|
+
| 'top-left'
|
|
15
|
+
| 'top-right'
|
|
16
|
+
| 'top';
|
|
17
|
+
|
|
18
|
+
export interface PlacementPrecise {
|
|
19
|
+
popup: PlacementDirection;
|
|
20
|
+
anchor: PlacementDirection;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Placement =
|
|
24
|
+
| PlacementPrecise
|
|
25
|
+
| PlacementDirection
|
|
26
|
+
| readonly PlacementDirection[]
|
|
27
|
+
| readonly PlacementPrecise[];
|
|
28
|
+
|
|
29
|
+
export interface Bounds {
|
|
30
|
+
top: number;
|
|
31
|
+
left: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function position(
|
|
35
|
+
hostBounds: Bounds,
|
|
36
|
+
anchorBounds: Bounds,
|
|
37
|
+
placement: Placement,
|
|
38
|
+
options?: {
|
|
39
|
+
fixed?: boolean;
|
|
40
|
+
adjustXY?: 'both';
|
|
41
|
+
offsetParent?: HTMLElement;
|
|
42
|
+
}
|
|
43
|
+
): {
|
|
44
|
+
popupOffset: {
|
|
45
|
+
top: number;
|
|
46
|
+
left: number;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/cosmoz-dropdown.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { component, useCallback } from 'haunted';
|
|
2
|
-
import { html, nothing } from 'lit-html';
|
|
3
|
-
import { when } from 'lit-html/directives/when.js';
|
|
4
|
-
import { usePosition } from './use-position';
|
|
5
|
-
import { useHostFocus } from './use-focus';
|
|
6
|
-
|
|
7
|
-
const preventDefault = (e) => e.preventDefault(),
|
|
8
|
-
Content = (host) => {
|
|
9
|
-
const { anchor, placement, render } = host;
|
|
10
|
-
usePosition({ anchor, placement, host });
|
|
11
|
-
return html` <style>
|
|
12
|
-
:host {
|
|
13
|
-
position: fixed;
|
|
14
|
-
left: -9999999999px;
|
|
15
|
-
min-width: 72px;
|
|
16
|
-
box-sizing: border-box;
|
|
17
|
-
padding: var(--cosmoz-dropdown-spacing, 0px);
|
|
18
|
-
z-index: var(--cosmoz-dropdown-z-index, 2);
|
|
19
|
-
}
|
|
20
|
-
.wrap {
|
|
21
|
-
background: var(--cosmoz-dropdown-bg-color, #fff);
|
|
22
|
-
box-shadow: var(
|
|
23
|
-
--cosmoz-dropdown-box-shadow,
|
|
24
|
-
0px 3px 4px 2px rgba(0, 0, 0, 0.1)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
::slotted(*) {
|
|
28
|
-
display: block;
|
|
29
|
-
}
|
|
30
|
-
</style>
|
|
31
|
-
<div class="wrap" part="wrap">
|
|
32
|
-
<slot></slot>${render?.() || nothing}
|
|
33
|
-
</div>`;
|
|
34
|
-
},
|
|
35
|
-
Dropdown = (host) => {
|
|
36
|
-
const { placement, render } = host,
|
|
37
|
-
anchor = useCallback(() => host.shadowRoot.querySelector('.anchor'), []),
|
|
38
|
-
{ active, onToggle } = useHostFocus(host);
|
|
39
|
-
return html`
|
|
40
|
-
<style>
|
|
41
|
-
.anchor {
|
|
42
|
-
pointer-events: none;
|
|
43
|
-
padding: var(--cosmoz-dropdown-anchor-spacing);
|
|
44
|
-
}
|
|
45
|
-
button {
|
|
46
|
-
border: none;
|
|
47
|
-
cursor: pointer;
|
|
48
|
-
position: relative;
|
|
49
|
-
pointer-events: auto;
|
|
50
|
-
outline: none;
|
|
51
|
-
background: var(
|
|
52
|
-
--cosmoz-dropdown-button-bg-color,
|
|
53
|
-
var(--cosmoz-button-bg-color, #101010)
|
|
54
|
-
);
|
|
55
|
-
color: var(
|
|
56
|
-
--cosmoz-dropdown-button-color,
|
|
57
|
-
var(--cosmoz-button-color, #fff)
|
|
58
|
-
);
|
|
59
|
-
border-radius: var(--cosmoz-dropdown-button-radius, 50%);
|
|
60
|
-
width: var(
|
|
61
|
-
--cosmoz-dropdown-button-width,
|
|
62
|
-
var(--cosmoz-dropdown-button-size, 40px)
|
|
63
|
-
);
|
|
64
|
-
height: var(
|
|
65
|
-
--cosmoz-dropdown-button-height,
|
|
66
|
-
var(--cosmoz-dropdown-button-size, 40px)
|
|
67
|
-
);
|
|
68
|
-
padding: var(--cosmoz-dropdown-button-padding);
|
|
69
|
-
}
|
|
70
|
-
button:hover {
|
|
71
|
-
background: var(
|
|
72
|
-
--cosmoz-dropdown-button-hover-bg-color,
|
|
73
|
-
var(--cosmoz-button-hover-bg-color, #3a3f44)
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
::slotted(svg) {
|
|
77
|
-
pointer-events: none;
|
|
78
|
-
}
|
|
79
|
-
@-moz-document url-prefix() {
|
|
80
|
-
#content {
|
|
81
|
-
left: auto;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
</style>
|
|
85
|
-
<div class="anchor" part="anchor">
|
|
86
|
-
<button
|
|
87
|
-
@click=${onToggle}
|
|
88
|
-
@mousedown=${preventDefault}
|
|
89
|
-
part="button"
|
|
90
|
-
id="dropdownButton"
|
|
91
|
-
>
|
|
92
|
-
<slot name="button">...</slot>
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
${when(
|
|
96
|
-
active,
|
|
97
|
-
() =>
|
|
98
|
-
html` <cosmoz-dropdown-content
|
|
99
|
-
id="content"
|
|
100
|
-
part="content"
|
|
101
|
-
exportparts="wrap, content"
|
|
102
|
-
.anchor=${anchor}
|
|
103
|
-
.placement=${placement}
|
|
104
|
-
.render=${render}
|
|
105
|
-
>
|
|
106
|
-
<slot></slot>
|
|
107
|
-
</cosmoz-dropdown-content>`
|
|
108
|
-
)}
|
|
109
|
-
`;
|
|
110
|
-
},
|
|
111
|
-
List = () => html`
|
|
112
|
-
<style>
|
|
113
|
-
:host {
|
|
114
|
-
display: contents;
|
|
115
|
-
max-height: var(--cosmoz-dropdown-menu-max-height, calc(96vh - 64px));
|
|
116
|
-
overflow-y: auto;
|
|
117
|
-
}
|
|
118
|
-
::slotted(:not(slot)) {
|
|
119
|
-
display: block;
|
|
120
|
-
--paper-button_-_display: block;
|
|
121
|
-
box-sizing: border-box;
|
|
122
|
-
padding: 10px 24px;
|
|
123
|
-
background: transparent;
|
|
124
|
-
color: var(--cosmoz-dropdown-menu-color, #101010);
|
|
125
|
-
transition: background 0.25s, color 0.25s;
|
|
126
|
-
border: none;
|
|
127
|
-
cursor: pointer;
|
|
128
|
-
font-size: 14px;
|
|
129
|
-
line-height: 20px;
|
|
130
|
-
text-align: left;
|
|
131
|
-
margin: 0;
|
|
132
|
-
width: 100%;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
::slotted(:not(slot):hover) {
|
|
136
|
-
background: var(
|
|
137
|
-
--cosmoz-dropdown-menu-hover-color,
|
|
138
|
-
var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
::slotted(:not(slot)[disabled]) {
|
|
143
|
-
opacity: 0.5;
|
|
144
|
-
pointer-events: none;
|
|
145
|
-
}
|
|
146
|
-
</style>
|
|
147
|
-
<slot></slot>
|
|
148
|
-
`,
|
|
149
|
-
Menu = ({ placement }) => html` <cosmoz-dropdown
|
|
150
|
-
.placement=${placement}
|
|
151
|
-
part="dropdown"
|
|
152
|
-
exportparts="anchor, button, content, wrap, dropdown"
|
|
153
|
-
>
|
|
154
|
-
<slot name="button" slot="button"></slot>
|
|
155
|
-
<cosmoz-dropdown-list><slot></slot></cosmoz-dropdown-list>
|
|
156
|
-
</cosmoz-dropdown>`;
|
|
157
|
-
|
|
158
|
-
customElements.define('cosmoz-dropdown-content', component(Content));
|
|
159
|
-
customElements.define('cosmoz-dropdown', component(Dropdown));
|
|
160
|
-
customElements.define('cosmoz-dropdown-list', component(List));
|
|
161
|
-
customElements.define('cosmoz-dropdown-menu', component(Menu));
|
|
162
|
-
|
|
163
|
-
export { Dropdown, Content };
|
package/src/on-scrolled.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Returns all ancestor ShadowRoot elements
|
|
3
|
-
* @param {HTMLElement} el The element
|
|
4
|
-
* @param {HTMLElement} limit The limit to stop searching
|
|
5
|
-
* @returns {ShadowRoot[]} Array of ancestor roots
|
|
6
|
-
*/
|
|
7
|
-
const ancestorRoots = (el, limit = document.body) => {
|
|
8
|
-
const roots = [];
|
|
9
|
-
let ancestor = el;
|
|
10
|
-
while (ancestor && ancestor !== limit) {
|
|
11
|
-
if (ancestor.assignedSlot) {
|
|
12
|
-
ancestor = ancestor.assignedSlot;
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (ancestor instanceof ShadowRoot) {
|
|
16
|
-
roots.push(ancestor);
|
|
17
|
-
ancestor = ancestor.host;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
ancestor = ancestor.parentNode;
|
|
21
|
-
}
|
|
22
|
-
return roots;
|
|
23
|
-
},
|
|
24
|
-
/**
|
|
25
|
-
* Listen to a scroll event in a ancestor root
|
|
26
|
-
* @param {HTMLElement} el The element
|
|
27
|
-
* @param {function} handler The event handler
|
|
28
|
-
* @returns {function} Function to cleanup the event listeners
|
|
29
|
-
*/
|
|
30
|
-
onScrolled = (el, handler) => {
|
|
31
|
-
const roots = ancestorRoots(el);
|
|
32
|
-
roots.forEach(r => r.addEventListener('scroll', handler, true));
|
|
33
|
-
return () => {
|
|
34
|
-
roots.forEach(r => r.removeEventListener('scroll', handler, true));
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
export { onScrolled };
|
package/src/use-focus.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'haunted';
|
|
2
|
-
import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
|
|
3
|
-
|
|
4
|
-
const isFocused = t => t.matches(':focus-within');
|
|
5
|
-
|
|
6
|
-
export const useFocus = ({ disabled, onFocus }) => {
|
|
7
|
-
const [{ focused, closed } = {}, setState] = useState(),
|
|
8
|
-
active = focused && !disabled,
|
|
9
|
-
meta = useMeta({ closed, onFocus }),
|
|
10
|
-
setClosed = useCallback(
|
|
11
|
-
closed => setState(p => ({ ...p, closed })),
|
|
12
|
-
[]
|
|
13
|
-
),
|
|
14
|
-
onToggle = useCallback(e => {
|
|
15
|
-
const target = e.currentTarget;
|
|
16
|
-
return isFocused(target)
|
|
17
|
-
? setState(p => ({ focused: true, closed: !p?.closed }))
|
|
18
|
-
: target.focus();
|
|
19
|
-
}, []);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!active) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const handler = e => {
|
|
26
|
-
if (e.defaultPrevented) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const { closed } = meta;
|
|
30
|
-
if (e.key === 'Escape' && !closed) {
|
|
31
|
-
e.preventDefault();
|
|
32
|
-
setClosed(true);
|
|
33
|
-
} else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
|
|
34
|
-
e.preventDefault();
|
|
35
|
-
setClosed(false);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
document.addEventListener('keydown', handler, true);
|
|
39
|
-
return () => document.removeEventListener('keydown', handler, true);
|
|
40
|
-
}, [active]);
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
active: active && !closed,
|
|
44
|
-
setClosed,
|
|
45
|
-
onToggle,
|
|
46
|
-
onFocus: useCallback(
|
|
47
|
-
e => {
|
|
48
|
-
const focused = isFocused(e.currentTarget);
|
|
49
|
-
setState({ focused });
|
|
50
|
-
meta.onFocus?.(focused);
|
|
51
|
-
},
|
|
52
|
-
[meta]
|
|
53
|
-
)
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const fevs = ['focusin', 'focusout'];
|
|
58
|
-
export const useHostFocus = host => {
|
|
59
|
-
const thru = useFocus(host),
|
|
60
|
-
{ onFocus } = thru;
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
host.setAttribute('tabindex', '-1');
|
|
64
|
-
fevs.forEach(ev => host.addEventListener(ev, onFocus));
|
|
65
|
-
return () => {
|
|
66
|
-
fevs.forEach(ev => host.removeEventListener(ev, onFocus));
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
return thru;
|
|
71
|
-
};
|
package/src/use-position.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'haunted';
|
|
2
|
-
import getPosition from 'position.js';
|
|
3
|
-
import { onScrolled } from './on-scrolled';
|
|
4
|
-
|
|
5
|
-
const defaultPlacement = ['bottom-left', 'bottom-right', 'bottom', 'top-left', 'top-right', 'top'],
|
|
6
|
-
position = ({ host, anchor, placement = defaultPlacement, confinement, limit }) => {
|
|
7
|
-
const anchorBounds = anchor.getBoundingClientRect(),
|
|
8
|
-
hostBounds = host.getBoundingClientRect(),
|
|
9
|
-
{ popupOffset: offset } = getPosition(hostBounds, anchorBounds, placement, {
|
|
10
|
-
fixed: true,
|
|
11
|
-
adjustXY: 'both',
|
|
12
|
-
offsetParent: confinement
|
|
13
|
-
}),
|
|
14
|
-
{ style } = host;
|
|
15
|
-
style.left = offset.left + 'px';
|
|
16
|
-
style.top = offset.top + 'px';
|
|
17
|
-
if (limit) {
|
|
18
|
-
style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
usePosition = ({ anchor: anchorage, host, ...thru }) => {
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
|
|
24
|
-
if (anchor == null) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
let rid;
|
|
28
|
-
const reposition = () => position({ host, anchor, ...thru }),
|
|
29
|
-
ro = new ResizeObserver(reposition);
|
|
30
|
-
|
|
31
|
-
ro.observe(host);
|
|
32
|
-
ro.observe(anchor);
|
|
33
|
-
|
|
34
|
-
const onReposition = () => {
|
|
35
|
-
cancelAnimationFrame(rid);
|
|
36
|
-
rid = requestAnimationFrame(reposition);
|
|
37
|
-
},
|
|
38
|
-
offScroll = onScrolled(anchor, onReposition);
|
|
39
|
-
window.addEventListener('resize', onReposition, true);
|
|
40
|
-
|
|
41
|
-
return () => {
|
|
42
|
-
ro.unobserve(host);
|
|
43
|
-
ro.unobserve(anchor);
|
|
44
|
-
offScroll();
|
|
45
|
-
window.removeEventListener('resize', onReposition, true);
|
|
46
|
-
cancelAnimationFrame(rid);
|
|
47
|
-
};
|
|
48
|
-
}, [anchorage, ...Object.values(thru)]);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export { usePosition, defaultPlacement };
|
|
File without changes
|