Compare commits

...

2 Commits

Author SHA1 Message Date
Aidan Timson
e4fb925e07 Wrap picker combobox (used inside of sheet with virtualizer) 2025-12-04 12:48:27 +00:00
Aidan Timson
a6698fa2fa Add scrollable fade mixin to ha-bottom-sheet 2025-12-04 12:34:45 +00:00
2 changed files with 232 additions and 193 deletions

View File

@@ -2,12 +2,13 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends LitElement {
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean }) public open = false;
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
@@ -17,6 +18,12 @@ export class HaBottomSheet extends LitElement {
@query("#drawer") private _drawer!: HTMLElement;
@query("#body") private _bodyElement!: HTMLDivElement;
protected get scrollableElement(): HTMLElement | null {
return this._bodyElement;
}
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
@@ -49,8 +56,11 @@ export class HaBottomSheet extends LitElement {
@touchstart=${this._handleTouchStart}
>
<slot name="header"></slot>
<div id="body" class="body ha-scrollbar">
<slot></slot>
<div class="content-wrapper">
<div id="body" class="body ha-scrollbar">
<slot></slot>
</div>
${this.renderScrollableFades()}
</div>
</wa-drawer>
`;
@@ -167,60 +177,70 @@ export class HaBottomSheet extends LitElement {
this._isDragging = false;
}
static styles = [
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
:host([flexcontent]) .body {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:host([flexcontent]) .body {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];
}
}
declare global {

View File

@@ -14,6 +14,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
@@ -59,7 +60,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
) => T[];
@customElement("ha-picker-combo-box")
export class HaPickerComboBox extends LitElement {
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@@ -126,6 +127,10 @@ export class HaPickerComboBox extends LitElement {
@state() private _items: (PickerComboBoxItem | string)[] = [];
protected get scrollableElement(): HTMLElement | null {
return this._virtualizerElement as HTMLElement | null;
}
@state() private _sectionTitle?: string;
private _allItems: (PickerComboBoxItem | string)[] = [];
@@ -180,19 +185,22 @@ export class HaPickerComboBox extends LitElement {
</div>
`
: nothing}
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>`;
<div class="virtualizer-wrapper">
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>
${this.renderScrollableFades()}
</div>`;
}
private _renderSectionButtons() {
@@ -584,156 +592,167 @@ export class HaPickerComboBox extends LitElement {
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
static styles = [
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
flex: 1;
}
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
flex: 1;
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
ha-combo-box-item {
width: 100%;
}
ha-combo-box-item {
width: 100%;
}
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
@media (prefers-color-scheme: dark) {
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
}
lit-virtualizer {
flex: 1;
}
@media (prefers-color-scheme: dark) {
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
lit-virtualizer:focus-visible {
outline: none;
}
.virtualizer-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
lit-virtualizer {
flex: 1;
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
width: 100%;
}
lit-virtualizer:focus-visible {
outline: none;
}
.empty {
text-align: center;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
.combo-box-row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
}
.combo-box-row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
width: 100%;
}
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
.empty {
text-align: center;
}
.combo-box-row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
}
.combo-box-row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
@media (prefers-color-scheme: dark) {
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
}
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
@media (prefers-color-scheme: dark) {
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.title {
width: 100%;
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
.title {
width: 100%;
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.section-title-wrapper {
height: 0;
position: relative;
}
.section-title.show {
opacity: 1;
z-index: 1;
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`,
];
.section-title.show {
opacity: 1;
z-index: 1;
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`,
];
}
}
declare global {