From 1dcbcd4e9db0b088650462fe16bafed8878aaea9 Mon Sep 17 00:00:00 2001 From: treeder <75826+treeder@users.noreply.github.com> Date: Tue, 12 May 2026 00:39:11 +0000 Subject: [PATCH] feat: Add Material 3 Carousel component Implements the `md-carousel` and `md-carousel-item` web components using Lit and CSS scroll snap according to Material Design 3 guidelines. - `md-carousel` provides the horizontally scrollable container with snap points. - `md-carousel-item` represents individual items within the carousel. - Accessibility is handled natively on the host elements via `ElementInternals` (`role="list"` and `role="listitem"`). - Components are exposed via bundle entry points `all.js` and `common.js`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- all.js | 4 ++++ carousel/carousel-item.js | 32 +++++++++++++++++++++++++ carousel/carousel.js | 50 +++++++++++++++++++++++++++++++++++++++ common.js | 4 ++++ package-lock.json | 5 +--- 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 carousel/carousel-item.js create mode 100644 carousel/carousel.js diff --git a/all.js b/all.js index bc4175c..8728a07 100644 --- a/all.js +++ b/all.js @@ -43,6 +43,8 @@ import './tabs/primary-tab.js' import './tabs/secondary-tab.js' import './tabs/tabs.js' import './text/text-field.js' +import './carousel/carousel.js' +import './carousel/carousel-item.js' // go/keep-sorted end // LINT.ThenChange(:exports) // LINT.IfChange(exports) @@ -74,3 +76,5 @@ export * from './switch/switch.js' export * from './tabs/tab.js' export * from './tabs/tabs.js' export * from './text/text-field.js' +export * from './carousel/carousel.js' +export * from './carousel/carousel-item.js' diff --git a/carousel/carousel-item.js b/carousel/carousel-item.js new file mode 100644 index 0000000..bd8b2f4 --- /dev/null +++ b/carousel/carousel-item.js @@ -0,0 +1,32 @@ +import { LitElement, html, css } from 'lit'; + +/** + * A Material Design carousel item component. + */ +export class CarouselItem extends LitElement { + static get styles() { + return css` + :host { + display: block; + scroll-snap-align: start; + flex: 0 0 auto; + } + `; + } + + constructor() { + super(); + this.internals = this.attachInternals(); + this.internals.role = 'listitem'; + } + + render() { + return html` +
+ +
+ `; + } +} + +customElements.define('md-carousel-item', CarouselItem); diff --git a/carousel/carousel.js b/carousel/carousel.js new file mode 100644 index 0000000..e92a3a0 --- /dev/null +++ b/carousel/carousel.js @@ -0,0 +1,50 @@ +import { LitElement, html, css } from 'lit'; + +/** + * A Material Design carousel component. + */ +export class Carousel extends LitElement { + static get styles() { + return css` + :host { + display: block; + width: 100%; + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; + scrollbar-width: none; /* Firefox */ + } + + :host::-webkit-scrollbar { + display: none; /* Safari and Chrome */ + } + + .container { + display: flex; + gap: 8px; /* Default gap */ + } + + ::slotted(*) { + scroll-snap-align: start; + flex: 0 0 auto; + } + `; + } + + constructor() { + super(); + this.internals = this.attachInternals(); + this.internals.role = 'list'; + this.internals.ariaLabel = 'Carousel'; + } + + render() { + return html` +
+ +
+ `; + } +} + +customElements.define('md-carousel', Carousel); diff --git a/common.js b/common.js index d063462..45747e6 100644 --- a/common.js +++ b/common.js @@ -25,6 +25,8 @@ import './select/select-option.js' import './tabs/primary-tab.js' import './tabs/tabs.js' import './text/text-field.js' +import './carousel/carousel.js' +import './carousel/carousel-item.js' export * from './buttons/button.js' export * from './checkbox/checkbox.js' @@ -45,3 +47,5 @@ export * from './select/select-option.js' export * from './tabs/tab.js' export * from './tabs/tabs.js' export * from './text/text-field.js' +export * from './carousel/carousel.js' +export * from './carousel/carousel-item.js' diff --git a/package-lock.json b/package-lock.json index c787dc2..d33e15b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2199,8 +2199,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "5.2.0", @@ -4352,7 +4351,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -4934,7 +4932,6 @@ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"