@startupjs-ui/input 0.1.23 → 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 +5 -2
- package/inputs.ts +23 -7
- package/package.json +21 -21
- package/wrapInput.tsx +103 -40
package/CHANGELOG.md
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
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
|
-
|
|
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
7
|
|
|
8
|
-
|
|
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))
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 = ({
|
|
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:
|
|
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.
|
|
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.
|
|
17
|
-
"@startupjs-ui/card": "^0.
|
|
18
|
-
"@startupjs-ui/checkbox": "^0.
|
|
19
|
-
"@startupjs-ui/color-picker": "^0.
|
|
20
|
-
"@startupjs-ui/core": "^0.
|
|
21
|
-
"@startupjs-ui/date-time-picker": "^0.
|
|
22
|
-
"@startupjs-ui/div": "^0.
|
|
23
|
-
"@startupjs-ui/file-input": "^0.
|
|
24
|
-
"@startupjs-ui/icon": "^0.
|
|
25
|
-
"@startupjs-ui/multi-select": "^0.
|
|
26
|
-
"@startupjs-ui/number-input": "^0.
|
|
27
|
-
"@startupjs-ui/object-input": "^0.
|
|
28
|
-
"@startupjs-ui/password-input": "^0.
|
|
29
|
-
"@startupjs-ui/radio": "^0.
|
|
30
|
-
"@startupjs-ui/range-input": "^0.
|
|
31
|
-
"@startupjs-ui/rank": "^0.
|
|
32
|
-
"@startupjs-ui/select": "^0.
|
|
33
|
-
"@startupjs-ui/span": "^0.
|
|
34
|
-
"@startupjs-ui/text-input": "^0.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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`
|