diff --git a/commitlint.config.js b/commitlint.config.js index 846ffec2..538274b3 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -17,7 +17,8 @@ module.exports = { 'progress-indicator', 'checkbox', 'alert', - 'select' + 'select', + 'pagination' ], ], }, diff --git a/package-lock.json b/package-lock.json index 8976e6d4..76d5880c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "@web/dev-server-esbuild": "0.2.16", + "@web/dev-server-import-maps": "^0.0.7", "@web/dev-server-rollup": "^0.3.17", "@web/test-runner": "^0.13.15", "@web/test-runner-playwright": "^0.8.6", @@ -2852,6 +2853,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@import-maps/resolve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@import-maps/resolve/-/resolve-1.0.1.tgz", + "integrity": "sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA==", + "dev": true + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -7475,16 +7482,17 @@ } }, "node_modules/@web/dev-server-core": { - "version": "0.3.17", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.19.tgz", + "integrity": "sha512-Q/Xt4RMVebLWvALofz1C0KvP8qHbzU1EmdIA2Y1WMPJwiFJFhPxdr75p9YxK32P2t0hGs6aqqS5zE0HW9wYzYA==", "dev": true, - "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", "@web/parse5-utils": "^1.2.0", "chokidar": "^3.4.3", "clone": "^2.1.2", - "es-module-lexer": "^0.9.0", + "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", "is-stream": "^2.0.0", "isbinaryfile": "^4.0.6", @@ -7502,6 +7510,12 @@ "node": ">=10.0.0" } }, + "node_modules/@web/dev-server-core/node_modules/es-module-lexer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.0.3.tgz", + "integrity": "sha512-iC67eXHToclrlVhQfpRawDiF8D8sQxNxmbqw5oebegOaJkyx/w9C/k57/5e6yJR2zIByRt9OXdqX50DV2t6ZKw==", + "dev": true + }, "node_modules/@web/dev-server-core/node_modules/ws": { "version": "7.5.7", "dev": true, @@ -7548,6 +7562,23 @@ "esbuild": "bin/esbuild" } }, + "node_modules/@web/dev-server-import-maps": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@web/dev-server-import-maps/-/dev-server-import-maps-0.0.7.tgz", + "integrity": "sha512-uq8SFRkh3Zic71boDP/GeNwc7BtOWFWLDam3JJF3G0L9gMZVm7WteeDxxn9ppdbGxRhvlJtxqBlSOvf3pl75qw==", + "dev": true, + "dependencies": { + "@import-maps/resolve": "^1.0.1", + "@types/parse5": "^6.0.1", + "@web/dev-server-core": "^0.3.19", + "@web/parse5-utils": "^1.3.0", + "parse5": "^6.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@web/dev-server-rollup": { "version": "0.3.17", "dev": true, @@ -7675,36 +7706,6 @@ "node": ">=12.0.0" } }, - "node_modules/@web/test-runner-commands/node_modules/@web/dev-server-core": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.18.tgz", - "integrity": "sha512-o8EBGKXlBjAaQZiYNE1RZsNZJFQHjjr9FwuJyP2fsN+dkdZRhOjp4/E46hl2rB17Qgq4AizFmds8enmF/hU5dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^1.2.0", - "chokidar": "^3.4.3", - "clone": "^2.1.2", - "es-module-lexer": "^0.9.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^4.0.6", - "koa": "^2.13.0", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^6.0.0", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "ws": "^7.4.2" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@web/test-runner-commands/node_modules/@web/test-runner-core": { "version": "0.10.27", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.27.tgz", @@ -7774,26 +7775,6 @@ "node": ">= 8" } }, - "node_modules/@web/test-runner-commands/node_modules/ws": { - "version": "7.5.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@web/test-runner-core": { "version": "0.10.25", "dev": true, @@ -30950,6 +30931,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@import-maps/resolve": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@import-maps/resolve/-/resolve-1.0.1.tgz", + "integrity": "sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -34359,7 +34346,9 @@ } }, "@web/dev-server-core": { - "version": "0.3.17", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.19.tgz", + "integrity": "sha512-Q/Xt4RMVebLWvALofz1C0KvP8qHbzU1EmdIA2Y1WMPJwiFJFhPxdr75p9YxK32P2t0hGs6aqqS5zE0HW9wYzYA==", "dev": true, "requires": { "@types/koa": "^2.11.6", @@ -34367,7 +34356,7 @@ "@web/parse5-utils": "^1.2.0", "chokidar": "^3.4.3", "clone": "^2.1.2", - "es-module-lexer": "^0.9.0", + "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", "is-stream": "^2.0.0", "isbinaryfile": "^4.0.6", @@ -34382,6 +34371,12 @@ "ws": "^7.4.2" }, "dependencies": { + "es-module-lexer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.0.3.tgz", + "integrity": "sha512-iC67eXHToclrlVhQfpRawDiF8D8sQxNxmbqw5oebegOaJkyx/w9C/k57/5e6yJR2zIByRt9OXdqX50DV2t6ZKw==", + "dev": true + }, "ws": { "version": "7.5.7", "dev": true, @@ -34410,6 +34405,20 @@ } } }, + "@web/dev-server-import-maps": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@web/dev-server-import-maps/-/dev-server-import-maps-0.0.7.tgz", + "integrity": "sha512-uq8SFRkh3Zic71boDP/GeNwc7BtOWFWLDam3JJF3G0L9gMZVm7WteeDxxn9ppdbGxRhvlJtxqBlSOvf3pl75qw==", + "dev": true, + "requires": { + "@import-maps/resolve": "^1.0.1", + "@types/parse5": "^6.0.1", + "@web/dev-server-core": "^0.3.19", + "@web/parse5-utils": "^1.3.0", + "parse5": "^6.0.1", + "picomatch": "^2.2.2" + } + }, "@web/dev-server-rollup": { "version": "0.3.17", "dev": true, @@ -34562,32 +34571,6 @@ "mkdirp": "^1.0.4" }, "dependencies": { - "@web/dev-server-core": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.18.tgz", - "integrity": "sha512-o8EBGKXlBjAaQZiYNE1RZsNZJFQHjjr9FwuJyP2fsN+dkdZRhOjp4/E46hl2rB17Qgq4AizFmds8enmF/hU5dw==", - "dev": true, - "requires": { - "@types/koa": "^2.11.6", - "@types/ws": "^7.4.0", - "@web/parse5-utils": "^1.2.0", - "chokidar": "^3.4.3", - "clone": "^2.1.2", - "es-module-lexer": "^0.9.0", - "get-stream": "^6.0.0", - "is-stream": "^2.0.0", - "isbinaryfile": "^4.0.6", - "koa": "^2.13.0", - "koa-etag": "^4.0.0", - "koa-send": "^5.0.1", - "koa-static": "^5.0.0", - "lru-cache": "^6.0.0", - "mime-types": "^2.1.27", - "parse5": "^6.0.1", - "picomatch": "^2.2.2", - "ws": "^7.4.2" - } - }, "@web/test-runner-core": { "version": "0.10.27", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.27.tgz", @@ -34641,11 +34624,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true - }, - "ws": { - "version": "7.5.8", - "dev": true, - "requires": {} } } }, diff --git a/package.json b/package.json index 2cfd8c64..30382769 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "@web/dev-server-esbuild": "0.2.16", + "@web/dev-server-import-maps": "^0.0.7", "@web/dev-server-rollup": "^0.3.17", "@web/test-runner": "^0.13.15", "@web/test-runner-playwright": "^0.8.6", diff --git a/src/baklava.ts b/src/baklava.ts index 4ec240b2..b6a383a1 100644 --- a/src/baklava.ts +++ b/src/baklava.ts @@ -11,4 +11,5 @@ export { default as BlCheckbox } from './components/checkbox/bl-checkbox'; export { default as BlAlert } from './components/alert/bl-alert'; export { default as BlSelect } from './components/select/bl-select'; export { default as BlSelectOption } from './components/select/option/bl-select-option'; +export { default as BlPagination } from './components/pagination/bl-pagination'; export { getIconPath, setIconPath } from './utilities/asset-paths'; diff --git a/src/components/button/bl-button.css b/src/components/button/bl-button.css index 608ba00a..91e8a332 100644 --- a/src/components/button/bl-button.css +++ b/src/components/button/bl-button.css @@ -117,6 +117,10 @@ text-decoration: none; } +:host([variant='tertiary'][disabled]) .button { + --main-color:transparent; +} + :host([variant='secondary']:hover:not([disabled])) .button { --content-color: var(--bl-color-primary-background); --bg-color: var(--main-hover-color); diff --git a/src/components/button/bl-button.stories.mdx b/src/components/button/bl-button.stories.mdx index 0f649cc4..95c1c402 100644 --- a/src/components/button/bl-button.stories.mdx +++ b/src/components/button/bl-button.stories.mdx @@ -188,10 +188,18 @@ If button has a limited width and a long text that can not fit in a single line, ## Disabled Buttons -Disable version of all buttons is the same. +We have 2 types of disabled buttons: Disable version of Primary and Secondary buttons is the same. - + + {SizesTemplate.bind({})} + + + +Whereas Tertiary buttons keep their transparent backgrounds. + + + {SizesTemplate.bind({})} diff --git a/src/components/pagination/bl-pagination.css b/src/components/pagination/bl-pagination.css new file mode 100644 index 00000000..b4ee73b0 --- /dev/null +++ b/src/components/pagination/bl-pagination.css @@ -0,0 +1,65 @@ +:host { + width: max-content; +} + +.pagination { + display: flex; +} + +.pagination * { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.page-container { + display: flex; + align-items: center; +} + +.page-list { + display: flex; + align-items: center; + list-style: none; + user-select: none; +} + +.dots { + margin:0 var(--bl-size-2xs); +} + +.dots::before { + content: ' \B7 \B7 \B7'; + color: var(--bl-color-secondary); +} + +.pagination-helpers { + display: flex; + align-items: center; + margin-right: var(--bl-size-s); + gap: var(--bl-size-m); +} + +.jumper, +.select { + display: flex; + align-items: center; + gap: var(--bl-size-2xs); +} + +label { + font: var(--bl-font-title-3-medium); + font-size: var(--bl-font-size-m); + font-weight: var(--bl-font-weight-medium); + line-height: var(--bl-font-size-m); + letter-spacing: 0; + user-select: none; +} + +bl-input { + width: 62px; +} + +bl-select { + width:128px; +} diff --git a/src/components/pagination/bl-pagination.stories.mdx b/src/components/pagination/bl-pagination.stories.mdx new file mode 100644 index 00000000..8dc3268d --- /dev/null +++ b/src/components/pagination/bl-pagination.stories.mdx @@ -0,0 +1,116 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs'; + + + +export const PaginationTemplate = (args) => html` + + + ` + +# Pagination + +The Pagination component enables the user to select a specific page from a range of pages. Number of content per page can also be selected by the user + + +## Simple Pagination +Use a simple pagination for navigating to the next or previous page or a selected page. + + + + {PaginationTemplate.bind({})} + + + +## Jumper +Use the jumper to directly go to a specific page. + + + + {PaginationTemplate.bind({})} + + + +## Select +Use the select helper to change the number of items per page. + + + + {PaginationTemplate.bind({})} + + + +## Customization +The labels and the texts of the jumper and select element are fully customizable. + + + + {PaginationTemplate.bind({})} + + + + +## Reference + + + + diff --git a/src/components/pagination/bl-pagination.test.ts b/src/components/pagination/bl-pagination.test.ts new file mode 100644 index 00000000..6334e41d --- /dev/null +++ b/src/components/pagination/bl-pagination.test.ts @@ -0,0 +1,289 @@ +import { assert, expect, fixture, oneEvent, html } from '@open-wc/testing'; +import BlPagination from './bl-pagination'; + +import type typeOfBlPagination from './bl-pagination'; + +describe('bl-pagination', () => { + it('is defined', () => { + const el = document.createElement('bl-pagination'); + assert.instanceOf(el, BlPagination); + }); + + it('should render with the default values', async () => { + const el = await fixture(html``); + assert.shadowDom.equal( + el, + ` + + ` + ); + }); + + it('should render with the correct default values', async () => { + const el = await fixture(html` `); + expect(el?.currentPage).to.equal(1); + expect(el.itemsPerPage).to.equal(100); + expect(el.hasJumper).to.equal(false); + expect(el.hasSelect).to.equal(false); + expect(el.jumperLabel).to.equal('Go To'); + expect(el.selectLabel).to.equal('Show'); + expect(el.optionText).to.equal('Items'); + }); + + it('should correctly set the attributes', async () => { + const el = await fixture( + html` + + + ` + ); + expect(el?.currentPage).to.equal(3); + expect(el.itemsPerPage).to.equal(5); + expect(el.hasJumper).to.equal(true); + expect(el.hasSelect).to.equal(true); + expect(el.jumperLabel).to.equal('Git'); + expect(el.selectLabel).to.equal('Göster'); + expect(el.optionText).to.equal('Sonuç'); + }); + + describe('back and forward arrows', () => { + it('should not allow a page back when the left arrow btn is disabled', async () => { + const el = await fixture( + html`` + ); + const arrowLeftBtn = el.shadowRoot?.querySelector('.previous') as HTMLButtonElement; + expect(arrowLeftBtn.disabled).to.eq(true); + setTimeout(() => { + arrowLeftBtn?.click(); + expect(el.currentPage).to.equal(1); + }); + }); + + it('should not allow a page forward when the right arrow btn is disabled', async () => { + const el = await fixture( + html`` + ); + const arrowRightBtn = el.shadowRoot?.querySelector('.next') as HTMLButtonElement; + expect(arrowRightBtn.disabled).to.eq(true); + setTimeout(() => { + arrowRightBtn?.click(); + expect(el.currentPage).to.equal(10); + }); + }); + }); + + describe('pages', () => { + it('renders the correct number of page buttons,dots if current page is in the first five', async () => { + const el = await fixture( + html`` + ); + expect(el.shadowRoot?.querySelectorAll('bl-button').length).to.eq(8); + expect(el.shadowRoot?.querySelectorAll('.dots').length).to.eq(1); + }); + + it('renders the correct number of page buttons,dots if current page is in the middle', async () => { + const el = await fixture( + html`` + ); + expect(el.shadowRoot?.querySelectorAll('bl-button').length).to.eq(7); + expect(el.shadowRoot?.querySelectorAll('.dots').length).to.eq(2); + }); + + it('renders the correct number of page buttons,dots if current page is in the last five', async () => { + const el = await fixture( + html`` + ); + expect(el.shadowRoot?.querySelectorAll('bl-button').length).to.eq(8); + expect(el.shadowRoot?.querySelectorAll('.dots').length).to.eq(1); + }); + + it('should change the current page when user clicks to a single page button ', async () => { + const el = await fixture( + html`` + ); + + const pageFour = el.shadowRoot + ?.querySelectorAll('bl-button')[4] + .shadowRoot?.querySelector('button'); + + setTimeout(() => { + pageFour?.click(); + expect(el.currentPage).to.equal(4); + }); + }); + }); + + describe('jumper and select element', () => { + it('not renders jumper or select when not provided', async () => { + const el = await fixture( + html`` + ); + expect(el.shadowRoot?.querySelector('bl-input')).not.to.exist; + expect(el.shadowRoot?.querySelector('bl-select')).not.to.exist; + expect(el.shadowRoot?.querySelector('.jumper')).not.to.exist; + expect(el.shadowRoot?.querySelector('.select')).not.to.exist; + }); + + it('renders jumper input and select if has-jumper and has-select attributes are given', async () => { + const el = await fixture( + html`` + ); + const selectLabel = el.shadowRoot?.querySelectorAll('label')[0]; + const jumperLabel = el.shadowRoot?.querySelectorAll('label')[1]; + expect(jumperLabel?.innerText).to.exist; + expect(jumperLabel?.innerText).to.equal('Git'); + expect(selectLabel?.innerText).to.exist; + expect(selectLabel?.innerText).to.equal('Seç'); + expect(el.shadowRoot?.querySelector('bl-input')).to.exist; + expect(el.shadowRoot?.querySelector('bl-select')).to.exist; + expect(el.shadowRoot?.querySelector('.jumper')).to.exist; + expect(el.shadowRoot?.querySelector('.select')).to.exist; + }); + + it('should set the jumper value to the current page', async () => { + const el = await fixture( + html`` + ); + + const jumper = el.shadowRoot?.querySelector('bl-input'); + expect(jumper?.value).to.equal('3'); + }); + }); + + describe('events', () => { + const paginationEl = html``; + + it('should go to the next or previous page and fire a bl-change event when user clicks to the arrow buttons', async () => { + const el = await fixture(paginationEl); + const arrowRightBtn = el.shadowRoot?.querySelector('.next') as HTMLButtonElement; + const arrowLeftBtn = el.shadowRoot?.querySelector('.previous') as HTMLButtonElement; + + setTimeout(() => { + arrowRightBtn?.click(); + expect(el.currentPage).to.equal(2); + }); + + setTimeout(() => { + arrowLeftBtn?.click(); + expect(el.currentPage).to.equal(1); + }); + + const ev = await oneEvent(el, 'bl-change'); + expect(ev).to.exist; + }); + + it('should fire a bl-change event when jumper is changed', async () => { + const el = await fixture(paginationEl); + const jumper = el.shadowRoot?.querySelector('bl-input')?.shadowRoot?.querySelector('input'); + + if (jumper) { + jumper.value = '5'; + } + + setTimeout(() => jumper?.dispatchEvent(new Event('change'))); + const ev = await oneEvent(el, 'bl-change'); + + expect(ev).to.exist; + expect(ev.detail).to.be.equal('5'); + }); + + it('should set the page to the last page if user enters a bigger number than the last page', async () => { + const el = await fixture(paginationEl); + + const jumper = el.shadowRoot?.querySelector('bl-input'); + + if (jumper) { + jumper.value = '20'; + } + + const jumperEvent = new CustomEvent('bl-change'); + jumper?.dispatchEvent(jumperEvent); + expect(el.currentPage).to.equal(10); + }); + + it('should set the page to 1 if user enters a negative number', async () => { + const el = await fixture(paginationEl); + + const jumper = el.shadowRoot?.querySelector('bl-input'); + + if (jumper) { + jumper.value = '-5'; + } + + const jumperEvent = new CustomEvent('bl-change'); + jumper?.dispatchEvent(jumperEvent); + expect(el.currentPage).to.equal(1); + }); + + it('should change the items per page and should set the current page to 1 on select changes', async () => { + const el = await fixture(html``); + + const select = el.shadowRoot?.querySelector('bl-select'); + const optionTwo = el?.shadowRoot?.querySelectorAll('bl-select-option')[1]; + const optionThree = el?.shadowRoot?.querySelectorAll('bl-select-option')[2]; + + if (optionTwo && optionThree) { + optionTwo.selected = true; + optionThree.selected = false; + optionThree.value = ""; + } + + const selectOptionEvent = new CustomEvent('bl-select', { + detail: [optionTwo], + }); + + select?.dispatchEvent(selectOptionEvent); + + expect(el.itemsPerPage).to.equal(optionTwo?.value); + expect(el.currentPage).to.equal(1); + + const undefinedEvent = new CustomEvent('bl-select', { + detail: [optionThree], + }); + + select?.dispatchEvent(undefinedEvent); + + expect(el.itemsPerPage).to.equal(100); + }); + }); +}); diff --git a/src/components/pagination/bl-pagination.ts b/src/components/pagination/bl-pagination.ts new file mode 100644 index 00000000..9d5f73a2 --- /dev/null +++ b/src/components/pagination/bl-pagination.ts @@ -0,0 +1,241 @@ +import { CSSResultGroup, html, LitElement, TemplateResult, PropertyValues } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { event, EventDispatcher } from '../../utilities/event'; +import '../button/bl-button'; +import '../input/bl-input'; +import '../select/bl-select'; + +import style from './bl-pagination.css'; + +/** + * @tag bl-pagination + * @summary Baklava Pagination component + */ + +const selectOptions = [ + { + text: '100', + value: 100, + }, + { + text: '250', + value: 250, + }, + { + text: '500', + value: 500, + }, + { + text: '1000', + value: 1000, + }, +]; + +@customElement('bl-pagination') +export default class BlPagination extends LitElement { + static get styles(): CSSResultGroup { + return [style]; + } + + /** + * Sets the current page + */ + @property({ attribute: 'current-page', type: Number, reflect: true }) + currentPage = 1; + + /** + * Sets the total items to be paginated + */ + @property({ attribute: 'total-items', type: Number }) + totalItems = 0; + + /** + * Sets the number of items per page + */ + @property({ attribute: 'items-per-page', type: Number, reflect: true }) + itemsPerPage = 100; + + /** + * Adds jumper element if provided as true + */ + @property({ attribute: 'has-jumper', type: Boolean }) + hasJumper = false; + + /** + * Sets the jumper label + */ + @property({ attribute: 'jumper-label', type: String }) + jumperLabel = 'Go To'; + + /** + * Adds select element to choose the items per page + */ + @property({ attribute: 'has-select', type: Boolean }) + hasSelect = false; + + /** + * Adds select element to choose the items per page + */ + @property({ attribute: 'select-label', type: String }) + selectLabel = 'Show'; + + /** + * Sets the option texts. + */ + @property({ attribute: 'option-text', type: String }) + optionText = 'Items'; + + @state() private pages: Array = []; + + /** + * Fires when the current page changes + */ + @event('bl-change') private onChange: EventDispatcher<{ selectedPage: number; prevPage: number }>; + + updated(changedProperties: PropertyValues) { + if (changedProperties.has('currentPage') || changedProperties.has('itemsPerPage')) { + this._paginate(); + this.onChange({ + selectedPage: this.currentPage, + prevPage: changedProperties.get('currentPage'), + }); + } + } + + private _paginate() { + this.pages = []; + const pageListLength = Math.ceil(Math.abs(this.totalItems / this.itemsPerPage)) || 1; + + if (pageListLength <= 8) { + this.pages = Array.from(Array(pageListLength), (_, index) => index + 1); + return; + } + + this.pages.push(1); + + if (this.currentPage < 5) { + this.pages.push(2, 3, 4, 5, '...'); + } else if (this.currentPage >= 5 && this.currentPage <= pageListLength - 4) { + this.pages.push('...', this.currentPage - 1, this.currentPage, this.currentPage + 1, '...'); + } else { + this.pages.push( + '...', + pageListLength - 4, + pageListLength - 3, + pageListLength - 2, + pageListLength - 1 + ); + } + + this.pages.push(pageListLength); + } + + private _changePage(page: number): void { + this.currentPage = page; + } + + private _pageBack(): void { + if (this.currentPage === 1) return; + this.currentPage--; + } + + private _pageForward(): void { + if (this.currentPage === this._getLastPage()) return; + this.currentPage++; + } + + private _getLastPage(): number { + return +this.pages[this.pages.length - 1]; + } + + private _inputHandler(event: CustomEvent) { + const inputValue = +(event.target as HTMLInputElement).value; + const newPage = inputValue > 0 ? Math.min(this._getLastPage(), inputValue) : 1; + this._changePage(newPage); + } + + private _selectHandler(event: CustomEvent) { + this.itemsPerPage = event?.detail[0]?.value || 100; + this.currentPage = 1; + } + + private renderSinglePage(page: number | string) { + if (typeof page === 'string') { + return html``; + } + return html`
  • + + ${page} + +
  • `; + } + + private renderPages() { + return html` +
    + +
      + ${this.pages.map(page => html`${this.renderSinglePage(page)}`)} +
    + +
    + `; + } + + render(): TemplateResult { + const selectEl = this.hasSelect ? html` +
    + + + ${selectOptions.map(option => { + return html`${option.text} ${this.optionText}`; + })} + +
    + ` : null; + + const jumperEl = this.hasJumper + ? html`
    + + +
    ` + : null; + + const getHelperElements = () => { + if (!this.hasSelect && !this.hasJumper) return; + return html` +
    ${selectEl} ${jumperEl}
    + `; + }; + + return html` `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'bl-pagination': BlPagination; + } +} diff --git a/src/components/select/bl-select.test.ts b/src/components/select/bl-select.test.ts index eddc9d81..d8545dbc 100644 --- a/src/components/select/bl-select.test.ts +++ b/src/components/select/bl-select.test.ts @@ -19,14 +19,6 @@ describe('bl-select', () => {
      - -
      @@ -139,11 +131,20 @@ describe('bl-select', () => { const event = await oneEvent(el, 'bl-select'); + expect(removeAll).to.exist; expect(event).to.exist; expect(event.detail).to.eql([]); expect(el.options.length).to.equal(2); expect(el.selectedOptions.length).to.equal(0); }); + it('should hide remove icon button on single selection', async () => { + const el = await fixture(html` + Option 1 + Option 2 + `); + + expect(el.shadowRoot?.querySelector('.remove-all')).not.to.exist; + }); it('should fire event when click select option when it is not selected', async () => { const el = await fixture(html` Option 1 diff --git a/src/components/select/bl-select.ts b/src/components/select/bl-select.ts index b319800a..1718ee0c 100644 --- a/src/components/select/bl-select.ts +++ b/src/components/select/bl-select.ts @@ -208,7 +208,7 @@ export default class BlSelect extends LitElement { > ${placeholder} ${inputSelectedOptions} ${_selectedItemCount}
      - ${removeButton} + ${this.multiple ? removeButton : null} - ` + testRunnerHtml: testFramework => `