@startupjs-ui/input 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/input
package/inputs.ts CHANGED
@@ -34,6 +34,13 @@ function useBoundProps<T extends Record<string, any>> (props: T): T {
34
34
  return useBind(props) as T
35
35
  }
36
36
 
37
+ function getLabelableConfiguration (props: Record<string, any>) {
38
+ return {
39
+ isLabelClickable: !props.disabled && !props.readonly,
40
+ _webLabelMode: 'native' as const
41
+ }
42
+ }
43
+
37
44
  const useArrayProps = (props: Record<string, any>): Record<string, any> => {
38
45
  return props
39
46
  }
@@ -78,7 +85,7 @@ const useDateProps = ({
78
85
  return {
79
86
  mode: 'date',
80
87
  date: value,
81
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
88
+ configuration: getLabelableConfiguration(props),
82
89
  onChangeDate,
83
90
  _onLabelPress: () => ref?.current?.focus(),
84
91
  ...props
@@ -96,7 +103,7 @@ const useDateTimeProps = ({
96
103
  return {
97
104
  mode: 'datetime',
98
105
  date: value,
99
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
106
+ configuration: getLabelableConfiguration(props),
100
107
  onChangeDate,
101
108
  _onLabelPress: () => ref?.current?.focus(),
102
109
  ...props
@@ -114,7 +121,7 @@ const useTimeProps = ({
114
121
  return {
115
122
  mode: 'time',
116
123
  date: value,
117
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
124
+ configuration: getLabelableConfiguration(props),
118
125
  onChangeDate,
119
126
  _onLabelPress: () => ref?.current?.focus(),
120
127
  ...props
@@ -148,7 +155,7 @@ const useNumberProps = ({
148
155
 
149
156
  return {
150
157
  value,
151
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
158
+ configuration: getLabelableConfiguration(props),
152
159
  onChangeNumber,
153
160
  _onLabelPress: () => ref?.current?.focus(),
154
161
  ...props
@@ -169,7 +176,7 @@ const usePasswordProps = ({
169
176
 
170
177
  return {
171
178
  value,
172
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
179
+ configuration: getLabelableConfiguration(props),
173
180
  onChangeText,
174
181
  _onLabelPress: () => ref?.current?.focus(),
175
182
  ...props
@@ -215,7 +222,14 @@ const useRangeProps = ({ value, $value, onChange, ...props }: Record<string, any
215
222
  }
216
223
  }
217
224
 
218
- const useSelectProps = ({ value, $value, enum: _enum, options, onChange, ...props }: Record<string, any>): Record<string, any> => {
225
+ const useSelectProps = ({
226
+ value,
227
+ $value,
228
+ enum: _enum,
229
+ options,
230
+ onChange,
231
+ ...props
232
+ }: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
219
233
  ;({ value, onChange } = useBoundProps({ value, $value, onChange }))
220
234
  // if json-schema `enum` is passed, use it as options
221
235
  if (!options && _enum) options = _enum
@@ -223,6 +237,8 @@ const useSelectProps = ({ value, $value, enum: _enum, options, onChange, ...prop
223
237
  value,
224
238
  onChange,
225
239
  options,
240
+ configuration: getLabelableConfiguration(props),
241
+ _onLabelPress: () => ref?.current?.focus?.(),
226
242
  ...props
227
243
  }
228
244
  }
@@ -236,7 +252,7 @@ const useTextProps = ({
236
252
  ;({ value, onChangeText } = useBoundProps({ value, $value, onChangeText }))
237
253
  return {
238
254
  value,
239
- configuration: { isLabelClickable: !props.disabled && !props.readonly },
255
+ configuration: getLabelableConfiguration(props),
240
256
  onChangeText,
241
257
  _onLabelPress: () => ref?.current?.focus(),
242
258
  ...props
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startupjs-ui/input",
3
- "version": "0.1.22",
3
+ "version": "0.2.0-alpha.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -13,25 +13,25 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@fortawesome/free-solid-svg-icons": "^7.1.0",
16
- "@startupjs-ui/array-input": "^0.1.22",
17
- "@startupjs-ui/card": "^0.1.22",
18
- "@startupjs-ui/checkbox": "^0.1.22",
19
- "@startupjs-ui/color-picker": "^0.1.22",
20
- "@startupjs-ui/core": "^0.1.22",
21
- "@startupjs-ui/date-time-picker": "^0.1.22",
22
- "@startupjs-ui/div": "^0.1.22",
23
- "@startupjs-ui/file-input": "^0.1.22",
24
- "@startupjs-ui/icon": "^0.1.22",
25
- "@startupjs-ui/multi-select": "^0.1.22",
26
- "@startupjs-ui/number-input": "^0.1.22",
27
- "@startupjs-ui/object-input": "^0.1.22",
28
- "@startupjs-ui/password-input": "^0.1.22",
29
- "@startupjs-ui/radio": "^0.1.22",
30
- "@startupjs-ui/range-input": "^0.1.22",
31
- "@startupjs-ui/rank": "^0.1.22",
32
- "@startupjs-ui/select": "^0.1.22",
33
- "@startupjs-ui/span": "^0.1.22",
34
- "@startupjs-ui/text-input": "^0.1.22",
16
+ "@startupjs-ui/array-input": "^0.2.0-alpha.0",
17
+ "@startupjs-ui/card": "^0.2.0-alpha.0",
18
+ "@startupjs-ui/checkbox": "^0.2.0-alpha.0",
19
+ "@startupjs-ui/color-picker": "^0.2.0-alpha.0",
20
+ "@startupjs-ui/core": "^0.2.0-alpha.0",
21
+ "@startupjs-ui/date-time-picker": "^0.2.0-alpha.0",
22
+ "@startupjs-ui/div": "^0.2.0-alpha.0",
23
+ "@startupjs-ui/file-input": "^0.2.0-alpha.0",
24
+ "@startupjs-ui/icon": "^0.2.0-alpha.0",
25
+ "@startupjs-ui/multi-select": "^0.2.0-alpha.0",
26
+ "@startupjs-ui/number-input": "^0.2.0-alpha.0",
27
+ "@startupjs-ui/object-input": "^0.2.0-alpha.0",
28
+ "@startupjs-ui/password-input": "^0.2.0-alpha.0",
29
+ "@startupjs-ui/radio": "^0.2.0-alpha.0",
30
+ "@startupjs-ui/range-input": "^0.2.0-alpha.0",
31
+ "@startupjs-ui/rank": "^0.2.0-alpha.0",
32
+ "@startupjs-ui/select": "^0.2.0-alpha.0",
33
+ "@startupjs-ui/span": "^0.2.0-alpha.0",
34
+ "@startupjs-ui/text-input": "^0.2.0-alpha.0",
35
35
  "lodash": "^4.17.20"
36
36
  },
37
37
  "peerDependencies": {
@@ -39,5 +39,5 @@
39
39
  "react-native": "*",
40
40
  "startupjs": "*"
41
41
  },
42
- "gitHead": "3bd18c16f0f203ee3d940bf2e09381edc0034665"
42
+ "gitHead": "a428246a18d0e7f77809043c8240253240d11d66"
43
43
  }
package/wrapInput.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState, type ReactNode, type RefObject } from 'react'
2
- import { Text } from 'react-native'
2
+ import { Platform, Text } from 'react-native'
3
3
  import { pug, styl, observer } from 'startupjs'
4
4
  import { themed } from '@startupjs-ui/core'
5
5
  import Div from '@startupjs-ui/div'
@@ -7,9 +7,11 @@ import Icon from '@startupjs-ui/icon'
7
7
  import Span from '@startupjs-ui/span'
8
8
  import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons/faExclamationCircle'
9
9
  import merge from 'lodash/merge'
10
+ import getInputTestId from './helpers/getInputTestId'
10
11
  import useLayout from './useLayout'
11
12
 
12
13
  export const IS_WRAPPED = Symbol('wrapped into wrapInput()')
14
+ const IS_WEB = Platform.OS === 'web'
13
15
 
14
16
  export type InputLayout = 'pure' | 'rows' | 'columns'
15
17
 
@@ -25,6 +27,7 @@ export interface InputWrapperConfiguration extends InputWrapperLayoutConfigurati
25
27
  columns?: InputWrapperLayoutConfiguration
26
28
  isLabelColoredWhenFocusing?: boolean
27
29
  isLabelClickable?: boolean
30
+ _webLabelMode?: 'aria' | 'native'
28
31
  }
29
32
 
30
33
  export interface InputWrapperProps {
@@ -49,7 +52,7 @@ export function isWrapped (Component: any): boolean {
49
52
  }
50
53
 
51
54
  export default function wrapInput (Component: any, configuration: InputWrapperConfiguration = {}): any {
52
- configuration = merge(
55
+ const defaultConfiguration = merge(
53
56
  {
54
57
  rows: {
55
58
  labelPosition: 'top',
@@ -80,15 +83,16 @@ export default function wrapInput (Component: any, configuration: InputWrapperCo
80
83
  description
81
84
  })
82
85
 
83
- configuration = merge(configuration, componentConfiguration)
84
- configuration = merge(configuration, configuration[currentLayout])
86
+ const mergedConfiguration = merge({}, defaultConfiguration, componentConfiguration)
87
+ const resolvedConfiguration = merge({}, mergedConfiguration, mergedConfiguration[currentLayout])
85
88
 
86
89
  const {
87
90
  labelPosition,
88
91
  descriptionPosition,
89
92
  isLabelColoredWhenFocusing,
90
- isLabelClickable
91
- } = configuration
93
+ isLabelClickable,
94
+ _webLabelMode = 'aria'
95
+ } = resolvedConfiguration
92
96
 
93
97
  const [focused, setFocused] = useState(false)
94
98
  const isReadOnlyOrDisabled = [props.readonly, props.disabled].some(Boolean)
@@ -110,32 +114,67 @@ export default function wrapInput (Component: any, configuration: InputWrapperCo
110
114
  }, [focused, isLabelColoredWhenFocusing, isReadOnlyOrDisabled])
111
115
 
112
116
  const hasError = Array.isArray(error) ? error.length > 0 : !!error
113
-
114
- const _label = pug`
115
- if label
116
- Span.label(
117
- key='label'
118
- part='label'
119
- styleName=[
120
- currentLayout,
121
- currentLayout + '-' + labelPosition,
122
- {
123
- focused: isLabelColoredWhenFocusing ? focused : false,
124
- error: hasError
125
- }
126
- ]
127
- onPress=isLabelClickable
128
- ? _onLabelPress
129
- : undefined
130
- )
131
- = label
132
- if required === true
133
- Text.required= ' *'
134
- `
117
+ const generatedTestID = props.testID ?? getInputTestId({
118
+ ...props,
119
+ label,
120
+ description,
121
+ testId: props.testID
122
+ })
123
+ const semanticBaseId = typeof generatedTestID === 'string' && generatedTestID !== ''
124
+ ? generatedTestID
125
+ : undefined
126
+ const inputId = semanticBaseId ? `${semanticBaseId}-input` : undefined
127
+ const labelId = label && semanticBaseId ? `${semanticBaseId}-label` : undefined
128
+ const descriptionId = description && semanticBaseId ? `${semanticBaseId}-description` : undefined
129
+ const errorId = hasError && semanticBaseId ? `${semanticBaseId}-error` : undefined
130
+ const useNativeWebLabel = IS_WEB && _webLabelMode === 'native' && !!inputId
131
+
132
+ const labelStyleName = [
133
+ currentLayout,
134
+ currentLayout + '-' + labelPosition,
135
+ {
136
+ focused: isLabelColoredWhenFocusing ? focused : false,
137
+ error: hasError
138
+ }
139
+ ]
140
+ const requiredAsterisk = required === true
141
+ ? pug`
142
+ Text.required(aria-hidden=IS_WEB ? true : undefined)= ' *'
143
+ `
144
+ : null
145
+ const WebLabelElement = 'label'
146
+ const _label = label
147
+ ? useNativeWebLabel
148
+ ? pug`
149
+ WebLabelElement.label(
150
+ key='label'
151
+ id=labelId
152
+ htmlFor=inputId
153
+ part='label'
154
+ styleName=labelStyleName
155
+ )
156
+ = label
157
+ = requiredAsterisk
158
+ `
159
+ : pug`
160
+ Span.label(
161
+ key='label'
162
+ id=labelId
163
+ part='label'
164
+ styleName=labelStyleName
165
+ onPress=isLabelClickable
166
+ ? _onLabelPress
167
+ : undefined
168
+ )
169
+ = label
170
+ = requiredAsterisk
171
+ `
172
+ : null
135
173
  const _description = pug`
136
174
  if description
137
175
  Span.description(
138
176
  key='description'
177
+ id=descriptionId
139
178
  part='description'
140
179
  styleName=[
141
180
  currentLayout,
@@ -147,6 +186,25 @@ export default function wrapInput (Component: any, configuration: InputWrapperCo
147
186
  `
148
187
 
149
188
  const passRef = ref ? { ref } : {}
189
+ const inputAccessibilityProps: Record<string, any> = {}
190
+ const describedBy = [descriptionId].filter(Boolean).join(' ') || undefined
191
+
192
+ if (props['aria-label'] == null) {
193
+ if (props.accessibilityLabel != null) inputAccessibilityProps['aria-label'] = props.accessibilityLabel
194
+ else if (label) inputAccessibilityProps['aria-label'] = label
195
+ }
196
+
197
+ if (inputId) {
198
+ inputAccessibilityProps.id = inputId
199
+ inputAccessibilityProps.nativeID = inputId
200
+ }
201
+ if (required === true) inputAccessibilityProps['aria-required'] = true
202
+ if (labelId) inputAccessibilityProps['aria-labelledby'] = labelId
203
+ if (describedBy) inputAccessibilityProps['aria-describedby'] = describedBy
204
+ if (hasError && errorId) {
205
+ inputAccessibilityProps['aria-errormessage'] = errorId
206
+ inputAccessibilityProps['aria-invalid'] = true
207
+ }
150
208
 
151
209
  const input = pug`
152
210
  Component(
@@ -156,24 +214,29 @@ export default function wrapInput (Component: any, configuration: InputWrapperCo
156
214
  _hasError=hasError
157
215
  onFocus=handleFocus
158
216
  onBlur=handleBlur
217
+ ...inputAccessibilityProps
159
218
  ...passRef
160
219
  ...props
161
220
  )
162
221
  `
163
222
  const err = pug`
164
223
  if hasError
165
- each _error, index in (Array.isArray(error) ? error : [error])
166
- Div.errorContainer(
167
- key='error-' + index
168
- styleName=[
169
- currentLayout,
170
- currentLayout + '-' + descriptionPosition,
171
- ]
172
- vAlign='center'
173
- row
174
- )
175
- Icon.errorContainer-icon(icon=faExclamationCircle)
176
- Span.errorContainer-text= _error
224
+ Div.errorContainer(
225
+ key='error'
226
+ id=errorId
227
+ styleName=[
228
+ currentLayout,
229
+ currentLayout + '-' + descriptionPosition,
230
+ ]
231
+ vAlign='center'
232
+ row
233
+ )
234
+ Icon.errorContainer-icon(icon=faExclamationCircle)
235
+ Span.errorContainer-text
236
+ each _error, index in (Array.isArray(error) ? error : [error])
237
+ if index
238
+ Text= ' '
239
+ = _error
177
240
  `
178
241
 
179
242
  return pug`