@startupjs-ui/select 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/select
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
+ * **select:** refactor Select component ([d6e9423](https://github.com/startupjs/startupjs-ui/commit/d6e94234c0732da8e0390b081ac3b0fcf0927f18))
package/README.mdx ADDED
@@ -0,0 +1,104 @@
1
+ import { useState } from 'react'
2
+ import Select, { _PropsJsonSchema as SelectPropsJsonSchema } from './index'
3
+ import { Sandbox } from '@startupjs-ui/docs'
4
+
5
+ # Select
6
+
7
+ Inherits [TextInput props](/docs/forms/TextInput).
8
+
9
+ Select lets user pick one of multiple options. Works similar to HTML's `<select>` tag.
10
+
11
+ ```jsx
12
+ import { Select } from 'startupjs-ui'
13
+ ```
14
+
15
+ ## Simple example
16
+
17
+ ```jsx example
18
+ const [color, setColor] = useState()
19
+ return (
20
+ <Select
21
+ value={color}
22
+ onChange={setColor}
23
+ options={[
24
+ 'red',
25
+ 'yellow',
26
+ ]}
27
+ />
28
+ )
29
+ ```
30
+
31
+ ## Disabled
32
+
33
+ ```jsx example
34
+ return (
35
+ <Select
36
+ disabled
37
+ value='red'
38
+ options={[
39
+ 'red',
40
+ 'yellow',
41
+ ]}
42
+ />
43
+ )
44
+ ```
45
+
46
+ ## Readonly
47
+
48
+ ```jsx example
49
+ return (
50
+ <Select
51
+ readonly
52
+ value='red'
53
+ options={[
54
+ 'red',
55
+ 'yellow',
56
+ ]}
57
+ />
58
+ )
59
+ ```
60
+
61
+ ## Showing Empty value
62
+
63
+ By default the empty value is shown. If you want to prevent users from being able to remove the value, pass `showEmptyValue={false}`.
64
+
65
+ ```jsx example
66
+ const [color, setColor] = useState('red')
67
+ return (
68
+ <Select
69
+ value={color}
70
+ onChange={setColor}
71
+ options={['red', 'yellow', 'blue']}
72
+ showEmptyValue={false}
73
+ />
74
+ )
75
+ ```
76
+
77
+ ## Change empty value label
78
+
79
+ You can change label of an empty value by using `emptyValueLabel` property.
80
+
81
+ ```jsx example
82
+ const [color, setColor] = useState()
83
+ return (
84
+ <Select
85
+ value={color}
86
+ onChange={setColor}
87
+ options={['red', 'yellow', 'blue']}
88
+ emptyValueLabel='none'
89
+ />
90
+ )
91
+ ```
92
+
93
+ ## Sandbox
94
+
95
+ <Sandbox
96
+ Component={Select}
97
+ propsJsonSchema={SelectPropsJsonSchema}
98
+ props={{
99
+ value: 'Los Angeles',
100
+ options: ['New York', 'Los Angeles','Tokyo'],
101
+ onChange: value => alert('Value ' + value + ' is selected')
102
+ }}
103
+ block
104
+ />
@@ -0,0 +1,64 @@
1
+ export type SelectOption =
2
+ | string
3
+ | number
4
+ | { value?: any, label?: string | number }
5
+
6
+ // TODO: create logic for objects with circular structure like jsx components
7
+
8
+ // Stringify values to omit bugs in Android/iOS Picker implementation
9
+
10
+ // Force undefined to be a special value to
11
+ // workaround the undefined value bug in Picker
12
+ export const PICKER_NULL = '-\u00A0\u00A0\u00A0\u00A0\u00A0'
13
+ export const NULL_OPTION: undefined = undefined
14
+
15
+ export function stringifyValue (option: any): string | undefined {
16
+ try {
17
+ let value: any
18
+ if (option?.value != null) {
19
+ value = option.value
20
+ } else {
21
+ value = option
22
+ }
23
+ if (value == null) return PICKER_NULL
24
+ return JSON.stringify(value)
25
+ } catch (error) {
26
+ console.warn('[@startupjs/ui] Select: ' + String(error))
27
+ }
28
+ }
29
+
30
+ export function parseValue (value: any): any {
31
+ try {
32
+ if (value === PICKER_NULL || value == null) {
33
+ return undefined
34
+ } else {
35
+ return JSON.parse(value)
36
+ }
37
+ } catch (error) {
38
+ console.warn('[@startupjs/ui] Select: ' + String(error))
39
+ }
40
+ }
41
+
42
+ export function getLabel (option: any): string {
43
+ let label: any
44
+ if (option?.label != null) {
45
+ label = option.label
46
+ } else {
47
+ label = option
48
+ }
49
+ if (label == null) return PICKER_NULL
50
+ return '' + label
51
+ }
52
+
53
+ export function getLabelFromValue (
54
+ value: any,
55
+ options: SelectOption[],
56
+ emptyValueLabel: any = NULL_OPTION
57
+ ): string {
58
+ for (const option of options) {
59
+ if (stringifyValue(value) === stringifyValue(option)) {
60
+ return getLabel(option)
61
+ }
62
+ }
63
+ return getLabel(emptyValueLabel)
64
+ }
@@ -0,0 +1,51 @@
1
+ $grayBlue = #d0d4da
2
+ $whiteGray = #f8f8f8
3
+ $grayLight = #dedede
4
+
5
+ .root
6
+ position relative
7
+
8
+ .overlay
9
+ position absolute
10
+ top 0
11
+ left 0
12
+ right 0
13
+ bottom 0
14
+ width 100%
15
+ height 100%
16
+ background-color transparent
17
+ padding 0
18
+ margin 0
19
+ z-index 10
20
+
21
+ +web()
22
+ appearance none
23
+ border-width 0
24
+ cursor pointer
25
+ opacity 0
26
+
27
+ +android()
28
+ opacity 0
29
+
30
+ .modalTop
31
+ flex 1
32
+
33
+ .modalMiddle
34
+ height 6u
35
+ flex-direction row
36
+ align-items center
37
+ padding-right 2u
38
+ justify-content flex-end
39
+ background-color $whiteGray
40
+ border-top-width 1px
41
+ border-top-color $grayLight
42
+
43
+ .modalBottom
44
+ justify-content center
45
+ background-color $grayBlue
46
+
47
+ .done
48
+ color var(--color-text-primary)
49
+ fontFamily('normal', 600)
50
+ font(body1)
51
+
@@ -0,0 +1,171 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { Modal, Platform } from 'react-native'
3
+ import { Picker } from '@react-native-picker/picker'
4
+ import { pug, observer } from 'startupjs'
5
+ import { themed } from '@startupjs-ui/core'
6
+ import Div from '@startupjs-ui/div'
7
+ import Span from '@startupjs-ui/span'
8
+ import {
9
+ stringifyValue,
10
+ getLabel,
11
+ parseValue,
12
+ NULL_OPTION,
13
+ type SelectOption
14
+ } from './helpers'
15
+ import STYLES from './index.cssx.styl'
16
+
17
+ export interface SelectWrapperProps {
18
+ /** Custom styles for wrapper */
19
+ style?: any
20
+ /** Input element rendered inside wrapper */
21
+ children?: ReactNode
22
+ /** Available options @default [] */
23
+ options?: SelectOption[]
24
+ /** Current selected value */
25
+ value?: any
26
+ /** Disable interactions */
27
+ disabled?: boolean
28
+ /** Show empty/none option */
29
+ showEmptyValue?: boolean
30
+ /** Label for empty/none option */
31
+ emptyValueLabel?: string | number
32
+ /** Test identifier */
33
+ testID?: string
34
+ /** Fired when selected value changes */
35
+ onChange?: (value: any) => void
36
+ }
37
+
38
+ function SelectWrapper (props: SelectWrapperProps): ReactNode {
39
+ if (Platform.OS === 'web') return pug`SelectWrapperWeb(...props)`
40
+ if (Platform.OS === 'ios') return pug`SelectWrapperIOS(...props)`
41
+ return pug`SelectWrapperAndroid(...props)`
42
+ }
43
+
44
+ function SelectWrapperWeb ({
45
+ style,
46
+ children,
47
+ options = [],
48
+ value,
49
+ disabled,
50
+ showEmptyValue,
51
+ emptyValueLabel,
52
+ testID,
53
+ onChange
54
+ }: SelectWrapperProps): ReactNode {
55
+ function onSelectChange (event: any) {
56
+ const value = event.target.value
57
+ if (onChange) onChange(parseValue(value))
58
+ }
59
+
60
+ return pug`
61
+ Div.root(style=style testID=testID)
62
+ = children
63
+ if !disabled
64
+ select(
65
+ style=STYLES.overlay
66
+ value=stringifyValue(value)
67
+ onChange=onSelectChange
68
+ )
69
+ if showEmptyValue
70
+ option(key=-1 value=stringifyValue(NULL_OPTION))
71
+ = emptyValueLabel || getLabel(NULL_OPTION)
72
+ each item, index in options
73
+ option(key=index value=stringifyValue(item))
74
+ = getLabel(item)
75
+ `
76
+ }
77
+
78
+ function SelectWrapperAndroid ({
79
+ style,
80
+ children,
81
+ options = [],
82
+ value,
83
+ disabled,
84
+ showEmptyValue,
85
+ emptyValueLabel,
86
+ onChange
87
+ }: SelectWrapperProps): ReactNode {
88
+ function onValueChange (value: any) {
89
+ if (onChange) onChange(parseValue(value))
90
+ }
91
+
92
+ return pug`
93
+ Div.root(style=style)
94
+ = children
95
+ if !disabled
96
+ Picker.overlay(
97
+ selectedValue=stringifyValue(value)
98
+ onValueChange=onValueChange
99
+ )
100
+ if showEmptyValue
101
+ Picker.Item(
102
+ key=-1
103
+ value=stringifyValue(NULL_OPTION)
104
+ label=emptyValueLabel || getLabel(NULL_OPTION)
105
+ )
106
+ each item, index in options
107
+ Picker.Item(
108
+ key=index
109
+ value=stringifyValue(item)
110
+ label=getLabel(item)
111
+ )
112
+ `
113
+ }
114
+
115
+ function SelectWrapperIOS ({
116
+ style,
117
+ children,
118
+ options = [],
119
+ value,
120
+ disabled,
121
+ showEmptyValue,
122
+ emptyValueLabel,
123
+ onChange
124
+ }: SelectWrapperProps): ReactNode {
125
+ const [showModal, setShowModal] = useState(false)
126
+
127
+ function onValueChange (value: any) {
128
+ if (onChange) onChange(parseValue(value))
129
+ }
130
+
131
+ return pug`
132
+ Div.root(style=style)
133
+ = children
134
+ if !disabled
135
+ Div.overlay(
136
+ activeOpacity=1
137
+ onPress=() => setShowModal(true)
138
+ )
139
+ Modal(
140
+ visible=showModal
141
+ transparent
142
+ animationType='slide'
143
+ )
144
+ Div.modalTop(onPress=()=> setShowModal(false))
145
+ Div.modalMiddle
146
+ Div(
147
+ onPress=()=> setShowModal(false)
148
+ hitSlop={ top: 4, right: 4, bottom: 4, left: 4 }
149
+ )
150
+ Span.done Done
151
+ Div.modalBottom
152
+ Picker(
153
+ selectedValue=stringifyValue(value)
154
+ onValueChange=onValueChange
155
+ )
156
+ if showEmptyValue
157
+ Picker.Item(
158
+ key=-1
159
+ value=stringifyValue(NULL_OPTION)
160
+ label=emptyValueLabel || getLabel(NULL_OPTION)
161
+ )
162
+ each item, index in options
163
+ Picker.Item(
164
+ key=index
165
+ value=stringifyValue(item)
166
+ label=getLabel(item)
167
+ )
168
+ `
169
+ }
170
+
171
+ export default observer(themed('Select', SelectWrapper))
package/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type RefObject } from 'react';
5
+ import { type UITextInputProps } from '@startupjs-ui/text-input';
6
+ import { type SelectOption } from './Wrapper/helpers';
7
+ declare const _default: import("react").ComponentType<SelectProps>;
8
+ export default _default;
9
+ export declare const _PropsJsonSchema: {};
10
+ export interface SelectProps extends Omit<UITextInputProps, 'value' | 'onChangeText' | 'icon' | 'iconPosition' | '_renderWrapper' | 'editable'> {
11
+ /** Available options (strings, numbers, or objects with `{ value, label }`) @default [] */
12
+ options?: SelectOption[];
13
+ /** Current selected value */
14
+ value?: any;
15
+ /** Show empty/none option @default true */
16
+ showEmptyValue?: boolean;
17
+ /** Label for the empty/none option */
18
+ emptyValueLabel?: string | number;
19
+ /** Ref forwarded to underlying TextInput */
20
+ ref?: RefObject<any>;
21
+ /** Test identifier passed to wrapper */
22
+ testID?: string;
23
+ /** Fired when selected value changes */
24
+ onChange?: (value: any) => void;
25
+ }
package/index.tsx ADDED
@@ -0,0 +1,72 @@
1
+ import { type ReactNode, type RefObject } from 'react'
2
+ import { pug, observer } from 'startupjs'
3
+ import TextInput, { type UITextInputProps } from '@startupjs-ui/text-input'
4
+ import { faAngleDown } from '@fortawesome/free-solid-svg-icons/faAngleDown'
5
+ import Wrapper from './Wrapper'
6
+ import { getLabelFromValue, type SelectOption } from './Wrapper/helpers'
7
+
8
+ export default observer(Select)
9
+
10
+ export const _PropsJsonSchema = {/* SelectProps */} // used in docs generation
11
+
12
+ export interface SelectProps extends Omit<UITextInputProps, 'value' | 'onChangeText' | 'icon' | 'iconPosition' | '_renderWrapper' | 'editable'> {
13
+ /** Available options (strings, numbers, or objects with `{ value, label }`) @default [] */
14
+ options?: SelectOption[]
15
+ /** Current selected value */
16
+ value?: any
17
+ /** Show empty/none option @default true */
18
+ showEmptyValue?: boolean
19
+ /** Label for the empty/none option */
20
+ emptyValueLabel?: string | number
21
+ /** Ref forwarded to underlying TextInput */
22
+ ref?: RefObject<any>
23
+ /** Test identifier passed to wrapper */
24
+ testID?: string
25
+ /** Fired when selected value changes */
26
+ onChange?: (value: any) => void
27
+ }
28
+
29
+ function Select ({
30
+ options = [],
31
+ value,
32
+ disabled = false,
33
+ showEmptyValue = true,
34
+ emptyValueLabel,
35
+ testID,
36
+ onChange,
37
+ ref,
38
+ ...props
39
+ }: SelectProps): ReactNode {
40
+ function renderWrapper (
41
+ { style }: { style?: any },
42
+ children: ReactNode
43
+ ): ReactNode {
44
+ return pug`
45
+ Wrapper(
46
+ style=style
47
+ options=options
48
+ disabled=disabled
49
+ value=value
50
+ onChange=onChange
51
+ showEmptyValue=showEmptyValue
52
+ emptyValueLabel=emptyValueLabel
53
+ testID=testID
54
+ )= children
55
+ `
56
+ }
57
+
58
+ return pug`
59
+ //- TODO
60
+ //- Add onKeyPress to 'keyDown' key that opens select dropdown
61
+ TextInput(
62
+ ref=ref
63
+ value=getLabelFromValue(value, options, emptyValueLabel)
64
+ disabled=disabled
65
+ icon=faAngleDown
66
+ iconPosition='right'
67
+ _renderWrapper=renderWrapper
68
+ editable=false /* HACK: Fixes cursor visibility when focusing on Select because we're focusing on TextInput */
69
+ ...props
70
+ )
71
+ `
72
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@startupjs-ui/select",
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
+ "@react-native-picker/picker": "^2.11.1",
12
+ "@startupjs-ui/core": "^0.1.3",
13
+ "@startupjs-ui/div": "^0.1.3",
14
+ "@startupjs-ui/span": "^0.1.3",
15
+ "@startupjs-ui/text-input": "^0.1.3"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "*",
19
+ "react-native": "*",
20
+ "startupjs": "*"
21
+ },
22
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
23
+ }