UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

306 lines (305 loc) • 14.6 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 { keyed } from "lit-html/directives/keyed.js"; import { nothing, html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { c as componentFocusable, g as getIconScale } from "../../chunks/component.js"; import { n as numberStringFormatter } from "../../chunks/locale.js"; import { c as createObserver } from "../../chunks/observers.js"; import { b as breakpoints } from "../../chunks/responsive.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { list: "list", listItem: "list-item", hiddenItem: "hidden-item", page: "page", selected: "selected", chevron: "chevron", disabled: "disabled", ellipsis: "ellipsis" }; const ICONS = { next: "chevron-right", previous: "chevron-left", first: "chevron-start", last: "chevron-end" }; const styles = css`:host{display:flex;writing-mode:horizontal-tb}.list{margin:0;display:flex;list-style-type:none;padding:0;column-gap:var(--calcite-spacing-base)}.list-item{margin:0;display:flex;padding:0}.hidden-item{display:none}:host([scale=s]) .chevron,:host([scale=s]) .page,:host([scale=s]) .ellipsis{block-size:1.5rem;padding-inline:.25rem;font-size:var(--calcite-font-size--2);line-height:1rem;min-inline-size:1.5rem}:host([scale=m]) .chevron,:host([scale=m]) .page,:host([scale=m]) .ellipsis{block-size:2rem;padding-inline:.5rem;font-size:var(--calcite-font-size--1);line-height:1rem;min-inline-size:2rem}:host([scale=l]) .chevron,:host([scale=l]) .page,:host([scale=l]) .ellipsis{block-size:2.75rem;font-size:var(--calcite-font-size-0);line-height:1.25rem;min-inline-size:2.75rem}:host([scale=l]) .chevron{padding-inline:.625rem}:host([scale=l]) .page,:host([scale=l]) .ellipsis{padding-inline:.75rem}:host button{outline-color:transparent}:host button: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))))}.chevron,.page,.ellipsis{margin:0;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-style:none;--tw-border-opacity: 0;background-color:transparent;padding:0;vertical-align:baseline;font-family:inherit;font-size:var(--calcite-font-size-0);line-height:1.25rem;color:var(--calcite-pagination-color, var(--calcite-color-text-3))}.chevron,.page{cursor:pointer;border-block:2px solid transparent}.chevron:hover,.page:hover{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;color:var(--calcite-pagination-color-hover, var(--calcite-color-text-1))}.chevron:active,.page:active{color:var(--calcite-pagination-color-hover, var(--calcite-color-text-1))}.page:hover{border-block-end-color:var(--calcite-pagination-color-border-hover, var(--calcite-color-border-2))}.page:active{background-color:var(--calcite-pagination-background-color, var(--calcite-color-foreground-3))}.page.selected{font-weight:var(--calcite-font-weight-medium);color:var(--calcite-pagination-color-hover, var(--calcite-color-text-1));border-block-end-color:var(--calcite-pagination-color-border-active, var(--calcite-color-brand))}.page.selected:focus{border-block-end-width:var(--calcite-spacing-xxs);padding-block-start:var(--calcite-spacing-base)}.chevron:hover{background-color:var(--calcite-pagination-icon-color-background-hover, var(--calcite-color-foreground-2))}.chevron:active{background-color:var(--calcite-pagination-background-color, var(--calcite-color-foreground-3))}.chevron.disabled{pointer-events:none;background-color:transparent}.chevron.disabled>calcite-icon{opacity:var(--calcite-opacity-disabled)}:host([hidden]){display:none}[hidden]{display:none}`; const firstAndLastPageCount = 2; const ellipsisCount = 2; const maxItemBreakpoints = { large: 11, medium: 9, small: 7, xsmall: 5, xxsmall: 1 }; class Pagination extends LitElement { constructor() { super(...arguments); this.resizeHandler = ({ contentRect: { width } }) => this.setMaxItemsToBreakpoint(width); this.resizeObserver = createObserver("resize", (entries) => entries.forEach(this.resizeHandler)); this.messages = useT9n(); this.maxItems = maxItemBreakpoints.xxsmall; this.groupSeparator = false; this.pageSize = 20; this.scale = "m"; this.startItem = 1; this.totalItems = 0; this.calcitePaginationChange = createEvent({ cancelable: false }); } static { this.properties = { isXXSmall: [16, {}, { state: true }], lastStartItem: [16, {}, { state: true }], maxItems: [16, {}, { state: true }], totalPages: [16, {}, { state: true }], groupSeparator: [7, {}, { reflect: true, type: Boolean }], messageOverrides: [0, {}, { attribute: false }], numberingSystem: 1, pageSize: [11, {}, { reflect: true, type: Number }], scale: [3, {}, { reflect: true }], startItem: [11, {}, { reflect: true, type: Number }], totalItems: [11, {}, { reflect: true, type: Number }] }; } static { this.shadowRootOptions = { mode: "open", delegatesFocus: true }; } static { this.styles = styles; } async goTo(page) { switch (page) { case "start": this.startItem = 1; break; case "end": this.startItem = this.lastStartItem; break; default: { if (page >= Math.ceil(this.totalPages)) { this.startItem = this.lastStartItem; } else if (page <= 0) { this.startItem = 1; } else { this.startItem = (page - 1) * this.pageSize + 1; } } } } async nextPage() { this.startItem = Math.min(this.lastStartItem, this.startItem + this.pageSize); } async previousPage() { this.startItem = Math.max(1, this.startItem - this.pageSize); } async setFocus() { await componentFocusable(this); this.el.focus(); } connectedCallback() { super.connectedCallback(); this.resizeObserver?.observe(this.el); } async load() { this.handleTotalPages(); this.handleLastStartItemChange(); this.handleIsXXSmall(); } willUpdate(changes) { if (changes.has("totalItems") && (this.hasUpdated || this.totalItems !== 0) || changes.has("pageSize") && (this.hasUpdated || this.pageSize !== 20)) { this.handleTotalPages(); } if (changes.has("totalItems") && (this.hasUpdated || this.totalItems !== 0) || changes.has("pageSize") && (this.hasUpdated || this.pageSize !== 20) || changes.has("totalPages")) { this.handleLastStartItemChange(); } if (changes.has("maxItems") && (this.hasUpdated || this.maxItems !== maxItemBreakpoints.xxsmall)) { this.handleIsXXSmall(); } if (changes.has("messages")) { this.effectiveLocaleChange(); } } loaded() { this.setMaxItemsToBreakpoint(this.el.clientWidth); } disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver?.disconnect(); } handleTotalPages() { if (this.pageSize < 1) { this.pageSize = 1; } this.totalPages = this.totalItems / this.pageSize; } effectiveLocaleChange() { numberStringFormatter.numberFormatOptions = { locale: this.messages._lang, numberingSystem: this.numberingSystem, useGrouping: this.groupSeparator }; } handleLastStartItemChange() { const { totalItems, pageSize, totalPages } = this; this.lastStartItem = (totalItems % pageSize === 0 ? totalItems - pageSize : Math.floor(totalPages) * pageSize) + 1; } handleIsXXSmall() { this.isXXSmall = this.maxItems === maxItemBreakpoints.xxsmall; } setMaxItemsToBreakpoint(width) { if (!breakpoints || !width) { return; } if (width >= breakpoints.width.medium) { this.maxItems = maxItemBreakpoints.large; return; } if (width >= breakpoints.width.small) { this.maxItems = maxItemBreakpoints.medium; return; } if (width >= breakpoints.width.xsmall) { this.maxItems = maxItemBreakpoints.small; return; } if (width >= breakpoints.width.xxsmall) { this.maxItems = maxItemBreakpoints.xsmall; return; } this.maxItems = maxItemBreakpoints.xxsmall; } firstClicked() { this.startItem = 1; this.emitUpdate(); } lastClicked() { this.startItem = this.lastStartItem; this.emitUpdate(); } async previousClicked() { await this.previousPage(); this.emitUpdate(); } async nextClicked() { await this.nextPage(); this.emitUpdate(); } showStartEllipsis() { return this.totalPages > this.maxItems && Math.floor(this.startItem / this.pageSize) > this.maxItems - firstAndLastPageCount - ellipsisCount; } showEndEllipsis() { return this.totalPages > this.maxItems && (this.totalItems - this.startItem) / this.pageSize > this.maxItems - firstAndLastPageCount - (ellipsisCount - 1); } emitUpdate() { this.calcitePaginationChange.emit(); } handlePageClick(event) { const target = event.target; this.startItem = parseInt(target.value); this.emitUpdate(); } renderEllipsis(type) { return keyed(type, html`<span class=${safeClassMap(CSS.ellipsis)} data-test-ellipsis=${type ?? nothing}>&hellip;</span>`); } renderItems() { const { totalItems, pageSize, startItem, maxItems, totalPages, lastStartItem, isXXSmall } = this; const items = []; if (isXXSmall) { items.push(this.renderPage(startItem)); return items; } const renderFirstPage = totalItems > pageSize; const renderStartEllipsis = this.showStartEllipsis(); const renderEndEllipsis = this.showEndEllipsis(); if (renderFirstPage) { items.push(this.renderPage(1)); } if (renderStartEllipsis) { items.push(this.renderEllipsis("start")); } const remainingItems = maxItems - firstAndLastPageCount - (renderEndEllipsis ? 1 : 0) - (renderStartEllipsis ? 1 : 0); let end; let nextStart; if (totalPages - 1 <= remainingItems) { nextStart = 1 + pageSize; end = lastStartItem - pageSize; } else { if (startItem / pageSize < remainingItems) { nextStart = 1 + pageSize; end = 1 + remainingItems * pageSize; } else { if (startItem + remainingItems * pageSize >= totalItems) { nextStart = lastStartItem - remainingItems * pageSize; end = lastStartItem - pageSize; } else { nextStart = startItem - pageSize * ((remainingItems - 1) / 2); end = startItem + pageSize * ((remainingItems - 1) / 2); } } } for (let i = 0; i < remainingItems && nextStart <= end; i++) { items.push(this.renderPage(nextStart)); nextStart = nextStart + pageSize; } if (renderEndEllipsis) { items.push(this.renderEllipsis("end")); } items.push(this.renderPage(lastStartItem)); return items; } renderPage(start) { const { pageSize } = this; const page = Math.floor(start / pageSize) + (pageSize === 1 ? 0 : 1); numberStringFormatter.numberFormatOptions = { locale: this.messages._lang, numberingSystem: this.numberingSystem, useGrouping: this.groupSeparator }; const displayedPage = numberStringFormatter.localize(page.toString()); const selected = start === this.startItem; return html`<li class=${safeClassMap(CSS.listItem)}><button .ariaCurrent=${selected ? "page" : "false"} class=${safeClassMap({ [CSS.page]: true, [CSS.selected]: selected })} @click=${this.handlePageClick} value=${start ?? nothing}>${displayedPage}</button></li>`; } renderPreviousChevron() { const { pageSize, startItem, messages } = this; const disabled = pageSize === 1 ? startItem <= pageSize : startItem < pageSize; return keyed("previous", html`<button .ariaLabel=${messages.previous} class=${safeClassMap({ [CSS.chevron]: true, [CSS.disabled]: disabled })} data-test-chevron=previous .disabled=${disabled} @click=${this.previousClicked}><calcite-icon flip-rtl .icon=${ICONS.previous} .scale=${getIconScale(this.scale)}></calcite-icon></button>`); } renderNextChevron() { const { totalItems, pageSize, startItem, messages } = this; const disabled = pageSize === 1 ? startItem + pageSize > totalItems : startItem + pageSize > totalItems; return keyed("next-button", html`<button .ariaLabel=${messages.next} class=${safeClassMap({ [CSS.chevron]: true, [CSS.disabled]: disabled })} data-test-chevron=next .disabled=${disabled} @click=${this.nextClicked}><calcite-icon flip-rtl .icon=${ICONS.next} .scale=${getIconScale(this.scale)}></calcite-icon></button>`); } renderFirstChevron() { const { messages, startItem, isXXSmall } = this; const disabled = startItem === 1; return isXXSmall ? keyed("first-button", html`<button .ariaLabel=${messages.first} class=${safeClassMap({ [CSS.chevron]: true, [CSS.disabled]: disabled })} .disabled=${disabled} @click=${this.firstClicked}><calcite-icon flip-rtl .icon=${ICONS.first} .scale=${getIconScale(this.scale)}></calcite-icon></button>`) : null; } renderLastChevron() { const { messages, startItem, isXXSmall, lastStartItem } = this; const disabled = startItem === lastStartItem; return isXXSmall ? keyed("last-button", html`<button .ariaLabel=${messages.last} class=${safeClassMap({ [CSS.chevron]: true, [CSS.disabled]: disabled })} .disabled=${disabled} @click=${this.lastClicked}><calcite-icon flip-rtl .icon=${ICONS.last} .scale=${getIconScale(this.scale)}></calcite-icon></button>`) : null; } render() { const firstChevron = this.renderFirstChevron(); const lastChevron = this.renderLastChevron(); return html`<ul class=${safeClassMap(CSS.list)}><li class=${safeClassMap({ [CSS.listItem]: true, [CSS.hiddenItem]: !firstChevron })}>${firstChevron}</li><li class=${safeClassMap(CSS.listItem)}>${this.renderPreviousChevron()}</li>${this.renderItems()}<li class=${safeClassMap(CSS.listItem)}>${this.renderNextChevron()}</li><li class=${safeClassMap({ [CSS.listItem]: true, [CSS.hiddenItem]: !lastChevron })}>${lastChevron}</li></ul>`; } } customElement("calcite-pagination", Pagination); export { Pagination };