@spectrum-css/textfield
Version:
The Spectrum CSS textfield component
305 lines (296 loc) • 8.66 kB
JavaScript
import { Template as FieldLabel } from "@spectrum-css/fieldlabel/stories/template.js";
import { Template as HelpText } from "@spectrum-css/helptext/stories/template.js";
import { Template as Icon } from "@spectrum-css/icon/stories/template.js";
import { Container, getRandomId } from "@spectrum-css/preview/decorators";
import { Template as ProgressCircle } from "@spectrum-css/progresscircle/stories/template.js";
import { html } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { styleMap } from "lit/directives/style-map.js";
import { when } from "lit/directives/when.js";
import "../index.css";
import "../themes/spectrum.css";
/* Must be imported last */
import "../themes/express.css";
/**
* @typedef API
* @property {string} [rootClass="spectrum-Textfield"]
* @property {string} [size="m"]
* @property {string[]} [customClasses=[]]
* @property {string[]} [customInputClasses=[]]
* @property {string[]} [customIconClasses=[]]
* @property {string[]} [customProgressCircleClasses=[]]
* @property {Record<string, string>} [customStyles={}]
* @property {boolean} [isInvalid=false]
* @property {boolean} [isValid=false]
* @property {boolean} [multiline=false]
* @property {boolean} [grows=false]
* @property {boolean} [isQuiet=false]
* @property {boolean} [isFocused=false]
* @property {boolean} [isDisabled=false]
* @property {boolean} [isRequired=false]
* @property {boolean} [isReadOnly=false]
* @property {boolean} [isKeyboardFocused=false]
* @property {boolean} [isLoading=false]
* @property {boolean} [displayLabel=false]
* @property {"top"|"side"} [labelPosition="top"]
* @property {string} [labelText]
* @property {string} [iconName]
* @property {string} [iconSet]
* @property {string} [pattern]
* @property {string|undefined} [placeholder]
* @property {string|undefined} [name]
* @property {string|undefined} [id]
* @property {HTMLInputElement[value]|HTMLTextAreaElement[value]} [value]
* @property {HTMLInputElement[type]} [type="text"]
* @property {boolean} [autocomplete=true]
* @property {Function} [onclick]
*/
/**
*
* @param {API} args
* @param {import('@storybook/types').StoryContext<import('@storybook/web-components').WebComponentsRenderer, API>} context
* @returns {import('lit').TemplateResult}
*/
export const Template = ({
rootClass = "spectrum-Textfield",
size = "m",
customClasses = [],
customInputClasses = [],
customIconClasses = [],
customProgressCircleClasses = [],
isInvalid = false,
isValid = false,
multiline = false,
grows = false,
isQuiet = false,
isFocused = false,
isDisabled = false,
isRequired = false,
isReadOnly = false,
isKeyboardFocused = false,
isLoading = false,
displayLabel = false,
labelPosition = "top",
labelText,
characterCount,
iconName,
iconSet,
pattern,
placeholder,
name,
helpText = "",
id = getRandomId("textfield"),
value = "",
type = "text",
autocomplete = true,
onclick,
customStyles = {},
} = {}, context = {}) => {
const { updateArgs } = context;
// Override icon name and set if the field is invalid or valid
if (isInvalid) {
iconName = "Alert";
iconSet = "workflow";
}
else if (isValid) {
iconName = "Checkmark";
iconSet = "ui";
}
return html`
<div
class=${classMap({
[rootClass]: true,
[`${rootClass}--size${size?.toUpperCase()}`]:
typeof size !== "undefined",
[`${rootClass}--multiline`]: multiline,
[`${rootClass}--grows`]: grows,
[`${rootClass}--quiet`]: isQuiet,
[`${rootClass}--sideLabel`]: labelPosition === "side",
"is-invalid": isInvalid,
"is-valid": isValid,
"is-focused": isFocused,
"is-keyboardFocused": isKeyboardFocused,
"is-disabled": isDisabled,
"is-readOnly": isReadOnly,
...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}),
})}
style=${styleMap(customStyles)}
=${onclick}
=${function() {
updateArgs?.({
isFocused: true,
});
}}
=${function(e) {
// Tab key was used.
if (e.keyCode === 9) {
// The element that was focused when the key was released is this textfield / input.
if (e.target == this || e.target?.parentNode == this) {
updateArgs?.({ isKeyboardFocused: true });
// Manually add class since updateArgs doesn't always work on the Docs page.
this.classList.add("is-keyboardFocused");
}
}
}}
=${function() {
updateArgs?.({
isFocused: false,
isKeyboardFocused: false,
});
// Manually remove class since updateArgs doesn't always work on the Docs page.
this.classList.remove("is-keyboardFocused");
}}
id=${ifDefined(id)}
>
${when(displayLabel, () => FieldLabel({
size,
label: labelText,
isDisabled,
}, context))}
${when(typeof characterCount !== "undefined", () => html`
<span class="${rootClass}-characterCount">${characterCount}</span>`)}
${when(iconName, () => Icon({
size,
iconName,
setName: iconSet,
customClasses: [
isInvalid || isValid
? `${rootClass}-validationIcon`
: `${rootClass}-icon`,
...customIconClasses,
],
}, context))}
${when(multiline,
() => html`<textarea
placeholder=${ifDefined(placeholder)}
name=${ifDefined(name)}
id=${ifDefined(id ? `${id}-input` : undefined)}
.value=${value}
autocomplete=${ifDefined(autocomplete ? undefined : "off")}
?required=${isRequired}
?disabled=${isDisabled}
?readonly=${isReadOnly}
pattern=${ifDefined(pattern)}
class=${classMap({
[`${rootClass}-input`]: true,
...customInputClasses.reduce((a, c) => ({ ...a, [c]: true }), {}),
})}
/>`,
() => html`<input
type=${ifDefined(type)}
placeholder=${ifDefined(placeholder)}
name=${ifDefined(name)}
id=${ifDefined(id ? `${id}-input` : undefined)}
.value=${value}
autocomplete=${ifDefined(autocomplete ? undefined : "off")}
?required=${isRequired}
?disabled=${isDisabled}
?readonly=${isReadOnly}
pattern=${ifDefined(pattern)}
class=${classMap({
[`${rootClass}-input`]: true,
...customInputClasses.reduce((a, c) => ({ ...a, [c]: true }), {}),
})}
/>`
)}
${when(isLoading, () => ProgressCircle({
isIndeterminate: true,
size: "s",
customClasses: customProgressCircleClasses,
}, context))}
${when(helpText, () =>
HelpText({
text: helpText,
variant: isInvalid ? "negative" : "neutral",
size,
hideIcon: true,
isDisabled
}, context)
)}
</div>
`;
};
export const HelpTextOptions = (args, context) => Container({
direction: "column",
withBorder: false,
withHeading: false,
content: html`
${Container({
withBorder: false,
heading: "Description",
content: Template({...args, isRequired: true, labelText: "Username", value: "lisawilson24", helpText: "Username must be at least 8 characters."}, context),
}, context)}
${Container({
withBorder: false,
heading: "Error message",
content: Template({...args, isRequired: true, labelText: "Email address", value: "abc@adobe.com", helpText: "Enter your email address", isInvalid: true }, context),
}, context)}
`
}, context);
export const TextFieldOptions = (args, context) => Container({
direction: "row",
withBorder: false,
wrapperStyles: {
rowGap: "12px",
},
content: html`
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Default",
content: Template(args, context)
}, context)}
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Invalid",
content: Template({...args, isInvalid: true}, context)
}, context)}
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Focused",
content: Template({...args, isFocused: true}, context)
}, context)}
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Invalid, focused",
content: Template({...args, isInvalid: true, isFocused: true}, context)
}, context)}
`
}, context);
export const KeyboardFocusTemplate = (args, context) => Container({
direction: "column",
withBorder: false,
wrapperStyles: {
rowGap: "12px",
},
content: html`
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Default",
content: Template({...args, isKeyboardFocused: true}, context)
}, context)}
${Container({
withBorder: false,
containerStyles: {
"gap": "8px",
},
heading: "Quiet",
content: Template({...args, isKeyboardFocused: true, isQuiet: true}, context)
}, context)}
`
}, context);