",
14 | "devDependencies": {
15 | "axe-core": "^4.3.3",
16 | "clean-css": "^5.1.5",
17 | "concurrently": "^6.2.1",
18 | "cross-env": "^7.0.3",
19 | "eslint": "^8.50.0",
20 | "eslint-config-prettier": "^9.0.0",
21 | "html-minifier": "^4.0.0",
22 | "html-validate": "^8.4.1",
23 | "jasmine": "^3.9.0",
24 | "nyc": "^15.1.0",
25 | "puppeteer": "^22.11.2",
26 | "puppeteer-to-istanbul": "^1.4.0",
27 | "rollup": "^2.79.1",
28 | "rollup-plugin-terser": "^7.0.2",
29 | "stylelint": "^15.10.3",
30 | "stylelint-config-standard": "^34.0.0"
31 | },
32 | "scripts": {
33 | "start": "concurrently -k \"node server.js docs\" \"cross-env NODE_ENV=local rollup -c -w\"",
34 | "build": "rollup -c",
35 | "pretest": "cross-env NODE_ENV=local npm run build",
36 | "test": "concurrently -k -s first \"nyc --reporter=lcov --reporter=text-summary jasmine\" \"node server.js docs\"",
37 | "prepublishOnly": "npm run lint && npm test && npm run build",
38 | "lint": "eslint src/**/*.js && stylelint src/**/*.css && html-validate src/**/*.html"
39 | },
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/zooplus/zoo-web-components.git"
43 | },
44 | "keywords": [
45 | "web-components",
46 | "shadow-dom",
47 | "custom-elements",
48 | "javascript",
49 | "css",
50 | "html"
51 | ],
52 | "author": "Yuriy Kravets",
53 | "license": "MIT",
54 | "bugs": {
55 | "url": "https://github.com/zooplus/zoo-web-components/issues"
56 | },
57 | "homepage": "https://github.com/zooplus/zoo-web-components#readme"
58 | }
59 |
--------------------------------------------------------------------------------
/rollup-config/getModules.js:
--------------------------------------------------------------------------------
1 | import {plugins} from './plugins.js';
2 | import fs from 'fs';
3 |
4 | let dev = process.env.NODE_ENV == 'local';
5 |
6 | const createConfig = (filePath) => {
7 | const fileName = filePath.replace('./src/zoo-modules/', '');
8 | const shortName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.')).split('-').join('')
9 | const shortFileName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.length);
10 | return {
11 | input: filePath,
12 | output: {
13 | file: dev ? `docs/components/${shortFileName}` : `dist/${shortFileName}`,
14 | format: 'iife',
15 | name: shortName,
16 | },
17 | plugins
18 | }
19 | };
20 |
21 |
22 | function getFiles(nextPath, modules) {
23 | if(fs.existsSync(nextPath) && fs.lstatSync(nextPath).isDirectory()) {
24 | const nextDirPath = fs.readdirSync(nextPath);
25 | nextDirPath.forEach(filePath => getFiles(`${nextPath}/${filePath}`, modules));
26 | } else {
27 | if (nextPath.indexOf('.js') > -1) {
28 | modules.push(nextPath);
29 | }
30 | }
31 | }
32 |
33 | export function getModules() {
34 | let modules = [];
35 | getFiles('./src/zoo-modules', modules);
36 | return modules.map(modulePath => createConfig(modulePath));
37 | }
--------------------------------------------------------------------------------
/rollup-config/injectInnerHTML.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import CleanCSS from 'clean-css';
3 | import minifyHTML from 'html-minifier';
4 |
5 | export default function injectInnerHTML() {
6 | return {
7 | name: 'injectInnerHTML',
8 |
9 | transform(code, id) {
10 | if (code.indexOf('@injectHTML') > -1) {
11 | const htmlFile = id.replace('.js', '.html');
12 | const cssFile = id.replace('.js', '.css');
13 | const html = fs.readFileSync(htmlFile, 'utf8');
14 | const minifiedHTML = minifyHTML.minify(html, {collapseWhitespace: true, collapseBooleanAttributes: true});
15 | const css = fs.readFileSync(cssFile, 'utf8');
16 | const minifiedCss = new CleanCSS({ level: { 2: { all: true } } }).minify(css);
17 | if (minifiedCss.errors && minifiedCss.errors.length > 0) {
18 | console.error(minifiedCss.errors);
19 | }
20 | if (minifiedCss.warnings && minifiedCss.warnings.length > 0) {
21 | console.warn(minifiedCss.warnings);
22 | }
23 | code = code.replace('super();', `super();this.attachShadow({mode:'open'}).innerHTML=\`${minifiedHTML}\`;`);
24 |
25 | // fs.appendFile('./docs/all.css', minifiedCss.styles, function (err) {
26 | // if (err) throw err;
27 | // console.log('Saved!');
28 | // });
29 | }
30 | return {
31 | code: code,
32 | map: null
33 | };
34 | }
35 | };
36 | }
--------------------------------------------------------------------------------
/rollup-config/plugins.js:
--------------------------------------------------------------------------------
1 | import injectInnerHTML from './injectInnerHTML.js';
2 | import { watcher, noOpWatcher } from './watcher.js';
3 | import { terser } from 'rollup-plugin-terser';
4 |
5 | let dev = process.env.NODE_ENV == 'local';
6 |
7 | export const plugins = [
8 | injectInnerHTML(),
9 | dev ? watcher() : noOpWatcher(),
10 | dev ? noOpWatcher() : terser({
11 | module: true,
12 | keep_classnames: true
13 | }),
14 | ];
--------------------------------------------------------------------------------
/rollup-config/watcher.js:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 | import path from 'path';
3 |
4 | export function watcher() {
5 | return {
6 | buildStart() {
7 | let include = ['src/zoo-modules/**/*.html', 'src/zoo-modules/**/*.css'];
8 | for (const item of include) {
9 | glob.sync(path.resolve(item)).forEach(filename => this.addWatchFile(filename));
10 | }
11 | },
12 | options(options) {
13 | options.cache = {};
14 | return options;
15 | }
16 | };
17 | }
18 |
19 | export function noOpWatcher() {
20 | return {
21 | options(options) {
22 | return options;
23 | }
24 | };
25 | }
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { plugins } from './rollup-config/plugins.js';
2 | import { getModules } from './rollup-config/getModules.js';
3 |
4 | let dev = process.env.NODE_ENV == 'local';
5 |
6 | const modules = !dev ? getModules() : [];
7 | export default [
8 | {
9 | input: 'src/zoo-web-components.js',
10 | output: [
11 | {
12 | sourcemap: true,
13 | format: 'iife',
14 | dir: dev ? 'docs/components' : 'dist',
15 | name: 'zooWebComponents'
16 | },
17 | {
18 | sourcemap: true,
19 | format: 'esm',
20 | dir: dev ? 'docs/components/esm' : 'dist/esm',
21 | preserveModules: true
22 | }
23 | ],
24 | plugins: plugins
25 | },
26 | ...modules
27 | ];
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import url from 'url';
3 | import fs from 'fs';
4 | import path from 'path';
5 | const port = 5050;
6 |
7 | http.createServer(function (req, res) {
8 | console.log(`${req.method} ${req.url}`);
9 |
10 | if (req.url === '/') req.url = '/index.html';
11 | // parse URL
12 | const parsedUrl = url.parse(req.url);
13 | // extract URL path
14 | let pathname = `./docs/${parsedUrl.pathname}`;
15 | // based on the URL path, extract the file extention. e.g. .js, .doc, ...
16 | const ext = path.parse(pathname).ext;
17 | // maps file extention to MIME typere
18 | const map = {
19 | '.ico': 'image/x-icon',
20 | '.html': 'text/html',
21 | '.js': 'text/javascript',
22 | '.json': 'application/json',
23 | '.css': 'text/css',
24 | '.png': 'image/png',
25 | '.jpg': 'image/jpeg',
26 | '.svg': 'image/svg+xml'
27 | };
28 |
29 | fs.exists(pathname, function (exist) {
30 | if (!exist) {
31 | // if the file is not found, return 404
32 | res.statusCode = 404;
33 | res.end(`File ${pathname} not found!`);
34 | return;
35 | }
36 | // if is a directory search for index file matching the extention
37 | if (fs.statSync(pathname).isDirectory()) pathname += '/index' + ext;
38 | // read file from file system
39 | fs.readFile(pathname, function (err, data) {
40 | if (err) {
41 | res.statusCode = 500;
42 | res.end(`Error getting the file: ${err}.`);
43 | } else {
44 | // if the file is found, set Content-type and send data
45 | res.setHeader('Content-type', map[ext] || 'text/plain');
46 | res.end(data);
47 | }
48 | });
49 | });
50 | }).listen(parseInt(port));
51 |
52 | console.log(`Server listening on port ${port}`);
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "src/zoo-modules",
3 | "spec_files": [
4 | "**/*[sS]pec.mjs"
5 | ],
6 | "helpers": [
7 | "helpers/*.mjs"
8 | ],
9 | "stopSpecOnExpectationFailure": false,
10 | "random": true
11 | }
12 |
--------------------------------------------------------------------------------
/src/zoo-modules/common/register-components.js:
--------------------------------------------------------------------------------
1 | export function registerComponents (...args) {
2 | args ? '' : console.error('Please register your components!');
3 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/checkbox/checkbox-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo checkbox', () => {
2 | it('should be a11y', async () => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 | `;
9 | return await axe.run('zoo-checkbox');
10 | });
11 |
12 | if (results.violations.length) {
13 | console.log('zoo-checkbox a11y violations ', results.violations);
14 | throw new Error('Accessibility issues found');
15 | }
16 | });
17 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/checkbox/checkbox.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | font-size: 14px;
6 | line-height: 20px;
7 | position: relative;
8 |
9 | --border: 0;
10 | --check-color: var(--primary-mid);
11 | }
12 |
13 | :host([disabled]) {
14 | --check-color: #767676;
15 | }
16 |
17 | :host([highlighted]) {
18 | --border: 1px solid var(--check-color);
19 | }
20 |
21 | :host([invalid]) {
22 | --check-color: var(--warning-mid);
23 | --border: 2px solid var(--warning-mid);
24 | }
25 |
26 | ::slotted(input) {
27 | width: 100%;
28 | height: 100%;
29 | top: 0;
30 | left: 0;
31 | position: absolute;
32 | display: flex;
33 | align-self: flex-start;
34 | appearance: none;
35 | cursor: pointer;
36 | margin: 0;
37 | border-radius: 3px;
38 | border: var(--border);
39 | }
40 |
41 | svg {
42 | border: 1px solid var(--check-color);
43 | fill: var(--check-color);
44 | border-radius: 3px;
45 | pointer-events: none;
46 | min-width: 24px;
47 | z-index: 1;
48 | padding: 1px;
49 | box-sizing: border-box;
50 | }
51 |
52 | svg path {
53 | display: none;
54 | }
55 |
56 | .indeterminate {
57 | display: none;
58 | background: var(--check-color);
59 | fill: white;
60 | }
61 |
62 | :host([checked]) svg path {
63 | display: flex;
64 | }
65 |
66 | :host([checked][indeterminate]) .indeterminate {
67 | display: flex;
68 | }
69 |
70 | :host([checked][indeterminate]) .checked {
71 | display: none;
72 | }
73 |
74 | :host(:focus-within) svg {
75 | border-width: 2px;
76 | }
77 |
78 | ::slotted(input:focus) {
79 | border-width: 2px;
80 | }
81 |
82 | :host([checked]) ::slotted(input) {
83 | border-width: 2px;
84 | }
85 |
86 | :host([disabled]) svg {
87 | background: var(--input-disabled, #F2F3F4);
88 | }
89 |
90 | .checkbox {
91 | display: flex;
92 | width: 100%;
93 | box-sizing: border-box;
94 | cursor: pointer;
95 | align-items: baseline;
96 | position: relative;
97 | }
98 |
99 | :host([highlighted]) .checkbox {
100 | padding: 11px 15px;
101 | }
102 |
103 | ::slotted(label) {
104 | display: flex;
105 | align-self: center;
106 | cursor: pointer;
107 | margin-left: 5px;
108 | z-index: 1;
109 | }
110 |
111 | ::slotted(input:disabled),
112 | :host([disabled]) ::slotted(label) {
113 | cursor: not-allowed;
114 | }
115 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/checkbox/checkbox.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/checkbox/checkbox.js:
--------------------------------------------------------------------------------
1 | import { FormElement } from '../common/FormElement.js';
2 | import { InfoMessage } from '../info/info.js';
3 | import { registerComponents } from '../../common/register-components.js';
4 |
5 | /**
6 | * @injectHTML
7 | */
8 | export class Checkbox extends FormElement {
9 | constructor() {
10 | super();
11 | registerComponents(InfoMessage);
12 | this.observer = new MutationObserver(mutationsList => {
13 | for (let mutation of mutationsList) {
14 | mutation.target.disabled ? this.setAttribute('disabled', '') : this.removeAttribute('disabled');
15 | mutation.target.hasAttribute('indeterminate') ? this.setAttribute('indeterminate', '') : this.removeAttribute('indeterminate');
16 | }
17 | });
18 | this.shadowRoot.querySelector('slot[name="checkbox"]').addEventListener('slotchange', e => {
19 | let checkbox = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
20 | if (!checkbox) return;
21 | checkbox.addEventListener('change', () => {
22 | checkbox.checked
23 | ? this.setAttribute('checked', '')
24 | : this.removeAttribute('checked');
25 | });
26 | this.registerElementForValidation(checkbox);
27 | if (checkbox.disabled) this.setAttribute('disabled', '');
28 | if (checkbox.checked) this.setAttribute('checked', '');
29 | if (checkbox.hasAttribute('indeterminate')) this.setAttribute('indeterminate', '');
30 | this.observer.observe(checkbox, { attributes: true, attributeFilter: ['disabled', 'indeterminate'] });
31 | });
32 | }
33 |
34 | disconnectedCallback() {
35 | this.observer.disconnect();
36 | }
37 | }
38 | if (!window.customElements.get('zoo-checkbox')) {
39 | window.customElements.define('zoo-checkbox', Checkbox);
40 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/checkbox/checkbox.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo checkbox', function () {
2 | it('should create highlighted checkbox', async () => {
3 | const inputLabelText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 | `;
10 | let checkbox = document.querySelector('zoo-checkbox');
11 |
12 | return checkbox.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML;
13 | });
14 | expect(inputLabelText).toEqual('label-text');
15 | });
16 |
17 | it('should create normal checkbox', async () => {
18 | const style = await page.evaluate(() => {
19 | document.body.innerHTML = `
20 |
21 |
22 |
23 |
24 | `;
25 | const root = document.querySelector('zoo-checkbox');
26 | const style = window.getComputedStyle(root);
27 | return {
28 | checkColor: style.getPropertyValue('--check-color').trim(),
29 | border: style.getPropertyValue('--border').trim()
30 | };
31 | });
32 | expect(style.checkColor).toEqual(colors.primaryMid);
33 | expect(style.border).toEqual('0');
34 | });
35 |
36 | it('should create highlighted checkbox', async () => {
37 | const style = await page.evaluate(() => {
38 | document.body.innerHTML = `
39 |
40 |
41 |
42 |
43 | `;
44 | const root = document.querySelector('zoo-checkbox');
45 | const style = window.getComputedStyle(root);
46 | return {
47 | checkColor: style.getPropertyValue('--check-color').trim(),
48 | border: style.getPropertyValue('--border').trim()
49 | };
50 | });
51 | expect(style.checkColor).toEqual(colors.primaryMid);
52 | expect(style.border).toEqual(`1px solid ${colors.primaryMid}`);
53 | });
54 |
55 | it('should create disabled checkbox', async () => {
56 | const style = await page.evaluate(async () => {
57 | document.body.innerHTML = `
58 |
59 |
60 |
61 |
62 | `;
63 | const root = document.querySelector('zoo-checkbox');
64 | const style = window.getComputedStyle(root);
65 | await new Promise(r => setTimeout(r, 50)); // wait for internal callbacks to kick in
66 | return {
67 | checkColor: style.getPropertyValue('--check-color').trim(),
68 | border: style.getPropertyValue('--border').trim()
69 | };
70 | });
71 | expect(style.checkColor).toEqual('#767676');
72 | expect(style.border).toEqual('1px solid #767676');
73 | });
74 |
75 | it('should create invalid checkbox', async () => {
76 | const style = await page.evaluate(() => {
77 | document.body.innerHTML = `
78 |
79 |
80 |
81 |
82 | `;
83 | const root = document.querySelector('zoo-checkbox');
84 | const style = window.getComputedStyle(root);
85 | return {
86 | checkColor: style.getPropertyValue('--check-color').trim(),
87 | border: style.getPropertyValue('--border').trim()
88 | };
89 | });
90 | expect(style.checkColor).toEqual(colors.warningMid);
91 | expect(style.border).toEqual(`2px solid ${colors.warningMid}`);
92 | });
93 |
94 | it('should set checked attribute on host when checkbox is checked', async () => {
95 | const checked = await page.evaluate(async () => {
96 | document.body.innerHTML = `
97 |
98 |
99 |
100 |
101 | `;
102 | const root = document.querySelector('zoo-checkbox');
103 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].click();
104 | await new Promise(r => setTimeout(r, 10));
105 | return root.hasAttribute('checked');
106 | });
107 | expect(checked).toBeTrue();
108 | });
109 |
110 | it('should set disabled attribute on host when checkbox is disabled', async () => {
111 | const disabled = await page.evaluate(async () => {
112 | document.body.innerHTML = `
113 |
114 |
115 |
116 |
117 | `;
118 | const root = document.querySelector('zoo-checkbox');
119 | await new Promise(r => setTimeout(r, 10));
120 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].disabled = true;
121 | await new Promise(r => setTimeout(r, 10));
122 |
123 | return root.hasAttribute('disabled');
124 | });
125 | expect(disabled).toBeTrue();
126 | });
127 |
128 | it('should remove disabled attribute on host when checkbox is no longer disabled', async () => {
129 | const disabled = await page.evaluate(async () => {
130 | document.body.innerHTML = `
131 |
132 |
133 |
134 |
135 | `;
136 | const root = document.querySelector('zoo-checkbox');
137 | await new Promise(r => setTimeout(r, 10));
138 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].disabled = false;
139 | await new Promise(r => setTimeout(r, 10));
140 |
141 | return root.hasAttribute('disabled');
142 | });
143 | expect(disabled).toBeFalse();
144 | });
145 |
146 | it('should set checked attribute when input becomes checked', async () => {
147 | const checked = await page.evaluate(async () => {
148 | document.body.innerHTML = `
149 |
150 |
151 |
152 |
153 | `;
154 | const root = document.querySelector('zoo-checkbox');
155 | await new Promise(r => setTimeout(r, 10));
156 | root.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0].click();
157 | await new Promise(r => setTimeout(r, 10));
158 | return root.hasAttribute('checked');
159 | });
160 | expect(checked).toBeTrue();
161 | });
162 |
163 | it('should set and then remove invalid attribute from host', async () => {
164 | const result = await page.evaluate(async () => {
165 | document.body.innerHTML = `
166 |
167 |
168 |
169 |
170 | `;
171 | const result = [];
172 | let input = document.querySelector('zoo-checkbox');
173 | const slottedInput = input.shadowRoot.querySelector('slot[name="checkbox"]').assignedElements()[0];
174 | slottedInput.click();
175 | await new Promise(r => setTimeout(r, 10));
176 | result.push(input.hasAttribute('invalid'));
177 |
178 | slottedInput.click();
179 | await new Promise(r => setTimeout(r, 10));
180 | result.push(input.hasAttribute('invalid'));
181 |
182 | return result;
183 | });
184 | expect(result[0]).toBeFalse();
185 | expect(result[1]).toBeTrue();
186 | });
187 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/common/FormElement.js:
--------------------------------------------------------------------------------
1 | export class FormElement extends HTMLElement {
2 | constructor() {
3 | super();
4 | }
5 |
6 | static get observedAttributes() {
7 | return ['invalid'];
8 | }
9 |
10 | registerElementForValidation(element) {
11 | element.addEventListener('invalid', () => {
12 | this.setInvalid();
13 | this.toggleInvalidAttribute(element);
14 | });
15 | element.addEventListener('input', () => {
16 | if (element.checkValidity()) {
17 | this.setValid();
18 | } else {
19 | this.setInvalid();
20 | }
21 | this.toggleInvalidAttribute(element);
22 | });
23 | }
24 |
25 | setInvalid() {
26 | this.setAttribute('invalid', '');
27 | this.setAttribute('aria-invalid', '');
28 | }
29 |
30 | setValid() {
31 | this.removeAttribute('aria-invalid');
32 | this.removeAttribute('invalid');
33 | }
34 |
35 | toggleInvalidAttribute(element) {
36 | const errorMsg = this.shadowRoot.querySelector('zoo-info[role="alert"]');
37 | element.validity.valid ? errorMsg.removeAttribute('invalid') : errorMsg.setAttribute('invalid', '');
38 | }
39 |
40 | attributeChangedCallback() {
41 | const errorMsg = this.shadowRoot.querySelector('zoo-info[role="alert"]');
42 | this.hasAttribute('invalid') ? errorMsg.setAttribute('invalid', '') : errorMsg.removeAttribute('invalid');
43 | }
44 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/date-range/date-range.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | grid-gap: 3px;
4 | width: 100%;
5 | height: max-content;
6 | box-sizing: border-box;
7 | }
8 |
9 | fieldset {
10 | border: 0;
11 | padding: 0;
12 | margin: 0;
13 | position: relative;
14 | }
15 |
16 | :host([invalid]) ::slotted(input) {
17 | border: 2px solid var(--warning-mid);
18 | padding: 12px 14px;
19 | }
20 |
21 | .content {
22 | display: flex;
23 | justify-content: space-between;
24 | grid-column: span 2;
25 | }
26 |
27 | .content zoo-input {
28 | width: 49%;
29 | }
30 |
31 | zoo-info {
32 | grid-column: span 2;
33 | }
34 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/date-range/date-range.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/date-range/date-range.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { FormElement } from '../common/FormElement.js';
3 | import { InfoMessage } from '../info/info.js';
4 | import { Label } from '../label/label.js';
5 | import { Input } from '../input/input.js';
6 |
7 | /**
8 | * @injectHTML
9 | */
10 | export class DateRange extends FormElement {
11 | constructor() {
12 | super();
13 | registerComponents(InfoMessage, Label, Input);
14 | const slottedInputs = {};
15 | this.shadowRoot.querySelector('slot[name="date-from"]')
16 | .addEventListener('slotchange', e => this.handleAndSaveSlottedInputAs(e, 'dateFrom', slottedInputs));
17 | this.shadowRoot.querySelector('slot[name="date-to"]')
18 | .addEventListener('slotchange', e => this.handleAndSaveSlottedInputAs(e, 'dateTo', slottedInputs));
19 | this.addEventListener('input', () => {
20 | const dateInputFrom = slottedInputs.dateFrom;
21 | const dateInputTo = slottedInputs.dateTo;
22 | if (dateInputFrom.value && dateInputTo.value && dateInputFrom.value > dateInputTo.value) {
23 | this.setInvalid();
24 | } else if (dateInputFrom.validity.valid && dateInputTo.validity.valid) {
25 | this.setValid();
26 | }
27 | });
28 | }
29 |
30 | handleAndSaveSlottedInputAs(e, propName, slottedInputs) {
31 | const input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
32 | slottedInputs[propName] = input;
33 | input && this.registerElementForValidation(input);
34 | }
35 | }
36 | if (!window.customElements.get('zoo-date-range')) {
37 | window.customElements.define('zoo-date-range', DateRange);
38 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/date-range/date-range.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo date range', function () {
2 | it('mark component as invalid when min > max', async () => {
3 | const invalid = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 |
10 | `;
11 | await new Promise(r => setTimeout(r, 10));
12 | const dateRange = document.querySelector('zoo-date-range');
13 |
14 | const dateFrom = dateRange.shadowRoot.querySelector('slot[name="date-from"]').assignedElements()[0];
15 | dateFrom.value = '2021-04-26';
16 | dateFrom.dispatchEvent(new Event('input', {bubbles: true}));
17 | await new Promise(r => setTimeout(r, 10));
18 |
19 | const dateTo = dateRange.shadowRoot.querySelector('slot[name="date-to"]').assignedElements()[0];
20 | dateTo.value = '2021-04-25';
21 | dateTo.dispatchEvent(new Event('input', {bubbles: true}));
22 | await new Promise(r => setTimeout(r, 10));
23 |
24 | return document.querySelector('zoo-date-range').hasAttribute('invalid');
25 | });
26 | expect(invalid).toBeTrue();
27 | });
28 |
29 | it('mark component as invalid when max < min', async () => {
30 | const invalid = await page.evaluate(async () => {
31 | document.body.innerHTML = `
32 |
33 |
34 |
35 |
36 |
37 | `;
38 | await new Promise(r => setTimeout(r, 10));
39 | const dateRange = document.querySelector('zoo-date-range');
40 |
41 | const dateTo = dateRange.shadowRoot.querySelector('slot[name="date-to"]').assignedElements()[0];
42 | dateTo.value = '2021-04-27';
43 | dateTo.dispatchEvent(new Event('input', {bubbles: true}));
44 | await new Promise(r => setTimeout(r, 10));
45 |
46 | const dateFrom = dateRange.shadowRoot.querySelector('slot[name="date-from"]').assignedElements()[0];
47 | dateFrom.value = '2021-04-28';
48 | dateFrom.dispatchEvent(new Event('input', {bubbles: true}));
49 | await new Promise(r => setTimeout(r, 10));
50 |
51 | return document.querySelector('zoo-date-range').hasAttribute('invalid');
52 | });
53 | expect(invalid).toBeTrue();
54 | });
55 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/info/info.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: none;
3 | padding: 2px;
4 | font-size: 12px;
5 | line-height: 16px;
6 | color: #555;
7 | align-items: center;
8 | }
9 |
10 | :host([shown]) {
11 | display: flex;
12 | }
13 |
14 | :host([role="alert"][shown]:not([invalid])) {
15 | display: none;
16 | }
17 |
18 | :host([role="alert"][invalid][shown]) {
19 | display: flex;
20 |
21 | --icon-color: var(--warning-mid);
22 | }
23 |
24 | zoo-attention-icon {
25 | align-self: flex-start;
26 | }
27 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/info/info.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/info/info.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { AttentionIcon } from '../../icon/attention-icon/attention-icon.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class InfoMessage extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(AttentionIcon);
11 | this.shadowRoot.querySelector('slot').addEventListener('slotchange', e => {
12 | e.target.assignedElements({ flatten: true }).length > 0 ? this.setAttribute('shown', '') : this.removeAttribute('shown');
13 | });
14 | }
15 | }
16 | if (!window.customElements.get('zoo-info')) {
17 | window.customElements.define('zoo-info', InfoMessage);
18 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/input-tag/input-tag-option.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | cursor: pointer;
5 | padding: 5px;
6 | overflow: auto;
7 | font-size: 12px;
8 | gap: 3px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input-tag/input-tag-option.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input-tag/input-tag-option.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class InputTagOption extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-input-tag-option')) {
10 | window.customElements.define('zoo-input-tag-option', InputTagOption);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/input-tag/input-tag.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | grid-gap: 3px;
4 | width: 100%;
5 | height: max-content;
6 | box-sizing: border-box;
7 |
8 | --input-tag-padding-top-bottom-default: 13px;
9 | --input-tag-padding-left-right-default: 15px;
10 | --input-tag-padding-reduced: calc(var(--input-tag-padding-top-bottom, var(--input-tag-padding-top-bottom-default)) - 1px) calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px);
11 | }
12 |
13 | #input-wrapper {
14 | display: flex;
15 | flex-wrap: wrap;
16 | align-items: center;
17 | height: max-content;
18 | gap: 5px;
19 | font-size: 14px;
20 | line-height: 20px;
21 | padding: var(--input-tag-padding-top-bottom, var(--input-tag-padding-top-bottom-default)) var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default));
22 | border: 1px solid #767676;
23 | border-radius: 5px;
24 | color: #555;
25 | box-sizing: border-box;
26 | grid-column: span 2;
27 | position: relative;
28 | overflow: visible;
29 | }
30 |
31 | :host(:focus-within) #input-wrapper {
32 | border: 2px solid #555;
33 | padding: var(--input-tag-padding-reduced);
34 | }
35 |
36 | :host([show-tags]) #input-wrapper {
37 | z-index: 2;
38 | }
39 |
40 | :host([invalid]) #input-wrapper {
41 | border: 2px solid var(--warning-mid);
42 | padding: var(--input-tag-padding-reduced);
43 | }
44 |
45 | ::slotted(input) {
46 | border: 0;
47 | min-width: 50px;
48 | flex: 1 0 auto;
49 | outline: none;
50 | font-size: 14px;
51 | line-height: 20px;
52 | color: #555;
53 | }
54 |
55 | zoo-label {
56 | grid-row: 1;
57 | }
58 |
59 | #tag-options {
60 | display: none;
61 | position: absolute;
62 | flex-wrap: wrap;
63 | background: white;
64 | padding: 5px var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default));
65 | border: 1px solid #555;
66 | border-radius: 0 0 3px 3px;
67 | left: -1px;
68 | top: calc(90% + 2px);
69 | border-top: 0;
70 | width: calc(100% + 2px);
71 | box-sizing: border-box;
72 | max-height: var(--input-tag-options-max-height, fit-content);
73 | overflow: var(--input-tag-options-overflow, auto);
74 | }
75 |
76 | :host(:focus-within) #tag-options,
77 | :host([invalid]) #tag-options {
78 | border-width: 2px;
79 | width: calc(100% + 4px);
80 | left: -2px;
81 | padding-left: calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px);
82 | padding-right: calc(var(--input-tag-padding-left-right, var(--input-tag-padding-left-right-default)) - 1px);
83 | }
84 |
85 | :host([invalid]) #tag-options {
86 | border-color: var(--warning-mid);
87 | }
88 |
89 | :host([show-tags]) #tag-options {
90 | display: flex;
91 | }
92 |
93 | ::slotted(*[slot="select"]) {
94 | display: none;
95 | }
96 |
97 | zoo-info {
98 | grid-column: span 2;
99 | }
100 |
101 | zoo-cross-icon {
102 | cursor: pointer;
103 |
104 | --icon-color: var(--primary-mid);
105 | }
106 |
107 | ::slotted(zoo-input-tag-option) {
108 | box-sizing: border-box;
109 | width: 100%;
110 | }
111 |
112 | ::slotted(zoo-input-tag-option:hover),
113 | ::slotted(zoo-input-tag-option[selected]:hover) {
114 | background: var(--item-hovered, #E6E6E6);
115 | }
116 |
117 | ::slotted(zoo-input-tag-option[selected]) {
118 | background: var(--primary-ultralight);
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input-tag/input-tag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input/input-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo input', function () {
2 | it('should be a11y', async () => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 | `;
9 | return await axe.run('zoo-input');
10 | });
11 |
12 | if (results.violations.length) {
13 | console.log('zoo-input a11y violations ', results.violations);
14 | throw new Error('Accessibility issues found');
15 | }
16 | });
17 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/input/input.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | grid-gap: 3px;
4 | width: 100%;
5 | height: max-content;
6 | box-sizing: border-box;
7 | }
8 |
9 | ::slotted(input),
10 | ::slotted(textarea) {
11 | width: 100%;
12 | font-size: 14px;
13 | line-height: 20px;
14 | padding: 13px 15px;
15 | margin: 0;
16 | border: 1px solid #767676;
17 | border-radius: 5px;
18 | color: #555;
19 | outline: none;
20 | box-sizing: border-box;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | }
24 |
25 | :host([invalid]) ::slotted(input),
26 | :host([invalid]) ::slotted(textarea) {
27 | border: 2px solid var(--warning-mid);
28 | padding: 12px 14px;
29 | }
30 |
31 | ::slotted(input[type="date"]),
32 | ::slotted(input[type="time"]) {
33 | -webkit-logical-height: 48px;
34 | max-height: 48px;
35 | }
36 |
37 | ::slotted(input::placeholder),
38 | ::slotted(textarea::placeholder) {
39 | color: #767676;
40 | }
41 |
42 | ::slotted(input:disabled),
43 | ::slotted(textarea:disabled) {
44 | border: 1px solid #E6E6E6;
45 | background: var(--input-disabled, #F2F3F4);
46 | color: #767676;
47 | cursor: not-allowed;
48 | }
49 |
50 | ::slotted(input:focus),
51 | ::slotted(textarea:focus) {
52 | border: 2px solid #555;
53 | padding: 12px 14px;
54 | }
55 |
56 | .content {
57 | display: flex;
58 | grid-column: span 2;
59 | }
60 |
61 | zoo-info {
62 | grid-column: span 2;
63 | }
64 |
65 | zoo-link {
66 | text-align: right;
67 | max-width: max-content;
68 | justify-self: flex-end;
69 | padding: 0;
70 | }
71 |
72 | :host([labelposition="left"]) zoo-link {
73 | grid-column: 2;
74 | }
75 |
76 | :host([labelposition="left"]) zoo-label,
77 | :host([labelposition="left"]) .content {
78 | display: flex;
79 | align-items: center;
80 | grid-row: 2;
81 | }
82 |
83 | :host([labelposition="left"]) zoo-info[role="status"] {
84 | grid-row: 3;
85 | grid-column: 2;
86 | }
87 |
88 | :host([labelposition="left"]) zoo-info[role="alert"] {
89 | grid-row: 4;
90 | grid-column: 2;
91 | }
92 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/input/input.js:
--------------------------------------------------------------------------------
1 | import { FormElement } from '../common/FormElement.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 | import { InfoMessage } from '../info/info.js';
4 | import { Label } from '../label/label.js';
5 | import { Link } from '../../misc/link/link.js';
6 |
7 | /**
8 | * @injectHTML
9 | */
10 | export class Input extends FormElement {
11 | constructor() {
12 | super();
13 | registerComponents(InfoMessage, Label, Link);
14 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => {
15 | let input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
16 | input && this.registerElementForValidation(input);
17 | });
18 | }
19 | }
20 | if (!window.customElements.get('zoo-input')) {
21 | window.customElements.define('zoo-input', Input);
22 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/input/input.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo input', function () {
2 | it('should pass attributes to input label component', async () => {
3 | const labelText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 | `;
10 | let input = document.querySelector('zoo-input');
11 | const label = input.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0];
12 | return label.innerHTML;
13 | });
14 | expect(labelText).toEqual('label');
15 | });
16 |
17 | it('should render input link', async () => {
18 | const linkAttrs = await page.evaluate(() => {
19 | document.body.innerHTML = `
20 |
21 |
22 |
23 | Possible values: Dog, Cat, Small Pet, Bird, Aquatic
24 | Learn your HTML and don't overcomplicate
25 |
26 | `;
27 | let input = document.querySelector('zoo-input');
28 | const linkAnchor = input.shadowRoot.querySelector('slot[name="link"]').assignedElements()[0];
29 | return {
30 | linkText: linkAnchor.innerHTML,
31 | linkTarget: linkAnchor.getAttribute('target'),
32 | linkHref: linkAnchor.getAttribute('href')
33 | };
34 | });
35 | expect(linkAttrs.linkText).toEqual('Learn your HTML and don\'t overcomplicate');
36 | expect(linkAttrs.linkHref).toEqual('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist');
37 | expect(linkAttrs.linkTarget).toEqual('about:blank');
38 | });
39 |
40 | it('should render input error', async () => {
41 | const errorDisplay = await page.evaluate(async () => {
42 | document.body.innerHTML = `
43 |
44 |
45 |
46 | error
47 |
48 | `;
49 | let input = document.querySelector('zoo-input');
50 | await new Promise(r => setTimeout(r, 10));
51 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]');
52 | return window.getComputedStyle(error).display;
53 | });
54 | expect(errorDisplay).toEqual('flex');
55 | });
56 |
57 | it('should not render input error', async () => {
58 | const errorDisplay = await page.evaluate(async () => {
59 | document.body.innerHTML = `
60 |
61 |
62 |
63 | error
64 |
65 | `;
66 | let input = document.querySelector('zoo-input');
67 | await new Promise(r => setTimeout(r, 10));
68 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]');
69 | return window.getComputedStyle(error).display;
70 | });
71 | expect(errorDisplay).toEqual('none');
72 | });
73 |
74 | it('should set and then remove invalid attribute from host', async () => {
75 | const result = await page.evaluate(async () => {
76 | document.body.innerHTML = `
77 |
78 |
79 |
80 | error
81 |
82 | `;
83 | const result = [];
84 | let input = document.querySelector('zoo-input');
85 | await new Promise(r => setTimeout(r, 10));
86 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
87 | slottedInput.value = 123;
88 | slottedInput.dispatchEvent(new Event('input'));
89 | await new Promise(r => setTimeout(r, 10));
90 | result.push(input.hasAttribute('invalid'));
91 |
92 | slottedInput.value = 'asd';
93 | slottedInput.dispatchEvent(new Event('input'));
94 | await new Promise(r => setTimeout(r, 10));
95 | result.push(input.hasAttribute('invalid'));
96 |
97 | return result;
98 | });
99 | expect(result[0]).toBeTrue();
100 | expect(result[1]).toBeFalse();
101 | });
102 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/label/label.css:
--------------------------------------------------------------------------------
1 | :host {
2 | font-size: 14px;
3 | line-height: 20px;
4 | font-weight: 700;
5 | color: #555;
6 | text-align: left;
7 | }
8 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/label/label.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/label/label.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Label extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-label')) {
10 | window.customElements.define('zoo-label', Label);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/quantity-control/quantity-contol-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo quantity control', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
9 |
10 |
11 |
17 | `;
18 | return await axe.run('zoo-quantity-control');
19 | });
20 | if (results.violations.length) {
21 | console.log('zoo-quantity-control a11y violations ', results.violations);
22 | throw new Error('Accessibility issues found');
23 | }
24 | });
25 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/quantity-control/quantity-control.css:
--------------------------------------------------------------------------------
1 | :host {
2 | --input-length: 1ch;
3 | }
4 |
5 | div {
6 | height: 36px;
7 | display: flex;
8 | }
9 |
10 | ::slotted(button) {
11 | border-width: 0;
12 | min-width: 30px;
13 | min-height: 30px;
14 | background: var(--primary-mid);
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | padding: 4px;
19 | cursor: pointer;
20 | stroke-width: 1.5;
21 | stroke: #FFF;
22 | }
23 |
24 | ::slotted(button[slot="decrease"]) {
25 | border-radius: 5px 0 0 5px;
26 | }
27 |
28 | ::slotted(button[slot="increase"]) {
29 | border-radius: 0 5px 5px 0;
30 | }
31 |
32 | ::slotted(button:disabled) {
33 | background: var(--input-disabled, #F2F3F4);
34 | cursor: not-allowed;
35 | }
36 |
37 | ::slotted(input) {
38 | width: var(--input-length);
39 | min-width: 30px;
40 | font-size: 14px;
41 | line-height: 20px;
42 | margin: 0;
43 | border: none;
44 | color: #555;
45 | outline: none;
46 | box-sizing: border-box;
47 | appearance: textfield;
48 | text-align: center;
49 | }
50 |
51 | :host([labelposition="left"]) {
52 | display: grid;
53 | grid-gap: 3px;
54 | height: max-content;
55 | }
56 |
57 | :host([labelposition="left"]) zoo-link {
58 | grid-column: 2;
59 | }
60 |
61 | :host([labelposition="left"]) zoo-label,
62 | :host([labelposition="left"]) div {
63 | display: flex;
64 | align-items: center;
65 | grid-row: 1;
66 | }
67 |
68 | :host([labelposition="left"]) zoo-info[role="status"] {
69 | grid-row: 2;
70 | grid-column: 2;
71 | }
72 |
73 | :host([labelposition="left"]) zoo-info[role="alert"] {
74 | grid-row: 3;
75 | grid-column: 2;
76 | }
77 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/quantity-control/quantity-control.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/quantity-control/quantity-control.js:
--------------------------------------------------------------------------------
1 | import { FormElement } from '../common/FormElement.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 | import { InfoMessage } from '../info/info.js';
4 | import { Label } from '../label/label.js';
5 |
6 | /**
7 | * @injectHTML
8 | */
9 | export class QuantityControl extends FormElement {
10 | constructor() {
11 | super();
12 | registerComponents(InfoMessage, Label);
13 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => {
14 | this.input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
15 | if (!this.input) return;
16 | this.registerElementForValidation(this.input);
17 | this.setInputWidth();
18 | });
19 |
20 | this.shadowRoot.querySelector('slot[name="increase"]')
21 | .addEventListener('slotchange', e => this.handleClick(true, e.target.assignedElements()[0]));
22 |
23 | this.shadowRoot.querySelector('slot[name="decrease"]')
24 | .addEventListener('slotchange', e => this.handleClick(false, e.target.assignedElements()[0]));
25 | }
26 |
27 | setInputWidth() {
28 | const length = this.input.value ? this.input.value.length || 1 : 1;
29 | this.style.setProperty('--input-length', length + 1 + 'ch');
30 | }
31 |
32 | handleClick(increment, el) {
33 | if (!el) return;
34 | el.addEventListener('click', () => {
35 | const step = this.input.step || 1;
36 | this.input.value = this.input.value || 0;
37 | this.input.value -= increment ? -step : step;
38 | this.input.dispatchEvent(new Event('change'));
39 | this.setInputWidth();
40 | });
41 | }
42 | }
43 |
44 | if (!window.customElements.get('zoo-quantity-control')) {
45 | window.customElements.define('zoo-quantity-control', QuantityControl);
46 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/quantity-control/quantityControl.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo quantity control', function() {
2 | it('should increase input value when plus is clicked', async() => {
3 | const ret = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
9 |
10 |
11 |
17 | `;
18 |
19 | await new Promise(r => setTimeout(r, 10));
20 |
21 | const input = document.querySelector('zoo-quantity-control');
22 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0];
23 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
24 | increaseBtn.click();
25 |
26 | return slottedInput.value;
27 | });
28 | expect(ret).toEqual('50');
29 | });
30 |
31 | it('should not increase input value when plus is clicked', async() => {
32 | const ret = await page.evaluate(async () => {
33 | document.body.innerHTML = `
34 |
35 |
38 |
39 |
40 |
46 | `;
47 |
48 | await new Promise(r => setTimeout(r, 10));
49 |
50 | const input = document.querySelector('zoo-quantity-control');
51 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
52 | slottedInput.value = 50;
53 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0];
54 | increaseBtn.click();
55 |
56 | return slottedInput.value;
57 | });
58 | expect(ret).toEqual('50');
59 | });
60 |
61 | it('should decrease input value when minus is clicked', async() => {
62 | const ret = await page.evaluate(async () => {
63 | document.body.innerHTML = `
64 |
65 |
68 |
69 |
70 |
76 | `;
77 |
78 | await new Promise(r => setTimeout(r, 10));
79 |
80 | const input = document.querySelector('zoo-quantity-control');
81 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
82 | const decreaseBtn = input.shadowRoot.querySelector('slot[name="decrease"]').assignedElements()[0];
83 | decreaseBtn.click();
84 |
85 | return slottedInput.value;
86 | });
87 | expect(ret).toEqual('-50');
88 | });
89 |
90 | it('should not decrease input value when minus is clicked', async() => {
91 | const ret = await page.evaluate(async () => {
92 | document.body.innerHTML = `
93 |
94 |
97 |
98 |
99 |
105 | `;
106 |
107 | await new Promise(r => setTimeout(r, 10));
108 |
109 | const input = document.querySelector('zoo-quantity-control');
110 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
111 | slottedInput.value = 50;
112 | const decreaseBtn = input.shadowRoot.querySelector('slot[name="decrease"]').assignedElements()[0];
113 | decreaseBtn.click();
114 | await new Promise(r => setTimeout(r, 10));
115 |
116 | return slottedInput.value;
117 | });
118 | expect(ret).toEqual('50');
119 | });
120 |
121 | it('should use default step of 1 when step is not defined', async() => {
122 | const ret = await page.evaluate(async () => {
123 | document.body.innerHTML = `
124 |
125 |
128 |
129 |
130 |
136 | `;
137 |
138 | await new Promise(r => setTimeout(r, 10));
139 |
140 | const input = document.querySelector('zoo-quantity-control');
141 | const increaseBtn = input.shadowRoot.querySelector('slot[name="increase"]').assignedElements()[0];
142 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
143 | increaseBtn.click();
144 |
145 | return slottedInput.value;
146 | });
147 | expect(ret).toEqual('1');
148 | });
149 |
150 | it('should set and then remove invalid attribute from host', async () => {
151 | const result = await page.evaluate(async () => {
152 | document.body.innerHTML = `
153 |
154 |
157 |
158 |
159 |
165 |
166 | `;
167 | const result = [];
168 | let quantityControl = document.querySelector('zoo-quantity-control');
169 | await new Promise(r => setTimeout(r, 10));
170 | const slottedInput = quantityControl.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
171 | slottedInput.value = '';
172 | slottedInput.dispatchEvent(new Event('input'));
173 | await new Promise(r => setTimeout(r, 10));
174 | result.push(quantityControl.hasAttribute('invalid'));
175 |
176 | slottedInput.value = 2;
177 | slottedInput.dispatchEvent(new Event('input'));
178 | await new Promise(r => setTimeout(r, 10));
179 | result.push(quantityControl.hasAttribute('invalid'));
180 |
181 | return result;
182 | });
183 | expect(result[0]).toBeTrue();
184 | expect(result[1]).toBeFalse();
185 | });
186 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/radio/radio-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo radio', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | `;
13 | return await axe.run('zoo-radio');
14 | });
15 | if (results.violations.length) {
16 | console.log('zoo-radio a11y violations ', results.violations);
17 | throw new Error('Accessibility issues found');
18 | }
19 | });
20 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/radio/radio.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | font-size: 14px;
5 | line-height: 20px;
6 |
7 | --box-shadow-color: #767676;
8 | --box-shadow-width: 1px;
9 | --box-shadow-color2: transparent;
10 | --box-shadow-width2: 1px;
11 | }
12 |
13 | fieldset {
14 | border: 0;
15 | padding: 0;
16 | margin: 0;
17 | position: relative;
18 | }
19 |
20 | .radio-group {
21 | display: flex;
22 | padding: 11px 0;
23 | }
24 |
25 | :host([invalid]) {
26 | color: var(--warning-mid);
27 | }
28 |
29 | ::slotted(input) {
30 | position: relative;
31 | min-width: 24px;
32 | height: 24px;
33 | border-radius: 50%;
34 | margin: 0 2px 0 0;
35 | padding: 4px;
36 | background-clip: content-box;
37 | appearance: none;
38 | outline: none;
39 | cursor: pointer;
40 | box-shadow: inset 0 0 0 var(--box-shadow-width) var(--box-shadow-color), inset 0 0 0 var(--box-shadow-width2) var(--box-shadow-color2);
41 | }
42 |
43 | :host([invalid]) ::slotted(input) {
44 | --box-shadow-color: var(--warning-mid);
45 | }
46 |
47 | ::slotted(input:focus) {
48 | --box-shadow-color: var(--primary-mid);
49 | --box-shadow-width: 2px;
50 | }
51 |
52 | ::slotted(input:checked) {
53 | background-color: var(--primary-mid);
54 |
55 | --box-shadow-color: var(--primary-mid);
56 | --box-shadow-width: 2px;
57 | --box-shadow-width2: 4px;
58 | --box-shadow-color2: white;
59 | }
60 |
61 |
62 | :host([invalid]) ::slotted(input:checked) {
63 | background-color: var(--warning-mid);
64 | }
65 |
66 | ::slotted(input:disabled) {
67 | cursor: not-allowed;
68 | background-color: #555;
69 |
70 | --box-shadow-width: 2px;
71 | --box-shadow-width2: 5px;
72 | --box-shadow-color: #555 !important;
73 | }
74 |
75 | ::slotted(label) {
76 | cursor: pointer;
77 | margin: 0 5px;
78 | align-self: center;
79 | }
80 |
81 | :host([labelposition="left"]) fieldset {
82 | display: grid;
83 | grid-gap: 3px;
84 | }
85 |
86 | :host([labelposition="left"]) .radio-group {
87 | grid-column: 2;
88 | }
89 |
90 | :host([labelposition="left"]) legend,
91 | :host([labelposition="left"]) .radio-group {
92 | grid-row: 1;
93 | display: flex;
94 | align-items: center;
95 | }
96 |
97 | :host([labelposition="left"]) legend {
98 | display: contents;
99 | }
100 |
101 | :host([labelposition="left"]) legend zoo-label {
102 | display: flex;
103 | align-items: center;
104 | }
105 |
106 | :host([labelposition="left"]) zoo-info[role="status"] {
107 | grid-row: 2;
108 | grid-column: 2;
109 | }
110 |
111 | :host([labelposition="left"]) zoo-info[role="alert"] {
112 | grid-row: 3;
113 | grid-column: 2;
114 | }
115 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/radio/radio.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/radio/radio.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { FormElement } from '../common/FormElement.js';
3 | import { InfoMessage } from '../info/info.js';
4 | import { Label } from '../label/label.js';
5 |
6 | /**
7 | * @injectHTML
8 | */
9 | export class Radio extends FormElement {
10 | constructor() {
11 | super();
12 | registerComponents(InfoMessage, Label);
13 | this.shadowRoot.querySelector('.radio-group slot').addEventListener('slotchange', e => {
14 | e.target.assignedElements().forEach(e => e.tagName === 'INPUT' && this.registerElementForValidation(e));
15 | });
16 | }
17 | }
18 | if (!window.customElements.get('zoo-radio')) {
19 | window.customElements.define('zoo-radio', Radio);
20 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/radio/radio.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo radio', function () {
2 | it('should pass attributes to input label component', async () => {
3 | const labelText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | `;
13 | let input = document.querySelector('zoo-radio');
14 | const label = input.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0];
15 | return label.innerHTML;
16 | });
17 | expect(labelText).toEqual('label');
18 | });
19 |
20 | it('should render input error', async () => {
21 | const errorDisplay = await page.evaluate(async () => {
22 | document.body.innerHTML = `
23 |
24 |
25 |
26 |
27 |
28 |
29 | error
30 |
31 | `;
32 | let input = document.querySelector('zoo-radio');
33 | await new Promise(r => setTimeout(r, 10));
34 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]');
35 | return window.getComputedStyle(error).display;
36 | });
37 | expect(errorDisplay).toEqual('flex');
38 | });
39 |
40 | it('should not render input error', async () => {
41 | const errorDisplay = await page.evaluate(() => {
42 | document.body.innerHTML = `
43 |
44 |
45 |
46 |
47 |
48 |
49 | error
50 |
51 | `;
52 | let input = document.querySelector('zoo-radio');
53 | const error = input.shadowRoot.querySelector('zoo-info[role="alert"]');
54 | return window.getComputedStyle(error).display;
55 | });
56 | expect(errorDisplay).toEqual('none');
57 | });
58 |
59 | it('should set and then remove invalid attribute from host', async () => {
60 | const result = await page.evaluate(async () => {
61 | document.body.innerHTML = `
62 |
63 |
64 |
65 |
66 |
67 |
68 | error
69 |
70 | `;
71 | const result = [];
72 | let input = document.querySelector('zoo-radio');
73 | await new Promise(r => setTimeout(r, 10));
74 | const slottedEls = input.shadowRoot.querySelector('.radio-group slot').assignedElements();
75 | const inputs = [];
76 | slottedEls.forEach(e => {
77 | if (e.tagName === 'INPUT') inputs.push(e);
78 | });
79 | inputs[0].checkValidity();
80 | await new Promise(r => setTimeout(r, 10));
81 | result.push(input.hasAttribute('invalid'));
82 |
83 | inputs[0].click();
84 | await new Promise(r => setTimeout(r, 10));
85 | result.push(input.hasAttribute('invalid'));
86 |
87 | return result;
88 | });
89 | expect(result[0]).toBeTrue();
90 | expect(result[1]).toBeFalse();
91 | });
92 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/searchable-select/searchable-select-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo searchable select', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 | Searchable multiple select legend
7 |
10 |
11 |
12 |
13 | `;
14 | return await axe.run('zoo-searchable-select');
15 | });
16 |
17 | if (results.violations.length) {
18 | console.log('zoo-searchable-select a11y violations ', results.violations);
19 | throw new Error('Accessibility issues found');
20 | }
21 | });
22 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/searchable-select/searchable-select.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | grid-gap: 3px;
4 | width: 100%;
5 | height: max-content;
6 | box-sizing: border-box;
7 | }
8 |
9 | .cross {
10 | display: none;
11 | position: absolute;
12 | top: 12px;
13 | right: 14px;
14 | cursor: pointer;
15 | border: 0;
16 | padding: 0;
17 | background: transparent;
18 | }
19 |
20 | .cross.hidden,
21 | :host([value-selected]) .cross.hidden {
22 | display: none;
23 | }
24 |
25 | :host([value-selected]) .cross {
26 | display: flex;
27 | }
28 |
29 | zoo-tooltip {
30 | display: none;
31 | }
32 |
33 | :host(:hover) zoo-tooltip,
34 | :host(:focus) zoo-tooltip {
35 | display: grid;
36 | }
37 |
38 | zoo-select {
39 | border-top: none;
40 | position: absolute;
41 | z-index: 2;
42 | top: 59%;
43 | display: none;
44 |
45 | --icons-display: none;
46 | }
47 |
48 | :host(:focus-within) zoo-select {
49 | display: grid;
50 | }
51 |
52 | slot[name="selectlabel"] {
53 | display: none;
54 | }
55 |
56 | :host(:focus-within) slot[name="selectlabel"] {
57 | display: block;
58 | }
59 |
60 | :host(:focus-within) ::slotted(select) {
61 | border-top-left-radius: 0;
62 | border-top-right-radius: 0;
63 | border: 2px solid #555;
64 | border-top: none !important;
65 | }
66 |
67 | :host([invalid]) ::slotted(select) {
68 | border: 2px solid var(--warning-mid);
69 | }
70 |
71 | zoo-preloader {
72 | display: none;
73 | }
74 |
75 | :host([loading]) zoo-preloader {
76 | display: flex;
77 | }
78 |
79 | ::slotted(*[slot="inputlabel"]),
80 | ::slotted(*[slot="selectlabel"]) {
81 | position: absolute;
82 | overflow: hidden;
83 | clip: rect(0 0 0 0);
84 | height: 1px;
85 | width: 1px;
86 | margin: -1px;
87 | padding: 0;
88 | border: 0;
89 | }
90 |
91 | zoo-link {
92 | align-items: flex-start;
93 | text-align: right;
94 | max-width: max-content;
95 | justify-self: flex-end;
96 | padding: 0;
97 | }
98 |
99 | zoo-label,
100 | zoo-link {
101 | grid-row: 1;
102 | }
103 |
104 | zoo-input {
105 | grid-gap: 0;
106 | grid-column: span 2;
107 | position: relative;
108 | }
109 |
110 | :host(:focus-within) ::slotted(input) {
111 | border: 2px solid #555;
112 | padding: 12px 14px;
113 | }
114 |
115 | :host([invalid]) ::slotted(input) {
116 | border: 2px solid var(--warning-mid);
117 | padding: 12px 14px;
118 | }
119 |
120 | :host([labelposition="left"]) zoo-link {
121 | grid-column: 2;
122 | }
123 |
124 | :host([labelposition="left"]) zoo-label,
125 | :host([labelposition="left"]) zoo-input {
126 | display: flex;
127 | align-items: center;
128 | grid-row: 2;
129 | }
130 |
131 | :host([labelposition="left"]) zoo-info[role="status"] {
132 | grid-row: 3;
133 | grid-column: 2;
134 | }
135 |
136 | :host([labelposition="left"]) zoo-info[role="alert"] {
137 | grid-row: 4;
138 | grid-column: 2;
139 | }
140 |
141 | zoo-info {
142 | grid-column: span 2;
143 | }
144 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/searchable-select/searchable-select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/searchable-select/searchable-select.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { CrossIcon } from '../../icon/cross-icon/cross-icon.js';
3 | import { FormElement } from '../common/FormElement.js';
4 | import { Input } from '../input/input.js';
5 | import { Select } from '../select/select.js';
6 | import { Preloader } from '../../misc/preloader/preloader.js';
7 | import { Tooltip } from '../../misc/tooltip/tooltip.js';
8 |
9 | /**
10 | * @injectHTML
11 | */
12 | export class SearchableSelect extends FormElement {
13 | constructor() {
14 | super();
15 | registerComponents(Input, Select, Preloader, CrossIcon, Tooltip);
16 | this.observer = new MutationObserver(mutationsList => {
17 | for (let mutation of mutationsList) {
18 | this.input.disabled = mutation.target.disabled;
19 | const crossIcon = this.shadowRoot.querySelector('.cross');
20 | if (mutation.target.disabled) {
21 | crossIcon.classList.add('hidden');
22 | } else {
23 | crossIcon.classList.remove('hidden');
24 | }
25 | }
26 | });
27 | this.shadowRoot.querySelector('.cross').addEventListener('click', () => {
28 | if (this.select.disabled) return;
29 | this.select.value = null;
30 | this.select.dispatchEvent(new Event('change', { bubbles: true, cancelable: false }));
31 | });
32 |
33 | this.shadowRoot.querySelector('slot[name="select"]').addEventListener('slotchange', e => {
34 | this.select = [...e.target.assignedElements()].find(el => el.tagName === 'SELECT');
35 | if (!this.select) return;
36 | this.registerElementForValidation(this.select);
37 | this.select.addEventListener('change', () => {
38 | this.handleOptionChange();
39 | this.valueChange();
40 | });
41 | this.select.size = 4;
42 | this.observer.observe(this.select, { attributes: true, attributeFilter: ['disabled'] });
43 | this.valueChange();
44 | this.slotChange();
45 | });
46 |
47 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => {
48 | this.input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
49 | if (!this.input) return;
50 | this.inputPlaceholderFallback = this.input.placeholder;
51 | this.input.addEventListener('input', () => this.handleSearchChange());
52 | this.slotChange();
53 | });
54 | }
55 |
56 | static get observedAttributes() {
57 | return ['closeicontitle'];
58 | }
59 |
60 | slotChange() {
61 | if (this.input && this.select) {
62 | this.handleOptionChange();
63 | this.input.disabled = this.select.disabled;
64 | }
65 | }
66 |
67 | valueChange() {
68 | this.select.value ? this.setAttribute('value-selected', '') : this.removeAttribute('value-selected');
69 | }
70 |
71 | attributeChangedCallback(attrName, oldVal, newVal) {
72 | this.shadowRoot.querySelector('zoo-cross-icon').setAttribute('title', newVal);
73 | }
74 |
75 | handleSearchChange() {
76 | const inputVal = this.input.value.toLowerCase();
77 | this.select.querySelectorAll('option').forEach(option => {
78 | if (option.text.toLowerCase().indexOf(inputVal) > -1) option.style.display = 'block';
79 | else option.style.display = 'none';
80 | });
81 | }
82 |
83 | handleOptionChange() {
84 | let inputValString = [...this.select.selectedOptions].map(o => o.text).join(', \n');
85 | this.input.placeholder = inputValString || this.inputPlaceholderFallback;
86 | if (inputValString) {
87 | this.input.value = null;
88 | this.tooltip = this.tooltip || this.createTooltip();
89 | this.tooltip.textContent = inputValString;
90 | this.shadowRoot.querySelector('zoo-input').appendChild(this.tooltip);
91 | } else if (this.tooltip) {
92 | this.tooltip.remove();
93 | }
94 | }
95 |
96 | createTooltip() {
97 | const tooltip = document.createElement('zoo-tooltip');
98 | tooltip.slot = 'additional';
99 | tooltip.setAttribute('position', 'right');
100 | return tooltip;
101 | }
102 |
103 | disconnectedCallback() {
104 | this.observer.disconnect();
105 | }
106 | }
107 | if (!window.customElements.get('zoo-searchable-select')) {
108 | window.customElements.define('zoo-searchable-select', SearchableSelect);
109 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/select/select-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo select', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
12 |
13 | `;
14 | return await axe.run('zoo-select');
15 | });
16 | if (results.violations.length) {
17 | console.log('zoo-select a11y violations ', results.violations);
18 | throw new Error('Accessibility issues found');
19 | }
20 | });
21 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/select/select.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | grid-gap: 3px;
4 | width: 100%;
5 | height: max-content;
6 | box-sizing: border-box;
7 |
8 | --icons-display: flex;
9 | }
10 |
11 | zoo-arrow-icon {
12 | position: absolute;
13 | right: 10px;
14 | display: var(--icons-display);
15 | pointer-events: none;
16 | }
17 |
18 | :host([invalid]) zoo-arrow-icon {
19 | --icon-color: var(--warning-mid);
20 | }
21 |
22 | :host([disabled]) zoo-arrow-icon {
23 | --icon-color: #666;
24 | }
25 |
26 | ::slotted(select) {
27 | appearance: none;
28 | width: 100%;
29 | font-size: 14px;
30 | line-height: 20px;
31 | padding: 13px 25px 13px 15px;
32 | border: 1px solid #767676;
33 | border-radius: 5px;
34 | color: #555;
35 | outline: none;
36 | box-sizing: border-box;
37 | }
38 |
39 | ::slotted(select:disabled) {
40 | border: 1px solid #E6E6E6;
41 | background: var(--input-disabled, #F2F3F4);
42 | color: #666;
43 | }
44 |
45 | ::slotted(select:disabled:hover) {
46 | cursor: not-allowed;
47 | }
48 |
49 | ::slotted(select:focus) {
50 | border: 2px solid #555;
51 | padding: 12px 24px 12px 14px;
52 | }
53 |
54 | :host([invalid]) ::slotted(select) {
55 | border: 2px solid var(--warning-mid);
56 | padding: 12px 24px 12px 14px;
57 | }
58 |
59 | .content {
60 | display: flex;
61 | justify-content: stretch;
62 | align-items: center;
63 | position: relative;
64 | grid-column: span 2;
65 | }
66 |
67 | zoo-info {
68 | grid-column: span 2;
69 | }
70 |
71 | :host([multiple]) zoo-arrow-icon {
72 | display: none;
73 | }
74 |
75 | zoo-link {
76 | text-align: right;
77 | max-width: max-content;
78 | justify-self: flex-end;
79 | padding: 0;
80 | }
81 |
82 | zoo-preloader {
83 | display: none;
84 | }
85 |
86 | :host([loading]) zoo-preloader {
87 | display: flex;
88 | }
89 |
90 | :host([labelposition="left"]) zoo-link {
91 | grid-column: 2;
92 | }
93 |
94 | :host([labelposition="left"]) zoo-label,
95 | :host([labelposition="left"]) .content {
96 | display: flex;
97 | align-items: center;
98 | grid-row: 2;
99 | }
100 |
101 | :host([labelposition="left"]) zoo-info[role="status"] {
102 | grid-row: 3;
103 | grid-column: 2;
104 | }
105 |
106 | :host([labelposition="left"]) zoo-info[role="alert"] {
107 | grid-row: 4;
108 | grid-column: 2;
109 | }
110 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/select/select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/select/select.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js';
3 | import { Link } from '../../misc/link/link.js';
4 | import { Preloader } from '../../misc/preloader/preloader.js';
5 | import { FormElement } from '../common/FormElement.js';
6 | import { InfoMessage } from '../info/info.js';
7 | import { Label } from '../label/label.js';
8 |
9 | /**
10 | * @injectHTML
11 | */
12 | export class Select extends FormElement {
13 | constructor() {
14 | super();
15 | registerComponents(InfoMessage, Label, Link, Preloader, ArrowDownIcon);
16 | this.observer = new MutationObserver(mutationsList => {
17 | for(let mutation of mutationsList) {
18 | const attr = mutation.attributeName;
19 | mutation.target[attr] ? this.setAttribute(attr, '') : this.removeAttribute(attr);
20 | }
21 | });
22 | this.shadowRoot.querySelector('slot[name="select"]').addEventListener('slotchange', e => {
23 | let select = [...e.target.assignedElements()].find(el => el.tagName === 'SELECT');
24 | if (!select) return;
25 | if (select.multiple) this.setAttribute('multiple', '');
26 | if (select.disabled) this.setAttribute('disabled', '');
27 | this.registerElementForValidation(select);
28 | this.observer.observe(select, { attributes: true, attributeFilter: ['disabled', 'multiple'] });
29 | });
30 | }
31 |
32 | disconnectedCallback() {
33 | this.observer.disconnect();
34 | }
35 | }
36 | if (!window.customElements.get('zoo-select')) {
37 | window.customElements.define('zoo-select', Select);
38 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/select/select.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo select', function () {
2 | it('should create select', async () => {
3 | const label = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
9 |
10 |
11 | `;
12 | let select = document.querySelector('zoo-select');
13 | return select.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML;
14 | });
15 | expect(label).toEqual('Multiselect');
16 | });
17 |
18 | it('should set disabled attribute on host when select is disabled', async () => {
19 | const disabled = await page.evaluate(async () => {
20 | document.body.innerHTML = `
21 |
22 |
25 |
26 |
27 | `;
28 | let select = document.querySelector('zoo-select');
29 | await new Promise(r => setTimeout(r, 10));
30 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].disabled = true;
31 | await new Promise(r => setTimeout(r, 10));
32 |
33 | return select.hasAttribute('disabled');
34 | });
35 | expect(disabled).toBeTrue();
36 | });
37 |
38 | it('should remove disabled attribute on host when select is not disabled', async () => {
39 | const disabled = await page.evaluate(async () => {
40 | document.body.innerHTML = `
41 |
42 |
45 |
46 |
47 | `;
48 | let select = document.querySelector('zoo-select');
49 | await new Promise(r => setTimeout(r, 10));
50 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].disabled = false;
51 | await new Promise(r => setTimeout(r, 10));
52 |
53 | return select.hasAttribute('disabled');
54 | });
55 | expect(disabled).toBeFalse();
56 | });
57 |
58 | it('should set multiple attribute on host when select is multiple', async () => {
59 | const multiple = await page.evaluate(async () => {
60 | document.body.innerHTML = `
61 |
62 |
65 |
66 |
67 | `;
68 | let select = document.querySelector('zoo-select');
69 | await new Promise(r => setTimeout(r, 10));
70 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].multiple = true;
71 | await new Promise(r => setTimeout(r, 10));
72 |
73 | return select.hasAttribute('multiple');
74 | });
75 | expect(multiple).toBeTrue();
76 | });
77 |
78 | it('should remove multiple attribute on host when select is not multiple', async () => {
79 | const multiple = await page.evaluate(async () => {
80 | document.body.innerHTML = `
81 |
82 |
85 |
86 |
87 | `;
88 | let select = document.querySelector('zoo-select');
89 | await new Promise(r => setTimeout(r, 10));
90 | select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0].multiple = false;
91 | await new Promise(r => setTimeout(r, 10));
92 |
93 | return select.hasAttribute('multiple');
94 | });
95 | expect(multiple).toBeFalse();
96 | });
97 |
98 | it('should set and then remove invalid attribute from host', async () => {
99 | const result = await page.evaluate(async () => {
100 | document.body.innerHTML = `
101 |
102 |
106 |
107 |
108 | `;
109 | const result = [];
110 | let select = document.querySelector('zoo-select');
111 | await new Promise(r => setTimeout(r, 10));
112 | const slottedSelect = select.shadowRoot.querySelector('slot[name="select"]').assignedElements()[0];
113 | slottedSelect.value = '';
114 | slottedSelect.dispatchEvent(new Event('input'));
115 | await new Promise(r => setTimeout(r, 10));
116 | result.push(select.hasAttribute('invalid'));
117 |
118 | slottedSelect.value = '2';
119 | slottedSelect.dispatchEvent(new Event('input'));
120 | await new Promise(r => setTimeout(r, 10));
121 | result.push(select.hasAttribute('invalid'));
122 |
123 | return result;
124 | });
125 | expect(result[0]).toBeTrue();
126 | expect(result[1]).toBeFalse();
127 | });
128 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/toggle-switch/toggle-switch-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo toggle switch', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 | `;
9 | return await axe.run('zoo-toggle-switch');
10 | });
11 | if (results.violations.length) {
12 | console.log('zoo-toggle-switch a11y violations ', results.violations);
13 | throw new Error('Accessibility issues found');
14 | }
15 | });
16 | });
--------------------------------------------------------------------------------
/src/zoo-modules/form/toggle-switch/toggle-switch.css:
--------------------------------------------------------------------------------
1 | :host {
2 | height: 100%;
3 | width: 100%;
4 | }
5 |
6 | div {
7 | display: flex;
8 | align-items: center;
9 | position: relative;
10 | height: 17px;
11 | width: 40px;
12 | background: #E6E6E6;
13 | border-radius: 10px;
14 | border-width: 0;
15 | margin: 5px 0;
16 | }
17 |
18 | ::slotted(input) {
19 | transition: transform 0.2s;
20 | transform: translateX(-30%);
21 | width: 60%;
22 | height: 24px;
23 | border: 1px solid #E6E6E6;
24 | border-radius: 50%;
25 | display: flex;
26 | appearance: none;
27 | outline: none;
28 | cursor: pointer;
29 | background: white;
30 | }
31 |
32 | ::slotted(input:checked) {
33 | transform: translateX(80%);
34 | background: var(--primary-mid);
35 | }
36 |
37 | ::slotted(input:focus) {
38 | border-width: 2px;
39 | border: 1px solid #767676;
40 | }
41 |
42 | ::slotted(input:disabled) {
43 | background: var(--input-disabled, #F2F3F4);
44 | cursor: not-allowed;
45 | }
46 |
47 | :host([labelposition="left"]) {
48 | display: grid;
49 | grid-gap: 3px;
50 | height: max-content;
51 | }
52 |
53 | :host([labelposition="left"]) zoo-link {
54 | grid-column: 2;
55 | }
56 |
57 | :host([labelposition="left"]) zoo-label,
58 | :host([labelposition="left"]) div {
59 | display: flex;
60 | align-items: center;
61 | grid-row: 1;
62 | }
63 |
64 | :host([labelposition="left"]) zoo-info[role="status"] {
65 | grid-row: 2;
66 | grid-column: 2;
67 | }
68 |
69 | :host([labelposition="left"]) zoo-info[role="alert"] {
70 | grid-row: 3;
71 | grid-column: 2;
72 | }
73 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/toggle-switch/toggle-switch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/zoo-modules/form/toggle-switch/toggle-switch.js:
--------------------------------------------------------------------------------
1 | import { registerComponents } from '../../common/register-components.js';
2 | import { FormElement } from '../common/FormElement.js';
3 | import { InfoMessage } from '../info/info.js';
4 | import { Label } from '../label/label.js';
5 |
6 | /**
7 | * @injectHTML
8 | */
9 | export class ToggleSwitch extends FormElement {
10 | constructor() {
11 | super();
12 | registerComponents(InfoMessage, Label);
13 | this.shadowRoot.querySelector('slot[name="input"]').addEventListener('slotchange', e => {
14 | const input = [...e.target.assignedElements()].find(el => el.tagName === 'INPUT');
15 | if (!input) return;
16 | this.registerElementForValidation(input);
17 |
18 | e.target.parentNode.addEventListener('click', (e) => {
19 | if (e.target.classList.contains('toggle-wrapper')) {
20 | input.click();
21 | }
22 | });
23 | });
24 | }
25 | }
26 |
27 | if (!window.customElements.get('zoo-toggle-switch')) {
28 | window.customElements.define('zoo-toggle-switch', ToggleSwitch);
29 | }
--------------------------------------------------------------------------------
/src/zoo-modules/form/toggle-switch/toggle-switch.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo toggle switch', function() {
2 | it('should create checkbox', async () => {
3 | const inputLabelText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 | `;
10 | let control = document.querySelector('zoo-toggle-switch');
11 |
12 | return control.shadowRoot.querySelector('slot[name="label"]').assignedElements()[0].innerHTML;
13 | });
14 | expect(inputLabelText).toEqual('Toggle switch');
15 | });
16 |
17 | it('should set and then remove invalid attribute from host', async () => {
18 | const result = await page.evaluate(async () => {
19 | document.body.innerHTML = `
20 |
21 |
22 |
23 |
24 | `;
25 | const result = [];
26 | let input = document.querySelector('zoo-toggle-switch');
27 | await new Promise(r => setTimeout(r, 10));
28 | const slottedInput = input.shadowRoot.querySelector('slot[name="input"]').assignedElements()[0];
29 | slottedInput.click();
30 | await new Promise(r => setTimeout(r, 10));
31 | result.push(input.hasAttribute('invalid'));
32 |
33 | slottedInput.click();
34 | await new Promise(r => setTimeout(r, 10));
35 | result.push(input.hasAttribute('invalid'));
36 |
37 | return result;
38 | });
39 | expect(result[0]).toBeFalse();
40 | expect(result[1]).toBeTrue();
41 | });
42 |
43 | it('should set and then remove invalid attribute from host on wrapper click', async () => {
44 | const result = await page.evaluate(async () => {
45 | document.body.innerHTML = `
46 |
47 |
48 |
49 |
50 | `;
51 | const result = [];
52 | const input = document.querySelector('zoo-toggle-switch');
53 | await new Promise(r => setTimeout(r, 10));
54 | const toggleWrapper = input.shadowRoot.querySelector('.toggle-wrapper');
55 | toggleWrapper.click();
56 | await new Promise(r => setTimeout(r, 10));
57 | result.push(input.hasAttribute('invalid'));
58 |
59 | toggleWrapper.click();
60 | await new Promise(r => setTimeout(r, 10));
61 | result.push(input.hasAttribute('invalid'));
62 |
63 | return result;
64 | });
65 | expect(result[0]).toBeFalse();
66 | expect(result[1]).toBeTrue();
67 | });
68 | });
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-header/grid-header.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | align-items: center;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | button {
9 | display: none;
10 | width: 24px;
11 | opacity: 0;
12 | transition: opacity 0.1s;
13 | margin-left: 5px;
14 | padding: 0;
15 | border: 0;
16 | cursor: pointer;
17 | border-radius: 5px;
18 | background: var(--input-disabled, #F2F3F4);
19 |
20 | --icon-color: black;
21 | }
22 |
23 | button:active {
24 | opacity: 0.5;
25 | transform: translateY(1px);
26 | }
27 |
28 | button:focus {
29 | opacity: 1;
30 | }
31 |
32 | :host(:hover) button {
33 | opacity: 1;
34 | }
35 |
36 | .swap {
37 | cursor: grab;
38 | }
39 |
40 | .swap:active {
41 | cursor: grabbing;
42 | }
43 |
44 | :host([sortable]) .sort,
45 | :host([reorderable]) .swap {
46 | display: flex;
47 | }
48 |
49 | :host([sortstate='asc']) .sort {
50 | transform: rotate(180deg);
51 | }
52 |
53 | :host([sortstate]) .sort {
54 | opacity: 1;
55 | background: #F2F3F4;
56 | }
57 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-header/grid-header.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-header/grid-header.js:
--------------------------------------------------------------------------------
1 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class GridHeader extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(ArrowDownIcon);
11 | this.addEventListener('dragend', () => this.removeAttribute('draggable'));
12 | this.shadowRoot.querySelector('.swap').addEventListener('mousedown', () => this.setAttribute('draggable', true));
13 | this.shadowRoot.querySelector('.sort').addEventListener('click', () => this.handleSortClick());
14 | }
15 |
16 | static get observedAttributes() {
17 | return ['sort-title', 'swap-title'];
18 | }
19 |
20 | handleSortClick() {
21 | if (!this.hasAttribute('sortstate')) {
22 | this.setAttribute('sortstate', 'desc');
23 | } else if (this.getAttribute('sortstate') == 'desc') {
24 | this.setAttribute('sortstate', 'asc');
25 | } else if (this.getAttribute('sortstate') == 'asc') {
26 | this.removeAttribute('sortstate');
27 | }
28 | const detail = this.hasAttribute('sortstate')
29 | ? { property: this.getAttribute('sortableproperty'), direction: this.getAttribute('sortstate') }
30 | : undefined;
31 | this.dispatchEvent(new CustomEvent('sortChange', {detail: detail, bubbles: true, composed: true }));
32 | }
33 |
34 | attributeChangedCallback(attrName, oldVal, newVal) {
35 | if (attrName === 'sort-title') {
36 | this.shadowRoot.querySelector('zoo-arrow-icon').setAttribute('title', newVal);
37 | } else if (attrName === 'swap-title') {
38 | this.shadowRoot.querySelector('.swap title').textContent = newVal;
39 | this.shadowRoot.querySelector('.swap').setAttribute('title', newVal);
40 | }
41 | }
42 | }
43 |
44 | if (!window.customElements.get('zoo-grid-header')) {
45 | window.customElements.define('zoo-grid-header', GridHeader);
46 | }
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-header/grid-header.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo grid header', function() {
2 | it('should show sort arrow when sortable attribute is present', async() => {
3 | const ret = await page.evaluate(async () => {
4 | document.body.innerHTML = 'Valid';
5 |
6 | const header = document.querySelector('zoo-grid-header');
7 | const arrow = header.shadowRoot.querySelector('.sort');
8 |
9 | const style = window.getComputedStyle(arrow);
10 | return style.display;
11 | });
12 | expect(ret).toEqual('flex');
13 | });
14 |
15 | it('should not show sort arrow when sortable attribute is absent', async() => {
16 | const ret = await page.evaluate(async () => {
17 | document.body.innerHTML = 'Valid';
18 |
19 | const header = document.querySelector('zoo-grid-header');
20 | const arrow = header.shadowRoot.querySelector('.sort');
21 |
22 | const style = window.getComputedStyle(arrow);
23 | return style.display;
24 | });
25 | expect(ret).toEqual('none');
26 | });
27 |
28 | it('should show reorder arrows when reorderable attribute is present', async() => {
29 | const ret = await page.evaluate(async () => {
30 | document.body.innerHTML = 'Valid';
31 |
32 | const header = document.querySelector('zoo-grid-header');
33 | const swap = header.shadowRoot.querySelector('.swap');
34 |
35 | const style = window.getComputedStyle(swap);
36 | return style.display;
37 | });
38 | expect(ret).toEqual('flex');
39 | });
40 |
41 | it('should not show reorder arrows when reorderable attribute is absent', async() => {
42 | const ret = await page.evaluate(async () => {
43 | document.body.innerHTML = 'Valid';
44 |
45 | const header = document.querySelector('zoo-grid-header');
46 | const swap = header.shadowRoot.querySelector('.swap');
47 |
48 | const style = window.getComputedStyle(swap);
49 | return style.display;
50 | });
51 | expect(ret).toEqual('none');
52 | });
53 |
54 | it('should add and then remove draggable attribute when on mousedown and draend events', async() => {
55 | const ret = await page.evaluate(async () => {
56 | document.body.innerHTML = 'Valid';
57 |
58 | const header = document.querySelector('zoo-grid-header');
59 | const swap = header.shadowRoot.querySelector('.swap');
60 |
61 | swap.dispatchEvent(new Event('mousedown'));
62 | await new Promise(r => setTimeout(r, 10));
63 | const draggableFirst = header.hasAttribute('draggable');
64 |
65 | header.dispatchEvent(new Event('dragend'));
66 | await new Promise(r => setTimeout(r, 10));
67 | const draggableSecond = header.hasAttribute('draggable');
68 |
69 | return [draggableFirst, draggableSecond];
70 | });
71 | expect(ret[0]).toBeTrue();
72 | expect(ret[1]).toBeFalse();
73 | });
74 |
75 | it('should set sortstate attribute on click on arrow', async() => {
76 | const sortState = await page.evaluate(async () => {
77 | document.body.innerHTML = 'Valid';
78 |
79 | const header = document.querySelector('zoo-grid-header');
80 | const arrow = header.shadowRoot.querySelector('.sort');
81 |
82 | const states = [];
83 |
84 | arrow.dispatchEvent(new Event('click'));
85 | await new Promise(r => setTimeout(r, 10));
86 | states.push(header.getAttribute('sortState'));
87 |
88 | arrow.dispatchEvent(new Event('click'));
89 | await new Promise(r => setTimeout(r, 10));
90 | states.push(header.getAttribute('sortState'));
91 |
92 | arrow.dispatchEvent(new Event('click'));
93 | await new Promise(r => setTimeout(r, 10));
94 | states.push(header.getAttribute('sortState'));
95 |
96 | return states;
97 | });
98 | expect(sortState).toEqual(['desc', 'asc', null]);
99 | });
100 | });
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-row/grid-row.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: layout;
3 | position: relative;
4 | flex-wrap: wrap;
5 |
6 | --grid-column-sizes: 1fr;
7 | }
8 |
9 | ::slotted(*[slot="row-details"]) {
10 | display: var(--zoo-grid-row-display, grid);
11 | grid-template-columns: var(--grid-details-column-sizes, repeat(var(--grid-column-num), minmax(50px, 1fr)));
12 | min-height: 50px;
13 | align-items: center;
14 | flex: 1 0 100%;
15 | }
16 |
17 | ::slotted(*[slot="row-content"]) {
18 | height: 0;
19 | overflow: hidden;
20 | background-color: white;
21 | padding: 0 10px;
22 | width: 100%;
23 | }
24 |
25 | ::slotted(*[slot="row-content"][expanded]) {
26 | height: var(--grid-row-content-height, auto);
27 | border-bottom: 2px solid rgb(0 0 0 / 20%);
28 | padding: 10px;
29 | margin: 4px;
30 | }
31 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-row/grid-row.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-row/grid-row.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class GridRow extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-grid-row')) {
11 | window.customElements.define('zoo-grid-row', GridRow);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid-row/grid-row.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo grid row', function() {
2 | it('should properly render row details', async () => {
3 | const result = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
Valid
8 |
2020-05-20
9 |
Grid Row Expand
10 |
5kg
11 |
12 |
13 | `;
14 |
15 | const row = document.querySelector('zoo-grid-row');
16 | const rowDetails = row.shadowRoot.querySelector('*[name="row-details"]').assignedElements();
17 |
18 | return rowDetails.map(singleRow => singleRow.textContent)[0];
19 | });
20 | expect(result).toContain('Valid');
21 | expect(result).toContain('2020-05-20');
22 | expect(result).toContain('Grid Row Expand');
23 | expect(result).toContain('5kg');
24 | });
25 |
26 | it('should properly render expandable content if expanded attribute exists', async () => {
27 | const result = await page.evaluate(async () => {
28 | document.body.innerHTML = `
29 |
30 |
31 |
Valid
32 |
2020-05-20
33 |
Grid Row Expand
34 |
5kg
35 |
36 |
39 |
40 | `;
41 |
42 | const row = document.querySelector('zoo-grid-row');
43 | const content = row.shadowRoot.querySelector('slot[name="row-content"]').assignedElements()[0];
44 |
45 |
46 | const style = window.getComputedStyle(content);
47 | return style.height;
48 | });
49 | expect(result).toEqual('150px');
50 | });
51 |
52 | it('should not render expandable content if expanded attribute does not exists', async () => {
53 | const result = await page.evaluate(async () => {
54 | document.body.innerHTML = `
55 |
56 |
57 |
Valid
58 |
2020-05-20
59 |
Grid Row Expand
60 |
5kg
61 |
62 |
65 |
66 | `;
67 |
68 | const row = document.querySelector('zoo-grid-row');
69 | const content = row.shadowRoot.querySelector('slot[name="row-content"]').assignedElements()[0];
70 |
71 |
72 | const style = window.getComputedStyle(content);
73 | return style.height;
74 | });
75 | expect(result).toEqual('0px');
76 | });
77 | });
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid/grid.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: layout;
3 | position: relative;
4 | display: block;
5 | }
6 |
7 | .loading-shade {
8 | display: none;
9 | position: absolute;
10 | left: 0;
11 | top: 0;
12 | right: 0;
13 | z-index: var(--zoo-grid-z-index, 9998);
14 | justify-content: center;
15 | height: 100%;
16 | background: rgb(0 0 0 / 15%);
17 | pointer-events: none;
18 | }
19 |
20 | :host([loading]) .loading-shade {
21 | display: flex;
22 | }
23 |
24 | .header-row {
25 | min-width: inherit;
26 | font-weight: 600;
27 | color: #555;
28 | box-sizing: border-box;
29 | z-index: 2;
30 | background: white;
31 | }
32 |
33 | .header-row,
34 | ::slotted(*[slot="row"]) {
35 | display: grid;
36 | grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-column-num), minmax(50px, 1fr)));
37 | padding: 5px 10px;
38 | border-bottom: 1px solid rgb(0 0 0 / 20%);
39 | min-height: 50px;
40 | font-size: 14px;
41 | line-height: 20px;
42 | }
43 |
44 | ::slotted(*[slot="row"]) {
45 | overflow: visible;
46 | align-items: center;
47 | box-sizing: border-box;
48 | }
49 |
50 | :host([resizable]) {
51 | --zoo-grid-row-display: flex;
52 | }
53 |
54 | :host([resizable]) .header-row,
55 | :host([resizable]) ::slotted(*[slot="row"]) {
56 | display: flex;
57 | }
58 |
59 | :host([resizable]) ::slotted(*[slot="headercell"]) {
60 | overflow: auto;
61 | resize: horizontal;
62 | height: inherit;
63 | }
64 |
65 | ::slotted(.drag-over) {
66 | box-shadow: inset 0 0 1px 1px rgb(0 0 0 / 40%);
67 | }
68 |
69 | :host([stickyheader]) .header-row {
70 | top: var(--grid-stickyheader-position-top, 0);
71 | position: sticky;
72 | }
73 |
74 | ::slotted(*[slot="row"]:nth-child(odd)) {
75 | background: #F2F3F4;
76 | }
77 |
78 | ::slotted(*[slot="row"]:hover),
79 | ::slotted(*[slot="row"]:focus) {
80 | background: var(--item-hovered, #E6E6E6);
81 | }
82 |
83 | ::slotted(*[slot="norecords"]) {
84 | color: var(--warning-dark);
85 | grid-column: span var(--grid-column-num);
86 | text-align: center;
87 | padding: 10px 0;
88 | }
89 |
90 | .footer {
91 | display: flex;
92 | position: sticky;
93 | bottom: 0;
94 | z-index: 2;
95 | width: 100%;
96 | background: #FFF;
97 | border-top: 1px solid #E6E6E6;
98 | box-sizing: border-box;
99 | padding: 10px;
100 | }
101 |
102 | slot[name="footer-content"] {
103 | display: flex;
104 | flex-grow: 1;
105 | }
106 |
107 | ::slotted(*[slot="footer-content"]) {
108 | justify-self: flex-start;
109 | }
110 |
111 | zoo-paginator {
112 | position: sticky;
113 | right: 10px;
114 | justify-content: flex-end;
115 | }
116 |
117 | slot[name="pagesizeselector"] {
118 | display: block;
119 | margin-right: 20px;
120 | }
121 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid/grid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/zoo-modules/grid/grid/grid.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '../../helpers/debounce.js';
2 | import { Paginator } from '../../misc/paginator/paginator.js';
3 | import { GridHeader } from '../grid-header/grid-header.js';
4 | import { GridRow } from '../grid-row/grid-row.js';
5 | import { registerComponents } from '../../common/register-components.js';
6 | /**
7 | * @injectHTML
8 | * https://github.com/whatwg/html/issues/6226
9 | * which leads to https://github.com/WICG/webcomponents/issues/59
10 | */
11 |
12 | export class ZooGrid extends HTMLElement {
13 | constructor() {
14 | super();
15 | registerComponents(Paginator, GridHeader, GridRow);
16 | const headerSlot = this.shadowRoot.querySelector('slot[name="headercell"]');
17 | headerSlot.addEventListener('slotchange', debounce(() => {
18 | const headers = headerSlot.assignedElements();
19 | this.style.setProperty('--grid-column-num', headers.length);
20 | headers.forEach((header, i) => {
21 | header.setAttribute('column', i+1);
22 | header.setAttribute('role', 'columnheader');
23 | });
24 | if (this.hasAttribute('reorderable')) {
25 | headers.forEach(header => this.handleDraggableHeader(header));
26 | }
27 | if (this.hasAttribute('resizable')) {
28 | this.handleResizableAttributeChange();
29 | }
30 | }));
31 | const rowSlot = this.shadowRoot.querySelector('slot[name="row"]');
32 | rowSlot.addEventListener('slotchange', debounce(() => {
33 | rowSlot.assignedElements().forEach(row => {
34 | row.setAttribute('role', 'row');
35 | if (row.tagName === 'ZOO-GRID-ROW') {
36 | [...row.querySelector('*[slot="row-details"]').children].forEach((child, i) => {
37 | child.setAttribute('column', i+1);
38 | child.setAttribute('role', 'cell');
39 | });
40 | } else {
41 | [...row.children].forEach((child, i) => {
42 | child.setAttribute('column', i+1);
43 | child.setAttribute('role', 'cell');
44 | });
45 | }
46 | });
47 | }));
48 |
49 | this.addEventListener('sortChange', e => {
50 | if (this.prevSortedHeader && !e.target.isEqualNode(this.prevSortedHeader)) {
51 | this.prevSortedHeader.removeAttribute('sortstate');
52 | }
53 | this.prevSortedHeader = e.target;
54 | });
55 | }
56 |
57 | static get observedAttributes() {
58 | return ['currentpage', 'maxpages', 'resizable', 'reorderable', 'prev-page-title', 'next-page-title'];
59 | }
60 |
61 | attributeChangedCallback(attrName, oldVal, newVal) {
62 | if (attrName == 'resizable') {
63 | this.handleResizableAttributeChange();
64 | } else if (attrName == 'reorderable' && this.hasAttribute('reorderable')) {
65 | this.shadowRoot.querySelector('slot[name="headercell"]').assignedElements().forEach(header => this.handleDraggableHeader(header));
66 | } else if (['maxpages', 'currentpage', 'prev-page-title', 'next-page-title'].includes(attrName)) {
67 | this.shadowRoot.querySelector('zoo-paginator').setAttribute(attrName, newVal);
68 | }
69 | }
70 |
71 | resizeCallback(entries) {
72 | entries.forEach(entry => {
73 | const columnNum = entry.target.getAttribute('column');
74 | const width = entry.contentRect.width;
75 | const columns = this.querySelectorAll(`[column="${columnNum}"]`);
76 | columns.forEach(columnEl => columnEl.style.width = `${width}px`);
77 | });
78 | }
79 |
80 | handleResizableAttributeChange() {
81 | if (this.hasAttribute('resizable')) {
82 | this.resizeObserver = this.resizeObserver || new ResizeObserver(debounce(this.resizeCallback.bind(this)));
83 | this.shadowRoot.querySelector('slot[name="headercell"]').assignedElements().forEach(header => this.resizeObserver.observe(header));
84 | }
85 | }
86 |
87 | handleDraggableHeader(header) {
88 | // avoid attaching multiple eventListeners to the same element
89 | if (header.hasAttribute('reorderable')) return;
90 | header.setAttribute('reorderable', '');
91 | header.setAttribute('ondragover', 'event.preventDefault()');
92 | header.setAttribute('ondrop', 'event.preventDefault()');
93 |
94 | header.addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', header.getAttribute('column')));
95 | // drag enter fires before dragleave, so stagger this function
96 | header.addEventListener('dragenter', debounce(() => {
97 | header.classList.add('drag-over');
98 | this.prevDraggedOverHeader = header;
99 | }));
100 | header.addEventListener('dragleave', () => header.classList.remove('drag-over'));
101 | header.addEventListener('drop', e => this.handleDrop(e));
102 | }
103 |
104 | handleDrop(e) {
105 | this.prevDraggedOverHeader && this.prevDraggedOverHeader.classList.remove('drag-over');
106 | const sourceColumn = e.dataTransfer.getData('text');
107 | const targetColumn = e.target.getAttribute('column');
108 | if (targetColumn == sourceColumn) return;
109 | // move columns
110 | this.querySelectorAll(`[column="${sourceColumn}"]`).forEach(source => {
111 | const target = source.parentElement.querySelector(`[column="${targetColumn}"]`);
112 | targetColumn > sourceColumn ? target.after(source) : target.before(source);
113 | });
114 | // reassign indexes for row cells
115 | this.shadowRoot.querySelector('slot[name="row"]').assignedElements()
116 | .forEach(row => {
117 | if (row.tagName === 'ZOO-GRID-ROW') {
118 | [...row.shadowRoot.querySelector('slot[name="row-details"]').assignedElements()[0].children]
119 | .forEach((child, i) => child.setAttribute('column', i+1));
120 | } else {
121 | [...row.children].forEach((child, i) => child.setAttribute('column', i+1));
122 | }
123 | });
124 | }
125 |
126 | disconnectedCallback() {
127 | if (this.resizeObserver) {
128 | this.resizeObserver.disconnect();
129 | }
130 | }
131 | }
132 |
133 | if (!window.customElements.get('zoo-grid')) {
134 | window.customElements.define('zoo-grid', ZooGrid);
135 | }
--------------------------------------------------------------------------------
/src/zoo-modules/helpers/debounce.js:
--------------------------------------------------------------------------------
1 | export function debounce(func, wait) {
2 | let timeout;
3 | return function() {
4 | const later = () => {
5 | timeout = null;
6 | func.apply(this, arguments);
7 | };
8 | clearTimeout(timeout);
9 | timeout = setTimeout(later, wait);
10 | if (!timeout) func.apply(this, arguments);
11 | };
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/helpers/test.setup.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import puppeteer from 'puppeteer';
3 | import jasmine from 'jasmine';
4 | import axe from 'axe-core';
5 | import pti from 'puppeteer-to-istanbul';
6 |
7 | beforeAll(async () => {
8 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
9 | global.browser = await puppeteer.launch({
10 | headless: "new",
11 | args: ['--no-sandbox', '--disable-setuid-sandbox']
12 | });
13 | global.page = await browser.newPage();
14 | global.colors = {
15 | primaryMid: '#368700',
16 | primaryLight: '#66B100',
17 | primaryDark: '#286400',
18 | primaryUltralight: '#EBF4E5',
19 | secondaryMid: '#FF6200',
20 | secondaryLight: '#F80',
21 | secondaryDark: '#CC4E00',
22 | infoUltralight: '#ECF5FA',
23 | infoMid: '#459FD0',
24 | warningUltralight: '#FDE8E9',
25 | warningMid: '#ED1C24'
26 | };
27 | page.on('console', msg => console.log('PAGE LOG:', msg.text()));
28 | await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]);
29 |
30 | await page.goto('http://localhost:5050');
31 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/jasmine.js'});
32 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/jasmine-html.js'});
33 | await page.addScriptTag( {'url' : 'https://cdn.jsdelivr.net/npm/jasmine-core@3.6.0/lib/jasmine-core/boot.js'});
34 | global.axeHandle = await page.evaluateHandle(`${axe.source}`);
35 | });
36 |
37 | afterAll(async () => {
38 | const [jsCoverage, cssCoverage] = await Promise.all([
39 | page.coverage.stopJSCoverage(),
40 | page.coverage.stopCSSCoverage(),
41 | ]);
42 | pti.write([...jsCoverage, ...cssCoverage], { storagePath: './.nyc_output' });
43 | await global.axeHandle ? global.axeHandle.dispose() : new Promise(res => res());
44 | await browser.close();
45 | });
--------------------------------------------------------------------------------
/src/zoo-modules/icon/arrow-icon/arrow-icon.css:
--------------------------------------------------------------------------------
1 | svg {
2 | display: flex;
3 | width: var(--icon-width, 24px);
4 | height: var(--icon-height, 24px);
5 | fill: var(--icon-color, var(--primary-mid));
6 | }
7 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/arrow-icon/arrow-icon.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/arrow-icon/arrow-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class ArrowDownIcon extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 |
9 | static get observedAttributes() {
10 | return ['title'];
11 | }
12 |
13 | attributeChangedCallback(attrName, oldVal, newVal) {
14 | this.shadowRoot.querySelector('svg title').textContent = newVal;
15 | }
16 | }
17 |
18 | if (!window.customElements.get('zoo-arrow-icon')) {
19 | window.customElements.define('zoo-arrow-icon', ArrowDownIcon);
20 | }
--------------------------------------------------------------------------------
/src/zoo-modules/icon/attention-icon/attention-icon.css:
--------------------------------------------------------------------------------
1 | svg {
2 | display: flex;
3 | padding-right: 5px;
4 | width: var(--icon-width, 18px);
5 | height: var(--icon-height, 18px);
6 | fill: var(--icon-color, var(--info-mid));
7 | }
8 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/attention-icon/attention-icon.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/attention-icon/attention-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class AttentionIcon extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-attention-icon')) {
11 | window.customElements.define('zoo-attention-icon', AttentionIcon);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/icon/cross-icon/cross-icon.css:
--------------------------------------------------------------------------------
1 | svg {
2 | display: flex;
3 | width: var(--icon-width, 18px);
4 | height: var(--icon-height, 18px);
5 | fill: var(--icon-color, black);
6 | }
7 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/cross-icon/cross-icon.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/cross-icon/cross-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class CrossIcon extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 |
9 | static get observedAttributes() {
10 | return ['title'];
11 | }
12 |
13 | attributeChangedCallback(attrName, oldVal, newVal) {
14 | this.shadowRoot.querySelector('svg title').textContent = newVal;
15 | }
16 | }
17 |
18 | if (!window.customElements.get('zoo-cross-icon')) {
19 | window.customElements.define('zoo-cross-icon', CrossIcon);
20 | }
--------------------------------------------------------------------------------
/src/zoo-modules/icon/paw-icon/paw-icon.css:
--------------------------------------------------------------------------------
1 | svg {
2 | display: flex;
3 | width: var(--icon-width, 44px);
4 | height: var(--icon-height, 44px);
5 | fill: var(--icon-color, white);
6 | }
7 |
8 | .fade-in {
9 | opacity: 0;
10 | animation: toes-fade-in-animation 2.2s infinite ease-in-out;
11 | }
12 |
13 | .fade-in-two {
14 | animation-delay: 0.4s;
15 | }
16 |
17 | .fade-in-three {
18 | animation-delay: 0.7s;
19 | }
20 |
21 | .fade-in-four {
22 | animation-delay: 1s;
23 | }
24 |
25 | @keyframes toes-fade-in-animation {
26 | 0% {
27 | opacity: 0;
28 | }
29 |
30 | 50% {
31 | opacity: 1;
32 | }
33 |
34 | 100% {
35 | opacity: 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/paw-icon/paw-icon.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/icon/paw-icon/paw-icon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class PawIcon extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 |
9 | static get observedAttributes() {
10 | return ['title'];
11 | }
12 |
13 | attributeChangedCallback(attrName, oldVal, newVal) {
14 | this.shadowRoot.querySelector('svg title').textContent = newVal;
15 | }
16 | }
17 |
18 | if (!window.customElements.get('zoo-paw-icon')) {
19 | window.customElements.define('zoo-paw-icon', PawIcon);
20 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button-group/button-group.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | opacity: 0;
4 | border: 1px solid #B8B8B8;
5 | border-radius: 5px;
6 | padding: 2px 0;
7 | justify-content: flex-end;
8 | width: fit-content;
9 | }
10 |
11 | ::slotted(zoo-button) {
12 | min-width: 50px;
13 | padding: 0 2px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button-group/button-group.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button-group/button-group.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '../../helpers/debounce.js';
2 | import { Button } from '../button/button.js';
3 | import { registerComponents } from '../../common/register-components.js';
4 | /**
5 | * @injectHTML
6 | */
7 | export class ButtonGroup extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(Button);
11 | }
12 |
13 | connectedCallback() {
14 | const buttonGroup = this.shadowRoot.querySelector('slot');
15 | this.registerSlotChangeListener(buttonGroup);
16 | this.registerButtonChangeHandler(buttonGroup);
17 | }
18 |
19 | registerSlotChangeListener(buttonGroup) {
20 | buttonGroup.addEventListener('slotchange', debounce(() => {
21 | buttonGroup.assignedElements().forEach((button, index) => {
22 | this.handleButtonInitialState(button, index);
23 | });
24 | this.style.opacity = '1';
25 | }));
26 | }
27 |
28 | registerButtonChangeHandler(buttonGroup) {
29 | this.addEventListener('click', (ev) => {
30 | const buttonIndex = buttonGroup.assignedElements().indexOf(ev.target.parentNode);
31 | if (buttonIndex > -1 && this.activeIndex !== buttonIndex) {
32 | this.deactivateButton(buttonGroup.assignedElements()[this.activeIndex]);
33 | this.activateButton(ev.target.parentNode, buttonIndex);
34 | }
35 | });
36 | }
37 |
38 | handleButtonInitialState(button, buttonIndex) {
39 | if (button.hasAttribute('data-active')) {
40 | this.activateButton(button, buttonIndex);
41 | } else {
42 | this.deactivateButton(button);
43 | }
44 | }
45 |
46 | activateButton(button, buttonIndex) {
47 | const activeType = this.getAttribute('active-type');
48 | button.setAttribute('type', activeType);
49 | this.activeIndex = buttonIndex;
50 | }
51 |
52 | deactivateButton(button) {
53 | const inactiveType = this.getAttribute('inactive-type');
54 | button.setAttribute('type', inactiveType);
55 | }
56 | }
57 |
58 | if (!window.customElements.get('zoo-button-group')) {
59 | window.customElements.define('zoo-button-group', ButtonGroup);
60 | }
61 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button-group/button-group.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo button group', () => {
2 | it('should properly set initially active button', async () => {
3 | const result = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | `;
15 | const firstBtn = document.querySelector('zoo-button-group').shadowRoot.querySelector('slot').assignedElements()[0];
16 | const secondBtn = document.querySelector('zoo-button-group').shadowRoot.querySelector('slot').assignedElements()[1];
17 | await new Promise(r => setTimeout(r, 10));
18 |
19 | return {
20 | firstButtonType: firstBtn.getAttribute('type'),
21 | secondButtonType: secondBtn.getAttribute('type')
22 | };
23 | });
24 | expect(result.firstButtonType).toEqual('transparent');
25 | expect(result.secondButtonType).toEqual('primary');
26 | });
27 |
28 | it('should properly change active button', async () => {
29 | const result = await page.evaluate(async () => {
30 | document.body.innerHTML = `
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | `;
41 | await new Promise(r => setTimeout(r));
42 | const firstBtn = document.querySelector('zoo-button-group');
43 | const secondBtn = document.querySelector('zoo-button-group');
44 | await new Promise(r => setTimeout(r));
45 |
46 | firstBtn.shadowRoot.querySelector('slot').assignedElements()[0].shadowRoot.querySelector('slot').assignedElements()[0].click();
47 |
48 | return {
49 | firstButtonType: firstBtn.shadowRoot.querySelector('slot').assignedElements()[0].getAttribute('type'),
50 | secondButtonType: secondBtn.shadowRoot.querySelector('slot').assignedElements()[1].getAttribute('type')
51 | };
52 | });
53 | expect(result.firstButtonType).toEqual('primary');
54 | expect(result.secondButtonType).toEqual('transparent');
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button/button-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo button', function () {
2 | it('should pass accessibility tests', async () => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 | `;
8 | return await axe.run('zoo-button');
9 | });
10 | if (results.violations.length) {
11 | console.log('zoo-button a11y violations', results.violations);
12 | throw new Error('Accessibility issues found');
13 | }
14 | });
15 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button/button.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | max-width: 330px;
4 | min-height: 36px;
5 | position: relative;
6 |
7 | --color-light: var(--primary-light);
8 | --color-mid: var(--primary-mid);
9 | --color-dark: var(--primary-dark);
10 | --text-normal: white;
11 | --text-active: white;
12 | --background: linear-gradient(to right, var(--color-mid), var(--color-light));
13 | --border: 0;
14 | }
15 |
16 | :host([type="secondary"]) {
17 | --color-light: var(--secondary-light);
18 | --color-mid: var(--secondary-mid);
19 | --color-dark: var(--secondary-dark);
20 | }
21 |
22 | :host([type="hollow"]) {
23 | --text-normal: var(--color-mid);
24 | --background: transparent;
25 | --border: 2px solid var(--color-mid);
26 | }
27 |
28 | :host([type="grayscale"]) {
29 | --background: transparent;
30 | --color-mid: transparent;
31 | --color-dark: transparent;
32 | --border: 0;
33 | --text-normal: #767676;
34 | --text-active: #9E9E9E;
35 | }
36 |
37 | :host([type="transparent"]) {
38 | --text-normal: var(--color-mid);
39 | --background: transparent;
40 | }
41 |
42 | ::slotted(button) {
43 | display: flex;
44 | align-items: center;
45 | justify-content: center;
46 | color: var(--text-normal);
47 | border: var(--border);
48 | border-radius: 5px;
49 | cursor: pointer;
50 | width: 100%;
51 | min-height: 100%;
52 | font-size: 14px;
53 | line-height: 20px;
54 | font-weight: bold;
55 | background: var(--background);
56 | }
57 |
58 | ::slotted(button:hover),
59 | ::slotted(button:focus) {
60 | background: var(--color-mid);
61 | color: var(--text-active);
62 | }
63 |
64 | ::slotted(button:active) {
65 | background: var(--color-dark);
66 | color: var(--text-active);
67 | }
68 |
69 | ::slotted(button:disabled) {
70 | cursor: not-allowed;
71 |
72 | --background: var(--input-disabled, #F2F3F4);
73 | --color-mid: var(--input-disabled, #F2F3F4);
74 | --color-dark: var(--input-disabled, #F2F3F4);
75 | --text-normal: #767676;
76 | --text-active: #767676;
77 | --border: 1px solid #E6E6E6;
78 | }
79 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button/button.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button/button.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Button extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-button')) {
10 | window.customElements.define('zoo-button', Button);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/button/button.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo button', () => {
2 | it('should create disabled button', async () => {
3 | const style = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 | `;
9 | const nestedButton = document.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0];
10 | const style = window.getComputedStyle(nestedButton);
11 | return {
12 | colorLight: style.getPropertyValue('--color-light').trim(),
13 | colorMid: style.getPropertyValue('--color-mid').trim(),
14 | colorDark: style.getPropertyValue('--color-dark').trim(),
15 | textNormal: style.getPropertyValue('--text-normal').trim(),
16 | textActive: style.getPropertyValue('--text-active').trim(),
17 | background: style.getPropertyValue('--background').trim(),
18 | border: style.getPropertyValue('--border').trim()
19 | };
20 | });
21 | expect(style.colorLight).toEqual(colors.primaryLight);
22 | expect(style.colorMid).toEqual('#F2F3F4');
23 | expect(style.colorDark).toEqual('#F2F3F4');
24 | expect(style.textNormal).toEqual('#767676');
25 | expect(style.textActive).toEqual('#767676');
26 | expect(style.background).toEqual('#F2F3F4');
27 | expect(style.border).toEqual('1px solid #E6E6E6');
28 | });
29 |
30 | it('should create normal button', async () => {
31 | const style = await page.evaluate(() => {
32 | document.body.innerHTML = `
33 |
34 |
35 |
36 | `;
37 | const nestedButton = document.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0];
38 | const style = window.getComputedStyle(nestedButton);
39 | return {
40 | colorLight: style.getPropertyValue('--color-light').trim(),
41 | colorMid: style.getPropertyValue('--color-mid').trim(),
42 | colorDark: style.getPropertyValue('--color-dark').trim(),
43 | textNormal: style.getPropertyValue('--text-normal').trim(),
44 | textActive: style.getPropertyValue('--text-active').trim(),
45 | background: style.getPropertyValue('--background').trim(),
46 | border: style.getPropertyValue('--border').trim()
47 | };
48 | });
49 | expect(style.colorLight).toEqual(colors.primaryLight);
50 | expect(style.colorMid).toEqual(colors.primaryMid);
51 | expect(style.colorDark).toEqual(colors.primaryDark);
52 | expect(style.textNormal).toEqual('white');
53 | expect(style.textActive).toEqual('white');
54 | expect(style.background).toEqual(`linear-gradient(to right, ${colors.primaryMid}, ${colors.primaryLight})`);
55 | expect(style.border).toEqual('0');
56 | });
57 |
58 | it('should create secondary button', async () => {
59 | const style = await page.evaluate(() => {
60 | document.body.innerHTML = `
61 |
62 |
63 |
64 | `;
65 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0];
66 | const style = window.getComputedStyle(nestedButton);
67 | return {
68 | colorLight: style.getPropertyValue('--color-light').trim(),
69 | colorMid: style.getPropertyValue('--color-mid').trim(),
70 | colorDark: style.getPropertyValue('--color-dark').trim(),
71 | textNormal: style.getPropertyValue('--text-normal').trim(),
72 | textActive: style.getPropertyValue('--text-active').trim(),
73 | background: style.getPropertyValue('--background').trim(),
74 | border: style.getPropertyValue('--border').trim()
75 | };
76 | });
77 | expect(style.colorLight).toEqual(colors.secondaryLight);
78 | expect(style.colorMid).toEqual(colors.secondaryMid);
79 | expect(style.colorDark).toEqual(colors.secondaryDark);
80 | expect(style.textNormal).toEqual('white');
81 | expect(style.textActive).toEqual('white');
82 | expect(style.background).toEqual(`linear-gradient(to right, ${colors.secondaryMid}, ${colors.secondaryLight})`);
83 | expect(style.border).toEqual('0');
84 | });
85 |
86 | it('should create hollow button', async () => {
87 | const style = await page.evaluate(() => {
88 | document.body.innerHTML = `
89 |
90 |
91 |
92 | `;
93 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0];
94 | const style = window.getComputedStyle(nestedButton);
95 | return {
96 | colorLight: style.getPropertyValue('--color-light').trim(),
97 | colorMid: style.getPropertyValue('--color-mid').trim(),
98 | colorDark: style.getPropertyValue('--color-dark').trim(),
99 | textNormal: style.getPropertyValue('--text-normal').trim(),
100 | textActive: style.getPropertyValue('--text-active').trim(),
101 | background: style.getPropertyValue('--background').trim(),
102 | border: style.getPropertyValue('--border').trim()
103 | };
104 | });
105 | expect(style.colorLight).toEqual(colors.primaryLight);
106 | expect(style.colorMid).toEqual(colors.primaryMid);
107 | expect(style.colorDark).toEqual(colors.primaryDark);
108 | expect(style.textNormal).toEqual(colors.primaryMid);
109 | expect(style.textActive).toEqual('white');
110 | expect(style.background).toEqual('transparent');
111 | expect(style.border).toEqual(`2px solid ${colors.primaryMid}`);
112 | });
113 |
114 | it('should create transparent button', async () => {
115 | const style = await page.evaluate(() => {
116 | document.body.innerHTML = `
117 |
118 |
119 |
120 | `;
121 | const nestedButton = document.body.querySelector('zoo-button').shadowRoot.querySelector('slot').assignedElements()[0];
122 | const style = window.getComputedStyle(nestedButton);
123 | return {
124 | colorLight: style.getPropertyValue('--color-light').trim(),
125 | colorMid: style.getPropertyValue('--color-mid').trim(),
126 | colorDark: style.getPropertyValue('--color-dark').trim(),
127 | textNormal: style.getPropertyValue('--text-normal').trim(),
128 | textActive: style.getPropertyValue('--text-active').trim(),
129 | background: style.getPropertyValue('--background').trim(),
130 | border: style.getPropertyValue('--border').trim()
131 | };
132 | });
133 | expect(style.colorLight).toEqual(colors.primaryLight);
134 | expect(style.colorMid).toEqual(colors.primaryMid);
135 | expect(style.colorDark).toEqual(colors.primaryDark);
136 | expect(style.textNormal).toEqual(colors.primaryMid);
137 | expect(style.textActive).toEqual('white');
138 | expect(style.background).toEqual('transparent');
139 | expect(style.border).toEqual('0');
140 | });
141 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.css:
--------------------------------------------------------------------------------
1 | :host {
2 | padding: 0 10px;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | :host([border-visible]) {
8 | margin: 8px 0;
9 | }
10 |
11 | details {
12 | padding: 10px;
13 | }
14 |
15 | :host([border-visible]) details {
16 | color: var(--primary-dark);
17 | border: 1px solid var(--primary-mid);
18 | border-radius: 3px;
19 | }
20 |
21 | details[open] {
22 | color: var(--primary-dark);
23 | border: 1px solid var(--primary-mid);
24 | border-radius: 3px;
25 | }
26 |
27 | summary {
28 | cursor: pointer;
29 | color: var(--primary-mid);
30 | font-weight: 700;
31 | }
32 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list-item/collapsable-list-item.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class CollapsableListItem extends HTMLElement {
5 | constructor() {
6 | super();
7 | this.details = this.shadowRoot.querySelector('details');
8 | this.details.addEventListener('toggle', e => {
9 | this.shadowRoot.host.dispatchEvent(new CustomEvent('toggle', {detail: e.target.open, composed: true}));
10 | });
11 | }
12 |
13 | close() {
14 | this.details.open = false;
15 | }
16 | }
17 | if (!window.customElements.get('zoo-collapsable-list-item')) {
18 | window.customElements.define('zoo-collapsable-list-item', CollapsableListItem);
19 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list/collapsable-list.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list/collapsable-list.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list/collapsable-list.js:
--------------------------------------------------------------------------------
1 | import { CollapsableListItem } from '../collapsable-list-item/collapsable-list-item.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class CollapsableList extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(CollapsableListItem);
11 | const slot = this.shadowRoot.querySelector('slot');
12 | slot.addEventListener('slotchange', () => {
13 | const items = slot.assignedElements();
14 |
15 | items.forEach(item => item.addEventListener('toggle', e => {
16 | if (!e.detail || this.hasAttribute('disable-autoclose')) return;
17 | items.forEach(i => !i.isEqualNode(item) && i.close());
18 | }));
19 |
20 |
21 | items.forEach((item) => {
22 | if (item.hasAttribute('opened-by-default')) {
23 | item.details.open = true;
24 | }
25 | });
26 | });
27 | }
28 | }
29 | if (!window.customElements.get('zoo-collapsable-list')) {
30 | window.customElements.define('zoo-collapsable-list', CollapsableList);
31 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/collapsable-list/collapsable-list.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo collapsable list', function () {
2 | it('should create default collapsable list', async () => {
3 | const ret = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 | header1
8 | content
9 |
10 |
11 | Header2
12 | content
13 |
14 |
15 | `;
16 | const listItem = document.querySelector('zoo-collapsable-list-item');
17 | const header = listItem.shadowRoot.querySelector('slot[name="header"]').assignedElements()[0].innerHTML;
18 | const content = listItem.shadowRoot.querySelector('slot[name="content"]').assignedElements()[0].innerHTML;
19 | return {
20 | header: header,
21 | content: content
22 | };
23 | });
24 | expect(ret.header).toEqual('header1');
25 | expect(ret.content).toEqual('content');
26 | });
27 |
28 | it('should close other items on toggle', async () => {
29 | const result = await page.evaluate(async () => {
30 | document.body.innerHTML = `
31 |
32 |
33 | header1
34 | content
35 |
36 |
37 | header2
38 | content
39 |
40 |
41 | `;
42 | const result = [];
43 | const listItems = [...document.querySelectorAll('zoo-collapsable-list-item')];
44 | await new Promise(r => setTimeout(r, 10));
45 | const details = listItems.map(item => item.shadowRoot.querySelector('details'));
46 |
47 | result.push(details.map(d => d.open));
48 | details[0].open = true;
49 | result.push(details.map(d => d.open));
50 | await new Promise(r => setTimeout(r, 10));
51 | details[1].open = true;
52 | await new Promise(r => setTimeout(r, 10));
53 | result.push(details.map(d => d.open));
54 | return result;
55 | });
56 | expect(result[0]).toEqual([false, false]);
57 | expect(result[1]).toEqual([true, false]);
58 | expect(result[2]).toEqual([false, true]);
59 | });
60 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/feedback/feeback.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo feedback', function () {
2 | it('should create default feedback', async () => {
3 | const retText = await page.evaluate(() => {
4 | let feedback = document.createElement('zoo-feedback');
5 | const span = document.createElement('span');
6 | span.innerHTML = 'example';
7 | feedback.appendChild(span);
8 | document.body.appendChild(feedback);
9 | const text = feedback.shadowRoot.querySelector('slot').assignedElements()[0].innerHTML;
10 | return text;
11 | });
12 | expect(retText).toEqual('example');
13 | });
14 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/feedback/feedback.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | align-items: center;
4 | box-sizing: border-box;
5 | font-size: 14px;
6 | line-height: 20px;
7 | border-left: 3px solid var(--info-mid);
8 | width: 100%;
9 | height: 100%;
10 | padding: 5px 0;
11 | background: var(--info-ultralight);
12 | border-radius: 5px;
13 |
14 | --svg-fill: var(--info-mid);
15 | }
16 |
17 | :host([type="error"]) {
18 | background: var(--warning-ultralight);
19 | border-color: var(--warning-mid);
20 |
21 | --svg-fill: var(--warning-mid);
22 | }
23 |
24 | :host([type="success"]) {
25 | background: var(--primary-ultralight);
26 | border-color: var(--primary-mid);
27 |
28 | --svg-fill: var(--primary-mid);
29 | }
30 |
31 | zoo-attention-icon {
32 | padding: 0 10px 0 15px;
33 |
34 | --icon-color: var(--svg-fill);
35 | --width: 30px;
36 | --height: 30px;
37 | }
38 |
39 | ::slotted(*) {
40 | display: flex;
41 | align-items: center;
42 | height: 100%;
43 | overflow: auto;
44 | box-sizing: border-box;
45 | padding: 5px 5px 5px 0;
46 | }
47 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/feedback/feedback.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/feedback/feedback.js:
--------------------------------------------------------------------------------
1 | import { AttentionIcon } from '../../icon/attention-icon/attention-icon.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class Feedback extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(AttentionIcon);
11 | }
12 | }
13 |
14 | if (!window.customElements.get('zoo-feedback')) {
15 | window.customElements.define('zoo-feedback', Feedback);
16 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/footer/footer-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo footer', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 | Github
8 |
9 |
10 | NPM
11 |
12 | `;
13 | return await axe.run('zoo-footer');
14 | });
15 | if (results.violations.length) {
16 | console.log('zoo-footer a11y violations ', results.violations);
17 | throw new Error('Accessibility issues found');
18 | }
19 | });
20 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/footer/footer.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: style;
3 | }
4 |
5 | nav {
6 | display: flex;
7 | justify-content: center;
8 | background: linear-gradient(to right, var(--primary-mid), var(--primary-light));
9 | padding: 10px 30px;
10 | }
11 |
12 | ::slotted(zoo-link) {
13 | width: max-content;
14 | }
15 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/footer/footer.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/footer/footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Footer extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-footer')) {
11 | window.customElements.define('zoo-footer', Footer);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/footer/footer.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo footer', function () {
2 | it('should create two links given array of two', async () => {
3 | const linksLength = await page.evaluate(() => {
4 | let footer = document.createElement('zoo-footer');
5 | const link1 = document.createElement('zoo-link');
6 | link1.href = 'https://google.com';
7 | link1.text = 'About us';
8 | footer.appendChild(link1);
9 | const link2 = document.createElement('zoo-link');
10 | link2.href = 'https://google.com';
11 | link2.text = 'About us';
12 | footer.appendChild(link2);
13 | document.body.appendChild(footer);
14 | const linkSlot = footer.shadowRoot.querySelector('slot');
15 | return linkSlot.assignedNodes().length;
16 | });
17 | expect(linksLength).toEqual(2);
18 | });
19 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/header/header-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo header', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | `;
19 | return await axe.run('zoo-header');
20 | });
21 | if (results.violations.length) {
22 | console.log('zoo-header a11y violations ', results.violations);
23 | throw new Error('Accessibility issues found');
24 | }
25 | });
26 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/header/header.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: style;
3 | }
4 |
5 | header {
6 | display: flex;
7 | align-items: center;
8 | padding: 0 25px;
9 | height: 70px;
10 | }
11 |
12 | ::slotted(img) {
13 | height: 46px;
14 | padding: 5px 25px 5px 0;
15 | cursor: pointer;
16 | }
17 |
18 | ::slotted(*[slot="headertext"]) {
19 | color: var(--primary-mid);
20 | }
21 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/header/header.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/header/header.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Header extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-header')) {
11 | window.customElements.define('zoo-header', Header);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/header/header.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo header', function () {
2 | it('should create header text', async () => {
3 | const headerText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
7 | header-text
8 |
9 | `;
10 | let header = document.querySelector('zoo-header');
11 |
12 | const text = header.shadowRoot.querySelector('slot[name="headertext"]').assignedNodes()[0];
13 | return text.innerHTML;
14 | });
15 | expect(headerText).toEqual('header-text');
16 | });
17 |
18 | it('should create image', async () => {
19 | const imageSrc = await page.evaluate(() => {
20 | document.body.innerHTML = `
21 |
22 |
23 | header-text
24 |
25 | `;
26 | let header = document.querySelector('zoo-header');
27 |
28 | const imageSlot = header.shadowRoot.querySelector('slot[name="img"]');
29 | return imageSlot.assignedNodes()[0].getAttribute('src');
30 | });
31 | expect(imageSrc).toEqual('logo.png');
32 | });
33 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/link/link-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo link', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 | Github
7 | `;
8 | // disable color-contrast check until design team changes it.
9 | return await axe.run('zoo-link', {rules: { 'color-contrast': { enabled: false } }});
10 | });
11 | if (results.violations.length) {
12 | console.log('zoo-link a11y violations ', results.violations);
13 | throw new Error('Accessibility issues found');
14 | }
15 | });
16 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/link/link.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: layout;
3 | display: flex;
4 | width: 100%;
5 | height: 100%;
6 | justify-content: center;
7 | align-items: center;
8 | position: relative;
9 | padding: 0 5px;
10 | font-size: 14px;
11 | line-height: 20px;
12 |
13 | --color-normal: var(--primary-mid);
14 | --color-active: var(--primary-dark);
15 | }
16 |
17 | :host([type="negative"]) {
18 | --color-normal: white;
19 | --color-active: var(--primary-dark);
20 | }
21 |
22 | :host([type="grey"]) {
23 | --color-normal: #767676;
24 | --color-active: var(--primary-dark);
25 | }
26 |
27 | :host([type="warning"]) {
28 | --color-normal: var(--warning-mid);
29 | --color-active: var(--warning-dark);
30 | }
31 |
32 | :host([size="large"]) {
33 | font-size: 18px;
34 | line-height: 22px;
35 | font-weight: bold;
36 | }
37 |
38 | ::slotted(a) {
39 | text-decoration: none;
40 | padding: 0 2px;
41 | color: var(--color-normal);
42 | width: 100%;
43 | }
44 |
45 | ::slotted(a:hover),
46 | ::slotted(a:focus),
47 | ::slotted(a:active) {
48 | color: var(--color-active);
49 | }
50 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/link/link.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/link/link.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Link extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-link')) {
10 | window.customElements.define('zoo-link', Link);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/link/link.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo link', function () {
2 | it('should create default empty link', async () => {
3 | const linkAttrs = await page.evaluate(() => {
4 | let link = document.createElement('zoo-link');
5 | document.body.appendChild(link);
6 | const linkBox = link.shadowRoot.querySelector('.link-box');
7 | const linkAttrs = {
8 | linkBox: linkBox
9 | };
10 | return linkAttrs;
11 | });
12 | expect(linkAttrs.linkBox).toBeNull();
13 | });
14 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/modal/modal-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo modal', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async() => {
4 | document.body.innerHTML = `
5 |
6 | Your basket contains licensed items
7 | some content
8 | `;
9 |
10 | document.querySelector('zoo-modal').openModal();
11 | // wait for animation to finish
12 | await new Promise(res => setTimeout(() => res(), 330));
13 | return await axe.run('zoo-modal');
14 | });
15 | if (results.violations.length) {
16 | console.log('zoo-modal a11y violations ', results.violations);
17 | throw new Error('Accessibility issues found');
18 | }
19 | });
20 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/modal/modal.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: none;
3 | contain: style;
4 | }
5 |
6 | .box {
7 | position: fixed;
8 | width: 100%;
9 | height: 100%;
10 | background: rgb(0 0 0 / var(--zoo-modal-opacity, 0.8));
11 | opacity: 0;
12 | transition: opacity 0.3s;
13 | z-index: var(--zoo-modal-z-index, 9999);
14 | left: 0;
15 | top: 0;
16 | display: flex;
17 | justify-content: center;
18 | align-items: center;
19 | will-change: opacity;
20 | transform: translateZ(0);
21 | }
22 |
23 | .dialog-content {
24 | padding: 0 20px 20px;
25 | box-sizing: border-box;
26 | background: white;
27 | overflow-y: auto;
28 | max-height: 95%;
29 | border-radius: 5px;
30 | animation-name: anim-show;
31 | animation-duration: 0.3s;
32 | animation-fill-mode: forwards;
33 | }
34 |
35 | @media only screen and (width <= 544px) {
36 | .dialog-content {
37 | padding: 25px;
38 | }
39 | }
40 |
41 | @media only screen and (width <= 375px) {
42 | .dialog-content {
43 | width: 100%;
44 | height: 100%;
45 | top: 0;
46 | left: 0;
47 | transform: none;
48 | }
49 | }
50 |
51 | .heading {
52 | display: flex;
53 | align-items: flex-start;
54 | }
55 |
56 | ::slotted(*[slot="header"]) {
57 | font-size: 24px;
58 | line-height: 29px;
59 | font-weight: bold;
60 | margin: 30px 0;
61 | }
62 |
63 | .close {
64 | cursor: pointer;
65 | background: transparent;
66 | border: 0;
67 | padding: 0;
68 | margin: 30px 0 30px auto;
69 |
70 | --icon-color: var(--primary-mid);
71 | }
72 |
73 | .show {
74 | opacity: 1;
75 | }
76 |
77 | .hide .dialog-content {
78 | animation-name: anim-hide;
79 | }
80 |
81 | @keyframes anim-show {
82 | 0% {
83 | opacity: 0;
84 | transform: scale3d(0.9, 0.9, 1);
85 | }
86 |
87 | 100% {
88 | opacity: 1;
89 | transform: scale3d(1, 1, 1);
90 | }
91 | }
92 |
93 | @keyframes anim-hide {
94 | 0% {
95 | opacity: 1;
96 | }
97 |
98 | 100% {
99 | opacity: 0;
100 | transform: scale3d(0.9, 0.9, 1);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/modal/modal.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/modal/modal.js:
--------------------------------------------------------------------------------
1 | import { CrossIcon } from '../../icon/cross-icon/cross-icon.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class Modal extends HTMLElement {
8 |
9 | constructor() {
10 | super();
11 | registerComponents(CrossIcon);
12 | this.shadowRoot.querySelector('.close').addEventListener('click', () => this.closeModal());
13 |
14 | const box = this.shadowRoot.querySelector('.box');
15 | this.closeModalOnClickHandler = (clickEvent) => {
16 | if (clickEvent.target == box) this.closeModal();
17 | };
18 | box.addEventListener('click', this.closeModalOnClickHandler);
19 |
20 | // https://github.com/HugoGiraudel/a11y-dialog/blob/main/a11y-dialog.js
21 | this.focusableSelectors = [
22 | 'a[href]:not([tabindex^="-"]):not([inert])',
23 | 'area[href]:not([tabindex^="-"]):not([inert])',
24 | 'input:not([disabled]):not([inert])',
25 | 'select:not([disabled]):not([inert])',
26 | 'textarea:not([disabled]):not([inert])',
27 | 'button:not([disabled]):not([inert])',
28 | 'iframe:not([tabindex^="-"]):not([inert])',
29 | 'audio[controls]:not([tabindex^="-"]):not([inert])',
30 | 'video[controls]:not([tabindex^="-"]):not([inert])',
31 | '[contenteditable]:not([tabindex^="-"]):not([inert])',
32 | '[tabindex]:not([tabindex^="-"]):not([inert])',
33 | ];
34 |
35 | this.keyUpEventHandler = (event) => {
36 | if (event.key === 'Escape') this.closeModal();
37 | if (event.key === 'Tab') this.maintainFocus(event.shiftKey);
38 | };
39 | }
40 |
41 | connectedCallback() {
42 | this.hidden = true;
43 | }
44 |
45 | static get observedAttributes() {
46 | return ['closelabel', 'button-closeable'];
47 | }
48 |
49 | attributeChangedCallback(attrName, oldVal, newVal) {
50 | if (attrName === 'button-closeable') {
51 | if (this.hasAttribute('button-closeable')) {
52 | const box = this.shadowRoot.querySelector('.box');
53 | box.removeEventListener('click', this.closeModalOnClickHandler);
54 | } else {
55 | this.shadowRoot.querySelector('.box').addEventListener('click', this.closeModalOnClickHandler);
56 | }
57 |
58 | } else if (attrName === 'closelabel') {
59 | this.shadowRoot.querySelector('zoo-cross-icon').setAttribute('title', newVal);
60 | }
61 | }
62 |
63 | openModal() {
64 | this.style.display = 'block';
65 | this.toggleModalClass();
66 | this.shadowRoot.querySelector('button').focus();
67 | document.addEventListener('keyup', this.keyUpEventHandler);
68 | }
69 |
70 | maintainFocus(shiftKey) {
71 | const button = this.shadowRoot.querySelector('button');
72 | const slottedFocusableElements = [...this.querySelectorAll(this.focusableSelectors.join(','))];
73 | const focusNotInSlotted = !slottedFocusableElements.some(el => el.isEqualNode(document.activeElement));
74 | const focusNotInShadowRoot = !button.isEqualNode(this.shadowRoot.activeElement);
75 | if (focusNotInSlotted && focusNotInShadowRoot) {
76 | if (shiftKey) {
77 | slottedFocusableElements[slottedFocusableElements.length - 1].focus();
78 | } else {
79 | button.focus();
80 | }
81 | }
82 | }
83 |
84 | closeModal() {
85 | if (this.timeoutVar) return;
86 | this.hidden = !this.hidden;
87 | this.toggleModalClass();
88 | this.timeoutVar = setTimeout(() => {
89 | this.style.display = 'none';
90 | this.dispatchEvent(new Event('modalClosed'));
91 | this.hidden = !this.hidden;
92 | this.timeoutVar = undefined;
93 | }, 300);
94 | }
95 |
96 | toggleModalClass() {
97 | const modalBox = this.shadowRoot.querySelector('.box');
98 | if (!this.hidden) {
99 | modalBox.classList.add('hide');
100 | modalBox.classList.remove('show');
101 | } else {
102 | modalBox.classList.add('show');
103 | modalBox.classList.remove('hide');
104 | }
105 | }
106 | }
107 |
108 | if (!window.customElements.get('zoo-modal')) {
109 | window.customElements.define('zoo-modal', Modal);
110 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/modal/modal.spec.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | describe('Zoo modal', function () {
3 | beforeEach(async () => await page.evaluate(() => jasmine.clock().install()));
4 | afterEach(async () => await page.evaluate(() => jasmine.clock().uninstall()));
5 | it('should create opened modal', async () => {
6 | const modalHeadingText = await page.evaluate(() => {
7 | document.body.innerHTML = `
8 |
9 | header-text
10 | content
11 |
12 | `;
13 | let modal = document.querySelector('zoo-modal');
14 | modal.style.display = 'block';
15 |
16 | return modal.shadowRoot.querySelector('slot[name="header"]').assignedNodes()[0].innerHTML;
17 | });
18 | expect(modalHeadingText).toEqual('header-text');
19 | });
20 |
21 | it('should create opened modal and close it', async () => {
22 | const modalDisplay = await page.evaluate(async () => {
23 | document.body.innerHTML = `
24 |
25 | header-text
26 | content
27 |
28 | `;
29 | let modal = document.querySelector('zoo-modal');
30 | modal.style.display = 'block';
31 |
32 | const closeButton = modal.shadowRoot.querySelector('.close');
33 | closeButton.click();
34 | jasmine.clock().tick(400);
35 | return modal.style.display;
36 | });
37 | expect(modalDisplay).toEqual('none');
38 | });
39 |
40 | it('should close opened modal when outer box is clicked', async () => {
41 | const modalDisplay = await page.evaluate(async () => {
42 | document.body.innerHTML = `
43 |
44 | header-text
45 | content
46 |
47 | `;
48 | let modal = document.querySelector('zoo-modal');
49 | modal.style.display = 'block';
50 |
51 | const box = modal.shadowRoot.querySelector('.box');
52 | box.dispatchEvent(new Event('click'));
53 | jasmine.clock().tick(400);
54 | return modal.style.display;
55 | });
56 | expect(modalDisplay).toEqual('none');
57 | });
58 |
59 | it('should not close opened modal when button-closeable attribute is set and outer box is clicked', async () => {
60 | const modalDisplay = await page.evaluate(async () => {
61 | document.body.innerHTML = `
62 |
63 | header-text
64 | content
65 |
66 | `;
67 | let modal = document.querySelector('zoo-modal');
68 | modal.style.display = 'block';
69 |
70 | const box = modal.shadowRoot.querySelector('.box');
71 | box.dispatchEvent(new Event('click'));
72 | jasmine.clock().tick(400);
73 | return modal.style.display;
74 | });
75 | expect(modalDisplay).toEqual('block');
76 | });
77 |
78 | it('should close opened modal when escape is clicked', async () => {
79 | const modalDisplay = await page.evaluate(async () => {
80 | document.body.innerHTML = `
81 |
82 | header-text
83 | content
84 |
85 | `;
86 | let modal = document.querySelector('zoo-modal');
87 | modal.openModal();
88 |
89 | const event = new KeyboardEvent('keyup', { key: 'Escape' });
90 | document.dispatchEvent(event);
91 | jasmine.clock().tick(400);
92 | return modal.style.display;
93 | });
94 | expect(modalDisplay).toEqual('none');
95 | });
96 |
97 | it('should dispatch only one modalClosed event despite multiple calls to closeModal', async () => {
98 | const calledTimes = await page.evaluate(async () => {
99 | document.body.innerHTML = `
100 |
101 | header-text
102 | content
103 |
104 | `;
105 | let modal = document.querySelector('zoo-modal');
106 | modal.openModal();
107 | jasmine.clock().tick(310);
108 |
109 | let called = 0;
110 | modal.addEventListener('modalClosed', () => called += 1);
111 |
112 | modal.closeModal();
113 | modal.closeModal();
114 | jasmine.clock().tick(620);
115 | return called;
116 | });
117 | expect(calledTimes).toEqual(1);
118 | });
119 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/navigation/navigation-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo navigation', function () {
2 | it('should pass accessibility tests', async () => {
3 | const results = await page.evaluate(async () => {
4 | document.body.innerHTML = `
5 |
6 | Can I use shadowdomv1?
7 | Can I use custom-elementsv1?
8 | Documentation
9 | `;
10 | return await axe.run('zoo-navigation');
11 | });
12 |
13 | if (results.violations.length) {
14 | console.log('zoo-navigation a11y violations ', results.violations);
15 | throw new Error('Accessibility issues found');
16 | }
17 | });
18 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/navigation/navigation.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | height: 56px;
4 | }
5 |
6 | nav {
7 | display: flex;
8 | width: 100%;
9 | padding: 0 20px;
10 | background: linear-gradient(to right, var(--primary-mid), var(--primary-light));
11 | }
12 |
13 | :host([direction="vertical"]) nav {
14 | flex-direction: column;
15 | height: auto;
16 | width: max-content;
17 | background: transparent;
18 | padding: 0;
19 | }
20 |
21 | ::slotted(*) {
22 | cursor: pointer;
23 | display: inline-flex;
24 | text-decoration: none;
25 | align-items: center;
26 | height: 100%;
27 | color: white;
28 | padding: 0 15px;
29 | font-weight: bold;
30 | font-size: 14px;
31 | line-height: 20px;
32 | }
33 |
34 | ::slotted(*:hover),
35 | ::slotted(*:focus) {
36 | background: rgb(255 255 255 / 20%);
37 | }
38 |
39 | :host([direction="vertical"]) ::slotted(*) {
40 | padding: 10px 5px;
41 | color: initial;
42 | box-sizing: border-box;
43 | }
44 |
45 | :host([direction="vertical"]) ::slotted(*:hover),
46 | :host([direction="vertical"]) ::slotted(*:focus) {
47 | background: rgb(0 0 0 / 7%);
48 | }
49 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/navigation/navigation.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/navigation/navigation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Navigation extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-navigation')) {
10 | window.customElements.define('zoo-navigation', Navigation);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/navigation/navigation.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo navigation', function () {
2 | it('should create nav element with slotted element', async () => {
3 | const slottedElement = await page.evaluate(() => {
4 | let nav = document.createElement('zoo-navigation');
5 | let element = document.createElement('span');
6 | element.innerHTML = 'slotted';
7 | nav.appendChild(element);
8 | document.body.appendChild(nav);
9 | const slot = nav.shadowRoot.querySelector('slot');
10 | return slot.assignedNodes()[0].innerHTML;
11 | });
12 | expect(slottedElement).toEqual('slotted');
13 | });
14 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/paginator/paginator.css:
--------------------------------------------------------------------------------
1 | :host {
2 | min-width: inherit;
3 | display: none;
4 | }
5 |
6 | .box {
7 | display: flex;
8 | align-items: center;
9 | font-size: 14px;
10 | width: max-content;
11 | position: var(--paginator-position, 'initial');
12 | right: var(--right, 'unset');
13 | }
14 |
15 | :host([currentpage]) {
16 | display: flex;
17 | }
18 |
19 | nav {
20 | display: flex;
21 | align-items: center;
22 | border: 1px solid #E6E6E6;
23 | border-radius: 5px;
24 | padding: 15px;
25 | }
26 |
27 | button {
28 | display: flex;
29 | cursor: pointer;
30 | opacity: 1;
31 | transition: opacity 0.1s;
32 | background: transparent;
33 | border: 0;
34 | padding: 0;
35 | font-size: inherit;
36 | border-radius: 5px;
37 | margin: 0 2px;
38 | }
39 |
40 | button:active {
41 | opacity: 0.5;
42 | }
43 |
44 | button:hover,
45 | button:focus {
46 | background: #F2F3F4;
47 | }
48 |
49 | button.hidden {
50 | display: none;
51 | }
52 |
53 | .page-element {
54 | padding: 4px 8px;
55 | }
56 |
57 | .page-element.active {
58 | background: var(--primary-ultralight);
59 | color: var(--primary-dark);
60 | }
61 |
62 | zoo-arrow-icon {
63 | pointer-events: none;
64 | }
65 |
66 | .prev zoo-arrow-icon {
67 | transform: rotate(90deg);
68 | }
69 |
70 | .next zoo-arrow-icon {
71 | transform: rotate(-90deg);
72 | }
73 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/paginator/paginator.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | ...
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/paginator/paginator.js:
--------------------------------------------------------------------------------
1 | import { ArrowDownIcon } from '../../icon/arrow-icon/arrow-icon.js';
2 | import { registerComponents } from '../../common/register-components.js';
3 |
4 | /**
5 | * @injectHTML
6 | */
7 | export class Paginator extends HTMLElement {
8 | constructor() {
9 | super();
10 | registerComponents(ArrowDownIcon);
11 | this.prev = this.shadowRoot.querySelector('.prev');
12 | this.next = this.shadowRoot.querySelector('.next');
13 | this.dots = this.shadowRoot.querySelector('#dots').content;
14 | this.pages = this.shadowRoot.querySelector('#pages').content;
15 |
16 | this.shadowRoot.addEventListener('click', e => {
17 | const pageNumber = e.target.getAttribute('page');
18 | if (pageNumber) {
19 | this.goToPage(pageNumber);
20 | } else if (e.target.classList.contains('prev')) {
21 | this.goToPage(+this.getAttribute('currentpage')-1);
22 | } else if (e.target.classList.contains('next')) {
23 | this.goToPage(+this.getAttribute('currentpage')+1);
24 | }
25 | });
26 | }
27 |
28 | goToPage(pageNumber) {
29 | this.setAttribute('currentpage', pageNumber);
30 | this.dispatchEvent(new CustomEvent('pageChange', {
31 | detail: {pageNumber: pageNumber}, bubbles: true, composed: true
32 | }));
33 | }
34 |
35 | static get observedAttributes() {
36 | return ['maxpages', 'currentpage', 'prev-page-title', 'next-page-title'];
37 | }
38 | handleHideShowArrows() {
39 | if (this.getAttribute('currentpage') == 1) {
40 | this.prev.classList.add('hidden');
41 | } else {
42 | this.prev.classList.remove('hidden');
43 | }
44 | if (+this.getAttribute('currentpage') >= +this.getAttribute('maxpages')) {
45 | this.next.classList.add('hidden');
46 | } else {
47 | this.next.classList.remove('hidden');
48 | }
49 | }
50 | rerenderPageButtons() {
51 | this.shadowRoot.querySelectorAll('*[class^="page-element"]').forEach(n => n.remove());
52 | const pageNum = +this.getAttribute('currentpage');
53 | const maxPages = this.getAttribute('maxpages');
54 | for (let page=maxPages;page>0;page--) {
55 | //first, previous, current, next or last page
56 | if (page == 1 || page == pageNum - 1 || page == pageNum || page == pageNum + 1 || page == maxPages) {
57 | const pageNode = this.pages.cloneNode(true).firstElementChild;
58 | pageNode.setAttribute('page', page);
59 | pageNode.setAttribute('title', page);
60 | if (pageNum == page) {
61 | pageNode.classList.add('active');
62 | }
63 | pageNode.textContent = page;
64 | this.prev.after(pageNode);
65 | } else if (page == pageNum-2 || pageNum+2 == page) {
66 | this.prev.after(this.dots.cloneNode(true));
67 | }
68 | }
69 | }
70 | attributeChangedCallback(attrName, oldVal, newVal) {
71 | if (attrName == 'currentpage' || attrName == 'maxpages') {
72 | this.handleHideShowArrows();
73 | this.rerenderPageButtons();
74 | } else if (attrName === 'prev-page-title') {
75 | this.shadowRoot.querySelector('.prev zoo-arrow-icon').setAttribute('title', newVal);
76 | } else if (attrName === 'next-page-title') {
77 | this.shadowRoot.querySelector('.next zoo-arrow-icon').setAttribute('title', newVal);
78 | }
79 | }
80 | }
81 | if (!window.customElements.get('zoo-paginator')) {
82 | window.customElements.define('zoo-paginator', Paginator);
83 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/paginator/paginator.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo paginator', function () {
2 | it('should create default paginator', async () => {
3 | const buttonsLength = await page.evaluate(() => {
4 | document.body.innerHTML = ``;
5 | const paginator = document.querySelector('zoo-paginator');
6 | const buttons = paginator.shadowRoot.querySelectorAll('button');
7 | return buttons.length;
8 | });
9 | expect(buttonsLength).toEqual(5);
10 | });
11 |
12 | it('should go to previous page', async () => {
13 | const currentpage = await page.evaluate(async () => {
14 | document.body.innerHTML = ``;
15 | const paginator = document.querySelector('zoo-paginator');
16 | const prev = paginator.shadowRoot.querySelector('.prev');
17 | prev.click();
18 | await new Promise(r => setTimeout(r, 10));
19 |
20 | return paginator.getAttribute('currentpage');
21 | });
22 | expect(currentpage).toEqual('1');
23 | });
24 |
25 | it('should go to next page', async () => {
26 | const currentpage = await page.evaluate(async () => {
27 | document.body.innerHTML = ``;
28 | const paginator = document.querySelector('zoo-paginator');
29 | const next = paginator.shadowRoot.querySelector('.next');
30 | next.click();
31 | await new Promise(r => setTimeout(r, 10));
32 |
33 | return paginator.getAttribute('currentpage');
34 | });
35 | expect(currentpage).toEqual('3');
36 | });
37 |
38 | it('should go to first page', async () => {
39 | const currentpage = await page.evaluate(async () => {
40 | document.body.innerHTML = ``;
41 | const paginator = document.querySelector('zoo-paginator');
42 | const button = paginator.shadowRoot.querySelector('button[page="1"]');
43 | button.click();
44 | await new Promise(r => setTimeout(r, 10));
45 |
46 | return paginator.getAttribute('currentpage');
47 | });
48 | expect(currentpage).toEqual('1');
49 | });
50 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/preloader/preloader.css:
--------------------------------------------------------------------------------
1 | :host {
2 | position: absolute;
3 | width: 100%;
4 | height: 100%;
5 | top: 0;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | pointer-events: none;
10 | z-index: 2;
11 | }
12 |
13 | .bounce {
14 | text-align: center;
15 | }
16 |
17 | .bounce > div {
18 | width: 10px;
19 | height: 10px;
20 | background-color: #333;
21 | border-radius: 100%;
22 | display: inline-block;
23 | animation: sk-bouncedelay 1.4s infinite ease-in-out both;
24 | }
25 |
26 | .bounce .bounce1 {
27 | animation-delay: -0.32s;
28 | }
29 |
30 | .bounce .bounce2 {
31 | animation-delay: -0.16s;
32 | }
33 |
34 | @keyframes sk-bouncedelay {
35 | 0%,
36 | 80%,
37 | 100% {
38 | transform: scale(0);
39 | }
40 |
41 | 40% {
42 | transform: scale(1);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/preloader/preloader.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/preloader/preloader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Preloader extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 | if (!window.customElements.get('zoo-preloader')) {
10 | window.customElements.define('zoo-preloader', Preloader);
11 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/spinner/spinner.css:
--------------------------------------------------------------------------------
1 | :host {
2 | contain: layout;
3 | }
4 |
5 | svg {
6 | position: absolute;
7 | inset: calc(50% - 60px) 0 0 calc(50% - 60px);
8 | height: 120px;
9 | width: 120px;
10 | transform-origin: center center;
11 | animation: rotate 2s linear infinite;
12 | z-index: var(--zoo-spinner-z-index, 10002);
13 | }
14 |
15 | svg circle {
16 | animation: dash 1.5s ease-in-out infinite;
17 | stroke: var(--primary-mid);
18 | stroke-dasharray: 1, 200;
19 | stroke-dashoffset: 0;
20 | stroke-linecap: round;
21 | }
22 |
23 | @keyframes rotate {
24 | 100% {
25 | transform: rotate(360deg);
26 | }
27 | }
28 |
29 | @keyframes dash {
30 | 0% {
31 | stroke-dasharray: 1, 200;
32 | stroke-dashoffset: 0;
33 | }
34 |
35 | 50% {
36 | stroke-dasharray: 89, 200;
37 | stroke-dashoffset: -35px;
38 | }
39 |
40 | 100% {
41 | stroke-dasharray: 89, 200;
42 | stroke-dashoffset: -124px;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/spinner/spinner.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/spinner/spinner.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Spinner extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-spinner')) {
11 | window.customElements.define('zoo-spinner', Spinner);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tag/tag.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | box-sizing: border-box;
4 | padding: 0 10px;
5 | align-items: center;
6 | width: max-content;
7 | color: var(--color);
8 | border-color: var(--color);
9 | max-width: var(--zoo-tag-max-width, 100px);
10 | border-radius: 3px;
11 | }
12 |
13 | :host(:hover) {
14 | background: var(--primary-ultralight);
15 | color: var(--primary-dark);
16 | }
17 |
18 | :host([type="info"]) {
19 | min-height: 20px;
20 | border-radius: 10px;
21 | border: 1px solid;
22 | }
23 |
24 | :host([type="cloud"]) {
25 | min-height: 46px;
26 | border-radius: 3px;
27 | border: 1px solid lightgray;
28 | }
29 |
30 | :host([type="tag"]) {
31 | border: 1px solid lightgray;
32 | }
33 |
34 | ::slotted(*[slot="content"]) {
35 | font-size: 12px;
36 | overflow-x: hidden;
37 | text-overflow: ellipsis;
38 | white-space: nowrap;
39 | }
40 |
41 | ::slotted(*[slot="pre"]) {
42 | margin-right: 5px;
43 | }
44 |
45 | ::slotted(*[slot="post"]) {
46 | margin-left: 5px;
47 | }
48 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tag/tag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tag/tag.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Tag extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-tag')) {
11 | window.customElements.define('zoo-tag', Tag);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/toast/toast-a11y.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo toast', function() {
2 | it('should pass accessibility tests', async() => {
3 | const results = await page.evaluate(async() => {
4 | document.body.innerHTML = `
5 | Search for more than 8.000 products.
6 | `;
7 | document.querySelector('zoo-toast').show();
8 | // wait for animation to finish
9 | await new Promise(res => setTimeout(() => res(), 330));
10 | return await axe.run('zoo-toast');
11 | });
12 | if (results.violations.length) {
13 | console.log('zoo-toast a11y violations ', results.violations);
14 | throw new Error('Accessibility issues found');
15 | }
16 | });
17 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/toast/toast.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: none;
3 | top: 20px;
4 | right: 20px;
5 | position: fixed;
6 | z-index: var(--zoo-toast-z-index, 10001);
7 | contain: layout;
8 |
9 | --color-ultralight: var(--info-ultralight);
10 | --color-mid: var(--info-mid);
11 | --svg-padding: 0;
12 | }
13 |
14 | :host([type="error"]) {
15 | --color-ultralight: var(--warning-ultralight);
16 | --color-mid: var(--warning-mid);
17 | }
18 |
19 | :host([type="success"]) {
20 | --color-ultralight: var(--primary-ultralight);
21 | --color-mid: var(--primary-mid);
22 | }
23 |
24 | div {
25 | max-width: 330px;
26 | min-height: 50px;
27 | box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
28 | border-left: 3px solid var(--color-mid);
29 | display: flex;
30 | align-items: center;
31 | word-break: break-word;
32 | font-size: 14px;
33 | line-height: 20px;
34 | padding: 15px;
35 | transition: transform 0.3s, opacity 0.4s;
36 | opacity: 0;
37 | transform: translate3d(100%, 0, 0);
38 | background: var(--color-ultralight);
39 | border-radius: 5px;
40 | }
41 |
42 | svg {
43 | padding-right: 10px;
44 | min-width: 48px;
45 | fill: var(--color-mid);
46 | }
47 |
48 | .show {
49 | opacity: 1;
50 | transform: translate3d(0, 0, 0);
51 | }
52 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/toast/toast.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/toast/toast.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Toast extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 |
9 | connectedCallback() {
10 | this.hidden = true;
11 | this.timeout = this.getAttribute('timeout') || 3;
12 | this.setAttribute('role', 'alert');
13 | }
14 |
15 | show() {
16 | if (!this.hidden) return;
17 | this.style.display = 'block';
18 | this.timeoutVar = setTimeout(() => {
19 | this.hidden = !this.hidden;
20 | this.toggleToastClass();
21 | this.timeoutVar = setTimeout(() => {
22 | if (this && !this.hidden) {
23 | this.hidden = !this.hidden;
24 | this.timeoutVar = setTimeout(() => {this.style.display = 'none';}, 300);
25 | this.toggleToastClass();
26 | }
27 | }, this.timeout * 1000);
28 | }, 30);
29 | }
30 | close() {
31 | if (this.hidden) return;
32 | clearTimeout(this.timeoutVar);
33 | setTimeout(() => {
34 | if (this && !this.hidden) {
35 | this.hidden = !this.hidden;
36 | setTimeout(() => {this.style.display = 'none';}, 300);
37 | this.toggleToastClass();
38 | }
39 | }, 30);
40 | }
41 |
42 | toggleToastClass() {
43 | const toast = this.shadowRoot.querySelector('div');
44 | toast.classList.toggle('show');
45 | }
46 | }
47 |
48 | if (!window.customElements.get('zoo-toast')) {
49 | window.customElements.define('zoo-toast', Toast);
50 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/toast/toast.spec.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | describe('Zoo toast', function () {
3 | beforeEach(async () => await page.evaluate(() => jasmine.clock().install()));
4 | afterEach(async () => await page.evaluate(() => jasmine.clock().uninstall()));
5 | it('should create default toast', async () => {
6 | const toasttext = await page.evaluate(() => {
7 | document.body.innerHTML = `
8 |
9 | some-text
10 |
11 | `;
12 | const toast = document.querySelector('zoo-toast');
13 | const toastBox = toast.shadowRoot.querySelector('slot[name="content"]').assignedElements()[0];
14 | return toastBox.innerHTML;
15 | });
16 | expect(toasttext).toEqual('some-text');
17 | });
18 |
19 | it('should show and then close toast after 330ms even when timeout is 5000ms', async () => {
20 | const styles = await page.evaluate(async () => {
21 | document.body.innerHTML = `
22 |
23 | some-text
24 |
25 | `;
26 | const styles = [];
27 | const toast = document.querySelector('zoo-toast');
28 | toast.show();
29 | jasmine.clock().tick(45);
30 | styles.push(window.getComputedStyle(toast).display);
31 |
32 | toast.close();
33 | jasmine.clock().tick(345);
34 | styles.push(window.getComputedStyle(toast).display);
35 | return styles;
36 | });
37 | expect(styles[0]).toEqual('block');
38 | expect(styles[1]).toEqual('none');
39 | });
40 |
41 | it('should show and then close toast after 330ms even when timeout is 5000ms and a button is clicked', async () => {
42 | const styles = await page.evaluate(async () => {
43 | document.body.innerHTML = `
44 |
45 | some-text
46 |
47 | `;
48 | const styles = [];
49 | const toast = document.querySelector('zoo-toast');
50 | toast.show();
51 | jasmine.clock().tick(45);
52 | styles.push(window.getComputedStyle(toast).display);
53 |
54 | toast.close();
55 | jasmine.clock().tick(355);
56 | styles.push(window.getComputedStyle(toast).display);
57 | return styles;
58 | });
59 | expect(styles[0]).toEqual('block');
60 | expect(styles[1]).toEqual('none');
61 | });
62 |
63 | it('should show and then close toast after 330ms automatically', async () => {
64 | const styles = await page.evaluate(async () => {
65 | document.body.innerHTML = `
66 |
67 | some-text
68 |
69 | `;
70 | const styles = [];
71 | const toast = document.querySelector('zoo-toast');
72 | toast.show();
73 | jasmine.clock().tick(45);
74 | styles.push(window.getComputedStyle(toast).display);
75 |
76 | jasmine.clock().tick(1350);
77 | styles.push(window.getComputedStyle(toast).display);
78 | return styles;
79 | });
80 | expect(styles[0]).toEqual('block');
81 | expect(styles[1]).toEqual('none');
82 | });
83 |
84 | it('should ignore multiple calls to show', async () => {
85 | const calledTimes = await page.evaluate(async () => {
86 | document.body.innerHTML = `
87 |
88 | some-text
89 |
90 | `;
91 | const toast = document.querySelector('zoo-toast');
92 | let calledTimes = 0;
93 | toast.toggleToastClass = () => calledTimes += 1;
94 | toast.show();
95 | jasmine.clock().tick(45);
96 | toast.show();
97 | jasmine.clock().tick(45);
98 |
99 | return calledTimes;
100 | });
101 | expect(calledTimes).toEqual(1);
102 | });
103 |
104 | it('should ignore multiple calls to close', async () => {
105 | const calledTimes = await page.evaluate(async () => {
106 | document.body.innerHTML = `
107 |
108 | some-text
109 |
110 | `;
111 | const toast = document.querySelector('zoo-toast');
112 | let calledTimes = 0;
113 | toast.toggleToastClass = () => calledTimes += 1;
114 | toast.show();
115 | jasmine.clock().tick(45);
116 | toast.close();
117 | jasmine.clock().tick(45);
118 | toast.close();
119 | jasmine.clock().tick(45);
120 |
121 | return calledTimes;
122 | });
123 | expect(calledTimes).toEqual(2);
124 | });
125 | });
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tooltip/tooltip.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: grid;
3 | position: absolute;
4 | width: max-content;
5 | z-index: var(--zoo-tooltip-z-index, 9997);
6 | pointer-events: none;
7 | color: black;
8 |
9 | --tip-bottom: 0;
10 | --tip-right: unset;
11 | --tip-justify: center;
12 | }
13 |
14 | :host([position="top"]) {
15 | bottom: 170%;
16 |
17 | --tip-bottom: calc(0% - 8px);
18 | }
19 |
20 | :host([position="right"]) {
21 | justify-content: end;
22 | left: 102%;
23 | bottom: 25%;
24 |
25 | --tip-bottom: unset;
26 | --tip-justify: start;
27 | --tip-right: calc(100% - 8px);
28 | }
29 |
30 | :host([position="bottom"]) {
31 | bottom: -130%;
32 |
33 | --tip-bottom: calc(100% - 8px);
34 | }
35 |
36 | :host([position="left"]) {
37 | justify-content: start;
38 | left: -101%;
39 | bottom: 25%;
40 |
41 | --tip-bottom: unset;
42 | --tip-justify: end;
43 | --tip-right: -8px;
44 | }
45 |
46 | .tip {
47 | justify-self: var(--tip-justify);
48 | align-self: center;
49 | position: absolute;
50 | width: 16px;
51 | height: 16px;
52 | box-shadow: 0 4px 15px 0 rgb(0 0 0 / 10%);
53 | transform: rotate(45deg);
54 | z-index: -1;
55 | background: white;
56 | right: var(--tip-right);
57 | bottom: var(--tip-bottom);
58 | }
59 |
60 | .tooltip-content {
61 | display: grid;
62 | padding: 10px;
63 | font-size: 12px;
64 | line-height: 16px;
65 | font-weight: initial;
66 | position: relative;
67 | background: white;
68 | border-radius: 5px;
69 | pointer-events: initial;
70 | box-shadow: 0 4px 15px 0 rgb(0 0 0 / 10%);
71 | }
72 |
73 | .tooltip-content span {
74 | white-space: pre;
75 | }
76 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tooltip/tooltip.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tooltip/tooltip.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @injectHTML
3 | */
4 | export class Tooltip extends HTMLElement {
5 | constructor() {
6 | super();
7 | }
8 | }
9 |
10 | if (!window.customElements.get('zoo-tooltip')) {
11 | window.customElements.define('zoo-tooltip', Tooltip);
12 | }
--------------------------------------------------------------------------------
/src/zoo-modules/misc/tooltip/tooltip.spec.mjs:
--------------------------------------------------------------------------------
1 | describe('Zoo tooltip', function () {
2 | it('should create default tooltip', async () => {
3 | const tooltipText = await page.evaluate(() => {
4 | document.body.innerHTML = `
5 |
6 |
10 |
11 | `;
12 | let tooltip = document.querySelector('zoo-tooltip');
13 | const tooltiptext = tooltip.shadowRoot.querySelector('slot').assignedNodes()[0];
14 | return tooltiptext.innerHTML;
15 | });
16 | expect(tooltipText).toEqual('some-text');
17 | });
18 | });
--------------------------------------------------------------------------------
/src/zoo-web-components.js:
--------------------------------------------------------------------------------
1 | export { InfoMessage } from './zoo-modules/form/info/info.js';
2 | export { Label } from './zoo-modules/form/label/label.js';
3 | export { Input } from './zoo-modules/form/input/input.js';
4 | export { Checkbox } from './zoo-modules/form/checkbox/checkbox.js';
5 | export { Radio } from './zoo-modules/form/radio/radio.js';
6 | export { Select } from './zoo-modules/form/select/select.js';
7 | export { SearchableSelect } from './zoo-modules/form/searchable-select/searchable-select.js';
8 | export { QuantityControl } from './zoo-modules/form/quantity-control/quantity-control.js';
9 | export { ToggleSwitch } from './zoo-modules/form/toggle-switch/toggle-switch.js';
10 | export { DateRange } from './zoo-modules/form/date-range/date-range.js';
11 | export { InputTag } from './zoo-modules/form/input-tag/input-tag.js';
12 |
13 | export { ZooGrid } from './zoo-modules/grid/grid/grid.js';
14 | export { GridHeader } from './zoo-modules/grid/grid-header/grid-header.js';
15 | export { GridRow } from './zoo-modules/grid/grid-row/grid-row.js';
16 |
17 | export { Button } from './zoo-modules/misc/button/button.js';
18 | export { ButtonGroup } from './zoo-modules/misc/button-group/button-group.js';
19 | export { Header } from './zoo-modules/misc/header/header.js';
20 | export { Modal } from './zoo-modules/misc/modal/modal.js';
21 | export { Footer } from './zoo-modules/misc/footer/footer.js';
22 | export { Feedback } from './zoo-modules/misc/feedback/feedback.js';
23 | export { Tooltip } from './zoo-modules/misc/tooltip/tooltip.js';
24 | export { Link } from './zoo-modules/misc/link/link.js';
25 | export { Navigation } from './zoo-modules/misc/navigation/navigation.js';
26 | export { Toast } from './zoo-modules/misc/toast/toast.js';
27 | export { CollapsableList } from './zoo-modules/misc/collapsable-list/collapsable-list.js';
28 | export { CollapsableListItem } from './zoo-modules/misc/collapsable-list-item/collapsable-list-item.js';
29 | export { Spinner } from './zoo-modules/misc/spinner/spinner.js';
30 | export { Paginator } from './zoo-modules/misc/paginator/paginator.js';
31 | export { Preloader } from './zoo-modules/misc/preloader/preloader.js';
32 | export { Tag } from './zoo-modules/misc/tag/tag.js';
33 |
34 | export { AttentionIcon } from './zoo-modules/icon/attention-icon/attention-icon.js';
35 | export { ArrowDownIcon } from './zoo-modules/icon/arrow-icon/arrow-icon.js';
36 | export { CrossIcon } from './zoo-modules/icon/cross-icon/cross-icon.js';
37 | export { PawIcon } from './zoo-modules/icon/paw-icon/paw-icon.js';
38 |
39 | export { registerComponents } from './zoo-modules/common/register-components.js';
--------------------------------------------------------------------------------