@neovici/cosmoz-autocomplete 2.17.2 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cosmoz-listbox.js +11 -9
- package/lib/autocomplete.css.js +115 -0
- package/lib/autocomplete.js +20 -87
- package/lib/use-autocomplete.js +18 -8
- package/lib/use-listbox.js +5 -4
- package/package.json +8 -9
- package/lib/use-focus.js +0 -53
- package/lib/use-position.js +0 -49
package/cosmoz-listbox.js
CHANGED
|
@@ -33,24 +33,24 @@ const Listbox = host => {
|
|
|
33
33
|
height: 100%;
|
|
34
34
|
}
|
|
35
35
|
.item {
|
|
36
|
+
font-size: 14px;
|
|
36
37
|
line-height: ${ itemHeight }px;
|
|
37
38
|
height: ${ itemHeight }px;
|
|
38
|
-
padding: 0
|
|
39
|
+
padding: 0 20px;
|
|
39
40
|
box-sizing: border-box;
|
|
40
41
|
width: 100%;
|
|
41
42
|
cursor: pointer;
|
|
42
43
|
overflow: hidden;
|
|
43
44
|
text-overflow: ellipsis;
|
|
44
45
|
white-space: nowrap;
|
|
46
|
+
transition: background 0.2s;
|
|
47
|
+
color: var(--cosmoz-listbox-color, #101010);
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
.item[aria-selected] {
|
|
51
|
-
background: #dadada;
|
|
52
|
-
color: #333;
|
|
49
|
+
|
|
50
|
+
.item[data-index="${ index }"]{
|
|
51
|
+
background: var(--cosmoz-listbox-active-color, var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1)));
|
|
53
52
|
}
|
|
53
|
+
|
|
54
54
|
.sizer {
|
|
55
55
|
position: relative;
|
|
56
56
|
visibility: hidden;
|
|
@@ -59,11 +59,13 @@ const Listbox = host => {
|
|
|
59
59
|
z-index: -1;
|
|
60
60
|
height: 0;
|
|
61
61
|
width: auto;
|
|
62
|
-
padding: 0
|
|
62
|
+
padding: 0 20px;
|
|
63
63
|
overflow: hidden;
|
|
64
64
|
max-width: inherit;
|
|
65
|
+
font-size: 14px;
|
|
65
66
|
}
|
|
66
67
|
.items > :first-child { line-height: 1;}
|
|
68
|
+
${ host.styles || '' }
|
|
67
69
|
</style>
|
|
68
70
|
<div class="items">${ scroll({ items, renderItem }) }</div>`;
|
|
69
71
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { tagged as css } from '@neovici/cosmoz-utils';
|
|
2
|
+
const svg = 'data:image/svg+xml,%3Csvg width=\'11\' height=\'8\' viewBox=\'0 0 11 8\' fill=\'none\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath '
|
|
3
|
+
+ 'd=\'M9.5 1L5.20039 7.04766L1.66348 3.46152\' stroke=\'white\' stroke-width=\'1.5\' stroke-linecap=\'round\' stroke-linejoin=\'round\'/%3E%3C/svg%3E';
|
|
4
|
+
|
|
5
|
+
export const listboxStyle = css`
|
|
6
|
+
.item {
|
|
7
|
+
padding-left: 0;
|
|
8
|
+
}
|
|
9
|
+
.item::before {
|
|
10
|
+
content: "";
|
|
11
|
+
font-size: 0;
|
|
12
|
+
padding: 7.5px;
|
|
13
|
+
margin: 0 8px;
|
|
14
|
+
background: #fff;
|
|
15
|
+
border: 1px solid #d6d6d5;
|
|
16
|
+
border-radius: 4px;
|
|
17
|
+
vertical-align: top;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.item[aria-selected]::before {
|
|
21
|
+
border-color: #5881f6;
|
|
22
|
+
background: url("${ svg }") #5881f6 no-repeat 50%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sizer {
|
|
26
|
+
padding-left: 33px;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export default css`
|
|
31
|
+
:host {
|
|
32
|
+
display: block;
|
|
33
|
+
position: relative;
|
|
34
|
+
}
|
|
35
|
+
.chip {
|
|
36
|
+
border-radius: 500px;
|
|
37
|
+
background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
|
|
38
|
+
margin: 1px 4px 1px 0;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: row;
|
|
43
|
+
align-items: center;
|
|
44
|
+
flex: 0.00001 1 min-content;
|
|
45
|
+
max-width: 285px;
|
|
46
|
+
max-width: 28ch;
|
|
47
|
+
}
|
|
48
|
+
.chip[data-one] {
|
|
49
|
+
position: absolute;
|
|
50
|
+
left: 0;
|
|
51
|
+
top: 1px;
|
|
52
|
+
bottom: 1px;
|
|
53
|
+
right: 0;
|
|
54
|
+
margin: 0;
|
|
55
|
+
z-index: 2;
|
|
56
|
+
pointer-events: auto;
|
|
57
|
+
}
|
|
58
|
+
.chip-text {
|
|
59
|
+
color: var(--cosmoz-autocomplete-chip-color, #424242);
|
|
60
|
+
margin: 0 10px;
|
|
61
|
+
font-size: 12px;
|
|
62
|
+
line-height: 22px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
text-overflow: ellipsis;
|
|
65
|
+
flex: 1;
|
|
66
|
+
}
|
|
67
|
+
.chip-clear {
|
|
68
|
+
margin: 2px 4px 2px -6px;
|
|
69
|
+
background-color: var(--cosmoz-autocomplete-chip-clear-bg-color, #81899B);
|
|
70
|
+
border-radius: 500px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
width: 16px;
|
|
73
|
+
height: 16px;
|
|
74
|
+
stroke: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
|
|
75
|
+
display: block;
|
|
76
|
+
}
|
|
77
|
+
.chip-clear svg {
|
|
78
|
+
display: block;
|
|
79
|
+
transform: translate(3.5px, 3.5px);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.badge {
|
|
83
|
+
background: var(--cosmoz-autocomplete-chip-bg-color, #cbcfdb);
|
|
84
|
+
color: var(--cosmoz-autocomplete-chip-color, #424242);
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
line-height: 22px;
|
|
88
|
+
padding: 0 2px;
|
|
89
|
+
border-radius: 90px;
|
|
90
|
+
text-align: center;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
flex: 0.01 2 30px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
slot {
|
|
97
|
+
display: contents !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes rotating {
|
|
101
|
+
100% {
|
|
102
|
+
transform: rotate(360deg);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
.spinner {
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
width: 22px;
|
|
108
|
+
height: 22px;
|
|
109
|
+
border: 2px solid rgba(0, 0, 0, 0.1);
|
|
110
|
+
border-top: 2px solid #5f5a92;
|
|
111
|
+
animation: rotating 1.2s infinite cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
margin-top: -3px;
|
|
114
|
+
}
|
|
115
|
+
`;
|
package/lib/autocomplete.js
CHANGED
|
@@ -7,96 +7,30 @@ import { array } from '@neovici/cosmoz-utils/lib/array';
|
|
|
7
7
|
import '@neovici/cosmoz-input';
|
|
8
8
|
import { useAutocomplete } from './use-autocomplete';
|
|
9
9
|
import { listbox } from '../cosmoz-listbox';
|
|
10
|
+
import style, { listboxStyle } from './autocomplete.css';
|
|
10
11
|
|
|
11
12
|
const
|
|
12
13
|
blank = () => nothing,
|
|
13
|
-
renderChip = (text, onClear, disabled) => html`
|
|
14
|
-
<div class="chip" part="chip" title=${ text }>
|
|
14
|
+
renderChip = (text, onClear, disabled, isOne) => html`
|
|
15
|
+
<div class="chip" part="chip" slot="suffix" title=${ text } ?data-one=${isOne}>
|
|
15
16
|
<span class="chip-text" part="chip-text">${ text }</span>
|
|
16
17
|
${ !disabled && html`
|
|
17
18
|
<span class="chip-clear" part="chip-clear" @click=${ onClear }>
|
|
18
|
-
<svg viewBox="0 0
|
|
19
|
-
<
|
|
19
|
+
<svg width="9" height="8" viewBox="0 0 9 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
20
|
+
<line x1="7.53033" y1="0.994808" x2="1.16637" y2="7.35877" stroke-width="1.5"/>
|
|
21
|
+
<line x1="7.46967" y1="7.35882" x2="1.10571" y2="0.99486" stroke-width="1.5"/>
|
|
20
22
|
</svg>
|
|
21
23
|
</span>` || nothing }
|
|
22
24
|
</div>
|
|
23
25
|
`,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
display: block;
|
|
30
|
-
position: relative;
|
|
26
|
+
renderBadge = len => len > 0 ? html`<div class="badge" slot="suffix" part="badge">${ len }</div>` : nothing,
|
|
27
|
+
renderSelection = (value, isOne, onDeselect, textual, disabled) => {
|
|
28
|
+
if (isOne || value.length === 1) {
|
|
29
|
+
const val = value[0];
|
|
30
|
+
return val ? renderChip(textual(val), () => onDeselect(val), disabled, isOne) : nothing;
|
|
31
31
|
}
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
flex-direction: row;
|
|
35
|
-
flex-wrap: wrap;
|
|
36
|
-
align-items: flex-end;
|
|
37
|
-
}
|
|
38
|
-
.chip {
|
|
39
|
-
border-radius: 500px;
|
|
40
|
-
background-color: #e0e0e0;
|
|
41
|
-
margin: 1px 4px 1px 0;
|
|
42
|
-
white-space: nowrap;
|
|
43
|
-
overflow: hidden;
|
|
44
|
-
display: flex;
|
|
45
|
-
flex-direction: row;
|
|
46
|
-
align-items: center;
|
|
47
|
-
}
|
|
48
|
-
.chip-text {
|
|
49
|
-
color: #424242;
|
|
50
|
-
margin: 0 10px;
|
|
51
|
-
font-size: 13px;
|
|
52
|
-
overflow: hidden;
|
|
53
|
-
text-overflow: ellipsis;
|
|
54
|
-
flex: 1;
|
|
55
|
-
}
|
|
56
|
-
.chip-clear {
|
|
57
|
-
margin: 2px 4px 2px -6px;
|
|
58
|
-
background-color: #a6a6a6;
|
|
59
|
-
border-radius: 500px;
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
width: 16px;
|
|
62
|
-
height: 16px;
|
|
63
|
-
fill: #cdcdcd;
|
|
64
|
-
display: block;
|
|
65
|
-
}
|
|
66
|
-
.chip-clear svg { display: block; }
|
|
67
|
-
|
|
68
|
-
cosmoz-input .chip {
|
|
69
|
-
position: absolute;
|
|
70
|
-
left: 0;
|
|
71
|
-
top: 1px;
|
|
72
|
-
bottom: 1px;
|
|
73
|
-
right: 0;
|
|
74
|
-
margin: 0;
|
|
75
|
-
z-index: 2;
|
|
76
|
-
pointer-events: auto;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
slot {
|
|
80
|
-
display: contents !important;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@keyframes rotating {
|
|
84
|
-
100% {
|
|
85
|
-
transform: rotate(360deg);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
.spinner {
|
|
89
|
-
border-radius: 50%;
|
|
90
|
-
width: 22px;
|
|
91
|
-
height: 22px;
|
|
92
|
-
border: 2px solid rgba(0, 0, 0, 0.1);
|
|
93
|
-
border-top: 2px solid #5f5a92;
|
|
94
|
-
animation: rotating 1.2s infinite cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
|
95
|
-
box-sizing: border-box;
|
|
96
|
-
margin-top: -3px;
|
|
97
|
-
}
|
|
98
|
-
`,
|
|
99
|
-
|
|
32
|
+
return renderBadge(value.length);
|
|
33
|
+
},
|
|
100
34
|
inputParts = ['input', 'label', 'line', 'error'].map(part => `${ part }: input-${ part }`).join(),
|
|
101
35
|
|
|
102
36
|
autocomplete = props => {
|
|
@@ -123,21 +57,18 @@ const
|
|
|
123
57
|
} = props,
|
|
124
58
|
host = useHost(),
|
|
125
59
|
|
|
126
|
-
|
|
127
|
-
isOne = limit == 1, //eslint-disable-line eqeqeq
|
|
60
|
+
isOne = limit == 1, // eslint-disable-line eqeqeq
|
|
128
61
|
isSingle = isOne && array(value)?.[0] != null,
|
|
129
62
|
anchor = useCallback(() => host.shadowRoot.querySelector('#input'), [host, value]),
|
|
130
63
|
suggestions = until(items$.then(items => (!isSingle || showSingle) && items.length && listbox({
|
|
131
64
|
...props,
|
|
132
65
|
anchor,
|
|
133
|
-
items
|
|
66
|
+
items,
|
|
67
|
+
styles: !isOne ? listboxStyle : undefined
|
|
134
68
|
}) || nothing));
|
|
135
69
|
|
|
136
70
|
return html`
|
|
137
71
|
<style>${ style }</style>
|
|
138
|
-
|
|
139
|
-
${ !isOne && html`<div class="chips">${ chips }</div>` || '' }
|
|
140
|
-
|
|
141
72
|
<cosmoz-input
|
|
142
73
|
id="input" part="input"
|
|
143
74
|
.label=${ label }
|
|
@@ -157,9 +88,10 @@ const
|
|
|
157
88
|
>
|
|
158
89
|
<slot name="prefix" slot="prefix"></slot>
|
|
159
90
|
<slot name="suffix" slot="suffix"></slot>
|
|
160
|
-
${ isOne
|
|
91
|
+
${ renderSelection(array(value), isOne, onDeselect, textual, disabled) }
|
|
161
92
|
${ until(values$.then(blank, blank), html`<div slot="suffix" class="spinner"></div>`) }
|
|
162
93
|
</cosmoz-input>
|
|
94
|
+
|
|
163
95
|
${ suggestions }`;
|
|
164
96
|
},
|
|
165
97
|
Autocomplete = props => autocomplete({
|
|
@@ -174,7 +106,8 @@ const
|
|
|
174
106
|
'text-property',
|
|
175
107
|
'limit',
|
|
176
108
|
'show-single',
|
|
177
|
-
'
|
|
109
|
+
'keep-query',
|
|
110
|
+
'keep-opened',
|
|
178
111
|
'hide-empty',
|
|
179
112
|
'default-index',
|
|
180
113
|
'item-height',
|
package/lib/use-autocomplete.js
CHANGED
|
@@ -2,11 +2,12 @@ import {
|
|
|
2
2
|
useCallback, useMemo
|
|
3
3
|
} from 'haunted';
|
|
4
4
|
import {
|
|
5
|
-
without, search, strProp, prop
|
|
5
|
+
without, search, strProp, prop, array
|
|
6
6
|
} from './utils';
|
|
7
7
|
import { useHost } from '@neovici/cosmoz-utils/lib/hooks/use-host';
|
|
8
|
+
import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
|
|
9
|
+
import { useFocus } from '@neovici/cosmoz-dropdown/src/use-focus.js';
|
|
8
10
|
import { useKeys } from './use-keys';
|
|
9
|
-
import { useFocus } from './use-focus';
|
|
10
11
|
|
|
11
12
|
const
|
|
12
13
|
EMPTY = [],
|
|
@@ -27,7 +28,7 @@ const
|
|
|
27
28
|
valueProperty,
|
|
28
29
|
external,
|
|
29
30
|
hideEmpty,
|
|
30
|
-
|
|
31
|
+
keepQuery,
|
|
31
32
|
...thru
|
|
32
33
|
}) => {
|
|
33
34
|
const
|
|
@@ -45,7 +46,7 @@ const
|
|
|
45
46
|
? source({ query, active })
|
|
46
47
|
: source
|
|
47
48
|
), [source, active, query]),
|
|
48
|
-
values$ = useMemo(() =>
|
|
49
|
+
values$ = useMemo(() => source$.then(source => [...array(value), ...without(value, prop(valueProperty))(source)]), [source$, value, valueProperty]);
|
|
49
50
|
|
|
50
51
|
useKeys({
|
|
51
52
|
active,
|
|
@@ -57,6 +58,8 @@ const
|
|
|
57
58
|
onText
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
const meta = useMeta({ onText, onChange, value, limit, keepQuery, setClosed });
|
|
62
|
+
|
|
60
63
|
return {
|
|
61
64
|
query,
|
|
62
65
|
textual,
|
|
@@ -76,10 +79,17 @@ const
|
|
|
76
79
|
setClosed(false);
|
|
77
80
|
}, [onText, text, setClosed]),
|
|
78
81
|
onSelect: useCallback(newVal => {
|
|
79
|
-
onText
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
const { onChange, onText, limit, value: val, keepQuery, keepOpened } = meta;
|
|
83
|
+
if (!keepQuery) {
|
|
84
|
+
onText('');
|
|
85
|
+
}
|
|
86
|
+
if (!keepOpened) {
|
|
87
|
+
setClosed(true);
|
|
88
|
+
}
|
|
89
|
+
const value = array(val);
|
|
90
|
+
onChange((value.includes(newVal) ? without(newVal)(value) : [...value, newVal]).slice(-limit));
|
|
91
|
+
}, [meta]),
|
|
92
|
+
onDeselect: useCallback(val => meta.onChange(without(val)(meta.value)), [meta])
|
|
83
93
|
};
|
|
84
94
|
};
|
|
85
95
|
|
package/lib/use-listbox.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, useMemo } from 'haunted';
|
|
2
2
|
import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
|
|
3
|
+
import { usePosition } from '@neovici/cosmoz-dropdown/src/use-position.js';
|
|
3
4
|
import { useKeyboard } from './use-keyboard';
|
|
4
5
|
import defaultItemRenderer from './item-renderer';
|
|
5
6
|
import { byValue } from './utils';
|
|
6
|
-
import { usePosition } from './use-position';
|
|
7
7
|
|
|
8
8
|
const useItems = ({
|
|
9
9
|
items,
|
|
@@ -50,7 +50,7 @@ const useItems = ({
|
|
|
50
50
|
query,
|
|
51
51
|
textual,
|
|
52
52
|
itemRenderer,
|
|
53
|
-
itemHeight =
|
|
53
|
+
itemHeight = 40,
|
|
54
54
|
itemLimit = 5,
|
|
55
55
|
anchor,
|
|
56
56
|
confinement,
|
|
@@ -72,7 +72,8 @@ const useItems = ({
|
|
|
72
72
|
anchor,
|
|
73
73
|
host,
|
|
74
74
|
confinement,
|
|
75
|
-
placement
|
|
75
|
+
placement,
|
|
76
|
+
limit: true
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
return {
|
|
@@ -92,7 +93,7 @@ const useItems = ({
|
|
|
92
93
|
})
|
|
93
94
|
};
|
|
94
95
|
},
|
|
95
|
-
properties = ['query', 'items', 'onSelect', 'textual', 'anchor', 'confinement', 'placement', 'itemHeight', 'itemLimit', 'defaultIndex', 'value'];
|
|
96
|
+
properties = ['query', 'items', 'onSelect', 'textual', 'anchor', 'confinement', 'placement', 'itemHeight', 'itemLimit', 'defaultIndex', 'value', 'styles'];
|
|
96
97
|
|
|
97
98
|
export {
|
|
98
99
|
useListbox,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-autocomplete",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "A autocomplete input web component",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lit-html",
|
|
@@ -65,19 +65,18 @@
|
|
|
65
65
|
"position.js": "^0.3.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@commitlint/cli": "^
|
|
69
|
-
"@commitlint/config-conventional": "^
|
|
70
|
-
"@neovici/
|
|
68
|
+
"@commitlint/cli": "^16.0.0",
|
|
69
|
+
"@commitlint/config-conventional": "^16.0.0",
|
|
70
|
+
"@neovici/cfg": "^1.13.0",
|
|
71
|
+
"@neovici/cosmoz-dropdown": "^1.8.0",
|
|
71
72
|
"@open-wc/demoing-storybook": "^2.1.0",
|
|
72
73
|
"@open-wc/testing": "^2.5.32",
|
|
73
74
|
"@semantic-release/changelog": "^6.0.0",
|
|
74
75
|
"@semantic-release/git": "^10.0.0",
|
|
75
76
|
"@storybook/storybook-deployer": "^2.8.5",
|
|
76
|
-
"@web/test-runner": "^0.13.0",
|
|
77
|
-
"@web/test-runner-selenium": "^0.5.0",
|
|
78
77
|
"husky": "^7.0.0",
|
|
79
|
-
"semantic-release": "^
|
|
80
|
-
"sinon": "^
|
|
81
|
-
"typescript": "^4.
|
|
78
|
+
"semantic-release": "^19.0.0",
|
|
79
|
+
"sinon": "^13.0.0",
|
|
80
|
+
"typescript": "^4.6.0"
|
|
82
81
|
}
|
|
83
82
|
}
|
package/lib/use-focus.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'haunted';
|
|
2
|
-
import { useMeta } from '@neovici/cosmoz-utils/lib/hooks/use-meta';
|
|
3
|
-
|
|
4
|
-
const isFocused = t => t.matches(':focus-within'),
|
|
5
|
-
blurEvs = ['focusout', 'blur'],
|
|
6
|
-
isBlurEvent = type => blurEvs.includes(type);
|
|
7
|
-
|
|
8
|
-
export const useFocus = ({ disabled, onFocus }) => {
|
|
9
|
-
const [{ focused, closed } = {}, setState] = useState(),
|
|
10
|
-
active = focused && !disabled,
|
|
11
|
-
meta = useMeta({ closed, onFocus }),
|
|
12
|
-
setClosed = useCallback(closed => setState(p => ({ ...p, closed })), []),
|
|
13
|
-
onToggle = useCallback(e => {
|
|
14
|
-
const target = e.currentTarget;
|
|
15
|
-
return isFocused(target)
|
|
16
|
-
? setState(p => ({ focused: true, closed: !p?.closed }))
|
|
17
|
-
: target.focus();
|
|
18
|
-
}, []);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (!active) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const handler = e => {
|
|
25
|
-
if (e.defaultPrevented) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const { closed } = meta;
|
|
29
|
-
if (e.key === 'Escape' && !closed) {
|
|
30
|
-
e.preventDefault();
|
|
31
|
-
setClosed(true);
|
|
32
|
-
} else if (['ArrowUp', 'Up'].includes(e.key) && closed) {
|
|
33
|
-
e.preventDefault();
|
|
34
|
-
setClosed(false);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
document.addEventListener('keydown', handler, true);
|
|
38
|
-
return () => document.removeEventListener('keydown', handler, true);
|
|
39
|
-
}, [active]);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
active: active && !closed,
|
|
43
|
-
setClosed,
|
|
44
|
-
onToggle,
|
|
45
|
-
onFocus: useCallback(e => {
|
|
46
|
-
const target = e.currentTarget,
|
|
47
|
-
focused = isFocused(target) || isBlurEvent(e.type)
|
|
48
|
-
&& e.relatedTarget?.closest('[data-dropdown]');
|
|
49
|
-
setState({ focused });
|
|
50
|
-
meta.onFocus?.(focused);
|
|
51
|
-
}, [meta])
|
|
52
|
-
};
|
|
53
|
-
};
|
package/lib/use-position.js
DELETED
|
@@ -1,49 +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) => {
|
|
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
|
-
style.minWidth = Math.max(anchorBounds.width, hostBounds.width) + 'px';
|
|
18
|
-
},
|
|
19
|
-
usePosition = ({ anchor: anchorage, host, confinement, placement }) => {
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const anchor = typeof anchorage === 'function' ? anchorage() : anchorage;
|
|
22
|
-
if (anchor == null) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
let rid;
|
|
26
|
-
const reposition = () => position(host, anchor, placement, confinement),
|
|
27
|
-
ro = new ResizeObserver(reposition);
|
|
28
|
-
|
|
29
|
-
ro.observe(host);
|
|
30
|
-
ro.observe(anchor);
|
|
31
|
-
|
|
32
|
-
const onReposition = () => {
|
|
33
|
-
cancelAnimationFrame(rid);
|
|
34
|
-
rid = requestAnimationFrame(reposition);
|
|
35
|
-
},
|
|
36
|
-
offScroll = onScrolled(anchor, onReposition);
|
|
37
|
-
window.addEventListener('resize', onReposition, true);
|
|
38
|
-
|
|
39
|
-
return () => {
|
|
40
|
-
ro.unobserve(host);
|
|
41
|
-
ro.unobserve(anchor);
|
|
42
|
-
offScroll();
|
|
43
|
-
window.removeEventListener('resize', onReposition, true);
|
|
44
|
-
cancelAnimationFrame(rid);
|
|
45
|
-
};
|
|
46
|
-
}, [anchorage, confinement, placement]);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export { usePosition };
|