-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathcreateTextField.ts
257 lines (230 loc) · 7.71 KB
/
createTextField.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/*
* Copyright 2022 Solid Aria Working Group.
* MIT License
*
* Portions of this file are based on code from react-spectrum.
* Copyright 2020 Adobe. All rights reserved.
*
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { createFocusable } from "@solid-aria/focus";
import { createField } from "@solid-aria/label";
import {
AriaLabelingProps,
AriaValidationProps,
FocusableDOMProps,
FocusableProps,
HelpTextProps,
InputBase,
IntrinsicHTMLElements,
LabelableProps,
TextInputBase,
TextInputDOMProps,
Validation,
ValueBase
} from "@solid-aria/types";
import { callHandler } from "@solid-aria/utils";
import { Accessor, JSX, mergeProps, splitProps } from "solid-js";
type DefaultElementType = "input";
/**
* The intrinsic HTML element names that `createTextField` supports; e.g. `input`, `textarea`.
*/
type TextFieldIntrinsicElements = "input" | "textarea";
/**
* The HTML element interfaces that `createTextField` supports based on what is
* defined for `TextFieldIntrinsicElements`; e.g. `HTMLInputElement`, `HTMLTextAreaElement`.
*/
type TextFieldHTMLElementType = Pick<IntrinsicHTMLElements, TextFieldIntrinsicElements>;
/**
* The HTML attributes interfaces that `createTextField` supports based on what
* is defined for `TextFieldIntrinsicElements`; e.g. `InputHTMLAttributes`, `TextareaHTMLAttributes`.
*/
type TextFieldHTMLAttributesType = Pick<JSX.IntrinsicElements, TextFieldIntrinsicElements>;
/**
* The type of `inputProps` returned by `createTextField`; e.g. `InputHTMLAttributes`, `TextareaHTMLAttributes`.
*/
type TextFieldInputProps<T extends TextFieldIntrinsicElements> = TextFieldHTMLAttributesType[T];
export interface AriaTextFieldProps<T extends TextFieldIntrinsicElements>
extends InputBase,
Validation,
HelpTextProps,
FocusableProps,
TextInputBase,
ValueBase<string>,
LabelableProps,
AriaLabelingProps,
FocusableDOMProps,
TextInputDOMProps,
AriaValidationProps {
/**
* Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application.
* See https://www.w3.org/TR/wai-aria-1.2/#textbox.
*/
"aria-activedescendant"?: string;
/**
* Indicates whether inputting text could trigger display of one or more predictions
* of the user's intended value for an input
* and specifies how predictions would be presented if they are made.
*/
"aria-autocomplete"?: "none" | "inline" | "list" | "both";
/**
* Indicates the availability and type of interactive popup element,
* such as menu or dialog, that can be triggered by an element.
*/
"aria-haspopup"?: boolean | "false" | "true" | "menu" | "listbox" | "tree" | "grid" | "dialog";
/**
* The HTML element used to render the input, e.g. 'input', or 'textarea'.
* It determines whether certain HTML attributes will be included in `inputProps`.
* For example, [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-type).
* @default 'input'
*/
inputElementType?: T;
}
export interface TextFieldAria<T extends TextFieldIntrinsicElements = DefaultElementType> {
/**
* Props for the input element.
*/
inputProps: TextFieldInputProps<T>;
/**
* Props for the text field's visible label element, if any.
*/
labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
/**
* Props for the text field's description element, if any.
*/
descriptionProps: JSX.HTMLAttributes<any>;
/**
* Props for the text field's error message element, if any.
*/
errorMessageProps: JSX.HTMLAttributes<any>;
}
const inputPropKeys = [
"type",
"pattern",
"aria-errormessage",
"aria-activedescendant",
"aria-autocomplete",
"aria-haspopup",
"value",
"defaultValue",
"autocomplete",
"maxLength",
"minLength",
"name",
"placeholder",
"inputMode"
] as const;
/**
* Provides the behavior and accessibility implementation for a text field.
* @param props - Props for the text field.
* @param ref - Ref to the HTML input or textarea element.
*/
export function createTextField<T extends TextFieldIntrinsicElements = DefaultElementType>(
props: AriaTextFieldProps<T>,
ref: Accessor<TextFieldHTMLElementType[T] | undefined>
): TextFieldAria<T> {
const defaultProps: AriaTextFieldProps<TextFieldIntrinsicElements> = {
type: "text",
inputElementType: "input",
isDisabled: false,
isRequired: false,
isReadOnly: false
};
// eslint-disable-next-line solid/reactivity
props = mergeProps(defaultProps, props) as AriaTextFieldProps<T>;
// local props are separated so that they don't mess with mergeProps
// e.g. the `type` prop should return `undefined` if the element is not an input
// but `mergeProps` will search for the first defined value (ignoring undefined)
const localProps = splitProps(props, inputPropKeys)[1];
const { focusableProps } = createFocusable(localProps, ref);
const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField(localProps);
const baseInputProps: JSX.IntrinsicElements["input"] & { defaultValue: string | undefined } = {
get type() {
return props.inputElementType === "input" ? props.type : undefined;
},
get pattern() {
return props.inputElementType === "input" ? props.pattern : undefined;
},
get disabled() {
return props.isDisabled;
},
get readOnly() {
return props.isReadOnly;
},
get "aria-required"() {
return props.isRequired || undefined;
},
get "aria-invalid"() {
return props.validationState === "invalid" || undefined;
},
get "aria-errormessage"() {
return props["aria-errormessage"];
},
get "aria-activedescendant"() {
return props["aria-activedescendant"];
},
get "aria-autocomplete"() {
return props["aria-autocomplete"];
},
get "aria-haspopup"() {
return props["aria-haspopup"];
},
get value() {
return props.value;
},
get defaultValue() {
return props.value ? undefined : props.defaultValue;
},
get autocomplete() {
return props.autocomplete;
},
get maxLength() {
return props.maxLength;
},
get minLength() {
return props.minLength;
},
get name() {
return props.name;
},
get placeholder() {
return props.placeholder;
},
get inputMode() {
return props.inputMode;
},
// Change events
onChange: e => props.onChange?.((e.target as HTMLInputElement).value),
// Clipboard events
onCopy: e => callHandler(props.onCopy, e),
onCut: e => callHandler(props.onCut, e),
onPaste: e => callHandler(props.onPaste, e),
// Composition events
onCompositionEnd: e => callHandler(props.onCompositionEnd, e),
onCompositionStart: e => callHandler(props.onCompositionStart, e),
onCompositionUpdate: e => callHandler(props.onCompositionUpdate, e),
// Selection events
onSelect: e => callHandler(props.onSelect, e),
// Input events
onBeforeInput: e => callHandler(props.onBeforeInput, e),
onInput: e => callHandler(props.onInput, e)
};
const inputProps = mergeProps(
focusableProps,
fieldProps,
baseInputProps
) as TextFieldInputProps<T>;
return {
labelProps,
inputProps,
descriptionProps,
errorMessageProps
};
}