[Input] Several improvements

- Simplify styles and DOM
- Add `.wa-text-field` utility class
- Eliminate `--border-color` (except when set by appearance utils), `--border-style`, `--border-radius`
This commit is contained in:
Lea Verou
2024-12-19 00:37:32 -05:00
parent 996fa6df57
commit e69632ff60
6 changed files with 239 additions and 246 deletions

View File

@@ -28,35 +28,37 @@ Use the `variant` attribute to set the button's semantic variant.
Use the `appearance` attribute to change the button's visual appearance.
```html {.example}
<div style="margin-block-end: 1rem;">
<wa-button appearance="accent" variant="neutral">Accent</wa-button>
<wa-button appearance="filled" variant="neutral">Filled</wa-button>
<wa-button appearance="outlined" variant="neutral">Outlined</wa-button>
<wa-button appearance="plain" variant="neutral">Text</wa-button>
</div>
<div style="margin-block-end: 1rem;">
<wa-button appearance="accent" variant="brand">Accent</wa-button>
<wa-button appearance="filled" variant="brand">Filled</wa-button>
<wa-button appearance="outlined" variant="brand">Outlined</wa-button>
<wa-button appearance="plain" variant="brand">Text</wa-button>
</div>
<div style="margin-block-end: 1rem;">
<wa-button appearance="accent" variant="success">Accent</wa-button>
<wa-button appearance="filled" variant="success">Filled</wa-button>
<wa-button appearance="outlined" variant="success">Outlined</wa-button>
<wa-button appearance="plain" variant="success">Text</wa-button>
</div>
<div style="margin-block-end: 1rem;">
<wa-button appearance="accent" variant="warning">Accent</wa-button>
<wa-button appearance="filled" variant="warning">Filled</wa-button>
<wa-button appearance="outlined" variant="warning">Outlined</wa-button>
<wa-button appearance="plain" variant="warning">Text</wa-button>
</div>
<div>
<wa-button appearance="accent" variant="danger">Accent</wa-button>
<wa-button appearance="filled" variant="danger">Filled</wa-button>
<wa-button appearance="outlined" variant="danger">Outlined</wa-button>
<wa-button appearance="plain" variant="danger">Text</wa-button>
<div class="wa-stack">
<div class="wa-gap-m">
<wa-button appearance="accent" variant="neutral">Accent</wa-button>
<wa-button appearance="outlined" variant="neutral">Outlined</wa-button>
<wa-button appearance="filled" variant="neutral">Filled</wa-button>
<wa-button appearance="plain" variant="neutral">Plain</wa-button>
</div>
<div class="wa-gap-m">
<wa-button appearance="accent" variant="brand">Accent</wa-button>
<wa-button appearance="outlined" variant="brand">Outlined</wa-button>
<wa-button appearance="filled" variant="brand">Filled</wa-button>
<wa-button appearance="plain" variant="brand">Plain</wa-button>
</div>
<div class="wa-gap-m">
<wa-button appearance="accent" variant="success">Accent</wa-button>
<wa-button appearance="outlined" variant="success">Outlined</wa-button>
<wa-button appearance="filled" variant="success">Filled</wa-button>
<wa-button appearance="plain" variant="success">Plain</wa-button>
</div>
<div class="wa-gap-m">
<wa-button appearance="accent" variant="warning">Accent</wa-button>
<wa-button appearance="outlined" variant="warning">Outlined</wa-button>
<wa-button appearance="filled" variant="warning">Filled</wa-button>
<wa-button appearance="plain" variant="warning">Plain</wa-button>
</div>
<div class="wa-gap-m">
<wa-button appearance="accent" variant="danger">Accent</wa-button>
<wa-button appearance="outlined" variant="danger">Outlined</wa-button>
<wa-button appearance="filled" variant="danger">Filled</wa-button>
<wa-button appearance="plain" variant="danger">Plain</wa-button>
</div>
</div>
```

View File

