@bsky.app/sift 0.2.6 → 0.2.8

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @bsky.app/sift
2
2
 
3
+ ## 0.2.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [`ff2c77b`](https://github.com/bluesky-social/toolbox/commit/ff2c77b752cc3e312f06f8d440cb3a46d7483715) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Support `insets` on `useSift`
8
+
9
+ ## 0.2.7
10
+
11
+ ### Patch Changes
12
+
13
+ - [`8f1f49c`](https://github.com/bluesky-social/toolbox/commit/8f1f49cdff0b5060ed0d7cafb56b77eb994bc758) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Position using `bottom` for top placements
14
+
15
+ - [`f9f7c41`](https://github.com/bluesky-social/toolbox/commit/f9f7c41b38ad8e33611effdedefc53271b666db4) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Update tapper/sift docs
16
+
3
17
  ## 0.2.6
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). Uses
4
- `@floating-ui/react-native` for popover positioning and handles keyboard
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`, `@floating-ui/react-native`.
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
- <View style={active && {backgroundColor: '#eee'}}>
50
+ render={({active, props, item}) => (
51
+ <SiftItem {...props} style={active && {backgroundColor: '#eee'}}>
53
52
  <Text>{item.label}</Text>
54
- </View>
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 reference element and the floating popover. Defaults
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 reference element. Accepts
79
- any `@floating-ui/react-native` placement value (`'bottom'`, `'top'`,
80
- `'bottom-start'`, etc.). Defaults to `'bottom'`.
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
- ### `onSelect`
111
+ ### `render`
89
112
 
90
- Called when the user selects an item, either by pressing it or by pressing
91
- Enter/Tab while it's highlighted via keyboard.
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
- onSelect={item => {
96
- setQuery(item.label)
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
- ### `onDismiss`
129
+ ### `onSelect`
102
130
 
103
- Called when the user presses the Escape key (web only). Use this to close the
104
- popover or clear the query.
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
- ```tsx
107
- <Sift onDismiss={() => setQuery('')} />
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
- <Sift
117
- sift={sift}
118
- data={filtered}
119
- inverted
120
- render={({active, item}) => (
121
- <View style={active && {backgroundColor: '#eee'}}>
122
- <Text>{item.label}</Text>
123
- </View>
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)
@@ -8,5 +8,9 @@ export declare function computeStyles({ anchor, input, popover, }: {
8
8
  offset: number;
9
9
  placement: Placement;
10
10
  dynamicWidth?: boolean;
11
+ insets?: {
12
+ top: number;
13
+ bottom: number;
14
+ };
11
15
  }): Promise<ViewStyle | null>;
12
16
  //# sourceMappingURL=computeStyles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"computeStyles.d.ts","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AAC3C,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,CA6C3B"}
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;IACtB,MAAM,CAAC,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAA;CACvC,GACA,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAwD3B"}
@@ -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,14 +17,22 @@ 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
+ const insetTop = options.insets?.top ?? 0;
22
+ const insetBottom = options.insets?.bottom ?? 0;
23
+ let top = 'auto';
24
+ let bottom = 'auto';
25
+ let maxHeight;
22
26
  if (side === 'top') {
23
- top = anchorRect.y - options.offset - popoverHeight;
27
+ bottom = Dimensions.get('window').height - anchorRect.y + options.offset;
28
+ maxHeight = anchorRect.y - options.offset - insetTop;
24
29
  }
25
30
  else {
26
31
  top = anchorRect.y + anchorRect.height + options.offset;
32
+ maxHeight =
33
+ Dimensions.get('window').height -
34
+ insetBottom -
35
+ (anchorRect.y + anchorRect.height + options.offset);
27
36
  }
28
37
  let left;
29
38
  if (align === 'start') {
@@ -37,14 +46,16 @@ export async function computeStyles({ anchor, input, popover, }, options) {
37
46
  }
38
47
  let maxWidth;
39
48
  if (options.dynamicWidth === false) {
40
- left = inputRect.x;
41
- maxWidth = inputRect.width;
49
+ left = anchorRect.x;
50
+ maxWidth = anchorRect.width;
42
51
  }
43
52
  return {
44
53
  position: 'absolute',
45
54
  top,
55
+ bottom,
46
56
  left,
47
57
  maxWidth,
58
+ maxHeight,
48
59
  };
49
60
  }
50
61
  //# sourceMappingURL=computeStyles.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"computeStyles.js","sourceRoot":"","sources":["../src/computeStyles.ts"],"names":[],"mappings":"AAGA,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,aAAa,GAAG,WAAW,EAAE,MAAM,IAAI,CAAC,CAAA;IAC9C,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAGhD,CAAA;IAED,IAAI,GAAW,CAAA;IACf,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,GAAG,GAAG,UAAU,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,aAAa,CAAA;IACrD,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,SAAS,CAAC,CAAC,CAAA;QAClB,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAA;IAC5B,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,UAAU;QACpB,GAAG;QACH,IAAI;QACJ,QAAQ;KACT,CAAA;AACH,CAAC","sourcesContent":["import {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 popoverHeight = popoverRect?.height ?? 0\n const [side, align] = options.placement.split('-') as [\n string,\n string | undefined,\n ]\n\n let top: number\n if (side === 'top') {\n top = anchorRect.y - options.offset - popoverHeight\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 = inputRect.x\n maxWidth = inputRect.width\n }\n\n return {\n position: 'absolute',\n top,\n left,\n maxWidth,\n }\n}\n"]}
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,OAKC;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,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;IACzC,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAA;IAE/C,IAAI,GAAG,GAAoB,MAAM,CAAA;IACjC,IAAI,MAAM,GAAoB,MAAM,CAAA;IACpC,IAAI,SAA6B,CAAA;IACjC,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;QACxE,SAAS,GAAG,UAAU,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAA;IACtD,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;QACvD,SAAS;YACP,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM;gBAC/B,WAAW;gBACX,CAAC,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvD,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;QACR,SAAS;KACV,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 insets?: {top: number; bottom: number}\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 const insetTop = options.insets?.top ?? 0\n const insetBottom = options.insets?.bottom ?? 0\n\n let top: number | 'auto' = 'auto'\n let bottom: number | 'auto' = 'auto'\n let maxHeight: number | undefined\n if (side === 'top') {\n bottom = Dimensions.get('window').height - anchorRect.y + options.offset\n maxHeight = anchorRect.y - options.offset - insetTop\n } else {\n top = anchorRect.y + anchorRect.height + options.offset\n maxHeight =\n Dimensions.get('window').height -\n insetBottom -\n (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 maxHeight,\n }\n}\n"]}
@@ -8,5 +8,9 @@ export declare function computeStyles({ anchor, input, popover, }: {
8
8
  offset: number;
9
9
  placement: Placement;
10
10
  dynamicWidth?: boolean;
11
- }): ViewStyle;
11
+ insets?: {
12
+ top: number;
13
+ bottom: number;
14
+ };
15
+ }): ViewStyle | null;
12
16
  //# sourceMappingURL=computeStyles.web.d.ts.map
