UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

474 lines (473 loc) • 26.1 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { repeat } from "lit-html/directives/repeat.js"; import { nothing, html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { z as whenAnimationDone, s as slotChangeGetAssignedElements, d as focusElementInGroup, g as getElementDir } from "../../chunks/dom.js"; import { g as guid } from "../../chunks/guid.js"; import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { c as createObserver } from "../../chunks/observers.js"; import { b as breakpoints } from "../../chunks/responsive.js"; import { g as getRoundRobinIndex } from "../../chunks/array.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { css } from "@lit/reactive-element/css-tag.js"; const DURATION = 6e3; const CSS = { container: "container", containerOverlaid: "container--overlaid", containerEdged: "container--edged", itemContainer: "item-container", itemContainerForward: "item-container--forward", itemContainerBackward: "item-container--backward", pagination: "pagination", paginationItems: "pagination-items", paginationItem: "pagination-item", paginationItemIndividual: "pagination-item--individual", paginationItemVisible: "pagination-item--visible", paginationItemOutOfRange: "pagination-item--out-of-range", paginationItemSelected: "pagination-item--selected", paginationItemRangeEdge: "pagination-item--range-edge", pageNext: "page-next", pagePrevious: "page-previous", autoplayControl: "autoplay-control", autoplayProgress: "autoplay-progress" }; const ICONS = { chevronLeft: "chevron-left", chevronRight: "chevron-right", inactive: "bullet-point", active: "bullet-point-large", pause: "pause-f", play: "play-f" }; const centerItemsByBreakpoint = { medium: 7, small: 5, xsmall: 3, xxsmall: 1 }; const styles = css`:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([disabled]) *,:host([disabled]) ::slotted(*){pointer-events:none}:host{display:flex;inline-size:100%;--calcite-internal-internal-carousel-item-space: 1.5rem;--calcite-internal-internal-carousel-item-space-wide: 3.5rem;--calcite-internal-internal-carousel-item-background-color: var( --calcite-internal-carousel-item-background-color, var(--calcite-color-foreground-1) );--calcite-internal-internal-carousel-item-background-color-hover: var( --calcite-internal-carousel-item-background-color-hover, var(--calcite-color-foreground-2) );--calcite-internal-internal-carousel-item-background-color-active: var( --calcite-internal-carousel-item-background-color-active, var(--calcite-color-foreground-2) );--calcite-internal-internal-carousel-item-background-color-selected: var( --calcite-internal-carousel-item-background-color-selected, var(--calcite-color-foreground-1) );--calcite-internal-internal-carousel-item-icon-color-hover: var( --calcite-internal-carousel-item-icon-color-hover, var(--calcite-color-text-1) );--calcite-internal-internal-carousel-item-icon-color: var( --calcite-internal-carousel-item-icon-color, var(--calcite-color-border-1) );--calcite-internal-internal-carousel-item-icon-color-selected: var( --calcite-internal-carousel-item-icon-color-selected, var(--calcite-color-brand) );--calcite-internal-internal-carousel-control-color-hover: var( --calcite-internal-carousel-control-color-hover, var(--calcite-color-text-1) );--calcite-internal-internal-carousel-control-color: var( --calcite-internal-carousel-item-icon-color, var(--calcite-color-text-3) );--calcite-internal-internal-carousel-autoplay-progress-background-color: var( --calcite-internal-carousel-autoplay-progress-background-color, var(--calcite-color-border-3) );--calcite-internal-internal-carousel-autoplay-progress-fill-color: var( --calcite-internal-carousel-autoplay-progress-fill-color, var(--calcite-color-brand) )}.container{position:relative;display:flex;inline-size:100%;flex-direction:column;overflow:hidden;font-size:var(--calcite-font-size--1);line-height:1rem;color:var(--calcite-color-text-2);outline-color:transparent}.container:focus{outline:2px solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(-2px*(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}.container--edged:not(.container--overlaid){padding-inline:var(--calcite-internal-internal-carousel-item-space-wide);inline-size:calc(100% - var(--calcite-internal-internal-carousel-item-space-wide) * 2)}.item-container{display:flex;flex:1 1 auto;align-items:flex-start;justify-content:center;overflow:auto;padding:.25rem;animation-name:none;animation-duration:var(--calcite-animation-timing)}.container--overlaid .item-container{padding:0}.item-container--forward{animation-name:item-forward}.item-container--backward{animation-name:item-backward}calcite-carousel-item:not([selected]){opacity:0}.pagination{margin:.75rem;display:flex;flex-direction:row;align-items:center;justify-content:center;inline-size:auto}.pagination-items{display:flex;flex-direction:row;align-items:center}.container--overlaid .pagination{position:absolute}.pagination-item.page-next,.pagination-item.page-previous{color:var(--calcite-internal-internal-carousel-control-color)}.pagination-item.page-next:hover,.pagination-item.page-previous:hover{color:var(--calcite-internal-internal-carousel-control-color-hover)}.container--edged .page-next,.container--edged .page-previous{block-size:3rem;inline-size:3rem;position:absolute;inset-block-start:50%;transform:translateY(-50%)}.container--edged .page-next{inset-inline-end:0}.container--edged .page-previous{inset-inline-start:0}.container--overlaid .pagination{inset-block-start:unset;inset-block-end:0;inset-inline:0}.pagination-item.autoplay-control{position:relative;color:var(--calcite-internal-internal-carousel-control-color);--calcite-progress-fill-color: var(--calcite-internal-internal-carousel-autoplay-progress-fill-color);--calcite-progress-background-color: var(--calcite-internal-internal-carousel-autoplay-progress-background-color)}.autoplay-control:focus .autoplay-progress{inset-block-end:4px;inset-inline:2px;inline-size:calc(100% - 4px)}.autoplay-progress{position:absolute;inset-block-end:2px;inset-inline:0;inline-size:100%}.pagination-item{margin:0;block-size:2rem;inline-size:2rem;cursor:pointer;align-items:center;border-style:none;background-color:transparent;outline-color:transparent;transition-property:background-color,block-size,border-color,box-shadow,color,inset-block-end,inset-block-start,inset-inline-end,inset-inline-start,inset-size,opacity,outline-color,transform;transition-duration:var(--calcite-animation-timing);transition-timing-function:ease-in-out;-webkit-appearance:none;display:flex;align-content:center;justify-content:center;--calcite-color-foreground-1: var(--calcite-internal-internal-carousel-item-background-color);color:var(--calcite-internal-internal-carousel-item-icon-color)}.pagination-item:hover{background-color:var(--calcite-internal-internal-carousel-item-background-color-hover);color:var(--calcite-internal-internal-carousel-item-icon-color-hover)}.pagination-item:focus{background-color:var(--calcite-internal-internal-carousel-item-background-color-active);outline:2px solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(-2px*(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}.pagination-item:active{background-color:var(--calcite-internal-internal-carousel-item-background-color-active);color:var(--calcite-internal-internal-carousel-item-icon-color-hover)}.pagination-item calcite-icon{color:inherit;pointer-events:none}.pagination-item.pagination-item--selected{--calcite-color-foreground-1: var(--calcite-internal-internal-carousel-item-background-color-selected);--calcite-color-foreground-3: var(--calcite-internal-internal-carousel-item-background-color-selected);color:var(--calcite-internal-internal-carousel-item-icon-color-selected)}.pagination-item--individual{pointer-events:none;inline-size:0px;padding:0;opacity:0;visibility:hidden;transition:var(--calcite-animation-timing) ease-in-out inline-size,var(--calcite-animation-timing) ease-in-out padding,var(--calcite-animation-timing) ease-in-out opacity}.pagination-item--individual.pagination-item--visible{pointer-events:auto;inline-size:2rem;opacity:1;visibility:visible}.pagination-item--range-edge calcite-icon{scale:.75;transition:var(--calcite-animation-timing) ease-in-out scale}.container--overlaid .pagination-item{background-color:var(--calcite-internal-internal-carousel-item-background-color)}.container--overlaid .pagination-item:hover{background-color:var(--calcite-internal-internal-carousel-item-background-color-hover)}.container--overlaid .pagination-item:focus{background-color:var(--calcite-internal-internal-carousel-item-background-color-active)}.container--overlaid .pagination-item:active{background-color:var(--calcite-internal-internal-carousel-item-background-color-active)}@keyframes item-forward{0%{transform:translate3d(100px,0,0)}to{transform:translateZ(0)}}@keyframes item-backward{0%{transform:translate3d(-100px,0,0)}to{transform:translateZ(0)}}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}:host([hidden]){display:none}[hidden]{display:none}`; class Carousel extends LitElement { constructor() { super(...arguments); this.autoplayHandler = () => { this.clearIntervals(); this.slideDurationInterval = setInterval(this.timer, this.autoplayDuration / 100); }; this.containerId = `calcite-carousel-container-${guid()}`; this.resizeHandler = ({ contentRect: { width } }) => { this.setMaxItemsToBreakpoint(width); }; this.resizeObserver = createObserver("resize", (entries) => entries.forEach(this.resizeHandler)); this.slideDurationInterval = null; this.slideInterval = null; this.timer = () => { let time = this.slideDurationRemaining; const notSuspended = !this.suspendedDueToFocus && !this.suspendedDueToHover || this.userPreventsSuspend; if (notSuspended) { if (time <= 0.01) { time = 1; this.direction = "forward"; this.nextItem(false); } else { time = time - 0.01; } } if (time > 0) { this.slideDurationRemaining = time; } }; this.messages = useT9n(); this.direction = "standby"; this.items = []; this.maxItems = centerItemsByBreakpoint.xxsmall; this.playing = false; this.slideDurationRemaining = 1; this.suspendedDueToFocus = false; this.suspendedDueToHover = false; this.suspendedSlideDurationRemaining = 1; this.userPreventsSuspend = false; this.arrowType = "inline"; this.autoplay = false; this.autoplayDuration = DURATION; this.controlOverlay = false; this.disabled = false; this.calciteCarouselChange = createEvent({ cancelable: false }); this.calciteCarouselPause = createEvent({ cancelable: false }); this.calciteCarouselPlay = createEvent({ cancelable: false }); this.calciteCarouselResume = createEvent({ cancelable: false }); this.calciteCarouselStop = createEvent({ cancelable: false }); } static { this.properties = { direction: [16, {}, { state: true }], items: [16, {}, { state: true }], maxItems: [16, {}, { state: true }], playing: [16, {}, { state: true }], selectedIndex: [16, {}, { state: true }], slideDurationRemaining: [16, {}, { state: true }], suspendedDueToFocus: [16, {}, { state: true }], suspendedDueToHover: [16, {}, { state: true }], suspendedSlideDurationRemaining: [16, {}, { state: true }], userPreventsSuspend: [16, {}, { state: true }], arrowType: [3, {}, { reflect: true }], autoplay: [3, {}, { reflect: true }], autoplayDuration: [11, {}, { type: Number, reflect: true }], controlOverlay: [7, {}, { reflect: true, type: Boolean }], disabled: [7, {}, { reflect: true, type: Boolean }], label: 1, messageOverrides: [0, {}, { attribute: false }], paused: [5, {}, { type: Boolean }], selectedItem: [0, {}, { attribute: false }] }; } static { this.styles = styles; } async play() { if (this.playing || this.autoplay !== "" && !this.autoplay && this.autoplay !== "paused") { return; } this.handlePlay(true); } async setFocus() { await componentFocusable(this); this.container?.focus(); } async stop() { if (!this.playing) { return; } this.handlePause(true); } connectedCallback() { super.connectedCallback(); this.resizeObserver?.observe(this.el); } async load() { if ((this.autoplay === "" || this.autoplay) && this.autoplay !== "paused") { this.handlePlay(false); } else if (this.autoplay === "paused") { this.paused = true; } } willUpdate(changes) { if (changes.has("autoplay") && this.hasUpdated) { this.autoplayWatcher(this.autoplay); } if (changes.has("direction") && (this.hasUpdated || this.direction !== "standby")) { this.directionWatcher(this.direction); } if (changes.has("playing") && (this.hasUpdated || this.playing !== false)) { this.paused = !this.playing; } if (changes.has("suspendedDueToFocus") && (this.hasUpdated || this.suspendedDueToFocus !== false) || changes.has("suspendedDueToHover") && (this.hasUpdated || this.suspendedDueToHover !== false)) { this.suspendWatcher(); } } updated() { updateHostInteraction(this); } disconnectedCallback() { super.disconnectedCallback(); this.clearIntervals(); this.resizeObserver?.disconnect(); } autoplayWatcher(autoplay) { if (!autoplay) { this.handlePause(false); } } async directionWatcher(direction) { if (direction === "standby") { return; } await whenAnimationDone(this.itemContainer, direction === "forward" ? "item-forward" : "item-backward"); this.direction = "standby"; } suspendWatcher() { if (!this.suspendedDueToFocus && !this.suspendedDueToHover) { this.suspendEnd(); } else { this.suspendStart(); } } setMaxItemsToBreakpoint(width) { if (!width) { return; } if (width >= breakpoints.width.small) { this.maxItems = centerItemsByBreakpoint.medium; return; } if (width >= breakpoints.width.xsmall) { this.maxItems = centerItemsByBreakpoint.small; return; } if (width >= breakpoints.width.xxsmall) { this.maxItems = centerItemsByBreakpoint.xsmall; return; } this.maxItems = centerItemsByBreakpoint.xxsmall; } clearIntervals() { clearInterval(this.slideDurationInterval); clearInterval(this.slideInterval); } nextItem(emit) { if (this.playing && emit) { this.playing = false; } const nextIndex = getRoundRobinIndex(this.selectedIndex + 1, this.items.length); this.setSelectedItem(nextIndex, emit); } previousItem() { this.playing = false; const prevIndex = getRoundRobinIndex(Math.max(this.selectedIndex - 1, -1), this.items.length); this.setSelectedItem(prevIndex, true); } handlePlay(emit) { this.playing = true; this.autoplayHandler(); this.slideInterval = setInterval(this.autoplayHandler, this.autoplayDuration); if (emit) { this.calciteCarouselPlay.emit(); } } handlePause(emit) { this.playing = false; this.clearIntervals(); this.slideDurationRemaining = 1; this.suspendedSlideDurationRemaining = 1; if (emit) { this.calciteCarouselStop.emit(); } } suspendStart() { this.suspendedSlideDurationRemaining = this.slideDurationRemaining; } suspendEnd() { this.slideDurationRemaining = this.suspendedSlideDurationRemaining; } handleSlotChange(event) { const items = slotChangeGetAssignedElements(event); if (items.length < 1) { return; } const activeItemIndex = items.findIndex((item) => item.selected); const requestedSelectedIndex = activeItemIndex > -1 ? activeItemIndex : 0; this.items = items; this.setSelectedItem(requestedSelectedIndex, false); } setSelectedItem(requestedIndex, emit) { const previousSelected = this.selectedIndex; this.items.forEach((el, index) => { const match = requestedIndex === index; el.selected = match; if (match) { this.selectedItem = el; this.selectedIndex = index; } }); if (emit) { this.playing = false; if (previousSelected !== this.selectedIndex) { this.calciteCarouselChange.emit(); } } } handleArrowClick(event) { const direction = event.target.dataset.direction; if (this.playing) { this.handlePause(true); } if (direction === "next") { this.direction = "forward"; this.nextItem(true); } else if (direction === "previous") { this.direction = "backward"; this.previousItem(); } } handleItemSelection(event) { const item = event.target; const requestedPosition = parseInt(item.dataset.index); if (requestedPosition === this.selectedIndex) { return; } if (this.playing) { this.handlePause(true); } this.direction = requestedPosition > this.selectedIndex ? "forward" : "backward"; this.setSelectedItem(requestedPosition, true); } toggleRotation() { this.userPreventsSuspend = true; if (this.playing) { this.handlePause(true); } else { this.handlePlay(true); } } handleFocusIn() { const isPlaying = this.playing; if (isPlaying) { this.suspendedDueToFocus = true; } if ((!this.suspendedDueToFocus || !this.suspendedDueToHover) && isPlaying) { this.calciteCarouselPause.emit(); } } handleMouseIn() { const isPlaying = this.playing; if (isPlaying) { this.suspendedDueToHover = true; } if ((!this.suspendedDueToFocus || !this.suspendedDueToHover) && isPlaying) { this.calciteCarouselPause.emit(); } } handleMouseOut(event) { const leavingComponent = !this.el.contains(event.relatedTarget); const isPlaying = this.playing; if (leavingComponent && isPlaying) { this.suspendedDueToHover = false; } if (leavingComponent && isPlaying && !this.suspendedDueToFocus) { this.userPreventsSuspend = false; this.calciteCarouselResume.emit(); } } handleFocusOut(event) { const leavingComponent = !event.composedPath().includes(event.relatedTarget); const isPlaying = this.playing; if (leavingComponent && isPlaying) { this.suspendedDueToFocus = false; } if (leavingComponent && isPlaying && !this.suspendedDueToHover) { this.userPreventsSuspend = false; this.calciteCarouselResume.emit(); } } containerKeyDownHandler(event) { if (event.target !== this.container) { return; } const lastItem = this.items.length - 1; switch (event.key) { case " ": case "Enter": event.preventDefault(); if (this.autoplay === "" || this.autoplay || this.autoplay === "paused") { this.toggleRotation(); } break; case "ArrowRight": event.preventDefault(); this.direction = "forward"; this.nextItem(true); break; case "ArrowLeft": event.preventDefault(); this.direction = "backward"; this.previousItem(); break; case "Home": event.preventDefault(); if (this.selectedIndex === 0) { return; } this.direction = "backward"; this.setSelectedItem(0, true); break; case "End": event.preventDefault(); if (this.selectedIndex === lastItem) { return; } this.direction = "forward"; this.setSelectedItem(lastItem, true); break; } } tabListKeyDownHandler(event) { const visiblePaginationEls = Array(...this.tabList.querySelectorAll(`button:not(.${CSS.paginationItemOutOfRange})`)); const currentEl = event.target; switch (event.key) { case "ArrowRight": focusElementInGroup(visiblePaginationEls, currentEl, "next"); break; case "ArrowLeft": focusElementInGroup(visiblePaginationEls, currentEl, "previous"); break; case "Home": event.preventDefault(); focusElementInGroup(visiblePaginationEls, currentEl, "first"); break; case "End": event.preventDefault(); focusElementInGroup(visiblePaginationEls, currentEl, "last"); break; } } storeTabListRef(el) { this.tabList = el; } storeContainerRef(el) { this.container = el; } storeItemContainerRef(el) { this.itemContainer = el; } renderRotationControl() { const text = this.playing ? this.messages.pause : this.messages.play; const formattedValue = this.slideDurationRemaining * 100; return html`<button .ariaLabel=${text} class=${safeClassMap({ [CSS.paginationItem]: true, [CSS.autoplayControl]: true })} @click=${this.toggleRotation} title=${text ?? nothing}><calcite-icon .icon=${this.playing ? ICONS.pause : ICONS.play} scale=s></calcite-icon>${this.playing && html`<calcite-progress class=${safeClassMap(CSS.autoplayProgress)} .label=${this.messages.carouselItemProgress} .value=${formattedValue}></calcite-progress>` || ""}</button>`; } renderPaginationArea() { return html`<div class=${safeClassMap({ [CSS.pagination]: true, [CSS.containerOverlaid]: this.controlOverlay })} @keydown=${this.tabListKeyDownHandler} ${ref(this.storeTabListRef)}>${(this.playing || this.autoplay === "" || this.autoplay || this.autoplay === "paused") && this.renderRotationControl() || ""}${this.arrowType === "inline" && this.renderArrow("previous") || ""}${this.renderPaginationItems()}${this.arrowType === "inline" && this.renderArrow("next") || ""}</div>`; } renderPaginationItems() { const { selectedIndex, maxItems, items, label, handleItemSelection } = this; return html`<div .ariaLabel=${label} class=${safeClassMap(CSS.paginationItems)} role=tablist>${repeat(items, (item) => item.id, (item, index) => { const itemCount = items.length; const match = index === selectedIndex; const first = index === 0; const last = index === itemCount - 1; const endRangeStart = itemCount - maxItems - 1; const inStartRange = selectedIndex < maxItems; const inEndRange = selectedIndex >= endRangeStart; const rangeStart = inStartRange ? 0 : selectedIndex - Math.floor(maxItems / 2); const rangeEnd = inEndRange ? itemCount : rangeStart + maxItems; const low = inStartRange ? 0 : inEndRange ? endRangeStart : rangeStart; const high = inStartRange ? maxItems + 1 : rangeEnd; const isEdge = !first && !last && !match && (index === low - 1 || index === high); const visible = match || index <= high && index >= low - 1; const overflowActive = itemCount - 1 <= maxItems; const icon = match ? ICONS.active : ICONS.inactive; return html`<button aria-controls=${(!match ? item.id : void 0) ?? nothing} .ariaSelected=${match} class=${safeClassMap({ [CSS.paginationItem]: true, [CSS.paginationItemIndividual]: true, [CSS.paginationItemSelected]: match, [CSS.paginationItemRangeEdge]: itemCount - 1 > maxItems && isEdge, [CSS.paginationItemOutOfRange]: !(overflowActive || visible), [CSS.paginationItemVisible]: overflowActive || visible })} data-index=${index ?? nothing} @click=${handleItemSelection} role=tab title=${item.label ?? nothing}><calcite-icon .icon=${icon} scale=l></calcite-icon></button>`; })}</div>`; } renderArrow(direction) { const isPrev = direction === "previous"; const dir = getElementDir(this.el); const scale = this.arrowType === "edge" ? "m" : "s"; const css2 = isPrev ? CSS.pagePrevious : CSS.pageNext; const title = isPrev ? this.messages.previous : this.messages.next; const icon = isPrev ? ICONS.chevronLeft : ICONS.chevronRight; return html`<button aria-controls=${this.containerId ?? nothing} class=${safeClassMap({ [CSS.paginationItem]: true, [css2]: true })} data-direction=${direction ?? nothing} @click=${this.handleArrowClick} title=${title ?? nothing}><calcite-icon .flipRtl=${dir === "rtl"} .icon=${icon} .scale=${scale}></calcite-icon></button>`; } render() { const { direction } = this; return InteractiveContainer({ disabled: this.disabled, children: html`<div .ariaLabel=${this.label} .ariaLive=${this.playing ? "off" : "polite"} .ariaRoleDescription=${this.messages.carousel} class=${safeClassMap({ [CSS.container]: true, [CSS.containerOverlaid]: this.controlOverlay, [CSS.containerEdged]: this.arrowType === "edge" })} @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut} @keydown=${this.containerKeyDownHandler} @mouseenter=${this.handleMouseIn} @mouseleave=${this.handleMouseOut} role=group tabindex=0 ${ref(this.storeContainerRef)}><section class=${safeClassMap({ [CSS.itemContainer]: true, [CSS.itemContainerForward]: direction === "forward", [CSS.itemContainerBackward]: direction === "backward" })} id=${this.containerId ?? nothing} ${ref(this.storeItemContainerRef)}><slot @slotchange=${this.handleSlotChange}></slot></section>${this.items.length > 1 && this.renderPaginationArea() || ""}${this.arrowType === "edge" && this.renderArrow("previous") || ""}${this.arrowType === "edge" && this.renderArrow("next") || ""}</div>` }); } } customElement("calcite-carousel", Carousel); export { Carousel };