@startupjs-ui/select 0.1.19 → 0.2.0-alpha.0
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 +27 -0
- package/Wrapper/helpers.ts +65 -22
- package/Wrapper/index.tsx +59 -37
- package/index.d.ts +18 -0
- package/index.tsx +33 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.2.0-alpha.0](https://github.com/startupjs/startupjs-ui/compare/v0.1.22...v0.2.0-alpha.0) (2026-03-27)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* fix and improve accessibility of various components. Add storybook with tests. ([#21](https://github.com/startupjs/startupjs-ui/issues/21)) ([83b6576](https://github.com/startupjs/startupjs-ui/commit/83b65767ed61b24209f71b143ba1c2986170ab58))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.1.22](https://github.com/startupjs/startupjs-ui/compare/v0.1.21...v0.1.22) (2026-03-25)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @startupjs-ui/select
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## [0.1.21](https://github.com/startupjs/startupjs-ui/compare/v0.1.20...v0.1.21) (2026-03-25)
|
|
26
|
+
|
|
27
|
+
**Note:** Version bump only for package @startupjs-ui/select
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
6
33
|
## [0.1.19](https://github.com/startupjs/startupjs-ui/compare/v0.1.18...v0.1.19) (2026-03-17)
|
|
7
34
|
|
|
8
35
|
**Note:** Version bump only for package @startupjs-ui/select
|
package/Wrapper/helpers.ts
CHANGED
|
@@ -3,23 +3,24 @@ export type SelectOption =
|
|
|
3
3
|
| number
|
|
4
4
|
| { value?: any, label?: string | number }
|
|
5
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
6
|
// Force undefined to be a special value to
|
|
11
7
|
// workaround the undefined value bug in Picker
|
|
12
|
-
export const PICKER_NULL = '
|
|
8
|
+
export const PICKER_NULL = 'empty'
|
|
13
9
|
export const NULL_OPTION: undefined = undefined
|
|
14
10
|
|
|
15
|
-
export
|
|
11
|
+
export interface SelectOptionEntry {
|
|
12
|
+
key: string
|
|
13
|
+
value: any
|
|
14
|
+
label: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getOptionValue (option: any): any {
|
|
18
|
+
return option?.value ?? option
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stringifyComparableValue (option: any): string | undefined {
|
|
16
22
|
try {
|
|
17
|
-
|
|
18
|
-
if (option?.value != null) {
|
|
19
|
-
value = option.value
|
|
20
|
-
} else {
|
|
21
|
-
value = option
|
|
22
|
-
}
|
|
23
|
+
const value = getOptionValue(option)
|
|
23
24
|
if (value == null) return PICKER_NULL
|
|
24
25
|
return JSON.stringify(value)
|
|
25
26
|
} catch (error) {
|
|
@@ -27,16 +28,58 @@ export function stringifyValue (option: any): string | undefined {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
export function areValuesEqual (a: any, b: any): boolean {
|
|
32
|
+
return stringifyComparableValue(a) === stringifyComparableValue(b)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getOptionKey (index: number): string {
|
|
36
|
+
return `opt:${index}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getOptionEntries (
|
|
40
|
+
options: SelectOption[],
|
|
41
|
+
showEmptyValue?: boolean,
|
|
42
|
+
emptyValueLabel?: string | number
|
|
43
|
+
): SelectOptionEntry[] {
|
|
44
|
+
const entries: SelectOptionEntry[] = []
|
|
45
|
+
|
|
46
|
+
if (showEmptyValue) {
|
|
47
|
+
entries.push({
|
|
48
|
+
key: PICKER_NULL,
|
|
49
|
+
value: undefined,
|
|
50
|
+
label: getLabel(emptyValueLabel ?? NULL_OPTION)
|
|
51
|
+
})
|
|
39
52
|
}
|
|
53
|
+
|
|
54
|
+
options.forEach((option, index) => {
|
|
55
|
+
entries.push({
|
|
56
|
+
key: getOptionKey(index),
|
|
57
|
+
value: getOptionValue(option),
|
|
58
|
+
label: getLabel(option)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return entries
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getOptionKeyFromValue (
|
|
66
|
+
value: any,
|
|
67
|
+
options: SelectOption[],
|
|
68
|
+
showEmptyValue?: boolean,
|
|
69
|
+
emptyValueLabel?: string | number
|
|
70
|
+
): string | undefined {
|
|
71
|
+
const entries = getOptionEntries(options, showEmptyValue, emptyValueLabel)
|
|
72
|
+
return entries.find(entry => areValuesEqual(entry.value, value))?.key
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getValueFromKey (
|
|
76
|
+
key: string,
|
|
77
|
+
options: SelectOption[],
|
|
78
|
+
showEmptyValue?: boolean,
|
|
79
|
+
emptyValueLabel?: string | number
|
|
80
|
+
): any {
|
|
81
|
+
const entries = getOptionEntries(options, showEmptyValue, emptyValueLabel)
|
|
82
|
+
return entries.find(entry => entry.key === key)?.value
|
|
40
83
|
}
|
|
41
84
|
|
|
42
85
|
export function getLabel (option: any): string {
|
|
@@ -56,7 +99,7 @@ export function getLabelFromValue (
|
|
|
56
99
|
emptyValueLabel: any = NULL_OPTION
|
|
57
100
|
): string {
|
|
58
101
|
for (const option of options) {
|
|
59
|
-
if (
|
|
102
|
+
if (areValuesEqual(value, option)) {
|
|
60
103
|
return getLabel(option)
|
|
61
104
|
}
|
|
62
105
|
}
|
package/Wrapper/index.tsx
CHANGED
|
@@ -6,10 +6,10 @@ import { themed } from '@startupjs-ui/core'
|
|
|
6
6
|
import Div from '@startupjs-ui/div'
|
|
7
7
|
import Span from '@startupjs-ui/span'
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
getOptionEntries,
|
|
10
|
+
getOptionKeyFromValue,
|
|
11
|
+
getValueFromKey,
|
|
12
|
+
PICKER_NULL,
|
|
13
13
|
type SelectOption
|
|
14
14
|
} from './helpers'
|
|
15
15
|
import STYLES from './index.cssx.styl'
|
|
@@ -31,6 +31,22 @@ export interface SelectWrapperProps {
|
|
|
31
31
|
emptyValueLabel?: string | number
|
|
32
32
|
/** Test identifier */
|
|
33
33
|
testID?: string
|
|
34
|
+
/** Cross-platform accessible name */
|
|
35
|
+
'aria-label'?: string
|
|
36
|
+
/** Accessible label for the web select overlay */
|
|
37
|
+
accessibilityLabel?: string
|
|
38
|
+
/** Accessible hint for the web select overlay */
|
|
39
|
+
accessibilityHint?: string
|
|
40
|
+
/** Web-only control id for label association */
|
|
41
|
+
id?: string
|
|
42
|
+
/** Web-only labelled-by relationship */
|
|
43
|
+
'aria-labelledby'?: string
|
|
44
|
+
/** Web-only described-by relationship */
|
|
45
|
+
'aria-describedby'?: string
|
|
46
|
+
/** Web-only error message relationship */
|
|
47
|
+
'aria-errormessage'?: string
|
|
48
|
+
/** Web-only invalid state */
|
|
49
|
+
'aria-invalid'?: boolean
|
|
34
50
|
/** Fired when selected value changes */
|
|
35
51
|
onChange?: (value: any) => void
|
|
36
52
|
}
|
|
@@ -50,11 +66,21 @@ function SelectWrapperWeb ({
|
|
|
50
66
|
showEmptyValue,
|
|
51
67
|
emptyValueLabel,
|
|
52
68
|
testID,
|
|
69
|
+
'aria-label': ariaLabel,
|
|
70
|
+
accessibilityLabel,
|
|
71
|
+
accessibilityHint,
|
|
72
|
+
id,
|
|
73
|
+
'aria-labelledby': ariaLabelledBy,
|
|
74
|
+
'aria-describedby': ariaDescribedBy,
|
|
75
|
+
'aria-errormessage': ariaErrorMessage,
|
|
76
|
+
'aria-invalid': ariaInvalid,
|
|
53
77
|
onChange
|
|
54
78
|
}: SelectWrapperProps): ReactNode {
|
|
79
|
+
const optionEntries = getOptionEntries(options, showEmptyValue, emptyValueLabel)
|
|
80
|
+
const selectedKey = getOptionKeyFromValue(value, options, showEmptyValue, emptyValueLabel) ?? PICKER_NULL
|
|
81
|
+
|
|
55
82
|
function onSelectChange (event: any) {
|
|
56
|
-
|
|
57
|
-
if (onChange) onChange(parseValue(value))
|
|
83
|
+
if (onChange) onChange(getValueFromKey(event.target.value, options, showEmptyValue, emptyValueLabel))
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
return pug`
|
|
@@ -62,16 +88,19 @@ function SelectWrapperWeb ({
|
|
|
62
88
|
= children
|
|
63
89
|
if !disabled
|
|
64
90
|
select(
|
|
91
|
+
id=id
|
|
65
92
|
style=STYLES.overlay
|
|
66
|
-
value=
|
|
93
|
+
value=selectedKey
|
|
67
94
|
onChange=onSelectChange
|
|
95
|
+
aria-label=ariaLabel ?? accessibilityLabel
|
|
96
|
+
aria-labelledby=ariaLabelledBy
|
|
97
|
+
aria-describedby=ariaDescribedBy
|
|
98
|
+
aria-errormessage=ariaErrorMessage
|
|
99
|
+
aria-invalid=ariaInvalid
|
|
68
100
|
)
|
|
69
|
-
|
|
70
|
-
option(key
|
|
71
|
-
=
|
|
72
|
-
each item, index in options
|
|
73
|
-
option(key=index value=stringifyValue(item))
|
|
74
|
-
= getLabel(item)
|
|
101
|
+
each entry in optionEntries
|
|
102
|
+
option(key=entry.key value=entry.key)
|
|
103
|
+
= entry.label
|
|
75
104
|
`
|
|
76
105
|
}
|
|
77
106
|
|
|
@@ -85,8 +114,11 @@ function SelectWrapperAndroid ({
|
|
|
85
114
|
emptyValueLabel,
|
|
86
115
|
onChange
|
|
87
116
|
}: SelectWrapperProps): ReactNode {
|
|
117
|
+
const optionEntries = getOptionEntries(options, showEmptyValue, emptyValueLabel)
|
|
118
|
+
const selectedKey = getOptionKeyFromValue(value, options, showEmptyValue, emptyValueLabel) ?? PICKER_NULL
|
|
119
|
+
|
|
88
120
|
function onValueChange (value: any) {
|
|
89
|
-
if (onChange) onChange(
|
|
121
|
+
if (onChange) onChange(getValueFromKey(value, options, showEmptyValue, emptyValueLabel))
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
return pug`
|
|
@@ -94,20 +126,14 @@ function SelectWrapperAndroid ({
|
|
|
94
126
|
= children
|
|
95
127
|
if !disabled
|
|
96
128
|
Picker.overlay(
|
|
97
|
-
selectedValue=
|
|
129
|
+
selectedValue=selectedKey
|
|
98
130
|
onValueChange=onValueChange
|
|
99
131
|
)
|
|
100
|
-
|
|
101
|
-
Picker.Item(
|
|
102
|
-
key=-1
|
|
103
|
-
value=stringifyValue(NULL_OPTION)
|
|
104
|
-
label=emptyValueLabel || getLabel(NULL_OPTION)
|
|
105
|
-
)
|
|
106
|
-
each item, index in options
|
|
132
|
+
each entry in optionEntries
|
|
107
133
|
Picker.Item(
|
|
108
|
-
key=
|
|
109
|
-
value=
|
|
110
|
-
label=
|
|
134
|
+
key=entry.key
|
|
135
|
+
value=entry.key
|
|
136
|
+
label=entry.label
|
|
111
137
|
)
|
|
112
138
|
`
|
|
113
139
|
}
|
|
@@ -123,9 +149,11 @@ function SelectWrapperIOS ({
|
|
|
123
149
|
onChange
|
|
124
150
|
}: SelectWrapperProps): ReactNode {
|
|
125
151
|
const [showModal, setShowModal] = useState(false)
|
|
152
|
+
const optionEntries = getOptionEntries(options, showEmptyValue, emptyValueLabel)
|
|
153
|
+
const selectedKey = getOptionKeyFromValue(value, options, showEmptyValue, emptyValueLabel) ?? PICKER_NULL
|
|
126
154
|
|
|
127
155
|
function onValueChange (value: any) {
|
|
128
|
-
if (onChange) onChange(
|
|
156
|
+
if (onChange) onChange(getValueFromKey(value, options, showEmptyValue, emptyValueLabel))
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
return pug`
|
|
@@ -150,20 +178,14 @@ function SelectWrapperIOS ({
|
|
|
150
178
|
Span.done Done
|
|
151
179
|
Div.modalBottom
|
|
152
180
|
Picker(
|
|
153
|
-
selectedValue=
|
|
181
|
+
selectedValue=selectedKey
|
|
154
182
|
onValueChange=onValueChange
|
|
155
183
|
)
|
|
156
|
-
|
|
157
|
-
Picker.Item(
|
|
158
|
-
key=-1
|
|
159
|
-
value=stringifyValue(NULL_OPTION)
|
|
160
|
-
label=emptyValueLabel || getLabel(NULL_OPTION)
|
|
161
|
-
)
|
|
162
|
-
each item, index in options
|
|
184
|
+
each entry in optionEntries
|
|
163
185
|
Picker.Item(
|
|
164
|
-
key=
|
|
165
|
-
value=
|
|
166
|
-
label=
|
|
186
|
+
key=entry.key
|
|
187
|
+
value=entry.key
|
|
188
|
+
label=entry.label
|
|
167
189
|
)
|
|
168
190
|
`
|
|
169
191
|
}
|
package/index.d.ts
CHANGED
|
@@ -20,6 +20,24 @@ export interface SelectProps extends Omit<UITextInputProps, 'value' | 'onChangeT
|
|
|
20
20
|
ref?: RefObject<any>;
|
|
21
21
|
/** Test identifier passed to wrapper */
|
|
22
22
|
testID?: string;
|
|
23
|
+
/** Cross-platform accessible name */
|
|
24
|
+
'aria-label'?: string;
|
|
25
|
+
/** Accessible label forwarded to the web select overlay */
|
|
26
|
+
accessibilityLabel?: string;
|
|
27
|
+
/** Accessible hint forwarded to the web select overlay */
|
|
28
|
+
accessibilityHint?: string;
|
|
29
|
+
/** Web-only control id forwarded to the native select overlay */
|
|
30
|
+
id?: string;
|
|
31
|
+
/** Native id alias forwarded to the native select overlay */
|
|
32
|
+
nativeID?: string;
|
|
33
|
+
/** Web-only labelled-by relationship */
|
|
34
|
+
'aria-labelledby'?: string;
|
|
35
|
+
/** Web-only described-by relationship */
|
|
36
|
+
'aria-describedby'?: string;
|
|
37
|
+
/** Web-only error message relationship */
|
|
38
|
+
'aria-errormessage'?: string;
|
|
39
|
+
/** Web-only invalid state */
|
|
40
|
+
'aria-invalid'?: boolean;
|
|
23
41
|
/** Fired when selected value changes */
|
|
24
42
|
onChange?: (value: any) => void;
|
|
25
43
|
}
|
package/index.tsx
CHANGED
|
@@ -22,6 +22,24 @@ export interface SelectProps extends Omit<UITextInputProps, 'value' | 'onChangeT
|
|
|
22
22
|
ref?: RefObject<any>
|
|
23
23
|
/** Test identifier passed to wrapper */
|
|
24
24
|
testID?: string
|
|
25
|
+
/** Cross-platform accessible name */
|
|
26
|
+
'aria-label'?: string
|
|
27
|
+
/** Accessible label forwarded to the web select overlay */
|
|
28
|
+
accessibilityLabel?: string
|
|
29
|
+
/** Accessible hint forwarded to the web select overlay */
|
|
30
|
+
accessibilityHint?: string
|
|
31
|
+
/** Web-only control id forwarded to the native select overlay */
|
|
32
|
+
id?: string
|
|
33
|
+
/** Native id alias forwarded to the native select overlay */
|
|
34
|
+
nativeID?: string
|
|
35
|
+
/** Web-only labelled-by relationship */
|
|
36
|
+
'aria-labelledby'?: string
|
|
37
|
+
/** Web-only described-by relationship */
|
|
38
|
+
'aria-describedby'?: string
|
|
39
|
+
/** Web-only error message relationship */
|
|
40
|
+
'aria-errormessage'?: string
|
|
41
|
+
/** Web-only invalid state */
|
|
42
|
+
'aria-invalid'?: boolean
|
|
25
43
|
/** Fired when selected value changes */
|
|
26
44
|
onChange?: (value: any) => void
|
|
27
45
|
}
|
|
@@ -33,6 +51,15 @@ function Select ({
|
|
|
33
51
|
showEmptyValue = true,
|
|
34
52
|
emptyValueLabel,
|
|
35
53
|
testID,
|
|
54
|
+
'aria-label': ariaLabel,
|
|
55
|
+
accessibilityLabel,
|
|
56
|
+
accessibilityHint,
|
|
57
|
+
id,
|
|
58
|
+
nativeID,
|
|
59
|
+
'aria-labelledby': ariaLabelledBy,
|
|
60
|
+
'aria-describedby': ariaDescribedBy,
|
|
61
|
+
'aria-errormessage': ariaErrorMessage,
|
|
62
|
+
'aria-invalid': ariaInvalid,
|
|
36
63
|
onChange,
|
|
37
64
|
ref,
|
|
38
65
|
...props
|
|
@@ -51,6 +78,12 @@ function Select ({
|
|
|
51
78
|
showEmptyValue=showEmptyValue
|
|
52
79
|
emptyValueLabel=emptyValueLabel
|
|
53
80
|
testID=testID
|
|
81
|
+
aria-label=ariaLabel ?? accessibilityLabel
|
|
82
|
+
id=id || nativeID
|
|
83
|
+
aria-labelledby=ariaLabelledBy
|
|
84
|
+
aria-describedby=ariaDescribedBy
|
|
85
|
+
aria-errormessage=ariaErrorMessage
|
|
86
|
+
aria-invalid=ariaInvalid
|
|
54
87
|
)= children
|
|
55
88
|
`
|
|
56
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startupjs-ui/select",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@react-native-picker/picker": "^2.11.1",
|
|
12
|
-
"@startupjs-ui/core": "^0.
|
|
13
|
-
"@startupjs-ui/div": "^0.
|
|
14
|
-
"@startupjs-ui/span": "^0.
|
|
15
|
-
"@startupjs-ui/text-input": "^0.
|
|
12
|
+
"@startupjs-ui/core": "^0.2.0-alpha.0",
|
|
13
|
+
"@startupjs-ui/div": "^0.2.0-alpha.0",
|
|
14
|
+
"@startupjs-ui/span": "^0.2.0-alpha.0",
|
|
15
|
+
"@startupjs-ui/text-input": "^0.2.0-alpha.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"react": "*",
|
|
19
19
|
"react-native": "*",
|
|
20
20
|
"startupjs": "*"
|
|
21
21
|
},
|
|
22
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "a428246a18d0e7f77809043c8240253240d11d66"
|
|
23
23
|
}
|