@startupjs-ui/select 0.1.22 → 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 CHANGED
@@ -3,6 +3,17 @@
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
+
6
17
  ## [0.1.22](https://github.com/startupjs/startupjs-ui/compare/v0.1.21...v0.1.22) (2026-03-25)
7
18
 
8
19
  **Note:** Version bump only for package @startupjs-ui/select
@@ -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 = '-\u00A0\u00A0\u00A0\u00A0\u00A0'
8
+ export const PICKER_NULL = 'empty'
13
9
  export const NULL_OPTION: undefined = undefined
14
10
 
15
- export function stringifyValue (option: any): string | undefined {
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
- let value: any
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 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))
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 (stringifyValue(value) === stringifyValue(option)) {
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
- stringifyValue,
10
- getLabel,
11
- parseValue,
12
- NULL_OPTION,
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
- const value = event.target.value
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=stringifyValue(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
- 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)
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(parseValue(value))
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=stringifyValue(value)
129
+ selectedValue=selectedKey
98
130
  onValueChange=onValueChange
99
131
  )
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
132
+ each entry in optionEntries
107
133
  Picker.Item(
108
- key=index
109
- value=stringifyValue(item)
110
- label=getLabel(item)
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(parseValue(value))
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=stringifyValue(value)
181
+ selectedValue=selectedKey
154
182
  onValueChange=onValueChange
155
183
  )
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
184
+ each entry in optionEntries
163
185
  Picker.Item(
164
- key=index
165
- value=stringifyValue(item)
166
- label=getLabel(item)
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.1.22",
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.1.22",
13
- "@startupjs-ui/div": "^0.1.22",
14
- "@startupjs-ui/span": "^0.1.22",
15
- "@startupjs-ui/text-input": "^0.1.22"
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": "3bd18c16f0f203ee3d940bf2e09381edc0034665"
22
+ "gitHead": "a428246a18d0e7f77809043c8240253240d11d66"
23
23
  }