@@ -133,33 +133,35 @@ Use the `prefix` and `suffix` slots to add icons.
Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the label to the left of the control, but the possible orientations are nearly endless. The same technique works for inputs, textareas, radio groups, and similar form controls.
```html {.example}
<wa-input class="label-on-left" label="Name" hint="Enter your name"></wa-input>
<wa-input class="label-on-left" label="Email" type="email" hint="Enter your email"></wa-input>
<wa-textarea class="label-on-left" label="Bio" hint="Tell us something about yourself"></wa-textarea>
<div class="label-on-left">
<wa-input label="Name" hint="Enter your name"></wa-input>
<wa-input label="Email" type="email" hint="Enter your email"></wa-input>
<wa-textarea label="Bio" hint="Tell us something about yourself"></wa-textarea>
</div>
<style>
.label-on-left {
--label-width: 3.75rem;
--gap-width: 1rem;
}
.label-on-left + .label-on-left {
margin-top: var(--wa-space-m);
}
.label-on-left::part(form-control) {
display: grid;
grid: auto / var(--label-width) 1fr;
gap: var(--wa-space-3xs) var(--gap-width);
grid-template-columns: auto 1fr;
gap: var(--wa-space-l);
align-items: center;
}
.label-on-left::part(form-control-label) {
text-align: right;
}
wa-input, wa-textarea {
grid-column: 1 / -1;
grid-row-end: span 2;
display: grid;
grid-template-rows: subgrid;
gap: 0 var(--wa-space-l);
align-items: center;
}
.label-on-left::part(hint) {
grid-column-start: 2;
::part(label) {
text-align: right;
}
::part(hint) {
grid-column: 2;
}
}
</style>
```

View File

