@startupjs-ui/auto-suggest 0.1.3

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/auto-suggest
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
20
+ * **auto-suggest:** refactor AutoSuggest component ([4602b67](https://github.com/startupjs/startupjs-ui/commit/4602b677b27b11c0cb31185bcaaf3cc40d3b1a87))
package/README.mdx ADDED
@@ -0,0 +1,139 @@
1
+ import { useState } from 'react'
2
+ import AutoSuggest, { _PropsJsonSchema as AutoSuggestPropsJsonSchema } from './index'
3
+ import Avatar from '@startupjs-ui/avatar'
4
+ import Div from '@startupjs-ui/div'
5
+ import Span from '@startupjs-ui/span'
6
+ import { Sandbox } from '@startupjs-ui/docs'
7
+
8
+ # AutoSuggest
9
+ A text field with a pop-up list of options.
10
+
11
+ ```jsx
12
+ import { AutoSuggest } from 'startupjs-ui'
13
+ ```
14
+
15
+ ## Initialization
16
+
17
+ Before use you need to configure [Portal](/docs/components/Portal)
18
+
19
+ ## Simple example
20
+ ```jsx example
21
+ const [value, setValue] = useState()
22
+
23
+ const options = [
24
+ { value: '1', label: 'Harry' },
25
+ { value: '2', label: 'Alfie' },
26
+ { value: '3', label: 'Jacob' },
27
+ ]
28
+
29
+ return (
30
+ <AutoSuggest
31
+ options={options}
32
+ value={value}
33
+ onChange={v => setValue(v)}
34
+ />
35
+ )
36
+ ```
37
+
38
+ ## Props
39
+ - options: array of objects for options, each option has a value and a label
40
+ - value: active value
41
+ - maxHeight: maximum height of the popup list
42
+ - placeholder: the title of the field
43
+ - onChange: callback function, called after selecting a value, takes the selected value as the first parameter
44
+
45
+ ```jsx example
46
+ const [value, setValue] = useState()
47
+
48
+ const options = [
49
+ { value: '1', label: 'Harry' },
50
+ { value: '2', label: 'Alfie' },
51
+ { value: '3', label: 'Jacob' },
52
+ { value: '4', label: 'Oscar' },
53
+ { value: '5', label: 'Charlie' },
54
+ { value: '6', label: 'James' },
55
+ { value: '7', label: 'William' }
56
+ ]
57
+
58
+ return (
59
+ <AutoSuggest
60
+ options={options}
61
+ value={value}
62
+ style={{ maxHeight: 160 }}
63
+ placeholder="Select value"
64
+ onChange={v=> setValue(v)}
65
+ />
66
+ )
67
+ ```
68
+
69
+ ## Customization
70
+ - renderItem: a function that is called when rendering each element of the array. Gets 3 arguments - the current element (item), its index, and the selected index.
71
+
72
+ ```jsx example
73
+ const [value, setValue] = useState()
74
+
75
+ const options = [
76
+ { value: '1', label: 'Harry' },
77
+ { value: '2', label: 'Alfie' },
78
+ { value: '3', label: 'Jacob' },
79
+ { value: '4', label: 'Oscar' },
80
+ { value: '5', label: 'Charlie' },
81
+ { value: '6', label: 'James' },
82
+ { value: '7', label: 'William' }
83
+ ]
84
+
85
+ const renderItem = (item, index, selectIndexValue)=> {
86
+ const style = { padding: 8 }
87
+
88
+ if (selectIndexValue === index) {
89
+ style.backgroundColor = "#eee"
90
+ }
91
+
92
+ return (
93
+ <Div
94
+ key={index}
95
+ vAlign='center'
96
+ style={style}
97
+ row
98
+ >
99
+ <Avatar size='s'>{item.label}</Avatar>
100
+ <Span style={{ marginLeft: 8 }}>{item.label}</Span>
101
+ </Div>
102
+ )
103
+ }
104
+
105
+ return (
106
+ <AutoSuggest
107
+ options={options}
108
+ value={value}
109
+ style={{ maxHeight: 160 }}
110
+ placeholder="Select value"
111
+ renderItem={renderItem}
112
+ onChange={v=> setValue(v)}
113
+ />
114
+ )
115
+ ```
116
+
117
+ ## Also
118
+ - style: responsible for styled hidden content
119
+ - captionStyle: responsible for heading styles (input)
120
+ - onDismiss: callback, called when the list is closed
121
+ - onChangeText: callback, called when entering text, accepts 1 argument text
122
+ - onScrollEnd: callback, called at the end of the scroll
123
+
124
+ ## Sandbox
125
+
126
+ <Sandbox
127
+ Component={AutoSuggest}
128
+ propsJsonSchema={AutoSuggestPropsJsonSchema}
129
+ props={{
130
+ options: [
131
+ { value: '1', label: 'Harry' },
132
+ { value: '2', label: 'Alfie' },
133
+ { value: '3', label: 'Jacob' }
134
+ ],
135
+ value: '1',
136
+ placeholder: 'Select value',
137
+ onChange: value => alert(`Selected: ${value}`)
138
+ }}
139
+ />
@@ -0,0 +1,30 @@
1
+ .root
2
+ .overlay
3
+ position absolute
4
+ top 0
5
+ left 0
6
+ right 0
7
+ bottom 0
8
+
9
+
10
+ .loaderCase
11
+ justify-content center
12
+ align-items center
13
+ min-height 8u
14
+
15
+ .item
16
+ width 100%
17
+
18
+ .content
19
+ width 100%
20
+ height 100%
21
+ background-color var(--color-bg-main)
22
+
23
+ .contentCase
24
+ border-radius 1u
25
+ width 100%
26
+ height 100%
27
+ overflow hidden
28
+
29
+ .selectMenu
30
+ background-color var(--AutoSuggest-itemBg)
package/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type ReactNode } from 'react';
5
+ import { type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
6
+ import './index.cssx.styl';
7
+ interface AutoSuggestOptionObject {
8
+ value?: any;
9
+ label?: string | number;
10
+ }
11
+ type AutoSuggestOption = string | number | AutoSuggestOptionObject;
12
+ type AutoSuggestValue = AutoSuggestOption | null | undefined;
13
+ export declare const _PropsJsonSchema: {};
14
+ export interface AutoSuggestProps {
15
+ /** Custom styles for the suggestion list container */
16
+ style?: StyleProp<ViewStyle>;
17
+ /** Custom styles for the TextInput wrapper */
18
+ captionStyle?: StyleProp<ViewStyle>;
19
+ /** Custom styles for the TextInput input field */
20
+ inputStyle?: StyleProp<TextStyle>;
21
+ /** Custom styles for the clear icon */
22
+ iconStyle?: StyleProp<TextStyle>;
23
+ /** Custom icon for the input */
24
+ inputIcon?: any;
25
+ /** Options list (strings, numbers, or objects with value/label) @default [] */
26
+ options?: AutoSuggestOption[];
27
+ /** Current selected value */
28
+ value?: AutoSuggestValue;
29
+ /** Placeholder text @default 'Select value' */
30
+ placeholder?: string | number;
31
+ /** Custom item renderer (item, index, highlightedIndex) */
32
+ renderItem?: (item: AutoSuggestOption, index: number, selectIndexValue: number) => ReactNode;
33
+ /** Show loader in the list @default false */
34
+ isLoading?: boolean;
35
+ /** Disable input interactions */
36
+ disabled?: boolean;
37
+ /** Render as non-interactive */
38
+ readonly?: boolean;
39
+ /** Change handler for selected value */
40
+ onChange?: (value?: any) => void | Promise<void>;
41
+ /** Called after the list is closed */
42
+ onDismiss?: () => void;
43
+ /** Change handler for input text */
44
+ onChangeText?: (text: string) => void;
45
+ /** Called when list scroll reaches the end */
46
+ onScrollEnd?: () => void;
47
+ /** Test identifier */
48
+ testID?: string;
49
+ }
50
+ declare const _default: import("react").ComponentType<AutoSuggestProps>;
51
+ export default _default;
package/index.tsx ADDED
@@ -0,0 +1,261 @@
1
+ import { useState, useRef, useEffect, useMemo, type ReactNode } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ TouchableWithoutFeedback,
5
+ View,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle
9
+ } from 'react-native'
10
+ import { pug, observer } from 'startupjs'
11
+ import { themed } from '@startupjs-ui/core'
12
+ import AbstractPopover from '@startupjs-ui/abstract-popover'
13
+ import FlatList from '@startupjs-ui/flat-list'
14
+ import Loader from '@startupjs-ui/loader'
15
+ import Menu from '@startupjs-ui/menu'
16
+ import TextInput from '@startupjs-ui/text-input'
17
+ import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'
18
+ import escapeRegExp from 'lodash/escapeRegExp'
19
+ import useKeyboard from './useKeyboard'
20
+ import './index.cssx.styl'
21
+
22
+ const SUPPORT_PLACEMENTS = [
23
+ 'bottom-start',
24
+ 'bottom-center',
25
+ 'bottom-end',
26
+ 'top-start',
27
+ 'top-center',
28
+ 'top-end'
29
+ ] as const
30
+
31
+ interface AutoSuggestOptionObject {
32
+ value?: any
33
+ label?: string | number
34
+ }
35
+
36
+ type AutoSuggestOption = string | number | AutoSuggestOptionObject
37
+
38
+ type AutoSuggestValue = AutoSuggestOption | null | undefined
39
+
40
+ export const _PropsJsonSchema = {/* AutoSuggestProps */}
41
+
42
+ export interface AutoSuggestProps {
43
+ /** Custom styles for the suggestion list container */
44
+ style?: StyleProp<ViewStyle>
45
+ /** Custom styles for the TextInput wrapper */
46
+ captionStyle?: StyleProp<ViewStyle>
47
+ /** Custom styles for the TextInput input field */
48
+ inputStyle?: StyleProp<TextStyle>
49
+ /** Custom styles for the clear icon */
50
+ iconStyle?: StyleProp<TextStyle>
51
+ /** Custom icon for the input */
52
+ inputIcon?: any
53
+ /** Options list (strings, numbers, or objects with value/label) @default [] */
54
+ options?: AutoSuggestOption[]
55
+ /** Current selected value */
56
+ value?: AutoSuggestValue
57
+ /** Placeholder text @default 'Select value' */
58
+ placeholder?: string | number
59
+ /** Custom item renderer (item, index, highlightedIndex) */
60
+ renderItem?: (item: AutoSuggestOption, index: number, selectIndexValue: number) => ReactNode
61
+ /** Show loader in the list @default false */
62
+ isLoading?: boolean
63
+ /** Disable input interactions */
64
+ disabled?: boolean
65
+ /** Render as non-interactive */
66
+ readonly?: boolean
67
+ /** Change handler for selected value */
68
+ onChange?: (value?: any) => void | Promise<void>
69
+ /** Called after the list is closed */
70
+ onDismiss?: () => void
71
+ /** Change handler for input text */
72
+ onChangeText?: (text: string) => void
73
+ /** Called when list scroll reaches the end */
74
+ onScrollEnd?: () => void
75
+ /** Test identifier */
76
+ testID?: string
77
+ }
78
+
79
+ function getOptionLabel (option: AutoSuggestOption): any {
80
+ return (option as AutoSuggestOptionObject)?.label ?? option
81
+ }
82
+
83
+ function stringifyValue (option: AutoSuggestValue): string {
84
+ return JSON.stringify((option as AutoSuggestOptionObject)?.value ?? option)
85
+ }
86
+
87
+ function parseValue (value: string): any {
88
+ return JSON.parse(value)
89
+ }
90
+
91
+ function getLabelFromValue (value: AutoSuggestValue, options: AutoSuggestOption[]): any {
92
+ for (const option of options) {
93
+ if (stringifyValue(value) === stringifyValue(option)) {
94
+ return getOptionLabel(option)
95
+ }
96
+ }
97
+ }
98
+
99
+ function AutoSuggest ({
100
+ style = {},
101
+ captionStyle,
102
+ inputStyle,
103
+ iconStyle,
104
+ options = [],
105
+ value,
106
+ placeholder = 'Select value',
107
+ renderItem,
108
+ isLoading = false,
109
+ disabled,
110
+ readonly,
111
+ onChange,
112
+ onDismiss,
113
+ onChangeText,
114
+ onScrollEnd,
115
+ testID
116
+ }: AutoSuggestProps): ReactNode {
117
+ const inputRef = useRef<any>(null)
118
+ const [isShow, setIsShow] = useState(false)
119
+ const [inputValue, setInputValue] = useState('')
120
+ const [wrapperHeight, setWrapperHeight] = useState<number | null>(null)
121
+ const [scrollHeightContent, setScrollHeightContent] = useState<number | null>(null)
122
+ const [textToFilter, setTextToFilter] = useState<string | undefined>()
123
+ const _options = useMemo(() => {
124
+ const escapedText = escapeRegExp(textToFilter ?? '')
125
+ return options.filter(option => {
126
+ return new RegExp(escapedText, 'gi')
127
+ .test(getLabelFromValue(option, options))
128
+ })
129
+ }, [options, textToFilter])
130
+
131
+ const [selectIndexValue, setSelectIndexValue, onKeyPress] = useKeyboard({
132
+ options: _options,
133
+ onChange,
134
+ onChangeShow: v => { setIsShow(v) }
135
+ })
136
+
137
+ const selectedLabel = useMemo(() => {
138
+ return getLabelFromValue(value, options)
139
+ }, [options, value])
140
+
141
+ useEffect(() => {
142
+ setInputValue(selectedLabel)
143
+ }, [selectedLabel])
144
+
145
+ function onClose () {
146
+ setIsShow(false)
147
+ setSelectIndexValue(-1)
148
+ inputRef.current.blur()
149
+ onDismiss?.()
150
+ }
151
+
152
+ function _onChangeText (text: string) {
153
+ setInputValue(text)
154
+ setTextToFilter(text)
155
+ if (!text) onChange?.()
156
+ setSelectIndexValue(-1)
157
+ onChangeText?.(text)
158
+ }
159
+
160
+ async function _onPress (item: AutoSuggestOption) {
161
+ onChange && await onChange(parseValue(stringifyValue(item)))
162
+ onClose()
163
+ }
164
+
165
+ function _renderItem ({ item, index }: { item: AutoSuggestOption, index: number }): ReactNode {
166
+ if (renderItem) {
167
+ return pug`
168
+ TouchableOpacity(
169
+ key=index
170
+ onPress=() => { void _onPress(item) }
171
+ )= renderItem(item, index, selectIndexValue)
172
+ `
173
+ }
174
+
175
+ return pug`
176
+ Menu.Item.item(
177
+ key=index
178
+ styleName={ selectMenu: selectIndexValue === index }
179
+ onPress=() => { void _onPress(item) }
180
+ active=stringifyValue(item) === stringifyValue(value)
181
+ )= getLabelFromValue(item, options)
182
+ `
183
+ }
184
+
185
+ function onScroll ({ nativeEvent }: any) {
186
+ if (nativeEvent.contentOffset.y + wrapperHeight === scrollHeightContent) {
187
+ onScrollEnd?.()
188
+ }
189
+ }
190
+
191
+ function onLayoutWrapper ({ nativeEvent }: any) {
192
+ setWrapperHeight(nativeEvent.layout.height)
193
+ }
194
+
195
+ function onChangeSizeScroll (width: number, height: number) {
196
+ setScrollHeightContent(height)
197
+ }
198
+
199
+ function renderWrapper (children: ReactNode): ReactNode {
200
+ return pug`
201
+ View.root
202
+ TouchableWithoutFeedback(onPress=() => {
203
+ setInputValue(selectedLabel)
204
+ onClose()
205
+ })
206
+ View.overlay
207
+ = children
208
+ `
209
+ }
210
+
211
+ const matchAnchorWidth = !(style as ViewStyle)?.width && !(style as ViewStyle)?.maxWidth
212
+
213
+ return pug`
214
+ TextInput(
215
+ ref=inputRef
216
+ style=captionStyle
217
+ inputStyle=inputStyle
218
+ icon=value && !disabled ? faTimes : undefined
219
+ iconPosition='right'
220
+ iconStyle=iconStyle
221
+ value=inputValue
222
+ placeholder=placeholder
223
+ disabled=disabled
224
+ readonly=readonly
225
+ onChangeText=_onChangeText
226
+ onFocus=() => setIsShow(true)
227
+ onKeyPress=onKeyPress
228
+ onIconPress=() => { void onChange?.() }
229
+ testID=testID
230
+ )
231
+
232
+ AbstractPopover(
233
+ visible=(isShow || isLoading)
234
+ anchorRef=inputRef
235
+ matchAnchorWidth=matchAnchorWidth
236
+ placements=SUPPORT_PLACEMENTS
237
+ durationOpen=200
238
+ durationClose=200
239
+ renderWrapper=renderWrapper
240
+ onCloseComplete=() => setTextToFilter()
241
+ )
242
+ if isLoading
243
+ View.loaderCase
244
+ Loader(size='s')
245
+ else
246
+ View.contentCase
247
+ FlatList.content(
248
+ style=style
249
+ data=_options
250
+ renderItem=_renderItem
251
+ keyExtractor=item => stringifyValue(item)
252
+ scrollEventThrottle=500
253
+ keyboardShouldPersistTaps='always'
254
+ onScroll=onScroll
255
+ onLayout=onLayoutWrapper
256
+ onContentSizeChange=onChangeSizeScroll
257
+ )
258
+ `
259
+ }
260
+
261
+ export default observer(themed('AutoSuggest', AutoSuggest))
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@startupjs-ui/auto-suggest",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/abstract-popover": "^0.1.3",
12
+ "@startupjs-ui/core": "^0.1.3",
13
+ "@startupjs-ui/flat-list": "^0.1.3",
14
+ "@startupjs-ui/loader": "^0.1.3",
15
+ "@startupjs-ui/menu": "^0.1.3",
16
+ "@startupjs-ui/text-input": "^0.1.3"
17
+ },
18
+ "peerDependencies": {
19
+ "react": "*",
20
+ "react-native": "*",
21
+ "startupjs": "*"
22
+ },
23
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
24
+ }
package/useKeyboard.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { useState, type Dispatch, type SetStateAction } from 'react'
2
+
3
+ interface UseKeyboardOptions {
4
+ options: any[]
5
+ onChange?: (value: any) => void
6
+ onChangeShow: (visible: boolean) => void
7
+ }
8
+
9
+ export default function useKeyboard ({
10
+ options,
11
+ onChange,
12
+ onChangeShow
13
+ }: UseKeyboardOptions): [number, Dispatch<SetStateAction<number>>, (e: any) => void] {
14
+ const [selectIndexValue, setSelectIndexValue] = useState(-1)
15
+
16
+ function onKeyPress (e: any) {
17
+ const keyName = e.key
18
+
19
+ switch (keyName) {
20
+ case 'ArrowUp': {
21
+ e.preventDefault()
22
+
23
+ const nextIndex = selectIndexValue - 1
24
+
25
+ if (nextIndex < 0) {
26
+ setSelectIndexValue(options.length - 1)
27
+ return
28
+ }
29
+
30
+ setSelectIndexValue(nextIndex)
31
+ break
32
+ }
33
+ case 'ArrowDown': {
34
+ e.preventDefault()
35
+
36
+ const nextIndex = selectIndexValue + 1
37
+
38
+ if (nextIndex === options.length) {
39
+ setSelectIndexValue(0)
40
+ return
41
+ }
42
+
43
+ setSelectIndexValue(nextIndex)
44
+ break
45
+ }
46
+ case 'Enter': {
47
+ e.preventDefault()
48
+ if (selectIndexValue === -1) return
49
+ const item = options.find((_: any, i: number) => i === selectIndexValue)
50
+ onChangeShow(false)
51
+ onChange && onChange(item)
52
+ setSelectIndexValue(-1)
53
+ break
54
+ }
55
+ }
56
+ }
57
+
58
+ return [selectIndexValue, setSelectIndexValue, onKeyPress]
59
+ }