@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
306 lines (305 loc) • 14.6 kB
JavaScript
/*! 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}>…</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
};