@@ -1,42 +1,39 @@
:host {
display: block;
display: flex;
flex-flow: column;
border-width: 0;
}
.input {
flex: 1 1 auto;
.wa-text-field {
flex: auto;
display: flex;
align-items: stretch;
justify-content: start;
position: relative;
font-size: inherit;
height: var(--wa-form-control-height);
border-color: inherit;
border-style: inherit;
border-radius: inherit;
}
.control {
input {
flex: 1 1 auto;
min-width: 0;
height: 100%;
font: inherit;
border: none;
/* prettier-ignore */
background-color: rgb(118 118 118 / 0); /* ensures proper placeholder styles in webkit's date input */
box-shadow: none;
padding: 0;
margin: 0;
cursor: inherit;
-webkit-appearance: none;
height: calc(var(--wa-form-control-height) - var(--border-width) * 2);
padding: 0 var(--wa-space);
}
.control::-webkit-search-decoration,
.control::-webkit-search-cancel-button,
.control::-webkit-search-results-button,
.control::-webkit-search-results-decoration {
input::-webkit-search-decoration,
input::-webkit-search-cancel-button,
input::-webkit-search-results-button,
input::-webkit-search-results-decoration {
-webkit-appearance: none;
}
.control:focus {
input:focus {
outline: none;
}
@@ -53,11 +50,11 @@
}
.prefix::slotted(*) {
margin-inline-start: var(--wa-space);
margin-inline-end: var(--wa-space);
}
.suffix::slotted(*) {
margin-inline-end: var(--wa-space);
margin-inline-start: var(--wa-space);
}
/*
@@ -69,7 +66,6 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: calc(1em + var(--wa-space) * 2);
font-size: inherit;
color: var(--wa-color-neutral-on-quiet);
border: none;

View File

@@ -44,21 +44,16 @@ import styles from './input.css';
* @event wa-input - Emitted when the control receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart form-control - The form control that wraps the label, input, and hint.
* @csspart form-control-label - The label's wrapper.
* @csspart form-control-input - The input's wrapper.
* @csspart label - The label
* @csspart hint - The hint's wrapper.
* @csspart base - The component's base wrapper.
* @csspart input - The internal `<input>` control.
* @csspart input - The wrapper being rendered as an input
* @csspart base - The internal `<input>` control.
* @csspart prefix - The container that wraps the prefix.
* @csspart clear-button - The clear button.
* @csspart password-toggle-button - The password toggle button.
* @csspart suffix - The container that wraps the suffix.
*
* @cssproperty --background-color - The input's background color.
* @cssproperty --border-color - The color of the input's borders.
* @cssproperty --border-radius - The radius of the input's corners.
* @cssproperty --border-style - The style of the input's borders.
* @cssproperty --border-width - The width of the input's borders. Expects a single value.
* @cssproperty --box-shadow - The shadow effects around the edges of the input.
*/
@@ -76,7 +71,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
private readonly hasSlotController = new HasSlotController(this, 'hint', 'label');
private readonly localize = new LocalizeController(this);
@query('.control') input: HTMLInputElement;
@query('input') input: HTMLInputElement;
@property() title = ''; // make reactive to pass through
@@ -403,103 +398,99 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
(typeof this.value === 'number' || (this.value && this.value.length > 0));
return html`
<div part="form-control" class="form-control">
<label part="form-control-label" class="label" for="input" aria-hidden=${hasLabel ? 'false' : 'true'}>
<slot name="label">${this.label}</slot>
</label>
<label part="form-control-label label" class="label" for="input" aria-hidden=${hasLabel ? 'false' : 'true'}>
<slot name="label">${this.label}</slot>
</label>
<div part="form-control-input" class="form-control-input">
<div part="base" class="input">
<slot name="prefix" part="prefix" class="prefix"></slot>
<div part="input" class="wa-text-field">
<slot name="prefix" part="prefix" class="prefix"></slot>
<input
part="input"
id="input"
class="control"
type=${this.type === 'password' && this.passwordVisible ? 'text' : this.type}
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${ifDefined(this.name)}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
placeholder=${ifDefined(this.placeholder)}
minlength=${ifDefined(this.minlength)}
maxlength=${ifDefined(this.maxlength)}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step as number)}
.value=${live(this.value || '')}
autocapitalize=${ifDefined(this.autocapitalize)}
autocomplete=${ifDefined(this.autocomplete)}
autocorrect=${ifDefined(this.autocorrect)}
?autofocus=${this.autofocus}
spellcheck=${this.spellcheck}
pattern=${ifDefined(this.pattern)}
enterkeyhint=${ifDefined(this.enterkeyhint)}
inputmode=${ifDefined(this.inputmode)}
aria-describedby="hint"
@change=${this.handleChange}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
<input
part="base"
id="input"
class="control"
type=${this.type === 'password' && this.passwordVisible ? 'text' : this.type}
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${ifDefined(this.name)}
?disabled=${this.disabled}
?readonly=${this.readonly}
?required=${this.required}
placeholder=${ifDefined(this.placeholder)}
minlength=${ifDefined(this.minlength)}
maxlength=${ifDefined(this.maxlength)}
min=${ifDefined(this.min)}
max=${ifDefined(this.max)}
step=${ifDefined(this.step as number)}
.value=${live(this.value || '')}
autocapitalize=${ifDefined(this.autocapitalize)}
autocomplete=${ifDefined(this.autocomplete)}
autocorrect=${ifDefined(this.autocorrect)}
?autofocus=${this.autofocus}
spellcheck=${this.spellcheck}
pattern=${ifDefined(this.pattern)}
enterkeyhint=${ifDefined(this.enterkeyhint)}
inputmode=${ifDefined(this.inputmode)}
aria-describedby="hint"
@change=${this.handleChange}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
${isClearIconVisible
? html`
<button
part="clear-button"
class="clear"
type="button"
aria-label=${this.localize.term('clearEntry')}
@click=${this.handleClearClick}
tabindex="-1"
>
<slot name="clear-icon">
<wa-icon name="circle-xmark" library="system" variant="regular"></wa-icon>
</slot>
</button>
`
: ''}
${this.passwordToggle && !this.disabled
? html`
<button
part="password-toggle-button"
class="password-toggle"
type="button"
aria-label=${this.localize.term(this.passwordVisible ? 'hidePassword' : 'showPassword')}
@click=${this.handlePasswordToggle}
tabindex="-1"
>
${this.passwordVisible
? html`
<slot name="show-password-icon">
<wa-icon name="eye-slash" library="system" variant="regular"></wa-icon>
</slot>
`
: html`
<slot name="hide-password-icon">
<wa-icon name="eye" library="system" variant="regular"></wa-icon>
</slot>
`}
</button>
`
: ''}
${isClearIconVisible
? html`
<button
part="clear-button"
class="clear"
type="button"
aria-label=${this.localize.term('clearEntry')}
@click=${this.handleClearClick}
tabindex="-1"
>
<slot name="clear-icon">
<wa-icon name="circle-xmark" library="system" variant="regular"></wa-icon>
</slot>
</button>
`
: ''}
${this.passwordToggle && !this.disabled
? html`
<button
part="password-toggle-button"
class="password-toggle"
type="button"
aria-label=${this.localize.term(this.passwordVisible ? 'hidePassword' : 'showPassword')}
@click=${this.handlePasswordToggle}
tabindex="-1"
>
${this.passwordVisible
? html`
<slot name="show-password-icon">
<wa-icon name="eye-slash" library="system" variant="regular"></wa-icon>
</slot>
`
: html`
<slot name="hide-password-icon">
<wa-icon name="eye" library="system" variant="regular"></wa-icon>
</slot>
`}
</button>
`
: ''}
<slot name="suffix" part="suffix" class="suffix"></slot>
</div>
</div>
<slot
name="hint"
part="hint"
class=${classMap({
'has-slotted': hasHint,
})}
aria-hidden=${hasHint ? 'false' : 'true'}
>${this.hint}</slot
>
<slot name="suffix" part="suffix" class="suffix"></slot>
</div>
<slot
name="hint"
part="hint"
class=${classMap({
'has-slotted': hasHint,
})}
aria-hidden=${hasHint ? 'false' : 'true'}
>${this.hint}</slot
>
`;
}
}

View File

@@ -1,49 +1,67 @@
/* Exclude inputs that don't accept text, referenced in subsequent rules with :where(&) */
:not(
[type='button'],
[type='checkbox'],
[type='color'],
[type='file'],
[type='hidden'],
[type='image'],
[type='radio'],
[type='range'],
[type='reset'],
[type='submit']
) {
/* Set custom properties for native and WA inputs */
input:where(:not(:host input)):where(&),
:host(&) {
--background-color: var(--wa-form-control-background-color);
--border-color: var(--wa-form-control-resting-color);
--border-radius: var(--wa-form-control-border-radius);
--border-style: var(--wa-form-control-border-style);
.wa-text-field,
:host,
input:not(
/* Exclude inputs that don't accept text */
[type='button'],
[type='checkbox'],
[type='color'],
[type='file'],
[type='hidden'],
[type='image'],
[type='radio'],
[type='range'],
[type='reset'],
[type='submit']
) {
/* Style native inputs and <wa-input>'s visible container */
&:where(:not(.wa-text-field *, :host input)) {
/* Do NOT reset --background-color and --border-color here so they trickle in from the appearance utils
* Instead we provide the fallback when setting
*/
--border-width: var(--wa-form-control-border-width);
--box-shadow: initial;
}
/* Set custom properties for filled input variants via class="wa-accent" or <wa-input appearance="filled"> */
input:where(:not(:host input)).wa-accent:where(&),
:host(.wa-accent:where(&)),
:host([appearance='filled']:where(&)) {
--background-color: var(--wa-color-neutral-fill-quiet);
--border-color: var(--background-color);
}
/* Set custom properties for pill input variants via class="wa-pill" or <wa-input pill> */
input:where(:not(:host input)).wa-pill:where(&),
:host(.wa-pill:where(&)),
:host([pill]:where(&)) {
--border-radius: var(--wa-border-radius-pill);
border-color: var(--border-color, var(--wa-form-control-resting-color));
border-radius: var(--wa-form-control-border-radius);
border-style: var(--wa-form-control-border-style);
cursor: text;
color: var(--wa-form-control-value-color);
font-size: var(--wa-size);
font-family: inherit;
line-height: var(--wa-form-control-value-line-height);
vertical-align: middle;
width: 100%;
transition:
background-color var(--wa-transition-normal),
border var(--wa-transition-normal),
outline var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
}
/* Style text controls of inputs, including within <wa-input> */
:is(input):where(&) {
color: var(--wa-form-control-value-color);
font-size: var(--wa-font-size-m);
line-height: var(--wa-form-control-value-line-height);
padding: 0 var(--wa-space-m);
&:not(:host, input:where(.wa-text-field *)) {
background-color: var(--background-color, var(--wa-form-control-background-color));
border-width: var(--border-width);
box-shadow: var(--box-shadow);
padding: 0 var(--wa-space);
height: var(--wa-form-control-height);
/* Style focused inputs */
&:focus-within {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Style disabled inputs */
&:has(:disabled:only-child),
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
&:where(input) {
/* Actual inputs */
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
@@ -58,38 +76,21 @@
-webkit-user-select: none;
}
}
/* Style native inputs and <wa-input>'s visible container */
:is(input:where(:not(:host input)), :host .input):where(&) {
background-color: var(--background-color);
border-color: var(--border-color);
border-radius: var(--border-radius);
border-style: var(--border-style);
border-width: var(--border-width);
box-shadow: var(--box-shadow);
cursor: text;
font-family: inherit;
line-height: var(--wa-form-control-value-line-height);
overflow: hidden;
vertical-align: middle;
width: 100%;
transition:
background-color var(--wa-transition-normal),
border var(--wa-transition-normal),
outline var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
height: var(--wa-form-control-height);
}
/* Style focused inputs */
:is(input:where(:not(:host input)):focus, :host .input:focus-within:not(:has(> input:disabled))):where(&) {
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
/* Style disabled inputs */
:is(input:where(:not(:host input)):disabled, :host .input:has(input:disabled)):where(&) {
cursor: not-allowed;
opacity: 0.5;
}
}
.wa-text-field input {
padding: 0;
border: none;
outline: none;
box-shadow: none;
padding: 0;
margin: 0;
cursor: inherit;
-webkit-appearance: none;
font: inherit;
}
.wa-pill,
:host([pill]) {
border-radius: var(--wa-border-radius-pill);
}

View File

@@ -26,10 +26,11 @@
.wa-filled,
:host([appearance~='filled']) {
--background-color: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
--background-color: var(--wa-color-fill-quiet, var(--wa-color-neutral-fill-quiet));
--border-color: transparent;
--text-color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
--background-color-hover: color-mix(in oklab, var(--background-color), transparent 10%);
--background-color-hover: var(--wa-color-fill-normal, var(--wa-color-neutral-fill-normal));
--background-color-active: color-mix(in oklab, var(--background-color), transparent 20%);
&:is(.wa-outlined, :host([appearance~='outlined'])) {