@bsky.app/sift 0.2.5 → 0.2.7
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/CHANGELOG.md +14 -0
- package/README.md +70 -35
- package/build/Sift.d.ts.map +1 -1
- package/build/Sift.js +30 -22
- package/build/Sift.js.map +1 -1
- package/build/computeStyles.d.ts.map +1 -1
- package/build/computeStyles.js +7 -5
- package/build/computeStyles.js.map +1 -1
- package/build/computeStyles.web.d.ts +1 -1
- package/build/computeStyles.web.d.ts.map +1 -1
- package/build/computeStyles.web.js +12 -5
- package/build/computeStyles.web.js.map +1 -1
- package/package.json +1 -1
- package/src/Sift.tsx +40 -29
- package/src/computeStyles.ts +7 -6
- package/src/computeStyles.web.ts +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bsky.app/sift
|
|
2
2
|
|
|
3
|
+
## 0.2.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`8f1f49c`](https://github.com/bluesky-social/toolbox/commit/8f1f49cdff0b5060ed0d7cafb56b77eb994bc758) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Position using `bottom` for top placements
|
|
8
|
+
|
|
9
|
+
- [`f9f7c41`](https://github.com/bluesky-social/toolbox/commit/f9f7c41b38ad8e33611effdedefc53271b666db4) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Update tapper/sift docs
|
|
10
|
+
|
|
11
|
+
## 0.2.6
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`4765658`](https://github.com/bluesky-social/toolbox/commit/4765658af7c35212d3fab55c306c60479162bbd2) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Minor optimizations
|
|
16
|
+
|
|
3
17
|
## 0.2.5
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# @bsky.app/sift
|
|
2
2
|
|
|
3
|
-
A headless autocomplete UI library for React Native (including web).
|
|
4
|
-
|
|
5
|
-
navigation on web.
|
|
3
|
+
A headless autocomplete UI library for React Native (including web). Handles
|
|
4
|
+
popover positioning, keyboard navigation, and accessibility.
|
|
6
5
|
|
|
7
6
|
## Installation
|
|
8
7
|
|
|
@@ -10,14 +9,14 @@ navigation on web.
|
|
|
10
9
|
pnpm add @bsky.app/sift
|
|
11
10
|
```
|
|
12
11
|
|
|
13
|
-
Peer dependencies: `react`, `react-native`, `expo
|
|
12
|
+
Peer dependencies: `react`, `react-native`, `expo`.
|
|
14
13
|
|
|
15
14
|
## Basic example
|
|
16
15
|
|
|
17
16
|
```tsx
|
|
18
17
|
import {useState} from 'react'
|
|
19
18
|
import {View, Text, TextInput} from 'react-native'
|
|
20
|
-
import {useSift, Sift} from '@bsky.app/sift'
|
|
19
|
+
import {useSift, Sift, SiftItem} from '@bsky.app/sift'
|
|
21
20
|
|
|
22
21
|
const items = [
|
|
23
22
|
{key: '1', label: 'Alice'},
|
|
@@ -48,10 +47,10 @@ function Autocomplete() {
|
|
|
48
47
|
data={filtered}
|
|
49
48
|
onSelect={item => setQuery(item.label)}
|
|
50
49
|
onDismiss={() => setQuery('')}
|
|
51
|
-
render={({active, item}) => (
|
|
52
|
-
<
|
|
50
|
+
render={({active, props, item}) => (
|
|
51
|
+
<SiftItem {...props} style={active && {backgroundColor: '#eee'}}>
|
|
53
52
|
<Text>{item.label}</Text>
|
|
54
|
-
</
|
|
53
|
+
</SiftItem>
|
|
55
54
|
)}
|
|
56
55
|
/>
|
|
57
56
|
)}
|
|
@@ -66,8 +65,7 @@ Items must have a `key: string` property, used internally for list rendering.
|
|
|
66
65
|
|
|
67
66
|
### `offset`
|
|
68
67
|
|
|
69
|
-
Gap in pixels between the
|
|
70
|
-
to `0`.
|
|
68
|
+
Gap in pixels between the anchor element and the popover. Defaults to `0`.
|
|
71
69
|
|
|
72
70
|
```tsx
|
|
73
71
|
const sift = useSift({offset: 8})
|
|
@@ -75,54 +73,91 @@ const sift = useSift({offset: 8})
|
|
|
75
73
|
|
|
76
74
|
### `placement`
|
|
77
75
|
|
|
78
|
-
Controls where the popover appears relative to the
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
Controls where the popover appears relative to the anchor. Accepts `'top'`,
|
|
77
|
+
`'top-start'`, `'top-end'`, `'bottom'`, `'bottom-start'`, or `'bottom-end'`.
|
|
78
|
+
Defaults to `'bottom'`.
|
|
81
79
|
|
|
82
80
|
```tsx
|
|
83
81
|
const sift = useSift({placement: 'top'})
|
|
84
82
|
```
|
|
85
83
|
|
|
84
|
+
### `dynamicWidth`
|
|
85
|
+
|
|
86
|
+
When `true` (default), the popover width follows the anchor. When `false`, the
|
|
87
|
+
popover snaps to the input's left edge and is constrained to the input's width
|
|
88
|
+
via `maxWidth`.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const sift = useSift({dynamicWidth: false})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## `useSift` return value
|
|
95
|
+
|
|
96
|
+
| Property | Description |
|
|
97
|
+
| ------------------ | ------------------------------------------------------------ |
|
|
98
|
+
| `targetProps` | Spread onto the input (`ref`, ARIA attributes) |
|
|
99
|
+
| `refs.setAnchor` | Ref callback for the anchor element (optional) |
|
|
100
|
+
| `refs.setPopover` | Ref callback for the popover (set automatically by `<Sift>`) |
|
|
101
|
+
| `popoverStyles` | Computed position styles applied to the popover |
|
|
102
|
+
| `updatePosition` | Re-measure and update popover position |
|
|
103
|
+
| `elements.input` | The input element (for keyboard handling) |
|
|
104
|
+
| `elements.popover` | The popover element |
|
|
105
|
+
| `isActive()` | Returns `true` when the popover is mounted |
|
|
106
|
+
|
|
107
|
+
If `refs.setAnchor` is not called, the input element is used as the anchor.
|
|
108
|
+
|
|
86
109
|
## `<Sift>` props
|
|
87
110
|
|
|
88
|
-
### `
|
|
111
|
+
### `render`
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
Render function for each item. Receives `active` (keyboard-highlighted),
|
|
114
|
+
`props` (accessibility attributes + `onPress`), and `item`.
|
|
115
|
+
|
|
116
|
+
Use `<SiftItem>` to wrap your item content — it's a `Pressable` that applies
|
|
117
|
+
the accessibility props correctly.
|
|
92
118
|
|
|
93
119
|
```tsx
|
|
94
120
|
<Sift
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
render={({active, props, item}) => (
|
|
122
|
+
<SiftItem {...props} style={active && {backgroundColor: '#eee'}}>
|
|
123
|
+
<Text>{item.label}</Text>
|
|
124
|
+
</SiftItem>
|
|
125
|
+
)}
|
|
98
126
|
/>
|
|
99
127
|
```
|
|
100
128
|
|
|
101
|
-
### `
|
|
129
|
+
### `onSelect`
|
|
102
130
|
|
|
103
|
-
Called when the user
|
|
104
|
-
|
|
131
|
+
Called when the user selects an item, either by pressing it or by pressing
|
|
132
|
+
Enter/Tab while it's highlighted via keyboard.
|
|
105
133
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
### `onDismiss`
|
|
135
|
+
|
|
136
|
+
Called when the user presses the Escape key (web only).
|
|
109
137
|
|
|
110
138
|
### `inverted`
|
|
111
139
|
|
|
112
140
|
Renders the list bottom-to-top and reverses keyboard navigation to match.
|
|
113
141
|
Useful when the popover opens above the input.
|
|
114
142
|
|
|
143
|
+
### `style`
|
|
144
|
+
|
|
145
|
+
Style applied to the popover wrapper `View`.
|
|
146
|
+
|
|
147
|
+
## `<SiftItem>`
|
|
148
|
+
|
|
149
|
+
A `Pressable` wrapper for autocomplete items. Accepts all `Pressable` props
|
|
150
|
+
plus a `style` that can be a function receiving `{pressed, hovered}` state.
|
|
151
|
+
|
|
115
152
|
```tsx
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)}
|
|
125
|
-
/>
|
|
153
|
+
<SiftItem
|
|
154
|
+
{...props}
|
|
155
|
+
style={s => [
|
|
156
|
+
{padding: 8},
|
|
157
|
+
(active || s.hovered) && {backgroundColor: '#eee'},
|
|
158
|
+
]}>
|
|
159
|
+
<Text>{item.label}</Text>
|
|
160
|
+
</SiftItem>
|
|
126
161
|
```
|
|
127
162
|
|
|
128
163
|
## Keyboard navigation (web)
|
package/build/Sift.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sift.d.ts","sourceRoot":"","sources":["../src/Sift.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,WAAW,CAAA;AAG5C,cAAc,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"Sift.d.ts","sourceRoot":"","sources":["../src/Sift.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAC,KAAK,aAAa,EAAC,MAAM,WAAW,CAAA;AAG5C,cAAc,WAAW,CAAA;AAIzB,wBAAgB,IAAI,CAAC,IAAI,SAAS;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,EAAE,EAC/C,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,KAAK,EACL,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,IAAI,EAAE,CAAA;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE;QACd,MAAM,EAAE,OAAO,CAAA;QACf,KAAK,EAAE;YACL,IAAI,EAAE,MAAM,CAAA;YACZ,eAAe,EAAE,OAAO,CAAA;YACxB,OAAO,EAAE,MAAM,IAAI,CAAA;SACpB,CAAA;QACD,IAAI,EAAE,IAAI,CAAA;KACX,KAAK,KAAK,CAAC,YAAY,CAAA;IACxB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,2CAkFA;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EACF,SAAS,CAAC,SAAS,CAAC,GACpB,CAAC,CAAC,KAAK,EAAE;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,KAAK,SAAS,CAAC,SAAS,CAAC,CAAC,GACvE,SAAS,CAAA;CACd,2CASA"}
|
package/build/Sift.js
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { View, FlatList, Pressable, } from 'react-native';
|
|
4
4
|
import { useKeyboardHandling } from './useKeyboardHandling';
|
|
5
5
|
export * from './useSift';
|
|
6
|
+
const keyExtractor = (item) => item.key;
|
|
6
7
|
export function Sift({ sift, data, render, style, onSelect, onDismiss, inverted, }) {
|
|
7
8
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
8
9
|
const activeIndexRef = useRef(activeIndex);
|
|
9
10
|
activeIndexRef.current = activeIndex;
|
|
11
|
+
const dataLenRef = useRef(data.length);
|
|
12
|
+
dataLenRef.current = data.length;
|
|
10
13
|
const updateRef = useRef(sift.updatePosition);
|
|
11
14
|
updateRef.current = sift.updatePosition;
|
|
15
|
+
const renderRef = useRef(render);
|
|
16
|
+
renderRef.current = render;
|
|
17
|
+
const onSelectRef = useRef(onSelect);
|
|
18
|
+
onSelectRef.current = onSelect;
|
|
12
19
|
useEffect(() => {
|
|
13
|
-
|
|
20
|
+
if (activeIndexRef.current !== 0)
|
|
21
|
+
setActiveIndex(0);
|
|
14
22
|
updateRef.current();
|
|
15
23
|
}, [data.length]);
|
|
16
|
-
const next = () => {
|
|
17
|
-
if (
|
|
24
|
+
const next = useCallback(() => {
|
|
25
|
+
if (dataLenRef.current === 0)
|
|
18
26
|
return;
|
|
19
|
-
setActiveIndex(i => (i + 1) %
|
|
20
|
-
};
|
|
21
|
-
const prev = () => {
|
|
22
|
-
if (
|
|
27
|
+
setActiveIndex(i => (i + 1) % dataLenRef.current);
|
|
28
|
+
}, []);
|
|
29
|
+
const prev = useCallback(() => {
|
|
30
|
+
if (dataLenRef.current === 0)
|
|
23
31
|
return;
|
|
24
|
-
setActiveIndex(i => (i - 1 +
|
|
25
|
-
};
|
|
26
|
-
const first = () => {
|
|
27
|
-
if (
|
|
32
|
+
setActiveIndex(i => (i - 1 + dataLenRef.current) % dataLenRef.current);
|
|
33
|
+
}, []);
|
|
34
|
+
const first = useCallback(() => {
|
|
35
|
+
if (dataLenRef.current === 0)
|
|
28
36
|
return;
|
|
29
37
|
setActiveIndex(0);
|
|
30
|
-
};
|
|
31
|
-
const last = () => {
|
|
32
|
-
if (
|
|
38
|
+
}, []);
|
|
39
|
+
const last = useCallback(() => {
|
|
40
|
+
if (dataLenRef.current === 0)
|
|
33
41
|
return;
|
|
34
|
-
setActiveIndex(
|
|
35
|
-
};
|
|
42
|
+
setActiveIndex(dataLenRef.current - 1);
|
|
43
|
+
}, []);
|
|
36
44
|
useKeyboardHandling({
|
|
37
45
|
sift,
|
|
38
46
|
onArrowDown: inverted ? prev : next,
|
|
@@ -49,15 +57,15 @@ export function Sift({ sift, data, render, style, onSelect, onDismiss, inverted,
|
|
|
49
57
|
// @ts-ignore web only
|
|
50
58
|
onMouseDown: e => {
|
|
51
59
|
e.preventDefault();
|
|
52
|
-
}, children: _jsx(FlatList, { data: data, inverted: inverted, keyExtractor:
|
|
53
|
-
active: items.index ===
|
|
60
|
+
}, children: _jsx(FlatList, { data: data, inverted: inverted, keyExtractor: keyExtractor, extraData: activeIndex, renderItem: useCallback((items) => renderRef.current({
|
|
61
|
+
active: items.index === activeIndexRef.current,
|
|
54
62
|
props: {
|
|
55
63
|
role: 'option',
|
|
56
|
-
'aria-selected': items.index ===
|
|
57
|
-
onPress: () =>
|
|
64
|
+
'aria-selected': items.index === activeIndexRef.current,
|
|
65
|
+
onPress: () => onSelectRef.current?.(items.item),
|
|
58
66
|
},
|
|
59
67
|
item: items.item,
|
|
60
|
-
}), keyboardShouldPersistTaps: "handled" }) }));
|
|
68
|
+
}), []), keyboardShouldPersistTaps: "handled" }) }));
|
|
61
69
|
}
|
|
62
70
|
/**
|
|
63
71
|
* A Pressable wrapper for items rendered in Sift. It applies the necessary
|
package/build/Sift.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sift.js","sourceRoot":"","sources":["../src/Sift.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Sift.js","sourceRoot":"","sources":["../src/Sift.tsx"],"names":[],"mappings":"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","sourcesContent":["import {useCallback, useEffect, useRef, useState} from 'react'\nimport {\n View,\n FlatList,\n Pressable,\n type StyleProp,\n type ViewStyle,\n type PressableProps,\n type Role,\n} from 'react-native'\nimport {type UseSiftReturn} from './useSift'\nimport {useKeyboardHandling} from './useKeyboardHandling'\n\nexport * from './useSift'\n\nconst keyExtractor = (item: {key: string}) => item.key\n\nexport function Sift<Item extends {key: string}>({\n sift,\n data,\n render,\n style,\n onSelect,\n onDismiss,\n inverted,\n}: {\n sift: UseSiftReturn\n data: Item[]\n render: (props: {\n active: boolean\n props: {\n role: string\n 'aria-selected': boolean\n onPress: () => void\n }\n item: Item\n }) => React.ReactElement\n style?: StyleProp<ViewStyle>\n onSelect?: (item: Item) => void\n onDismiss?: () => void\n inverted?: boolean\n}) {\n const [activeIndex, setActiveIndex] = useState(0)\n const activeIndexRef = useRef(activeIndex)\n activeIndexRef.current = activeIndex\n const dataLenRef = useRef(data.length)\n dataLenRef.current = data.length\n const updateRef = useRef(sift.updatePosition)\n updateRef.current = sift.updatePosition\n const renderRef = useRef(render)\n renderRef.current = render\n const onSelectRef = useRef(onSelect)\n onSelectRef.current = onSelect\n\n useEffect(() => {\n if (activeIndexRef.current !== 0) setActiveIndex(0)\n updateRef.current()\n }, [data.length])\n\n const next = useCallback(() => {\n if (dataLenRef.current === 0) return\n setActiveIndex(i => (i + 1) % dataLenRef.current)\n }, [])\n const prev = useCallback(() => {\n if (dataLenRef.current === 0) return\n setActiveIndex(i => (i - 1 + dataLenRef.current) % dataLenRef.current)\n }, [])\n const first = useCallback(() => {\n if (dataLenRef.current === 0) return\n setActiveIndex(0)\n }, [])\n const last = useCallback(() => {\n if (dataLenRef.current === 0) return\n setActiveIndex(dataLenRef.current - 1)\n }, [])\n\n useKeyboardHandling({\n sift,\n onArrowDown: inverted ? prev : next,\n onArrowUp: inverted ? next : prev,\n onHome: inverted ? last : first,\n onEnd: inverted ? first : last,\n onSelect: () => {\n onSelect?.(data[activeIndexRef.current])\n },\n onDismiss,\n })\n\n const hasStyles = sift.popoverStyles.top != null\n\n return (\n <View\n collapsable={false}\n ref={sift.refs.setPopover}\n role={'listbox' as any}\n id={sift.id}\n style={[style, sift.popoverStyles, !hasStyles && {opacity: 0}]}\n // @ts-ignore web only\n onMouseDown={e => {\n e.preventDefault()\n }}>\n <FlatList\n data={data}\n inverted={inverted}\n keyExtractor={keyExtractor}\n extraData={activeIndex}\n renderItem={useCallback(\n (items: {item: Item; index: number}) =>\n renderRef.current({\n active: items.index === activeIndexRef.current,\n props: {\n role: 'option',\n 'aria-selected': items.index === activeIndexRef.current,\n onPress: () => onSelectRef.current?.(items.item),\n },\n item: items.item,\n }),\n [],\n )}\n keyboardShouldPersistTaps=\"handled\"\n />\n </View>\n )\n}\n\n/**\n * A Pressable wrapper for items rendered in Sift. It applies the necessary\n * accessibility props for each item.\n */\nexport function SiftItem({\n children,\n role,\n style,\n ...props\n}: Omit<PressableProps, 'role' | 'style'> & {\n role?: string\n style?:\n | StyleProp<ViewStyle>\n | ((state: {pressed: boolean; hovered: boolean}) => StyleProp<ViewStyle>)\n | undefined\n}) {\n return (\n <Pressable\n role={role as Role}\n style={style as PressableProps['style']}\n {...props}>\n {children}\n </Pressable>\n )\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computeStyles.d.ts","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"computeStyles.d.ts","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AACvD,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,WAAW,CAAA;AAcxC,wBAAsB,aAAa,CACjC,EACE,MAAM,EACN,KAAK,EACL,OAAO,GACR,EAAE;IACD,MAAM,EAAE,GAAG,CAAA;IACX,KAAK,EAAE,GAAG,CAAA;IACV,OAAO,EAAE,GAAG,GAAG,IAAI,CAAA;CACpB,EACD,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,SAAS,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,GACA,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CA8C3B"}
|
package/build/computeStyles.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Dimensions } from 'react-native';
|
|
1
2
|
function measureInWindow(node) {
|
|
2
3
|
return new Promise(resolve => {
|
|
3
4
|
node.measureInWindow((x, y, width, height) => {
|
|
@@ -16,11 +17,11 @@ export async function computeStyles({ anchor, input, popover, }, options) {
|
|
|
16
17
|
if (popoverRect && !popoverRect.width && !popoverRect.height)
|
|
17
18
|
return null;
|
|
18
19
|
const popoverWidth = popoverRect?.width ?? 0;
|
|
19
|
-
const popoverHeight = popoverRect?.height ?? 0;
|
|
20
20
|
const [side, align] = options.placement.split('-');
|
|
21
|
-
let top;
|
|
21
|
+
let top = 'auto';
|
|
22
|
+
let bottom = 'auto';
|
|
22
23
|
if (side === 'top') {
|
|
23
|
-
|
|
24
|
+
bottom = Dimensions.get('window').height - anchorRect.y + options.offset;
|
|
24
25
|
}
|
|
25
26
|
else {
|
|
26
27
|
top = anchorRect.y + anchorRect.height + options.offset;
|
|
@@ -37,12 +38,13 @@ export async function computeStyles({ anchor, input, popover, }, options) {
|
|
|
37
38
|
}
|
|
38
39
|
let maxWidth;
|
|
39
40
|
if (options.dynamicWidth === false) {
|
|
40
|
-
left =
|
|
41
|
-
maxWidth =
|
|
41
|
+
left = anchorRect.x;
|
|
42
|
+
maxWidth = anchorRect.width;
|
|
42
43
|
}
|
|
43
44
|
return {
|
|
44
45
|
position: 'absolute',
|
|
45
46
|
top,
|
|
47
|
+
bottom,
|
|
46
48
|
left,
|
|
47
49
|
maxWidth,
|
|
48
50
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computeStyles.js","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"computeStyles.js","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,UAAU,EAAiB,MAAM,cAAc,CAAA;AAGvD,SAAS,eAAe,CACtB,IAAS;IAET,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,IAAI,CAAC,eAAe,CAClB,CAAC,CAAS,EAAE,CAAS,EAAE,KAAa,EAAE,MAAc,EAAE,EAAE;YACtD,OAAO,CAAC,EAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAC,CAAC,CAAA;QAChC,CAAC,CACF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EACE,MAAM,EACN,KAAK,EACL,OAAO,GAKR,EACD,OAIC;IAED,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,CAAA;IAChD,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,CAAA;IAC9C,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAEnE,qEAAqE;IACrE,2CAA2C;IAC3C,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACtD,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEzE,MAAM,YAAY,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAGhD,CAAA;IAED,IAAI,GAAG,GAAoB,MAAM,CAAA;IACjC,IAAI,MAAM,GAAoB,MAAM,CAAA;IACpC,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAA;IAC1E,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IACzD,CAAC;IAED,IAAI,IAAY,CAAA;IAChB,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,IAAI,GAAG,UAAU,CAAC,CAAC,CAAA;IACrB,CAAC;SAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;QAC3B,IAAI,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,GAAG,YAAY,CAAA;IACvD,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,UAAU,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAC7D,CAAC;IAED,IAAI,QAA4B,CAAA;IAChC,IAAI,OAAO,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;QACnC,IAAI,GAAG,UAAU,CAAC,CAAC,CAAA;QACnB,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAA;IAC7B,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,UAAU;QACpB,GAAG;QACH,MAAM;QACN,IAAI;QACJ,QAAQ;KACT,CAAA;AACH,CAAC","sourcesContent":["import {Dimensions, type ViewStyle} from 'react-native'\nimport {type Placement} from './useSift'\n\nfunction measureInWindow(\n node: any,\n): Promise<{x: number; y: number; width: number; height: number}> {\n return new Promise(resolve => {\n node.measureInWindow(\n (x: number, y: number, width: number, height: number) => {\n resolve({x, y, width, height})\n },\n )\n })\n}\n\nexport async function computeStyles(\n {\n anchor,\n input,\n popover,\n }: {\n anchor: any\n input: any\n popover: any | null\n },\n options: {\n offset: number\n placement: Placement\n dynamicWidth?: boolean\n },\n): Promise<ViewStyle | null> {\n const anchorRect = await measureInWindow(anchor)\n const inputRect = await measureInWindow(input)\n const popoverRect = popover ? await measureInWindow(popover) : null\n\n // If any measurement failed (view not in hierarchy yet), return null\n // so the caller keeps the previous styles.\n if (!anchorRect.width || !inputRect.width) return null\n if (popoverRect && !popoverRect.width && !popoverRect.height) return null\n\n const popoverWidth = popoverRect?.width ?? 0\n const [side, align] = options.placement.split('-') as [\n string,\n string | undefined,\n ]\n\n let top: number | 'auto' = 'auto'\n let bottom: number | 'auto' = 'auto'\n if (side === 'top') {\n bottom = Dimensions.get('window').height - anchorRect.y + options.offset\n } else {\n top = anchorRect.y + anchorRect.height + options.offset\n }\n\n let left: number\n if (align === 'start') {\n left = anchorRect.x\n } else if (align === 'end') {\n left = anchorRect.x + anchorRect.width - popoverWidth\n } else {\n left = anchorRect.x + (anchorRect.width - popoverWidth) / 2\n }\n\n let maxWidth: number | undefined\n if (options.dynamicWidth === false) {\n left = anchorRect.x\n maxWidth = anchorRect.width\n }\n\n return {\n position: 'absolute',\n top,\n bottom,\n left,\n maxWidth,\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computeStyles.web.d.ts","sourceRoot":"","sources":["../src/computeStyles.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,WAAW,CAAA;AAExC,wBAAgB,aAAa,CAC3B,EACE,MAAM,EACN,KAAK,EACL,OAAO,GACR,EAAE;IACD,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CAC5B,EACD,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,SAAS,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,GACA,SAAS,
|
|
1
|
+
{"version":3,"file":"computeStyles.web.d.ts","sourceRoot":"","sources":["../src/computeStyles.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,WAAW,CAAA;AAExC,wBAAgB,aAAa,CAC3B,EACE,MAAM,EACN,KAAK,EACL,OAAO,GACR,EAAE;IACD,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CAC5B,EACD,OAAO,EAAE;IACP,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,SAAS,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,GACA,SAAS,GAAG,IAAI,CAgDlB"}
|
|
@@ -2,12 +2,18 @@ export function computeStyles({ anchor, input, popover, }, options) {
|
|
|
2
2
|
const anchorRect = anchor.getBoundingClientRect();
|
|
3
3
|
const inputRect = input.getBoundingClientRect();
|
|
4
4
|
const popoverRect = popover?.getBoundingClientRect();
|
|
5
|
+
// If any measurement failed (view not in hierarchy yet), return null
|
|
6
|
+
// so the caller keeps the previous styles.
|
|
7
|
+
if (!anchorRect.width || !inputRect.width)
|
|
8
|
+
return null;
|
|
9
|
+
if (popoverRect && !popoverRect.width && !popoverRect.height)
|
|
10
|
+
return null;
|
|
5
11
|
const popoverWidth = popoverRect?.width ?? 0;
|
|
6
|
-
const popoverHeight = popoverRect?.height ?? 0;
|
|
7
12
|
const [side, align] = options.placement.split('-');
|
|
8
|
-
let top;
|
|
13
|
+
let top = 'auto';
|
|
14
|
+
let bottom = 'auto';
|
|
9
15
|
if (side === 'top') {
|
|
10
|
-
|
|
16
|
+
bottom = window.innerHeight - anchorRect.top + options.offset;
|
|
11
17
|
}
|
|
12
18
|
else {
|
|
13
19
|
top = anchorRect.bottom + options.offset + window.scrollY;
|
|
@@ -25,13 +31,14 @@ export function computeStyles({ anchor, input, popover, }, options) {
|
|
|
25
31
|
left += window.scrollX;
|
|
26
32
|
let maxWidth;
|
|
27
33
|
if (options.dynamicWidth === false) {
|
|
28
|
-
left =
|
|
29
|
-
maxWidth =
|
|
34
|
+
left = anchorRect.left + window.scrollX;
|
|
35
|
+
maxWidth = anchorRect.width;
|
|
30
36
|
}
|
|
31
37
|
return {
|
|
32
38
|
// @ts-ignore
|
|
33
39
|
position: 'fixed',
|
|
34
40
|
top,
|
|
41
|
+
bottom,
|
|
35
42
|
left,
|
|
36
43
|
maxWidth,
|
|
37
44
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computeStyles.web.js","sourceRoot":"","sources":["../src/computeStyles.web.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,aAAa,CAC3B,EACE,MAAM,EACN,KAAK,EACL,OAAO,GAKR,EACD,OAIC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAA;IACjD,MAAM,SAAS,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAA;IAC/C,MAAM,WAAW,GAAG,OAAO,EAAE,qBAAqB,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"computeStyles.web.js","sourceRoot":"","sources":["../src/computeStyles.web.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,aAAa,CAC3B,EACE,MAAM,EACN,KAAK,EACL,OAAO,GAKR,EACD,OAIC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAA;IACjD,MAAM,SAAS,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAA;IAC/C,MAAM,WAAW,GAAG,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAEpD,qEAAqE;IACrE,2CAA2C;IAC3C,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACtD,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEzE,MAAM,YAAY,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAGhD,CAAA;IAED,IAAI,GAAG,GAAoB,MAAM,CAAA;IACjC,IAAI,MAAM,GAAoB,MAAM,CAAA;IACpC,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,GAAG,MAAM,CAAC,WAAW,GAAG,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAA;IAC/D,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;IAC3D,CAAC;IAED,IAAI,IAAY,CAAA;IAChB,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,IAAI,GAAG,UAAU,CAAC,IAAI,CAAA;IACxB,CAAC;SAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;QAC3B,IAAI,GAAG,UAAU,CAAC,KAAK,GAAG,YAAY,CAAA;IACxC,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,UAAU,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAChE,CAAC;IACD,IAAI,IAAI,MAAM,CAAC,OAAO,CAAA;IAEtB,IAAI,QAA4B,CAAA;IAChC,IAAI,OAAO,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;QACnC,IAAI,GAAG,UAAU,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,CAAA;QACvC,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAA;IAC7B,CAAC;IAED,OAAO;QACL,aAAa;QACb,QAAQ,EAAE,OAAO;QACjB,GAAG;QACH,MAAM;QACN,IAAI;QACJ,QAAQ;KACT,CAAA;AACH,CAAC","sourcesContent":["import {type ViewStyle} from 'react-native'\nimport {type Placement} from './useSift'\n\nexport function computeStyles(\n {\n anchor,\n input,\n popover,\n }: {\n anchor: HTMLElement\n input: HTMLElement\n popover: HTMLElement | null\n },\n options: {\n offset: number\n placement: Placement\n dynamicWidth?: boolean\n },\n): ViewStyle | null {\n const anchorRect = anchor.getBoundingClientRect()\n const inputRect = input.getBoundingClientRect()\n const popoverRect = popover?.getBoundingClientRect()\n\n // If any measurement failed (view not in hierarchy yet), return null\n // so the caller keeps the previous styles.\n if (!anchorRect.width || !inputRect.width) return null\n if (popoverRect && !popoverRect.width && !popoverRect.height) return null\n\n const popoverWidth = popoverRect?.width ?? 0\n const [side, align] = options.placement.split('-') as [\n string,\n string | undefined,\n ]\n\n let top: number | 'auto' = 'auto'\n let bottom: number | 'auto' = 'auto'\n if (side === 'top') {\n bottom = window.innerHeight - anchorRect.top + options.offset\n } else {\n top = anchorRect.bottom + options.offset + window.scrollY\n }\n\n let left: number\n if (align === 'start') {\n left = anchorRect.left\n } else if (align === 'end') {\n left = anchorRect.right - popoverWidth\n } else {\n left = anchorRect.left + (anchorRect.width - popoverWidth) / 2\n }\n left += window.scrollX\n\n let maxWidth: number | undefined\n if (options.dynamicWidth === false) {\n left = anchorRect.left + window.scrollX\n maxWidth = anchorRect.width\n }\n\n return {\n // @ts-ignore\n position: 'fixed',\n top,\n bottom,\n left,\n maxWidth,\n }\n}\n"]}
|
package/package.json
CHANGED
package/src/Sift.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useEffect, useRef, useState} from 'react'
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
FlatList,
|
|
@@ -13,6 +13,8 @@ import {useKeyboardHandling} from './useKeyboardHandling'
|
|
|
13
13
|
|
|
14
14
|
export * from './useSift'
|
|
15
15
|
|
|
16
|
+
const keyExtractor = (item: {key: string}) => item.key
|
|
17
|
+
|
|
16
18
|
export function Sift<Item extends {key: string}>({
|
|
17
19
|
sift,
|
|
18
20
|
data,
|
|
@@ -41,30 +43,36 @@ export function Sift<Item extends {key: string}>({
|
|
|
41
43
|
const [activeIndex, setActiveIndex] = useState(0)
|
|
42
44
|
const activeIndexRef = useRef(activeIndex)
|
|
43
45
|
activeIndexRef.current = activeIndex
|
|
46
|
+
const dataLenRef = useRef(data.length)
|
|
47
|
+
dataLenRef.current = data.length
|
|
44
48
|
const updateRef = useRef(sift.updatePosition)
|
|
45
49
|
updateRef.current = sift.updatePosition
|
|
50
|
+
const renderRef = useRef(render)
|
|
51
|
+
renderRef.current = render
|
|
52
|
+
const onSelectRef = useRef(onSelect)
|
|
53
|
+
onSelectRef.current = onSelect
|
|
46
54
|
|
|
47
55
|
useEffect(() => {
|
|
48
|
-
setActiveIndex(0)
|
|
56
|
+
if (activeIndexRef.current !== 0) setActiveIndex(0)
|
|
49
57
|
updateRef.current()
|
|
50
58
|
}, [data.length])
|
|
51
59
|
|
|
52
|
-
const next = () => {
|
|
53
|
-
if (
|
|
54
|
-
setActiveIndex(i => (i + 1) %
|
|
55
|
-
}
|
|
56
|
-
const prev = () => {
|
|
57
|
-
if (
|
|
58
|
-
setActiveIndex(i => (i - 1 +
|
|
59
|
-
}
|
|
60
|
-
const first = () => {
|
|
61
|
-
if (
|
|
60
|
+
const next = useCallback(() => {
|
|
61
|
+
if (dataLenRef.current === 0) return
|
|
62
|
+
setActiveIndex(i => (i + 1) % dataLenRef.current)
|
|
63
|
+
}, [])
|
|
64
|
+
const prev = useCallback(() => {
|
|
65
|
+
if (dataLenRef.current === 0) return
|
|
66
|
+
setActiveIndex(i => (i - 1 + dataLenRef.current) % dataLenRef.current)
|
|
67
|
+
}, [])
|
|
68
|
+
const first = useCallback(() => {
|
|
69
|
+
if (dataLenRef.current === 0) return
|
|
62
70
|
setActiveIndex(0)
|
|
63
|
-
}
|
|
64
|
-
const last = () => {
|
|
65
|
-
if (
|
|
66
|
-
setActiveIndex(
|
|
67
|
-
}
|
|
71
|
+
}, [])
|
|
72
|
+
const last = useCallback(() => {
|
|
73
|
+
if (dataLenRef.current === 0) return
|
|
74
|
+
setActiveIndex(dataLenRef.current - 1)
|
|
75
|
+
}, [])
|
|
68
76
|
|
|
69
77
|
useKeyboardHandling({
|
|
70
78
|
sift,
|
|
@@ -94,18 +102,21 @@ export function Sift<Item extends {key: string}>({
|
|
|
94
102
|
<FlatList
|
|
95
103
|
data={data}
|
|
96
104
|
inverted={inverted}
|
|
97
|
-
keyExtractor={
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
keyExtractor={keyExtractor}
|
|
106
|
+
extraData={activeIndex}
|
|
107
|
+
renderItem={useCallback(
|
|
108
|
+
(items: {item: Item; index: number}) =>
|
|
109
|
+
renderRef.current({
|
|
110
|
+
active: items.index === activeIndexRef.current,
|
|
111
|
+
props: {
|
|
112
|
+
role: 'option',
|
|
113
|
+
'aria-selected': items.index === activeIndexRef.current,
|
|
114
|
+
onPress: () => onSelectRef.current?.(items.item),
|
|
115
|
+
},
|
|
116
|
+
item: items.item,
|
|
117
|
+
}),
|
|
118
|
+
[],
|
|
119
|
+
)}
|
|
109
120
|
keyboardShouldPersistTaps="handled"
|
|
110
121
|
/>
|
|
111
122
|
</View>
|
package/src/computeStyles.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type ViewStyle} from 'react-native'
|
|
1
|
+
import {Dimensions, type ViewStyle} from 'react-native'
|
|
2
2
|
import {type Placement} from './useSift'
|
|
3
3
|
|
|
4
4
|
function measureInWindow(
|
|
@@ -39,15 +39,15 @@ export async function computeStyles(
|
|
|
39
39
|
if (popoverRect && !popoverRect.width && !popoverRect.height) return null
|
|
40
40
|
|
|
41
41
|
const popoverWidth = popoverRect?.width ?? 0
|
|
42
|
-
const popoverHeight = popoverRect?.height ?? 0
|
|
43
42
|
const [side, align] = options.placement.split('-') as [
|
|
44
43
|
string,
|
|
45
44
|
string | undefined,
|
|
46
45
|
]
|
|
47
46
|
|
|
48
|
-
let top: number
|
|
47
|
+
let top: number | 'auto' = 'auto'
|
|
48
|
+
let bottom: number | 'auto' = 'auto'
|
|
49
49
|
if (side === 'top') {
|
|
50
|
-
|
|
50
|
+
bottom = Dimensions.get('window').height - anchorRect.y + options.offset
|
|
51
51
|
} else {
|
|
52
52
|
top = anchorRect.y + anchorRect.height + options.offset
|
|
53
53
|
}
|
|
@@ -63,13 +63,14 @@ export async function computeStyles(
|
|
|
63
63
|
|
|
64
64
|
let maxWidth: number | undefined
|
|
65
65
|
if (options.dynamicWidth === false) {
|
|
66
|
-
left =
|
|
67
|
-
maxWidth =
|
|
66
|
+
left = anchorRect.x
|
|
67
|
+
maxWidth = anchorRect.width
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
71
|
position: 'absolute',
|
|
72
72
|
top,
|
|
73
|
+
bottom,
|
|
73
74
|
left,
|
|
74
75
|
maxWidth,
|
|
75
76
|
}
|
package/src/computeStyles.web.ts
CHANGED
|
@@ -16,20 +16,26 @@ export function computeStyles(
|
|
|
16
16
|
placement: Placement
|
|
17
17
|
dynamicWidth?: boolean
|
|
18
18
|
},
|
|
19
|
-
): ViewStyle {
|
|
19
|
+
): ViewStyle | null {
|
|
20
20
|
const anchorRect = anchor.getBoundingClientRect()
|
|
21
21
|
const inputRect = input.getBoundingClientRect()
|
|
22
22
|
const popoverRect = popover?.getBoundingClientRect()
|
|
23
|
+
|
|
24
|
+
// If any measurement failed (view not in hierarchy yet), return null
|
|
25
|
+
// so the caller keeps the previous styles.
|
|
26
|
+
if (!anchorRect.width || !inputRect.width) return null
|
|
27
|
+
if (popoverRect && !popoverRect.width && !popoverRect.height) return null
|
|
28
|
+
|
|
23
29
|
const popoverWidth = popoverRect?.width ?? 0
|
|
24
|
-
const popoverHeight = popoverRect?.height ?? 0
|
|
25
30
|
const [side, align] = options.placement.split('-') as [
|
|
26
31
|
string,
|
|
27
32
|
string | undefined,
|
|
28
33
|
]
|
|
29
34
|
|
|
30
|
-
let top: number
|
|
35
|
+
let top: number | 'auto' = 'auto'
|
|
36
|
+
let bottom: number | 'auto' = 'auto'
|
|
31
37
|
if (side === 'top') {
|
|
32
|
-
|
|
38
|
+
bottom = window.innerHeight - anchorRect.top + options.offset
|
|
33
39
|
} else {
|
|
34
40
|
top = anchorRect.bottom + options.offset + window.scrollY
|
|
35
41
|
}
|
|
@@ -46,14 +52,15 @@ export function computeStyles(
|
|
|
46
52
|
|
|
47
53
|
let maxWidth: number | undefined
|
|
48
54
|
if (options.dynamicWidth === false) {
|
|
49
|
-
left =
|
|
50
|
-
maxWidth =
|
|
55
|
+
left = anchorRect.left + window.scrollX
|
|
56
|
+
maxWidth = anchorRect.width
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
return {
|
|
54
60
|
// @ts-ignore
|
|
55
61
|
position: 'fixed',
|
|
56
62
|
top,
|
|
63
|
+
bottom,
|
|
57
64
|
left,
|
|
58
65
|
maxWidth,
|
|
59
66
|
}
|