Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions carousel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Carousel

Carousels display a set of items, such as images or cards, that can be scrolled horizontally.

## Example Usage

```html
<script type="module">
import 'material/carousel/carousel.js'
import 'material/carousel/carousel-item.js'
import 'material/card/card.js'
</script>

<md-carousel>
<md-carousel-item>
<md-card>Card 1</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card>Card 2</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card>Card 3</md-card>
</md-carousel-item>
</md-carousel>
```

## CSS Variables

### `<md-carousel>`
* `--md-carousel-gap`: Gap between carousel items (default: `8px`)
* `--md-carousel-padding`: Padding around the carousel scroll container (default: `0`)

### `<md-carousel-item>`
* `--md-carousel-item-width`: Width of the carousel item (default: `300px`)
* `--md-carousel-item-shape`: Border radius of the carousel item (default: `28px`)
38 changes: 38 additions & 0 deletions carousel/carousel-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { html, LitElement, css } from 'lit'

/**
* A Material Design carousel item component.
*
* @element md-carousel-item
*/
export class CarouselItem extends LitElement {
render() {
return html`
<div class="item-container" role="listitem">
<slot></slot>
</div>
`
}
Comment on lines +8 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The role="listitem" should be applied to the host element rather than an internal div. This ensures that the component is correctly identified as a list item by assistive technologies when placed inside a role="list" container (like the one in md-carousel).

export class CarouselItem extends LitElement {
  connectedCallback() {
    super.connectedCallback()
    if (!this.hasAttribute('role')) {
      this.setAttribute('role', 'listitem')
    }
  }

  render() {
    return html`
      <div class="item-container">
        <slot></slot>
      </div>
    `
  }


static styles = css`
:host {
display: inline-block;
flex-shrink: 0;
scroll-snap-align: start;
/* default size for demo, can be overridden */
width: var(--md-carousel-item-width, 300px);
}

.item-container {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
/* Usually border radius of 28px or 24px in MD3 depending on variant */
border-radius: var(--md-carousel-item-shape, 28px);
overflow: hidden;
}
`
}

customElements.define('md-carousel-item', CarouselItem)
53 changes: 53 additions & 0 deletions carousel/carousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { html, LitElement, css } from 'lit'

/**
* A Material Design carousel component.
*
* @element md-carousel
*/
export class Carousel extends LitElement {
static properties = {
ariaLabel: { type: String, attribute: 'aria-label' },
}

constructor() {
super()
this.ariaLabel = 'Carousel'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default value for ariaLabel is a hardcoded English string. This should be internationalized using the project's localization system (e.g., @lit/localize) to support multiple languages and improve accessibility for non-English users.

}

render() {
return html`
<div class="scroll-container" role="list" aria-label="${this.ariaLabel}">
<slot></slot>
</div>
`
}

static styles = css`
:host {
display: block;
/* Carousel styling based on Material Design 3 */
/* Note: specific width/height may need to be configurable */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment appears to be a development note. If the component is intended to be production-ready, this note should be removed or the suggested configurability should be implemented.

}

.scroll-container {
display: flex;
gap: var(--md-carousel-gap, 8px);
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;

/* Hide scrollbar for cleaner look, but keep functionality */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hiding the scrollbar can negatively impact accessibility and usability, as it removes a visual cue for the amount of content and a means of navigation for some users. Consider allowing the scrollbar to be visible or providing alternative navigation indicators (like dots or arrows) as per Material Design 3 guidelines.

scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */

padding: var(--md-carousel-padding, 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When using scroll-snap-type, it is important to set scroll-padding to match the container's padding. This ensures that items snap correctly relative to the visible area of the carousel, preventing them from being partially obscured by the padding.

Suggested change
padding: var(--md-carousel-padding, 0);
padding: var(--md-carousel-padding, 0);
scroll-padding: var(--md-carousel-padding, 0);

}

.scroll-container::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
`
}

customElements.define('md-carousel', Carousel)
34 changes: 33 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
import 'material/snackbar/snackbar.js'
import 'material/app/bar.js'
import 'material/progress/progress.js'
import 'material/carousel/carousel.js'
import 'material/carousel/carousel-item.js'
import 'material/card/card.js'
import './components/expressive-component.js'
</script>

Expand Down Expand Up @@ -178,7 +181,36 @@ <h2>Inputs</h2>
<expressive-component></expressive-component>
</div>

<h3>Progress</h3>

<h3>Carousel</h3>
<md-carousel style="margin-bottom: 24px;">
<md-carousel-item>
<md-card style="height: 200px; display: flex; align-items: center; justify-content: center; background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);">
<div style="padding: 24px;">Item 1</div>
</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card style="height: 200px; display: flex; align-items: center; justify-content: center; background-color: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container);">
<div style="padding: 24px;">Item 2</div>
</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card style="height: 200px; display: flex; align-items: center; justify-content: center; background-color: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container);">
<div style="padding: 24px;">Item 3</div>
</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card style="height: 200px; display: flex; align-items: center; justify-content: center; background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container);">
<div style="padding: 24px;">Item 4</div>
</md-card>
</md-carousel-item>
<md-carousel-item>
<md-card style="height: 200px; display: flex; align-items: center; justify-content: center; background-color: var(--md-sys-color-surface-variant); color: var(--md-sys-color-on-surface-variant);">
<div style="padding: 24px;">Item 5</div>
</md-card>
</md-carousel-item>
</md-carousel>
<h3>Progress</h3>
<md-progress type="circular" indeterminate></md-progress>
<md-progress type="linear" value="0.5" indeterminate></md-progress>

Expand Down