@@ -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,CAyCX"}
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;IACtB,MAAM,CAAC,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAA;CACvC,GACA,SAAS,GAAG,IAAI,CAwDlB"}
@@ -2,15 +2,27 @@ 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
+ const insetTop = options.insets?.top ?? 0;
14
+ const insetBottom = options.insets?.bottom ?? 0;
15
+ let top = 'auto';
16
+ let bottom = 'auto';
17
+ let maxHeight;
9
18
  if (side === 'top') {
10
- top = anchorRect.top - options.offset - popoverHeight + window.scrollY;
19
+ bottom = window.innerHeight - anchorRect.top + options.offset;
20
+ maxHeight = anchorRect.top - options.offset - insetTop;
11
21
  }
12
22
  else {
13
23
  top = anchorRect.bottom + options.offset + window.scrollY;
24
+ maxHeight =
25
+ window.innerHeight - insetBottom - anchorRect.bottom - options.offset;
14
26
  }
15
27
  let left;
16
28
  if (align === 'start') {
@@ -25,15 +37,17 @@ export function computeStyles({ anchor, input, popover, }, options) {
25
37
  left += window.scrollX;
26
38
  let maxWidth;
27
39
  if (options.dynamicWidth === false) {
28
- left = inputRect.left + window.scrollX;
29
- maxWidth = inputRect.width;
40
+ left = anchorRect.left + window.scrollX;
41
+ maxWidth = anchorRect.width;
30
42
  }
31
43
  return {
32
44
  // @ts-ignore
33
45
  position: 'fixed',
34
46
  top,
47
+ bottom,
35
48
  left,
36
49
  maxWidth,
50
+ maxHeight,
37
51
  };
38
52
  }
39
53
  //# sourceMappingURL=computeStyles.web.js.map
@@ -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;IACpD,MAAM,YAAY,GAAG,WAAW,EAAE,KAAK,IAAI,CAAC,CAAA;IAC5C,MAAM,aAAa,GAAG,WAAW,EAAE,MAAM,IAAI,CAAC,CAAA;IAC9C,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAGhD,CAAA;IAED,IAAI,GAAW,CAAA;IACf,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,GAAG,GAAG,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC,OAAO,CAAA;IACxE,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,SAAS,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,CAAA;QACtC,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAA;IAC5B,CAAC;IAED,OAAO;QACL,aAAa;QACb,QAAQ,EAAE,OAAO;QACjB,GAAG;QACH,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 {\n const anchorRect = anchor.getBoundingClientRect()\n const inputRect = input.getBoundingClientRect()\n const popoverRect = popover?.getBoundingClientRect()\n const popoverWidth = popoverRect?.width ?? 0\n const popoverHeight = popoverRect?.height ?? 0\n const [side, align] = options.placement.split('-') as [\n string,\n string | undefined,\n ]\n\n let top: number\n if (side === 'top') {\n top = anchorRect.top - options.offset - popoverHeight + window.scrollY\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 = inputRect.left + window.scrollX\n maxWidth = inputRect.width\n }\n\n return {\n // @ts-ignore\n position: 'fixed',\n top,\n left,\n maxWidth,\n }\n}\n"]}
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,OAKC;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,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;IACzC,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAA;IAE/C,IAAI,GAAG,GAAoB,MAAM,CAAA;IACjC,IAAI,MAAM,GAAoB,MAAM,CAAA;IACpC,IAAI,SAA6B,CAAA;IACjC,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,GAAG,MAAM,CAAC,WAAW,GAAG,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAA;QAC7D,SAAS,GAAG,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAA;IACxD,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;QACzD,SAAS;YACP,MAAM,CAAC,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IACzE,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;QACR,SAAS;KACV,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 insets?: {top: number; bottom: number}\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 const insetTop = options.insets?.top ?? 0\n const insetBottom = options.insets?.bottom ?? 0\n\n let top: number | 'auto' = 'auto'\n let bottom: number | 'auto' = 'auto'\n let maxHeight: number | undefined\n if (side === 'top') {\n bottom = window.innerHeight - anchorRect.top + options.offset\n maxHeight = anchorRect.top - options.offset - insetTop\n } else {\n top = anchorRect.bottom + options.offset + window.scrollY\n maxHeight =\n window.innerHeight - insetBottom - anchorRect.bottom - options.offset\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 maxHeight,\n }\n}\n"]}
@@ -1,10 +1,14 @@
1
1
  import { type ViewStyle } from 'react-native';
2
2
  export type Placement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end';
3
3
  export type UseSiftReturn = ReturnType<typeof useSift>;
4
- export declare function useSift({ offset: offsetValue, placement, dynamicWidth, }?: {
4
+ export declare function useSift({ offset: offsetValue, placement, dynamicWidth, insets, }?: {
5
5
  offset?: number;
6
6
  placement?: Placement;
7
7
  dynamicWidth?: boolean;
8
+ insets?: {
9
+ top: number;
10
+ bottom: number;
11
+ };
8
12
  }): {
9
13
  id: string;
10
14
  refs: {
@@ -1 +1 @@
1
- {"version":3,"file":"useSift.d.ts","sourceRoot":"","sources":["../src/useSift.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AAK3C,MAAM,MAAM,SAAS,GACjB,KAAK,GACL,WAAW,GACX,SAAS,GACT,QAAQ,GACR,cAAc,GACd,YAAY,CAAA;AAEhB,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAEtD,wBAAgB,OAAO,CAAC,EACtB,MAAM,EAAE,WAAe,EACvB,SAAoB,EACpB,YAAoB,GACrB,GAAE;IACD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;CAClB;;;2BAiDK,GAAG;0BAaH,GAAG;;;;;;;;;;oBAnB8B,GAAG;;;;;;EAiD9C"}
1
+ {"version":3,"file":"useSift.d.ts","sourceRoot":"","sources":["../src/useSift.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,cAAc,CAAA;AAK3C,MAAM,MAAM,SAAS,GACjB,KAAK,GACL,WAAW,GACX,SAAS,GACT,QAAQ,GACR,cAAc,GACd,YAAY,CAAA;AAEhB,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAEtD,wBAAgB,OAAO,CAAC,EACtB,MAAM,EAAE,WAAe,EACvB,SAAoB,EACpB,YAAoB,EACpB,MAAM,GACP,GAAE;IACD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,MAAM,CAAC,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAA;CAClC;;;2BAmDK,GAAG;0BAaH,GAAG;;;;;;;;;;oBAnB8B,GAAG;;;;;;EAiD9C"}
package/build/useSift.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useId, useRef, useState } from 'react';
2
2
  import { computeStyles } from './computeStyles';
3
3
  const DEFAULT_POPOVER_STYLES = { position: 'absolute' };
4
- export function useSift({ offset: offsetValue = 0, placement = 'bottom', dynamicWidth = false, } = {}) {
4
+ export function useSift({ offset: offsetValue = 0, placement = 'bottom', dynamicWidth = false, insets, } = {}) {
5
5
  const id = useId();
6
6
  /*
7
7
  * These are reactive values and need to remain in state
@@ -20,11 +20,13 @@ export function useSift({ offset: offsetValue = 0, placement = 'bottom', dynamic
20
20
  offset: offsetValue,
21
21
  placement,
22
22
  dynamicWidth,
23
+ insets,
23
24
  });
24
25
  options.current = {
25
26
  offset: offsetValue,
26
27
  placement,
27
28
  dynamicWidth,
29
+ insets,
28
30
  };
29
31
  const update = useCallback(async () => {
30
32
  if (!inputRef.current || !popoverRef.current)
@@ -1 +1 @@
1
- {"version":3,"file":"useSift.js","sourceRoot":"","sources":["../src/useSift.ts"],"names":[],"mappings":"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","sourcesContent":["import {useCallback, useId, useRef, useState} from 'react'\nimport {type ViewStyle} from 'react-native'\nimport {computeStyles} from './computeStyles'\n\nconst DEFAULT_POPOVER_STYLES: ViewStyle = {position: 'absolute'}\n\nexport type Placement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n\nexport type UseSiftReturn = ReturnType<typeof useSift>\n\nexport function useSift({\n offset: offsetValue = 0,\n placement = 'bottom',\n dynamicWidth = false,\n}: {\n offset?: number\n placement?: Placement\n dynamicWidth?: boolean\n} = {}) {\n const id = useId()\n\n /*\n * These are reactive values and need to remain in state\n */\n const [input, setInput] = useState<any>(null)\n const [popover, setPopover] = useState<any>(null)\n const [popoverStyles, setPopoverStyles] = useState<ViewStyle>(\n DEFAULT_POPOVER_STYLES,\n )\n\n /*\n * These are non-reactive values that we want to persist across renders\n * without causing re-renders when they change, so we store them in refs.\n */\n const inputRef = useRef<any>(null)\n const popoverRef = useRef<any>(null)\n const anchorRef = useRef<any>(null)\n const options = useRef({\n offset: offsetValue,\n placement,\n dynamicWidth,\n })\n options.current = {\n offset: offsetValue,\n placement,\n dynamicWidth,\n }\n\n const update = useCallback(async () => {\n if (!inputRef.current || !popoverRef.current) return\n const styles = await computeStyles(\n {\n anchor: anchorRef.current || inputRef.current,\n input: inputRef.current,\n popover: popoverRef.current,\n },\n options.current,\n )\n if (styles) setPopoverStyles(styles)\n }, [])\n\n const handleSetInput = useCallback((node: any) => {\n inputRef.current = node\n setInput(node)\n }, [])\n\n const handleSetPopover = useCallback(\n (node: any) => {\n popoverRef.current = node\n setPopover(node)\n if (node) {\n update()\n } else {\n setPopoverStyles(DEFAULT_POPOVER_STYLES)\n }\n },\n [update],\n )\n\n const handleSetAnchor = useCallback(\n (node: any) => {\n anchorRef.current = node\n if (node) update()\n },\n [update],\n )\n\n return {\n id,\n refs: {\n setPopover: handleSetPopover,\n setAnchor: handleSetAnchor,\n },\n elements: {\n input,\n popover,\n },\n isActive() {\n return !!popover\n },\n popoverStyles,\n updatePosition: update,\n targetProps: {\n ref: handleSetInput,\n role: 'combobox' as const,\n 'aria-controls': id,\n 'aria-expanded': !!popover,\n 'aria-autocomplete': 'list' as const,\n },\n }\n}\n"]}
1
+ {"version":3,"file":"useSift.js","sourceRoot":"","sources":["../src/useSift.ts"],"names":[],"mappings":"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","sourcesContent":["import {useCallback, useId, useRef, useState} from 'react'\nimport {type ViewStyle} from 'react-native'\nimport {computeStyles} from './computeStyles'\n\nconst DEFAULT_POPOVER_STYLES: ViewStyle = {position: 'absolute'}\n\nexport type Placement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n\nexport type UseSiftReturn = ReturnType<typeof useSift>\n\nexport function useSift({\n offset: offsetValue = 0,\n placement = 'bottom',\n dynamicWidth = false,\n insets,\n}: {\n offset?: number\n placement?: Placement\n dynamicWidth?: boolean\n insets?: {top: number; bottom: number}\n} = {}) {\n const id = useId()\n\n /*\n * These are reactive values and need to remain in state\n */\n const [input, setInput] = useState<any>(null)\n const [popover, setPopover] = useState<any>(null)\n const [popoverStyles, setPopoverStyles] = useState<ViewStyle>(\n DEFAULT_POPOVER_STYLES,\n )\n\n /*\n * These are non-reactive values that we want to persist across renders\n * without causing re-renders when they change, so we store them in refs.\n */\n const inputRef = useRef<any>(null)\n const popoverRef = useRef<any>(null)\n const anchorRef = useRef<any>(null)\n const options = useRef({\n offset: offsetValue,\n placement,\n dynamicWidth,\n insets,\n })\n options.current = {\n offset: offsetValue,\n placement,\n dynamicWidth,\n insets,\n }\n\n const update = useCallback(async () => {\n if (!inputRef.current || !popoverRef.current) return\n const styles = await computeStyles(\n {\n anchor: anchorRef.current || inputRef.current,\n input: inputRef.current,\n popover: popoverRef.current,\n },\n options.current,\n )\n if (styles) setPopoverStyles(styles)\n }, [])\n\n const handleSetInput = useCallback((node: any) => {\n inputRef.current = node\n setInput(node)\n }, [])\n\n const handleSetPopover = useCallback(\n (node: any) => {\n popoverRef.current = node\n setPopover(node)\n if (node) {\n update()\n } else {\n setPopoverStyles(DEFAULT_POPOVER_STYLES)\n }\n },\n [update],\n )\n\n const handleSetAnchor = useCallback(\n (node: any) => {\n anchorRef.current = node\n if (node) update()\n },\n [update],\n )\n\n return {\n id,\n refs: {\n setPopover: handleSetPopover,\n setAnchor: handleSetAnchor,\n },\n elements: {\n input,\n popover,\n },\n isActive() {\n return !!popover\n },\n popoverStyles,\n updatePosition: update,\n targetProps: {\n ref: handleSetInput,\n role: 'combobox' as const,\n 'aria-controls': id,\n 'aria-expanded': !!popover,\n 'aria-autocomplete': 'list' as const,\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsky.app/sift",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "license": "MIT",
5
5
  "description": "A little React Native library for building autocompletes.",
6
6
  "repository": "https://github.com/bluesky-social/toolbox",
@@ -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(
@@ -27,6 +27,7 @@ export async function computeStyles(
27
27
  offset: number
28
28
  placement: Placement
29
29
  dynamicWidth?: boolean
30
+ insets?: {top: number; bottom: number}
30
31
  },
31
32
  ): Promise<ViewStyle | null> {
32
33
  const anchorRect = await measureInWindow(anchor)
@@ -39,17 +40,26 @@ export async function computeStyles(
39
40
  if (popoverRect && !popoverRect.width && !popoverRect.height) return null
40
41
 
41
42
  const popoverWidth = popoverRect?.width ?? 0
42
- const popoverHeight = popoverRect?.height ?? 0
43
43
  const [side, align] = options.placement.split('-') as [
44
44
  string,
45
45
  string | undefined,
46
46
  ]
47
47
 
48
- let top: number
48
+ const insetTop = options.insets?.top ?? 0
49
+ const insetBottom = options.insets?.bottom ?? 0
50
+
51
+ let top: number | 'auto' = 'auto'
52
+ let bottom: number | 'auto' = 'auto'
53
+ let maxHeight: number | undefined
49
54
  if (side === 'top') {
50
- top = anchorRect.y - options.offset - popoverHeight
55
+ bottom = Dimensions.get('window').height - anchorRect.y + options.offset
56
+ maxHeight = anchorRect.y - options.offset - insetTop
51
57
  } else {
52
58
  top = anchorRect.y + anchorRect.height + options.offset
59
+ maxHeight =
60
+ Dimensions.get('window').height -
61
+ insetBottom -
62
+ (anchorRect.y + anchorRect.height + options.offset)
53
63
  }
54
64
 
55
65
  let left: number
@@ -63,14 +73,16 @@ export async function computeStyles(
63
73
 
64
74
  let maxWidth: number | undefined
65
75
  if (options.dynamicWidth === false) {
66
- left = inputRect.x
67
- maxWidth = inputRect.width
76
+ left = anchorRect.x
77
+ maxWidth = anchorRect.width
68
78
  }
69
79
 
70
80
  return {
71
81
  position: 'absolute',
72
82
  top,
83
+ bottom,
73
84
  left,
74
85
  maxWidth,
86
+ maxHeight,
75
87
  }
76
88
  }
@@ -15,23 +15,37 @@ export function computeStyles(
15
15
  offset: number
16
16
  placement: Placement
17
17
  dynamicWidth?: boolean
18
+ insets?: {top: number; bottom: number}
18
19
  },
19
- ): ViewStyle {
20
+ ): ViewStyle | null {
20
21
  const anchorRect = anchor.getBoundingClientRect()
21
22
  const inputRect = input.getBoundingClientRect()
22
23
  const popoverRect = popover?.getBoundingClientRect()
24
+
25
+ // If any measurement failed (view not in hierarchy yet), return null
26
+ // so the caller keeps the previous styles.
27
+ if (!anchorRect.width || !inputRect.width) return null
28
+ if (popoverRect && !popoverRect.width && !popoverRect.height) return null
29
+
23
30
  const popoverWidth = popoverRect?.width ?? 0
24
- const popoverHeight = popoverRect?.height ?? 0
25
31
  const [side, align] = options.placement.split('-') as [
26
32
  string,
27
33
  string | undefined,
28
34
  ]
29
35
 
30
- let top: number
36
+ const insetTop = options.insets?.top ?? 0
37
+ const insetBottom = options.insets?.bottom ?? 0
38
+
39
+ let top: number | 'auto' = 'auto'
40
+ let bottom: number | 'auto' = 'auto'
41
+ let maxHeight: number | undefined
31
42
  if (side === 'top') {
32
- top = anchorRect.top - options.offset - popoverHeight + window.scrollY
43
+ bottom = window.innerHeight - anchorRect.top + options.offset
44
+ maxHeight = anchorRect.top - options.offset - insetTop
33
45
  } else {
34
46
  top = anchorRect.bottom + options.offset + window.scrollY
47
+ maxHeight =
48
+ window.innerHeight - insetBottom - anchorRect.bottom - options.offset
35
49
  }
36
50
 
37
51
  let left: number
@@ -46,15 +60,17 @@ export function computeStyles(
46
60
 
47
61
  let maxWidth: number | undefined
48
62
  if (options.dynamicWidth === false) {
49
- left = inputRect.left + window.scrollX
50
- maxWidth = inputRect.width
63
+ left = anchorRect.left + window.scrollX
64
+ maxWidth = anchorRect.width
51
65
  }
52
66
 
53
67
  return {
54
68
  // @ts-ignore
55
69
  position: 'fixed',
56
70
  top,
71
+ bottom,
57
72
  left,
58
73
  maxWidth,
74
+ maxHeight,
59
75
  }
60
76
  }
package/src/useSift.ts CHANGED
@@ -18,10 +18,12 @@ export function useSift({
18
18
  offset: offsetValue = 0,
19
19
  placement = 'bottom',
20
20
  dynamicWidth = false,
21
+ insets,
21
22
  }: {
22
23
  offset?: number
23
24
  placement?: Placement
24
25
  dynamicWidth?: boolean
26
+ insets?: {top: number; bottom: number}
25
27
  } = {}) {
26
28
  const id = useId()
27
29
 
@@ -45,11 +47,13 @@ export function useSift({
45
47
  offset: offsetValue,
46
48
  placement,
47
49
  dynamicWidth,
50
+ insets,
48
51
  })
49
52
  options.current = {
50
53
  offset: offsetValue,
51
54
  placement,
52
55
  dynamicWidth,
56
+ insets,
53
57
  }
54
58
 
55
59
  const update = useCallback(async () => {