Skip to content

Commit 0f50c14

Browse files
authored
docs: fix and document use of keyboard focus classes on text fields (#3354)
* docs: fix and document use of keyboard focus classes on text fields Our Storybook was adding the keyboard focus class on click, which isn't the intended design. Click focus and focused by use of a keyboard have different styles. This update stops the class from being applied on click, and documents how the class should be added by the implementation. * docs(textfield): add keyboard focused class on tab focus Example functionality in our Storybook to add the keyboard focused class to the text field when it was focused with the tab key, similar to what is documented for implementations. * docs(textfield): remove some unneeded keyboard focus examples Remove some examples of keyboard focus from docs stories, that are not necessary to show now that keyboard focus has its own docs example.
1 parent f2b82cf commit 0f50c14

File tree

4 files changed

+95
-38
lines changed

4 files changed

+95
-38
lines changed

components/textfield/stories/template.js

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,28 @@ export const Template = ({
120120
style=${styleMap(customStyles)}
121121
@click=${onclick}
122122
@focusin=${function() {
123-
updateArgs({
123+
updateArgs?.({
124124
isFocused: true,
125-
isKeyboardFocused: true
126125
});
127126
}}
127+
@keyup=${function(e) {
128+
// Tab key was used.
129+
if (e.keyCode === 9) {
130+
// The element that was focused when the key was released is this textfield / input.
131+
if (e.target == this || e.target?.parentNode == this) {
132+
updateArgs?.({ isKeyboardFocused: true });
133+
// Manually add class since updateArgs doesn't always work on the Docs page.
134+
this.classList.add("is-keyboardFocused");
135+
}
136+
}
137+
}}
128138
@focusout=${function() {
129-
updateArgs({
139+
updateArgs?.({
130140
isFocused: false,
131-
isKeyboardFocused: false
141+
isKeyboardFocused: false,
132142
});
143+
// Manually remove class since updateArgs doesn't always work on the Docs page.
144+
this.classList.remove("is-keyboardFocused");
133145
}}
134146
id=${ifDefined(id)}
135147
>
@@ -189,13 +201,14 @@ export const Template = ({
189201
customClasses: customProgressCircleClasses,
190202
}, context))}
191203
${when(helpText, () =>
192-
HelpText({
193-
text: helpText,
194-
variant: isInvalid ? "negative" : "neutral",
195-
size,
196-
hideIcon: true,
197-
isDisabled
198-
}, context ))}
204+
HelpText({
205+
text: helpText,
206+
variant: isInvalid ? "negative" : "neutral",
207+
size,
208+
hideIcon: true,
209+
isDisabled
210+
}, context)
211+
)}
199212
</div>
200213
`;
201214
};
@@ -231,7 +244,7 @@ export const TextFieldOptions = (args, context) => Container({
231244
"gap": "8px",
232245
},
233246
heading: "Default",
234-
content: Template({...args, context})
247+
content: Template(args, context)
235248
}, context)}
236249
${Container({
237250
withBorder: false,
@@ -257,21 +270,31 @@ export const TextFieldOptions = (args, context) => Container({
257270
heading: "Invalid, focused",
258271
content: Template({...args, isInvalid: true, isFocused: true}, context)
259272
}, context)}
273+
`
274+
}, context);
275+
276+
export const KeyboardFocusTemplate = (args, context) => Container({
277+
direction: "column",
278+
withBorder: false,
279+
wrapperStyles: {
280+
rowGap: "12px",
281+
},
282+
content: html`
260283
${Container({
261284
withBorder: false,
262285
containerStyles: {
263286
"gap": "8px",
264287
},
265-
heading: "Keyboard-focused",
288+
heading: "Default",
266289
content: Template({...args, isKeyboardFocused: true}, context)
267290
}, context)}
268291
${Container({
269292
withBorder: false,
270293
containerStyles: {
271294
"gap": "8px",
272295
},
273-
heading: "Invalid, keyboard-focused",
274-
content: Template({...args, isInvalid: true, isKeyboardFocused: true}, context)
296+
heading: "Quiet",
297+
content: Template({...args, isKeyboardFocused: true, isQuiet: true}, context)
275298
}, context)}
276299
`
277300
}, context);

components/textfield/stories/textarea.stories.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { Sizes } from "@spectrum-css/preview/decorators";
22
import { disableDefaultModes } from "@spectrum-css/preview/modes";
33
import metadata from "../metadata/metadata.json";
44
import packageJson from "../package.json";
5-
import { HelpTextOptionsTextArea, Template, TextAreaOptions } from "./textarea.template.js";
5+
import { HelpTextOptionsTextArea, KeyboardFocusTemplate, Template, TextAreaOptions } from "./textarea.template.js";
66
import { TextAreaGroup } from "./textarea.test.js";
77
import { default as Textfield } from "./textfield.stories.js";
88

99
/**
1010
* A text area is multi-line text field using the `<textarea>` element that lets a user input a longer amount of text than a standard text field. It can include all of the standard validation options supported by the text field component.
11-
*/
12-
11+
*/
1312
export default {
1413
title: "Text area",
1514
component: "TextArea",
@@ -53,7 +52,7 @@ CharacterCount.parameters = {
5352

5453
/**
5554
* A text area in a disabled state shows that an input field exists, but is not available in that circumstance. This can be used to maintain layout continuity and communicate that a field may become available later.
56-
*/
55+
*/
5756
export const Disabled = Template.bind({});
5857
Disabled.tags = ["!dev"];
5958
Disabled.args = {
@@ -67,7 +66,7 @@ Disabled.parameters = {
6766
* A text area can have [help text](/docs/components-help-text--docs) below the field to give extra context or instruction about what a user should input in the field. The help text area has two options: a description and an error message. The description communicates a hint or helpful information, such as specific requirements for correctly filling out the field. The error message communicates an error for when the field requirements aren’t met, prompting a user to adjust what they had originally input.
6867
*
6968
* Instead of placeholder text, use the help text description to convey requirements or to show any formatting examples that would help user comprehension. Putting instructions for how to complete an input, requirements, or any other essential information into placeholder text is not accessible.
70-
*/
69+
*/
7170
export const HelpText = HelpTextOptionsTextArea.bind({});
7271
HelpText.tags = ["!dev"];
7372
HelpText.parameters = {
@@ -85,7 +84,7 @@ Quiet.parameters = {
8584

8685
/**
8786
* Text area has a read-only option for when content in the disabled state still needs to be shown. This allows for content to be copied, but not interacted with or changed.
88-
*/
87+
*/
8988
export const Readonly = Template.bind({});
9089
Readonly.tags = ["!dev"];
9190
Readonly.args = {
@@ -99,7 +98,7 @@ Readonly.storyName = "Read-only";
9998

10099
/**
101100
* Side labels are most useful when vertical space is limited.
102-
*/
101+
*/
103102
export const SideLabel = Template.bind({});
104103
SideLabel.tags = ["!dev"];
105104
SideLabel.args = {
@@ -113,10 +112,9 @@ SideLabel.parameters = {
113112
chromatic: { disableSnapshot: true }
114113
};
115114

116-
117115
/**
118116
* Text area can display a validation icon when the text entry is expected to conform to a specific format (e.g., email address, credit card number, password creation requirements, etc.). The icon appears as soon as a user types a valid entry in the field.
119-
*/
117+
*/
120118
export const Validation = Template.bind({});
121119
Validation.tags = ["!dev"];
122120
Validation.args = {
@@ -127,7 +125,6 @@ Validation.parameters = {
127125
};
128126
Validation.storyName = "Validation icon";
129127

130-
131128
export const Sizing = (args, context) => Sizes({
132129
Template: Template,
133130
withHeading: false,
@@ -143,6 +140,20 @@ Sizing.parameters = {
143140
chromatic: { disableSnapshot: true }
144141
};
145142

143+
/**
144+
* When the text area was focused using the keyboard (e.g. with the tab key), the implementation must add the `is-keyboardFocused` class, which
145+
* displays the focus indicator. This indicator should not appear on focus from a click or tap.
146+
* The example below has this class applied on first load for demonstration purposes.
147+
*/
148+
export const KeyboardFocus = KeyboardFocusTemplate.bind({});
149+
KeyboardFocus.tags = ["!dev"];
150+
KeyboardFocus.args = {
151+
isKeyboardFocused: true,
152+
};
153+
KeyboardFocus.parameters = {
154+
chromatic: { disableSnapshot: true }
155+
};
156+
146157
// ********* VRT ONLY ********* //
147158
// @todo should this show text field and text area in the same snapshot?
148159
export const WithForcedColors = TextAreaGroup.bind({});

components/textfield/stories/textarea.template.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const TextAreaOptions = (args, context) => Container({
4949
"gap": "8px",
5050
},
5151
heading: "Default",
52-
content: Template({...args, context})
52+
content: Template(args, context)
5353
}, context)}
5454
${Container({
5555
withBorder: false,
@@ -75,21 +75,31 @@ export const TextAreaOptions = (args, context) => Container({
7575
heading: "Invalid, focused",
7676
content: Template({...args, isInvalid: true, isFocused: true}, context)
7777
}, context)}
78+
`
79+
}, context);
80+
81+
export const KeyboardFocusTemplate = (args, context) => Container({
82+
direction: "column",
83+
withBorder: false,
84+
wrapperStyles: {
85+
rowGap: "12px",
86+
},
87+
content: html`
7888
${Container({
7989
withBorder: false,
8090
containerStyles: {
8191
"gap": "8px",
8292
},
83-
heading: "Keyboard-focused",
93+
heading: "Default",
8494
content: Template({...args, isKeyboardFocused: true}, context)
8595
}, context)}
8696
${Container({
8797
withBorder: false,
8898
containerStyles: {
8999
"gap": "8px",
90100
},
91-
heading: "Invalid, keyboard-focused",
92-
content: Template({...args, isInvalid: true, isKeyboardFocused: true}, context)
101+
heading: "Quiet",
102+
content: Template({...args, isKeyboardFocused: true, isQuiet: true}, context)
93103
}, context)}
94104
`
95105
}, context);

components/textfield/stories/textfield.stories.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { disableDefaultModes } from "@spectrum-css/preview/modes";
33
import { isDisabled, isFocused, isInvalid, isKeyboardFocused, isLoading, isQuiet, isReadOnly, isRequired, isValid, size } from "@spectrum-css/preview/types";
44
import metadata from "../metadata/metadata.json";
55
import packageJson from "../package.json";
6-
import { HelpTextOptions, Template, TextFieldOptions } from "./template.js";
6+
import { HelpTextOptions, KeyboardFocusTemplate, Template, TextFieldOptions } from "./template.js";
77
import { TextFieldGroup } from "./textfield.test.js";
88

99
/**
@@ -143,7 +143,7 @@ export default {
143143

144144
/**
145145
* Text fields should always have a label. In rare cases where context is sufficient and an accessibility expert has reviewed the design, the label could be undefined. These text fields without a visible label should still include an aria-label in HTML (depending on the context, “aria-label” or “aria-labelledby”).
146-
*/
146+
*/
147147

148148
export const Default = TextFieldGroup.bind({});
149149
Default.tags = ["!autodocs"];
@@ -160,7 +160,7 @@ Standard.parameters = {
160160

161161
/**
162162
* Text fields can display a character count indicator when the length of the text entry needs to be kept under a predefined value. Character count indicators can be used in conjunction with other indicators (validation icon, “optional” or “required” indicators) when necessary.
163-
*/
163+
*/
164164
export const CharacterCount = Template.bind({});
165165
CharacterCount.tags = ["!dev"];
166166
CharacterCount.args = {
@@ -174,7 +174,7 @@ CharacterCount.parameters = {
174174

175175
/**
176176
* A text field in a disabled state shows that an input field exists, but is not available in that circumstance. This can be used to maintain layout continuity and communicate that a field may become available later.
177-
*/
177+
*/
178178
export const Disabled = Template.bind({});
179179
Disabled.tags = ["!dev"];
180180
Disabled.args = {
@@ -188,7 +188,7 @@ Disabled.parameters = {
188188
* A text field can have [help text](/docs/components-help-text--docs) below the field to give extra context or instruction about what a user should input in the field. The help text area has two options: a description and an error message. The description communicates a hint or helpful information, such as specific requirements for correctly filling out the field. The error message communicates an error for when the field requirements aren’t met, prompting a user to adjust what they had originally input.
189189
*
190190
* Instead of placeholder text, use the help text description to convey requirements or to show any formatting examples that would help user comprehension. Putting instructions for how to complete an input, requirements, or any other essential information into placeholder text is not accessible.
191-
*/
191+
*/
192192
export const HelpText = HelpTextOptions.bind({});
193193
HelpText.tags = ["!dev"];
194194
HelpText.parameters = {
@@ -197,7 +197,7 @@ HelpText.parameters = {
197197

198198
/**
199199
* Quiet text fields can have no visible background. This style works best when a clear layout (vertical stack, table, grid) makes it easy to parse. Too many quiet components in a small space can be hard to read.
200-
*/
200+
*/
201201
export const Quiet = TextFieldOptions.bind({});
202202
Quiet.tags = ["!dev"];
203203
Quiet.args = {
@@ -210,7 +210,7 @@ Quiet.parameters = {
210210

211211
/**
212212
* Text fields have a read-only option for when content in the disabled state still needs to be shown. This allows for content to be copied, but not interacted with or changed.
213-
*/
213+
*/
214214
export const Readonly = Template.bind({});
215215
Readonly.tags = ["!dev"];
216216
Readonly.args = {
@@ -224,7 +224,7 @@ Readonly.storyName = "Read-only";
224224

225225
/**
226226
* Side labels are most useful when vertical space is limited.
227-
*/
227+
*/
228228
export const SideLabel = Template.bind({});
229229
SideLabel.tags = ["!dev"];
230230
SideLabel.args = {
@@ -254,7 +254,7 @@ Sizing.parameters = {
254254

255255
/**
256256
* Text fields can display a validation icon when the text entry is expected to conform to a specific format (e.g., email address, credit card number, password creation requirements, etc.). The icon appears as soon as a user types a valid entry in the field.
257-
*/
257+
*/
258258
export const Validation = Template.bind({});
259259
Validation.tags = ["!dev"];
260260
Validation.args = {
@@ -265,6 +265,19 @@ Validation.parameters = {
265265
};
266266
Validation.storyName = "Validation icon";
267267

268+
/**
269+
* When the text field was focused using the keyboard (e.g. with the tab key), the implementation must add the `is-keyboardFocused` class, which
270+
* displays the focus indicator. This indicator should not appear on focus from a click or tap.
271+
* The example below has this class applied on first load for demonstration purposes.
272+
*/
273+
export const KeyboardFocus = KeyboardFocusTemplate.bind({});
274+
KeyboardFocus.tags = ["!dev"];
275+
KeyboardFocus.args = {
276+
isKeyboardFocused: true,
277+
};
278+
KeyboardFocus.parameters = {
279+
chromatic: { disableSnapshot: true }
280+
};
268281

269282
// ********* VRT ONLY ********* //
270283
// @todo should this show text field and text area in the same snapshot?

0 commit comments

Comments
 (0)