Compare commits

..

5 Commits

Author SHA1 Message Date
Aidan Timson
e681aa46ed CSS approach 2026-02-26 11:36:21 +00:00
Aidan Timson
737adef642 Update src/panels/lovelace/hui-root.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 10:28:52 +00:00
Aidan Timson
d3c7594de2 Use observers 2026-02-26 10:16:04 +00:00
Aidan Timson
be0ff7c6e2 Fix view container backgrouns after scrollbar change 2026-02-26 10:04:09 +00:00
Petar Petrov
f6c1a890e4 Dynamically calculate the date range picker's vertical opening direction (#29850) 2026-02-26 09:33:34 +00:00
7 changed files with 42 additions and 414 deletions

View File

@@ -93,6 +93,8 @@ export class HaDateRangePicker extends LitElement {
| "center"
| "inline";
@state() private _calcedVerticalOpeningDirection?: "up" | "down";
protected willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && this.ranges === undefined) ||
@@ -134,7 +136,9 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
opens-vertical=${ifDefined(
this.verticalOpeningDirection || this._calcedVerticalOpeningDirection
)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}
@@ -328,17 +332,24 @@ export class HaDateRangePicker extends LitElement {
private _handleClick() {
// calculate opening direction if not set
if (!this._dateRangePicker.open && !this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
if (!this._dateRangePicker.open) {
if (!this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
if (!this.verticalOpeningDirection) {
const rect = this.getBoundingClientRect();
this._calcedVerticalOpeningDirection =
rect.top > window.innerHeight / 2 ? "up" : "down";
}
this._calcedOpeningDirection = opens;
}
}

View File

@@ -1,371 +0,0 @@
import "@home-assistant/webawesome/dist/components/input/input";
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
import { mdiClose, mdiEye, mdiEyeOff, mdiInformationOutline } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { withViewTransition } from "../common/util/view-transition";
import "./ha-svg-icon";
import "./ha-tooltip";
@customElement("ha-input")
export class HaInput extends LitElement {
/** The type of input. */
@property()
public type:
| "date"
| "datetime-local"
| "email"
| "number"
| "password"
| "search"
| "tel"
| "text"
| "time"
| "url" = "text";
/** The current value of the input. */
@property()
public value: string | null = null;
/** The input's size. */
@property()
public size: "small" | "medium" | "large" = "medium";
/** The input's visual appearance. */
@property()
public appearance: "filled" | "outlined" | "filled-outlined" = "outlined";
/** Draws a pill-style input with rounded edges. */
@property({ type: Boolean })
public pill = false;
/** The input's label. */
@property()
public label = "";
/** The input's hint. */
@property()
public hint = "";
/** Adds a clear button when the input is not empty. */
@property({ type: Boolean, attribute: "with-clear" })
public withClear = false;
/** Placeholder text to show as a hint when the input is empty. */
@property()
public placeholder = "";
/** Makes the input readonly. */
@property({ type: Boolean })
public readonly = false;
/** Adds a button to toggle the password's visibility. */
@property({ type: Boolean, attribute: "password-toggle" })
public passwordToggle = false;
/** Determines whether or not the password is currently visible. */
@property({ type: Boolean, attribute: "password-visible" })
public passwordVisible = false;
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
@property({ type: Boolean, attribute: "without-spin-buttons" })
public withoutSpinButtons = false;
/** Makes the input a required field. */
@property({ type: Boolean })
public required = false;
/** A regular expression pattern to validate input against. */
@property()
public pattern?: string;
/** The minimum length of input that will be considered valid. */
@property({ type: Number })
public minlength?: number;
/** The maximum length of input that will be considered valid. */
@property({ type: Number })
public maxlength?: number;
/** The input's minimum value. Only applies to date and number input types. */
@property()
public min?: number | string;
/** The input's maximum value. Only applies to date and number input types. */
@property()
public max?: number | string;
/** Specifies the granularity that the value must adhere to. */
@property()
public step?: number | "any";
/** Controls whether and how text input is automatically capitalized. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public autocapitalize:
| "off"
| "none"
| "on"
| "sentences"
| "words"
| "characters"
| "" = "";
/** Indicates whether the browser's autocorrect feature is on or off. */
@property({ type: Boolean })
public autocorrect = false;
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
@property()
public autocomplete?: string;
/** Indicates that the input should receive focus on page load. */
@property({ type: Boolean })
// eslint-disable-next-line lit/no-native-attributes
public autofocus = false;
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public enterkeyhint:
| "enter"
| "done"
| "go"
| "next"
| "previous"
| "search"
| "send"
| "" = "";
/** Enables spell checking on the input. */
@property({ type: Boolean })
// eslint-disable-next-line lit/no-native-attributes
public spellcheck = true;
/** Tells the browser what type of data will be entered by the user. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public inputmode:
| "none"
| "text"
| "decimal"
| "numeric"
| "tel"
| "search"
| "email"
| "url"
| "" = "";
/** The name of the input, submitted as a name/value pair with form data. */
@property()
public name?: string;
/** Disables the form control. */
@property({ type: Boolean })
public disabled = false;
/** Custom validation message to show when the input is invalid. */
@property({ attribute: "validation-message" })
public validationMessage = "";
/** When true, validates the input on blur instead of on form submit. */
@property({ type: Boolean, attribute: "auto-validate" })
public autoValidate = false;
@state()
private _invalid = false;
@query("wa-input")
private _input!: WaInput;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
/** Selects all the text in the input. */
public select(): void {
this._input?.select();
}
/** Sets the start and end positions of the text selection (0-based). */
public setSelectionRange(
selectionStart: number,
selectionEnd: number,
selectionDirection?: "forward" | "backward" | "none"
): void {
this._input?.setSelectionRange(
selectionStart,
selectionEnd,
selectionDirection
);
}
/** Replaces a range of text with a new string. */
public setRangeText(
replacement: string,
start?: number,
end?: number,
selectMode?: "select" | "start" | "end" | "preserve"
): void {
this._input?.setRangeText(replacement, start, end, selectMode);
}
/** Displays the browser picker for an input element. */
public showPicker(): void {
this._input?.showPicker();
}
/** Increments the value of a numeric input type by the value of the step attribute. */
public stepUp(): void {
this._input?.stepUp();
}
/** Decrements the value of a numeric input type by the value of the step attribute. */
public stepDown(): void {
this._input?.stepDown();
}
protected render() {
return html`
<wa-input
.type=${this.type}
.value=${this.value}
.size=${this.size}
.appearance=${this.appearance}
.hint=${this._invalid ? this.validationMessage : ""}
.withClear=${this.withClear}
.placeholder=${this.placeholder}
.readonly=${this.readonly}
.passwordToggle=${this.passwordToggle}
.passwordVisible=${this.passwordVisible}
.withoutSpinButtons=${this.withoutSpinButtons}
.required=${this.required}
.pattern=${this.pattern}
.minlength=${this.minlength}
.maxlength=${this.maxlength}
.min=${this.min}
.max=${this.max}
.step=${this.step}
.autocapitalize=${this.autocapitalize || undefined}
.autocorrect=${this.autocorrect ? "on" : "off"}
.autocomplete=${this.autocomplete}
.autofocus=${this.autofocus}
.enterkeyhint=${this.enterkeyhint || undefined}
.spellcheck=${this.spellcheck}
.inputmode=${this.inputmode || undefined}
.name=${this.name}
.disabled=${this.disabled}
class=${this._invalid ? "invalid" : ""}
@input=${this._handleInput}
@change=${this._handleChange}
@blur=${this._handleBlur}
>
<div class="label" slot="label">
<span>
<slot name="label">${this.label}</slot>
</span>
${this.hint
? html`<ha-svg-icon
.path=${mdiInformationOutline}
id="hint"
></ha-svg-icon>
<ha-tooltip for="hint">${this.hint}</ha-tooltip> `
: nothing}
</div>
<slot name="start" slot="start"></slot>
<slot name="end" slot="end"></slot>
<slot name="clear-icon" slot="clear-icon">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</slot>
<slot name="show-password-icon" slot="show-password-icon">
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
</slot>
<slot name="hide-password-icon" slot="hide-password-icon">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
</slot>
</wa-input>
`;
}
private _handleInput() {
this.value = this._input?.value ?? null;
if (this._invalid) {
this._invalid = false;
}
}
private _handleChange() {
this.value = this._input?.value ?? null;
}
private _handleBlur() {
if (this.autoValidate) {
withViewTransition(() => {
this._invalid = !this._input.checkValidity();
});
}
}
static styles = css`
:host {
display: flex;
align-items: flex-start;
}
wa-input {
flex: 1;
min-width: 0;
}
wa-input::part(base):focus-within {
outline: none;
--wa-form-control-border-color: var(--ha-color-border-primary-normal);
}
wa-input.invalid {
--wa-form-control-border-color: var(--ha-color-border-danger-normal);
}
wa-input::part(label) {
margin-block-end: 2px;
}
.label {
height: 24px;
display: flex;
width: 100%;
align-items: center;
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
gap: var(--ha-space-1);
}
.label span {
line-height: 1;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.label ha-svg-icon {
color: var(--ha-color-on-disabled-normal);
--mdc-icon-size: 16px;
}
wa-input.invalid::part(hint) {
margin-block-start: var(--ha-space-1);
color: var(--ha-color-on-danger-quiet);
font-size: var(--ha-font-size-s);
margin-inline-start: var(--ha-space-3);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-input": HaInput;
}
}

View File

@@ -8,7 +8,7 @@ import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-input";
import "../../../../components/ha-textfield";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
@@ -66,21 +66,21 @@ class HaTimerForm extends LitElement {
return html`
<div class="form">
<ha-input
<ha-textfield
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
auto-validate
autoValidate
required
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-input>
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}

View File

@@ -1474,7 +1474,7 @@ class HUIRoot extends LitElement {
}
hui-view-container {
position: relative;
display: flex;
display: grid;
height: calc(
100vh - var(--header-height) - var(--safe-area-inset-top) - var(
--view-container-padding-top,
@@ -1498,9 +1498,13 @@ class HUIRoot extends LitElement {
padding-inline-start: var(--safe-area-inset-left);
}
hui-view-container > * {
flex: 1 1 100%;
grid-area: 1 / 1;
min-width: 0;
max-width: 100%;
}
hui-view-container > :not(hui-view-background) {
z-index: 1;
}
/**
* In edit mode we have the tab bar on a new line *
*/

View File

@@ -139,18 +139,17 @@ export class HUIViewBackground extends LitElement {
display: block;
z-index: -1;
position: fixed;
background-attachment: scroll !important;
}
:host(:not([fixed-background])) {
z-index: -1;
position: absolute;
}
:host {
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
background-attachment: scroll !important;
}
:host(:not([fixed-background])) {
z-index: 0;
pointer-events: none;
}
:host {
width: 100%;
background: var(
--view-background,

View File

@@ -64,10 +64,5 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-form-control-background-color: var(--wa-color-surface-raised);
--wa-form-control-border-color: var(--ha-color-border-neutral-quiet);
--wa-form-control-value-color: var(--primary-text-color);
--wa-form-control-placeholder-color: var(--ha-color-text-secondary);
}
`;

View File

@@ -14,8 +14,10 @@ export const waMainStyles = css`
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-font-weight-body: var(--ha-font-weight-normal);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -27,25 +29,13 @@ export const waMainStyles = css`
--wa-border-radius-s: var(--ha-border-radius-sm);
--wa-border-radius-m: var(--ha-border-radius-md);
--wa-border-radius-l: var(--ha-border-radius-lg);
--wa-border-radius-pill: var(--ha-border-radius-pill);
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-s: var(--ha-font-size-s);
--wa-font-size-m: var(--ha-font-size-m);
--wa-font-size-l: var(--ha-font-size-l);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--wa-line-height-condensed);
--wa-form-control-value-font-weight: var(--wa-font-weight-body);
--wa-form-control-border-radius: var(--wa-border-radius-l);
--wa-form-control-border-style: var(--wa-border-style);
--wa-form-control-border-width: var(--wa-border-width-s);
--wa-form-control-height: 40px;
--wa-form-control-padding-inline: var(--ha-space-3);
}
${scrollLockStyles}