// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/icons_lit.html.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {getCss} from './horizontal_carousel.css.js';
import {getHtml} from './horizontal_carousel.html.js';
import {$$} from './utils.js';
/**
* @fileoverview
* Navigates a `product-specifications-table` horizontally by one column width,
* using forward and backward buttons.
*
* Prevents vertical scrolling by directly controlling horizontal scroll
* position. Updates button visibility using `IntersectionObserver` to detect if
* either end of the container has been reached.
*
* Relies on CSS scroll-snap properties for precise column alignment:
* - `scroll-snap-type: x mandatory` on container
* - `scroll-snap-align: start` on `product-specifications-table`'s columns
*
* Note: Key differences between this HorizontalCarouselElement and the one
* located at ui/webui/resources/cr_components/history_clusters.
* - This carousel considers the width of its slotted children, and uses
* CSS scroll-snap properties to enable fine-grained control over how much
* content is shown/hidden at once.
* - The other carousel bases its scrolling on the container's width, making
* it less suitable for cases where precise content control is needed.
*/
export interface HorizontalCarouselElement {
$: {
backButton: HTMLElement,
carouselContainer: HTMLElement,
endProbe: HTMLElement,
forwardButton: HTMLElement,
slottedTable: HTMLSlotElement,
startProbe: HTMLElement,
};
}
export class HorizontalCarouselElement extends CrLitElement {
static get is() {
return 'horizontal-carousel';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
/**
* True if slotted table is overflown, regardless of any elements that may
* appear before or after it.
*/
canScroll: {
type: Boolean,
reflect: true,
},
/**
True if slotted table is overflown on the left side of the carousel.
*/
showBackButton_: {type: Boolean},
/**
True if slotted table is overflown on the right side of the carousel.
*/
showForwardButton_: {type: Boolean},
};
}
canScroll: boolean = false;
protected showBackButton_: boolean = false;
protected showForwardButton_: boolean = false;
private intersectionObserver_: IntersectionObserver|null = null;
private scrolledToEnd_: boolean = false;
private scrolledToStart_: boolean = false;
override connectedCallback() {
super.connectedCallback();
this.intersectionObserver_ = this.getIntersectionObserver_();
}
override disconnectedCallback() {
super.disconnectedCallback();
if (this.intersectionObserver_) {
this.intersectionObserver_.disconnect();
this.intersectionObserver_ = null;
}
}
protected onCarouselBackClick_() {
this.$.carouselContainer.scrollBy({left: -1 * this.columnOffsetWidth_});
}
protected onCarouselForwardClick_() {
this.$.carouselContainer.scrollBy({left: this.columnOffsetWidth_});
}
private getIntersectionObserver_(): IntersectionObserver {
const observer = new IntersectionObserver(entries => {
let tmpCanScroll = false;
entries.forEach(entry => {
const {target, intersectionRatio} = entry;
if (target === this.$.startProbe) {
tmpCanScroll = intersectionRatio === 0 || !this.scrolledToEnd_;
this.scrolledToStart_ = intersectionRatio !== 0;
} else if (target === this.$.endProbe) {
tmpCanScroll = intersectionRatio === 0 || !this.scrolledToStart_;
this.scrolledToEnd_ = intersectionRatio !== 0;
}
});
this.canScroll = tmpCanScroll;
this.showBackButton_ = tmpCanScroll && !this.scrolledToStart_;
this.showForwardButton_ = tmpCanScroll && !this.scrolledToEnd_;
this.dispatchEvent(new CustomEvent(
'intersection-observed', {bubbles: true, composed: true}));
}, {root: this.$.carouselContainer});
observer.observe(this.$.startProbe);
observer.observe(this.$.endProbe);
return observer;
}
private get columnOffsetWidth_(): number {
if (this.$.slottedTable.assignedElements().length === 0) {
return 0;
}
const tableElement = this.$.slottedTable.assignedElements()[0];
const column = $$<HTMLElement>(tableElement, '.col');
return column ? column.offsetWidth : 0;
}
}
declare global {
interface HTMLElementTagNameMap {
'horizontal-carousel': HorizontalCarouselElement;
}
}
customElements.define(HorizontalCarouselElement.is, HorizontalCarouselElement);