<input-text>
<label for="name-input">Name</label>
<div class="row">
<div class="group auto">
<input type="text" id="name-input" name="name" autocomplete="name" required>
</div>
</div>
<p class="error" aria-live="assertive" id="name-error"></p>
</input-text>
input-text {
width: 100%;
&[value="0"] input {
color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
&:hover button {
opacity: var(--opacity-translucent);
&:not(:disabled) {
opacity: var(--opacity-solid);
cursor: pointer;
}
}
&:focus-within {
& label,
& p,
& span {
opacity: var(--opacity-solid);
}
& button {
opacity: var(--opacity-translucent);
&:not(:disabled) {
opacity: var(--opacity-solid);
cursor: pointer;
}
}
& input {
color: var(--color-text);
}
}
& label,
& p,
& span {
opacity: var(--opacity-dimmed);
transition: opacity var(--transition-short) var(--easing-inout);
}
& label {
display: block;
font-size: var(--font-size-s);
color: var(--color-text);
margin-bottom: var(--space-xxs);
}
.row {
display: flex;
gap: var(--space-s);
}
.group {
display: flex;
align-items: baseline;
background: var(--color-input);
border-bottom: 1px solid var(--color-border);
width: 100%;
&.short {
width: 6rem;
}
.clear {
border: 0;
border-radius: 50%;
color: var(--color-input);
width: var(--space-m);
height: var(--space-m);
line-height: 1.1;
align-self: center;
text-align: center;
padding: 0;
margin: 0 var(--space-xxs);
}
& span:first-child {
padding-left: var(--space-xs);
}
& span:last-child {
padding-right: var(--space-xs);
}
}
& input {
flex-grow: 1;
display: inline-block;
box-sizing: border-box;
background: var(--color-input);
color: var(--color-text);
border: 0;
padding: var(--space-xs) var(--space-xxs);
font-size: var(--font-size-m);
height: 2rem;
width: 100%;
transition: color var(--transition-short) var(--easing-inout);
&::placeholder {
color: var(--color-text);
opacity: var(--opacity-translucent);
}
}
& input[aria-invalid="true"] {
box-shadow: 0 0 var(--space-xxs) 2px var(--color-error);
}
& span {
flex-grow: 0;
}
::-webkit-textfield-decoration-container {
height: 100%;
}
.error,
.description {
margin: var(--space-xs) 0 0;
font-size: var(--font-size-xs);
line-height: var(--line-height-s);
&:empty {
display: none;
}
}
.error {
color: color-mix(in srgb, var(--color-text) 50%, var(--color-error));
}
.description {
color: var(--color-text-soft);
}
}
import {
type Component,
asString,
component,
first,
on,
setAttribute,
setProperty,
setText,
UNSET,
} from "../../../";
export type InputTextProps = {
value: string;
length: number;
error: string;
description: string;
};
export default component(
"input-text",
{
value: asString(),
length: 0,
error: "",
description: "",
},
(el) => {
const input = el.querySelector("input");
if (!input) throw new Error("No input element found");
const errorId = el.querySelector(".error")?.id;
const descriptionId = el.querySelector(".description")?.id;
return [
first(
"input",
on("change", () => {
el.value = input.value;
el.error = input.validationMessage ?? "";
}),
on("input", () => {
el.length = input.value.length;
}),
setProperty("ariaInvalid", () => (el.error ? "true" : "false")),
setAttribute("aria-errormessage", () =>
el.error && errorId ? errorId : UNSET,
),
setAttribute("aria-describedby", () =>
el.description && descriptionId ? descriptionId : UNSET,
),
),
first<InputTextProps, HTMLButtonElement>(
".clear",
on("click", () => {
el.value = "";
el.length = 0;
input.focus();
}),
setProperty("hidden", () => !el.length),
),
first(".error", setText("error")),
first(".description", setText("description")),
];
},
);
declare global {
interface HTMLElementTagNameMap {
"input-text": Component<InputTextProps>;
}
}