├── src
├── components
│ ├── links
│ │ ├── link.json
│ │ ├── link.css
│ │ ├── link.spec.html
│ │ └── link.spec.js
│ ├── selection
│ │ ├── radio
│ │ │ ├── radio.json
│ │ │ ├── radio.spec.html
│ │ │ ├── radio.css
│ │ │ └── radio.spec.js
│ │ ├── switch
│ │ │ ├── switch.json
│ │ │ ├── switch.spec.html
│ │ │ ├── switch.css
│ │ │ └── switch.spec.js
│ │ └── checkbox
│ │ │ ├── checkbox.json
│ │ │ ├── checkbox.spec.html
│ │ │ └── checkbox.css
│ ├── buttons
│ │ ├── text
│ │ │ ├── button-text.json
│ │ │ ├── button-text.spec.html
│ │ │ ├── button-text.css
│ │ │ └── button-text.spec.js
│ │ ├── outlined
│ │ │ ├── button-outlined.json
│ │ │ ├── button-outlined.spec.html
│ │ │ ├── button-outlined.css
│ │ │ └── button-outlined.spec.js
│ │ ├── contained
│ │ │ ├── button-contained.json
│ │ │ ├── button-contained.spec.html
│ │ │ ├── button-contained.css
│ │ │ └── button-contained.spec.js
│ │ └── unelevated
│ │ │ ├── button-unelevated.json
│ │ │ ├── button-unelevated.spec.html
│ │ │ ├── button-unelevated.css
│ │ │ └── button-unelevated.spec.js
│ ├── progress
│ │ ├── linear
│ │ │ ├── progress-linear.json
│ │ │ ├── progress-linear.spec.html
│ │ │ ├── progress-linear.css
│ │ │ └── progress-linear.spec.js
│ │ └── circular
│ │ │ ├── progress-circular.json
│ │ │ ├── progress-circular.spec.html
│ │ │ ├── progress-circular.css
│ │ │ └── progress-circular.spec.js
│ ├── tooltips
│ │ ├── tooltip.json
│ │ ├── tooltip.spec.html
│ │ ├── tooltip.css
│ │ └── tooltip.spec.js
│ ├── textfields
│ │ ├── filled
│ │ │ ├── textfield-filled.json
│ │ │ ├── textfield-filled.spec.html
│ │ │ └── textfield-filled.css
│ │ ├── standard
│ │ │ ├── textfield-standard.json
│ │ │ ├── textfield-standard.spec.html
│ │ │ └── textfield-standard.css
│ │ └── outlined
│ │ │ ├── textfield-outlined.json
│ │ │ ├── textfield-outlined.spec.html
│ │ │ └── textfield-outlined.css
│ └── components.css
├── matter.css
└── utilities
│ ├── utilities.css
│ ├── colors
│ ├── colors.spec.html
│ └── colors.css
│ └── typography
│ ├── typography.spec.html
│ └── typography.css
├── .gitignore
├── docs
├── hero.png
├── browsers.png
├── m.svg
└── browsers.html
├── .github
└── FUNDING.yml
├── test
├── helpers
│ ├── browser.js
│ ├── fixture.js
│ └── capture.js
└── matchers
│ └── resemble.js
├── package.json
├── LICENSE
├── scripts
├── dist.js
└── merge.js
├── karma.conf.js
└── README.md
/src/components/links/link.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/components/selection/radio/radio.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | reports/
4 |
--------------------------------------------------------------------------------
/src/components/selection/switch/switch.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/components/selection/checkbox/checkbox.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/docs/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/finnhvman/matter/HEAD/docs/hero.png
--------------------------------------------------------------------------------
/docs/browsers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/finnhvman/matter/HEAD/docs/browsers.png
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: matter
2 | custom: [paypal.me/finnhvman, "https://www.buymeacoffee.com/finnhvman"]
3 |
--------------------------------------------------------------------------------
/src/components/buttons/text/button-text.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "The ripple effect always comes from the center"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/components/buttons/outlined/button-outlined.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "The ripple effect always comes from the center"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/components/buttons/contained/button-contained.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "The ripple effect always comes from the center"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/components/buttons/unelevated/button-unelevated.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "The ripple effect always comes from the center"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/matter.css:
--------------------------------------------------------------------------------
1 |
2 | /* Components */
3 | @import "./components/components.css";
4 |
5 | /* Utilities */
6 | @import "./utilities/utilities.css";
7 |
--------------------------------------------------------------------------------
/src/utilities/utilities.css:
--------------------------------------------------------------------------------
1 |
2 | /* Colors */
3 | @import "./colors/colors.css";
4 |
5 | /* Typography */
6 | @import "./typography/typography.css";
7 |
--------------------------------------------------------------------------------
/src/components/progress/linear/progress-linear.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "There is no transition on determinate value change in Firefox"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/components/progress/circular/progress-circular.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "Edge displays ms type of dot animation"
4 | ],
5 | "issues": [
6 | "There is no support for determinate progress"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/tooltips/tooltip.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "Screen Readers announce tooltip content as part of the component it's attached to"
4 | ],
5 | "issues": [
6 | "No focus-within on Edge"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/docs/m.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/textfields/filled/textfield-filled.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "Transitions are faster in Safari"
4 | ],
5 | "issues": [
6 | "The placeholder attribute has to be present on the input with the value of \" \" (space)",
7 | "No floating label on Edge",
8 | "The label cannot be selected"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/textfields/standard/textfield-standard.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "Transitions are faster in Safari"
4 | ],
5 | "issues": [
6 | "The placeholder attribute has to be present on the input with the value of \" \" (space)",
7 | "No floating label on Edge",
8 | "The label cannot be selected"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/textfields/outlined/textfield-outlined.json:
--------------------------------------------------------------------------------
1 | {
2 | "degradation": [
3 | "Transitions are faster in Safari"
4 | ],
5 | "issues": [
6 | "The placeholder attribute has to be present on the input with the value of \" \" (space)",
7 | "No floating label on Edge",
8 | "Textarea scroll overflow is not clipped under label text"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/progress/circular/progress-circular.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/helpers/browser.js:
--------------------------------------------------------------------------------
1 | export const getCurrentBrowser = () => {
2 | const { userAgent } =window.navigator;
3 | if (0 <= userAgent.indexOf('Chrome')) {
4 | return 'Chrome';
5 | } else if (0 <= userAgent.indexOf('Firefox')) {
6 | return 'Firefox';
7 | } else if (0 <= userAgent.indexOf('Safari')) {
8 | return 'Safari';
9 | }
10 | };
11 |
12 | export const isBrowser = (browser) => getCurrentBrowser() === browser;
13 | export const isBrowserNot = (browser) => getCurrentBrowser() !== browser;
14 |
--------------------------------------------------------------------------------
/src/components/buttons/text/button-text.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | BUTTON
4 |
5 | MIN
6 |
7 | SIZED
8 |
9 | DISABLED
10 |
11 | XMAS TREE
--------------------------------------------------------------------------------
/src/components/buttons/outlined/button-outlined.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | BUTTON
4 |
5 | MIN
6 |
7 | SIZED
8 |
9 | DISABLED
10 |
11 | XMAS TREE
--------------------------------------------------------------------------------
/src/components/buttons/contained/button-contained.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | BUTTON
4 |
5 | MIN
6 |
7 | SIZED
8 |
9 | DISABLED
10 |
11 | XMAS TREE
--------------------------------------------------------------------------------
/src/components/buttons/unelevated/button-unelevated.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | BUTTON
4 |
5 | MIN
6 |
7 | SIZED
8 |
9 | DISABLED
10 |
11 | XMAS TREE
12 |
--------------------------------------------------------------------------------
/src/components/progress/linear/progress-linear.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/links/link.css:
--------------------------------------------------------------------------------
1 | .matter-link {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | --matter-helper-safari1: rgba(var(--matter-helper-theme), 0.12);
4 | border-radius: 4px;
5 | color: rgb(var(--matter-helper-theme));
6 | text-decoration: none;
7 | transition: background-color 0.2s, box-shadow 0.2s;
8 | }
9 |
10 | /* Hover */
11 | .matter-link:hover {
12 | text-decoration: underline;
13 | }
14 |
15 | /* Focus */
16 | .matter-link:focus {
17 | background-color: var(--matter-helper-safari1);
18 | box-shadow: 0 0 0 0.16em var(--matter-helper-safari1);
19 | outline: none;
20 | }
21 |
22 | /* Active */
23 | .matter-link:active {
24 | background-color: transparent;
25 | box-shadow: none;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utilities/colors/colors.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | Primary
11 |
12 |
13 |
14 | Secondary
15 |
16 |
17 |
18 | Error
19 |
20 |
21 |
22 | Warning
23 |
24 |
25 |
26 | Success
27 |
28 |
29 |
30 |
I am primary color
31 |
32 | I am secondary color
33 |
34 | I am error color
35 |
36 | I am warning color
37 |
38 | I am success color
39 |
--------------------------------------------------------------------------------
/src/components/selection/radio/radio.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Radio Option 1
6 |
7 |
8 |
9 |
10 |
11 |
12 | Radio Option 2
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Disabled 1
22 |
23 |
24 |
25 |
26 |
27 |
28 | Disabled 2
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Xmas Tree
38 |
39 |
40 |
41 |
42 |
43 |
44 | Xmas Tree, but different
45 |
46 |
--------------------------------------------------------------------------------
/src/components/selection/switch/switch.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Switched On
6 |
7 |
8 |
9 |
10 |
11 |
12 | Switched Off
13 |
14 |
15 |
16 |
17 |
18 |
19 | Switched On
20 |
21 |
22 |
23 |
24 |
25 |
26 | Switched Off
27 |
28 |
29 |
30 |
31 |
32 |
33 | Sized
34 |
35 |
36 |
37 |
38 |
39 |
40 | Xmas Tree
41 |
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matter",
3 | "version": "0.2.2",
4 | "description": "Material Design Components in Pure CSS",
5 | "repository": "finnhvman/matter",
6 | "author": "Ben Szabo (finnhvman)",
7 | "license": "MIT",
8 | "main": "index.js",
9 | "scripts": {
10 | "dist": "node scripts/dist.js",
11 | "test": "karma start karma.conf.js",
12 | "patch": "karma start karma.conf.js --single-run && npm --no-git-tag-version version patch && node scripts/dist.js",
13 | "minor": "karma start karma.conf.js --single-run && npm --no-git-tag-version version minor && node scripts/dist.js",
14 | "merge": "node scripts/merge.js"
15 | },
16 | "devDependencies": {
17 | "cssnano": "4.1.10",
18 | "jasmine": "3.4.0",
19 | "karma": "4.2.0",
20 | "karma-chrome-launcher": "3.0.0",
21 | "karma-firefox-launcher": "1.1.0",
22 | "karma-html2js-preprocessor": "1.1.0",
23 | "karma-htmlfile-reporter": "0.3.8",
24 | "karma-jasmine": "2.0.1",
25 | "karma-safari-private-launcher": "1.0.0",
26 | "pngjs": "3.4.0",
27 | "postcss": "7.0.17",
28 | "postcss-import": "12.0.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/components.css:
--------------------------------------------------------------------------------
1 |
2 | /* Button Contained */
3 | @import "buttons/contained/button-contained.css";
4 |
5 | /* Button Unelevated */
6 | @import "buttons/unelevated/button-unelevated.css";
7 |
8 | /* Button Outlined */
9 | @import "./buttons/outlined/button-outlined.css";
10 |
11 | /* Button Text */
12 | @import "./buttons/text/button-text.css";
13 |
14 | /* Link */
15 | @import "./links/link.css";
16 |
17 | /* Progress Circular */
18 | @import "./progress/circular/progress-circular.css";
19 |
20 | /* Progress Linear */
21 | @import "./progress/linear/progress-linear.css";
22 |
23 | /* Checkbox */
24 | @import "./selection/checkbox/checkbox.css";
25 |
26 | /* Radio */
27 | @import "./selection/radio/radio.css";
28 |
29 | /* Switch */
30 | @import "./selection/switch/switch.css";
31 |
32 | /* Textfield Standard */
33 | @import "./textfields/standard/textfield-standard.css";
34 |
35 | /* Textfield Filled */
36 | @import "./textfields/filled/textfield-filled.css";
37 |
38 | /* Textfield Outlined */
39 | @import "./textfields/outlined/textfield-outlined.css";
40 |
41 | /* Tooltip */
42 | @import "./tooltips/tooltip.css";
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Bence Szabó
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/dist.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const atImport = require('postcss-import');
3 | const cssnano = require('cssnano');
4 | const postcss = require('postcss');
5 | const { version } = require('../package.json');
6 |
7 | const source = '@import "./matter.css";';
8 |
9 | const tag = (css, modifier) => {
10 | const mod = modifier ? `(${modifier}) ` : '';
11 | return `/* Matter ${version} ${mod}*/\n${css}`;
12 | };
13 |
14 | console.log('Dist Started...');
15 |
16 | (async () => {
17 | try {
18 | // matter.css
19 | const normal = await postcss([atImport]).process(source, {
20 | from: './src/source.css',
21 | to: './dist/matter.css'
22 | });
23 |
24 | const tagged = tag(normal.css);
25 | fs.writeFileSync('./dist/matter.css', tagged);
26 |
27 | // matter.min.css
28 | const min = await postcss([atImport, cssnano]).process(source, {
29 | from: './src/source.css',
30 | to: './dist/matter.min.css'
31 | });
32 |
33 | const taggedMin = tag(min.css, 'min');
34 | fs.writeFileSync('./dist/matter.min.css', taggedMin);
35 |
36 | console.log('Dist Finished!');
37 | } catch (error) {
38 | console.log('Dist Failed!');
39 | console.log(error);
40 | }
41 | })();
42 |
--------------------------------------------------------------------------------
/src/utilities/colors/colors.css:
--------------------------------------------------------------------------------
1 | /* Components */
2 | .matter-primary {
3 | --matter-theme-rgb: var(--matter-primary-rgb, 33, 150, 243);
4 | --matter-ontheme-rgb: var(--matter-onprimary-rgb, 255, 255, 255);
5 | }
6 |
7 | .matter-secondary {
8 | --matter-theme-rgb: var(--matter-secondary-rgb, 102, 0, 238);
9 | --matter-ontheme-rgb: var(--matter-onsecondary-rgb, 255, 255, 255);
10 | }
11 |
12 | .matter-error {
13 | --matter-theme-rgb: var(--matter-error-rgb, 238, 0, 0);
14 | --matter-ontheme-rgb: var(--matter-error-rgb, 255, 255, 255);
15 | }
16 |
17 | .matter-warning {
18 | --matter-theme-rgb: var(--matter-warning-rgb, 238, 102, 0);
19 | --matter-ontheme-rgb: var(--matter-onwarning-rgb, 255, 255, 255);
20 | }
21 |
22 | .matter-success {
23 | --matter-theme-rgb: var(--matter-success-rgb, 17, 136, 34);
24 | --matter-ontheme-rgb: var(--matter-onsuccess-rgb, 255, 255, 255);
25 | }
26 |
27 | /* Text */
28 | .matter-primary-text {
29 | color: rgb(var(--matter-primary-rgb, 33, 150, 243));
30 | }
31 |
32 | .matter-secondary-text {
33 | color: rgb(var(--matter-secondary-rgb, 102, 0, 238));
34 | }
35 |
36 | .matter-error-text {
37 | color: rgb(var(--matter-error-rgb, 238, 0, 0));
38 | }
39 |
40 | .matter-warning-text {
41 | color: rgb(var(--matter-warning-rgb, 238, 102, 0));
42 | }
43 |
44 | .matter-success-text {
45 | color: rgb(var(--matter-success-rgb, 17, 136, 34));
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/selection/checkbox/checkbox.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Unchecked
6 |
7 |
8 |
9 |
10 |
11 |
12 | Indeterminate
13 |
14 |
15 |
16 |
17 |
18 |
19 | Checked
20 |
21 |
22 |
23 |
24 |
25 |
26 | Disabled Unchecked
27 |
28 |
29 |
30 |
31 |
32 |
33 | Disabled Indeterminate
34 |
35 |
36 |
37 |
38 |
39 |
40 | Disabled Checked
41 |
42 |
43 |
44 |
45 |
46 |
47 | Xmas Tree
48 |
49 |
50 |
57 |
--------------------------------------------------------------------------------
/docs/browsers.html:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/components/textfields/filled/textfield-filled.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Textfield
6 |
7 |
8 |
9 |
10 | Sized
11 |
12 |
13 |
14 |
15 | Disabled
16 |
17 |
18 |
19 |
20 | Xmas Tree
21 |
22 |
23 |
24 |
25 |
26 | Textfield
27 |
28 |
29 |
30 |
31 | Sized
32 |
33 |
34 |
35 |
36 | Disabled
37 |
38 |
39 |
40 |
41 | Xmas Tree
42 |
43 |
--------------------------------------------------------------------------------
/src/components/textfields/outlined/textfield-outlined.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Textfield
6 |
7 |
8 |
9 |
10 | Sized
11 |
12 |
13 |
14 |
15 | Disabled
16 |
17 |
18 |
19 |
20 | Xmas Tree
21 |
22 |
23 |
24 |
25 |
26 | Textfield
27 |
28 |
29 |
30 |
31 | Sized
32 |
33 |
34 |
35 |
36 | Disabled
37 |
38 |
39 |
40 |
41 | Xmas Tree
42 |
43 |
--------------------------------------------------------------------------------
/src/components/textfields/standard/textfield-standard.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Textfield
6 |
7 |
8 |
9 |
10 | Sized
11 |
12 |
13 |
14 |
15 | Disabled
16 |
17 |
18 |
19 |
20 | Xmas Tree
21 |
22 |
23 |
24 |
25 |
26 | Textfield
27 |
28 |
29 |
30 |
31 | Sized
32 |
33 |
34 |
35 |
36 | Disabled
37 |
38 |
39 |
40 |
41 | Xmas Tree
42 |
43 |
--------------------------------------------------------------------------------
/src/utilities/typography/typography.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 | I am h1 Ég
12 |
13 | I am h2 Ég
14 |
15 | I am h3 Ég
16 |
17 | I am h4 Ég
18 |
19 | I am h5 Ég
20 |
21 | I am h5 Ég
22 |
23 | I am subtitle 1 Ég
24 |
25 | I am subtitle 2 Ég
26 |
27 | I am body 1 Ég. I'm a big fan of Pure CSS Stuff too. I wondered if these components could be done without JavaScript. It turns out you can go a long way with this restriction. Not full blown functionality though, but enough for a lot of use-cases.
28 |
29 | I am body 2 Ég. This project goes back to around half a year before, when I needed just a few nice looking components for a CodePen demo. Buttons, Slider, Switch, Textfield, some of the usual components of a web page. I'm a big fan of Material Design, and I couldn't find any lean and simple way to integrate these Material components into my Pen. I thought it shouldn't be hard to implement them.
30 |
31 | Button Ég
32 |
33 | I am a caption Ég
34 |
35 | I am the overline Ég
36 |
--------------------------------------------------------------------------------
/src/components/tooltips/tooltip.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Small Help
5 | I'm a div
6 |
7 |
8 |
9 |
10 |
11 | Small Help
12 | I'm a wider div
13 |
14 |
15 |
16 |
17 | Small Help I'm
18 |
19 |
20 |
21 |
22 | Small Help
23 | Top variant
24 |
25 |
26 |
27 |
28 |
29 | Small Help
30 |
31 | Label
32 |
33 |
34 |
35 |
36 |
37 | Small Help
38 | Xmas Tree
39 |
40 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | config.set({
3 | basePath: '',
4 | frameworks: ['jasmine'],
5 | files: [
6 | {
7 | pattern: './test/helpers/**/*.js',
8 | type: 'module'
9 | },
10 | {
11 | pattern: './test/matchers/**/*js'
12 | },
13 | {
14 | pattern: './src/components/**/*.css',
15 | type: 'css'
16 | },
17 | {
18 | pattern: './src/components/**/*.spec.js',
19 | type: 'module'
20 | },
21 | {
22 | pattern: './src/components/**/*.spec.html'
23 | }
24 | ],
25 | exclude: [],
26 | preprocessors: {
27 | '**/*.html': ['html2js']
28 | },
29 | reporters: [
30 | 'html',
31 | 'progress'
32 | ],
33 | htmlReporter: {
34 | outputFile: 'reports/unit-tests.html',
35 | pageTitle: 'Matter',
36 | subPageTitle: 'Unit Tests',
37 | showOnlyFailed: true,
38 | groupSuites: true,
39 | useCompactStyle: true
40 | },
41 | port: 9876,
42 | colors: true,
43 | logLevel: config.LOG_INFO,
44 | autoWatch: true,
45 | browsers: [
46 | 'Chrome',
47 | 'Firefox',
48 | 'SafariPrivate'
49 | ],
50 | singleRun: false,
51 | concurrency: Infinity
52 | })
53 | };
54 |
--------------------------------------------------------------------------------
/scripts/merge.js:
--------------------------------------------------------------------------------
1 | /* Utility to create cross-browser shape verification matrices, first arg is colors.json the rest are pngs */
2 | const fs = require('fs');
3 | const { PNG } = require('pngjs');
4 |
5 | const [ node, script, json, ...pngs ] = process.argv;
6 |
7 | const match = (expected, actual) => {
8 | if (typeof expected === 'number') {
9 | return expected === actual;
10 | } else if (expected instanceof Array) {
11 | return expected[0] <= actual && actual <= expected[1];
12 | } else {
13 | return true;
14 | }
15 | };
16 |
17 | const matchPixel = (expected, r, g, b, a) => {
18 | return match(expected.a, a) && match(expected.r, r) && match(expected.g, g) && match(expected.b, b);
19 | };
20 |
21 |
22 | const colors = JSON.parse(fs.readFileSync(json, 'utf8'));
23 |
24 | const images = pngs.map((png) => {
25 | const file = fs.readFileSync(png);
26 | return PNG.sync.read(file);
27 | });
28 |
29 | const { width, height } = images[0];
30 |
31 | const matrix = new Array(height).fill(null).map(() => new Array(width).fill(null));
32 |
33 | colors.forEach((color, colorIndex) => {
34 | // use and logical operator
35 | for (let y = 0; y < height; y++) {
36 | for (let x = 0; x < width; x++) {
37 | const index = width * y * 4 + x * 4;
38 |
39 | const matched = images.reduce((accu, image) => {
40 | return accu && matchPixel(color, image.data[index], image.data[index + 1], image.data[index + 2], image.data[index + 3]);
41 | }, true);
42 |
43 | if (matched) {
44 | matrix[y][x] = colorIndex;
45 | }
46 | }
47 | }
48 | });
49 |
50 | matrix.forEach((row) => console.log(JSON.stringify(row) + ','));
51 |
--------------------------------------------------------------------------------
/src/components/links/link.spec.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | The purpose of Matter is to provide the most easy-to-use but accurate implementation of Material Design Components .
4 |
5 | Matter has probably the lowest entry-barrier among Material Design Component libraries. The only technical knowledge needed to use it is basic HTML5 . It doesn't rely on JavaScript , it only needs one to three HTML elements and a CSS class per component to work. The markup of the components is semantic by design.
6 |
7 | Matter is built with theming in mind. Its components can be customized by specifying certain colors and/or fonts. The granularity of customization is variable: components can be themed on global level, component level, component instance level, or on any level between.
8 |
9 |
10 |
11 | Large
12 |
13 |
14 |
15 | Medium
16 |
17 |
18 |
19 | Small
20 |
21 |
22 |
23 | Xmas Tree
24 |
--------------------------------------------------------------------------------
/test/helpers/fixture.js:
--------------------------------------------------------------------------------
1 | export const setUp = (spec, states = {}) => {
2 | const fixture = document.createElement('div');
3 | fixture.id = 'fixture';
4 | fixture.innerHTML = window.__html__[spec + '.spec.html'];
5 |
6 | // Set up states
7 | Object.entries(states).forEach(([ selector, attributes ]) => {
8 | const element = fixture.querySelector(selector);
9 | if (attributes instanceof Array) {
10 | attributes.forEach(attribute => element.setAttribute(attribute, ''));
11 | } else if (typeof attributes === 'object') {
12 | Object.entries(attributes).forEach(([ attribute, value ]) => {
13 | if (element.tagName === 'TEXTAREA' && attribute === 'value') {
14 | element.textContent = value;
15 | } else {
16 | element.setAttribute(attribute, value);
17 | }
18 | });
19 | }
20 | });
21 |
22 | document.body.appendChild(fixture);
23 | return getStyle(document.styleSheets, spec + '.css');
24 | };
25 |
26 | const getStyle = (styleSheets, href) => {
27 | const styleSheet = findStyleSheet(styleSheets, href);
28 | let style = '';
29 | if (styleSheet) {
30 | for (let index = 0; index < styleSheet.cssRules.length; index++) {
31 | style += isMediaQuery(styleSheet.cssRules[index]) ? '' : replacePseudos(styleSheet.cssRules[index].cssText);
32 | }
33 | }
34 |
35 | return style;
36 | };
37 |
38 | const findStyleSheet = (styleSheets, href) => {
39 | for (let index = 0; index < styleSheets.length; index++) {
40 | if (styleSheets[index].href.includes(href)) {
41 | return styleSheets[index];
42 | }
43 | }
44 | return null;
45 | };
46 |
47 | const isMediaQuery = (cssRule) => cssRule.type === 4;
48 |
49 | const replacePseudos = (cssText) => {
50 | const regular = [ 'active', 'focus-within', 'focus', 'hover', 'indeterminate', 'placeholder-shown' ];
51 | return regular.reduce((css, pseudo) => css.replace(new RegExp(`:${pseudo}`, 'g'), `[${pseudo}]`), cssText);
52 | };
53 |
54 | export const tearDown = () => {
55 | const fixture = document.querySelector('#fixture');
56 | document.body.removeChild(fixture);
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/buttons/text/button-text.css:
--------------------------------------------------------------------------------
1 | .matter-button-text {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | position: relative;
4 | display: inline-block;
5 | box-sizing: border-box;
6 | margin: 0;
7 | border: none;
8 | border-radius: 4px;
9 | padding: 0 8px;
10 | min-width: 64px;
11 | height: 36px;
12 | vertical-align: middle;
13 | text-align: center;
14 | text-overflow: ellipsis;
15 | color: rgb(var(--matter-helper-theme));
16 | background-color: transparent;
17 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
18 | font-size: 14px;
19 | font-weight: 500;
20 | line-height: 36px;
21 | outline: none;
22 | cursor: pointer;
23 | }
24 |
25 | .matter-button-text::-moz-focus-inner {
26 | border: none;
27 | }
28 |
29 | /* Highlight, Ripple */
30 | .matter-button-text::before,
31 | .matter-button-text::after {
32 | content: "";
33 | position: absolute;
34 | top: 0;
35 | left: 0;
36 | right: 0;
37 | bottom: 0;
38 | border-radius: inherit;
39 | opacity: 0;
40 | }
41 |
42 | .matter-button-text::before {
43 | background-color: rgb(var(--matter-helper-theme));
44 | transition: opacity 0.2s;
45 | }
46 |
47 | .matter-button-text::after {
48 | background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat;
49 | transition: opacity 1s, background-size 0.5s;
50 | }
51 |
52 | /* Hover, Focus */
53 | .matter-button-text:hover::before {
54 | opacity: 0.04;
55 | }
56 |
57 | .matter-button-text:focus::before {
58 | opacity: 0.12;
59 | }
60 |
61 | .matter-button-text:hover:focus::before {
62 | opacity: 0.16;
63 | }
64 |
65 | /* Active */
66 | .matter-button-text:active::after {
67 | opacity: 0.16;
68 | background-size: 100% 100%;
69 | transition: background-size 0s;
70 | }
71 |
72 | /* Disabled */
73 | .matter-button-text:disabled {
74 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
75 | background-color: transparent;
76 | cursor: initial;
77 | }
78 |
79 | .matter-button-text:disabled::before,
80 | .matter-button-text:disabled::after {
81 | opacity: 0;
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/tooltips/tooltip.css:
--------------------------------------------------------------------------------
1 | .matter-tooltip,
2 | .matter-tooltip-top {
3 | z-index: 10;
4 | position: absolute;
5 | left: 0;
6 | right: 0;
7 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
8 | font-size: 10px;
9 | font-weight: 400;
10 | line-height: 16px;
11 | white-space: nowrap;
12 | text-transform: none;
13 | text-align: center;
14 | pointer-events: none;
15 | }
16 |
17 | .matter-tooltip {
18 | bottom: -40px;
19 | }
20 |
21 | .matter-tooltip-top {
22 | top: -40px;
23 | }
24 |
25 | .matter-tooltip > span,
26 | .matter-tooltip-top > span {
27 | position: -webkit-sticky;
28 | position: sticky;
29 | left: 0;
30 | right: 0;
31 | display: inline-block;
32 | box-sizing: content-box;
33 | margin: 0 -100vw;
34 | border: solid 8px transparent;
35 | border-radius: 12px;
36 | padding: 4px 8px;
37 | color: rgb(var(--matter-surface-rgb, 255, 255, 255));
38 | background-clip: padding-box;
39 | background-image: linear-gradient(rgba(var(--matter-surface-rgb, 255, 255, 255), 0.34), rgba(var(--matter-surface-rgb, 255, 255, 255), 0.34));
40 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.85);
41 | transform: scale(0);
42 | opacity: 0;
43 | pointer-events: auto;
44 | transition: transform 0.075s, opacity 0.075s;
45 | }
46 |
47 | :not(html):hover > .matter-tooltip > span,
48 | .matter-tooltip:hover > span,
49 | :not(html):hover > .matter-tooltip-top > span,
50 | .matter-tooltip-top:hover > span {
51 | transform: scale(1);
52 | opacity: 1;
53 | transition: transform 0.15s, opacity 0.15s;
54 | }
55 |
56 | :focus-within > .matter-tooltip > span,
57 | :focus-within > .matter-tooltip-top > span {
58 | transform: scale(1);
59 | opacity: 1;
60 | transition: transform 0.15s, opacity 0.15s;
61 | }
62 |
63 | /* Non-desktop */
64 | @media (pointer: coarse), (hover: none) {
65 |
66 | .matter-tooltip,
67 | .matter-tooltip-top {
68 | font-size: 14px;
69 | line-height: 20px;
70 | }
71 |
72 | .matter-tooltip {
73 | bottom: -48px;
74 | }
75 | .matter-tooltip-top {
76 | top: -48px;
77 | }
78 |
79 | .matter-tooltip > span,
80 | .matter-tooltip-top > span {
81 | padding: 6px 16px;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/buttons/outlined/button-outlined.css:
--------------------------------------------------------------------------------
1 | .matter-button-outlined {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | position: relative;
4 | display: inline-block;
5 | box-sizing: border-box;
6 | margin: 0;
7 | border: solid 1px; /* Safari */
8 | border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.24);
9 | border-radius: 4px;
10 | padding: 0 16px;
11 | min-width: 64px;
12 | height: 36px;
13 | vertical-align: middle;
14 | text-align: center;
15 | text-overflow: ellipsis;
16 | color: rgb(var(--matter-helper-theme));
17 | background-color: transparent;
18 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
19 | font-size: 14px;
20 | font-weight: 500;
21 | line-height: 34px;
22 | outline: none;
23 | cursor: pointer;
24 | }
25 |
26 | .matter-button-outlined::-moz-focus-inner {
27 | border: none;
28 | }
29 |
30 | /* Highlight, Ripple */
31 | .matter-button-outlined::before,
32 | .matter-button-outlined::after {
33 | content: "";
34 | position: absolute;
35 | top: 0;
36 | left: 0;
37 | right: 0;
38 | bottom: 0;
39 | border-radius: 3px;
40 | opacity: 0;
41 | }
42 |
43 | .matter-button-outlined::before {
44 | background-color: rgb(var(--matter-helper-theme));
45 | transition: opacity 0.2s;
46 | }
47 |
48 | .matter-button-outlined::after {
49 | background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat;
50 | transition: opacity 1s, background-size 0.5s;
51 | }
52 |
53 | /* Hover, Focus */
54 | .matter-button-outlined:hover::before {
55 | opacity: 0.04;
56 | }
57 |
58 | .matter-button-outlined:focus::before {
59 | opacity: 0.12;
60 | }
61 |
62 | .matter-button-outlined:hover:focus::before {
63 | opacity: 0.16;
64 | }
65 |
66 | /* Active */
67 | .matter-button-outlined:active::after {
68 | opacity: 0.16;
69 | background-size: 100% 100%;
70 | transition: background-size 0s;
71 | }
72 |
73 | /* Disabled */
74 | .matter-button-outlined:disabled {
75 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
76 | background-color: transparent;
77 | cursor: initial;
78 | }
79 |
80 | .matter-button-outlined:disabled::before,
81 | .matter-button-outlined:disabled::after {
82 | opacity: 0;
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/buttons/unelevated/button-unelevated.css:
--------------------------------------------------------------------------------
1 | .matter-button-unelevated {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255));
4 | position: relative;
5 | display: inline-block;
6 | box-sizing: border-box;
7 | border: none;
8 | border-radius: 4px;
9 | padding: 0 16px;
10 | min-width: 64px;
11 | height: 36px;
12 | vertical-align: middle;
13 | text-align: center;
14 | text-overflow: ellipsis;
15 | color: rgb(var(--matter-helper-ontheme));
16 | background-color: rgb(var(--matter-helper-theme));
17 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
18 | font-size: 14px;
19 | font-weight: 500;
20 | line-height: 36px;
21 | outline: none;
22 | cursor: pointer;
23 | }
24 |
25 | .matter-button-unelevated::-moz-focus-inner {
26 | border: none;
27 | }
28 |
29 | /* Highlight, Ripple */
30 | .matter-button-unelevated::before,
31 | .matter-button-unelevated::after {
32 | content: "";
33 | position: absolute;
34 | top: 0;
35 | left: 0;
36 | right: 0;
37 | bottom: 0;
38 | border-radius: inherit;
39 | opacity: 0;
40 | }
41 |
42 | .matter-button-unelevated::before {
43 | background-color: rgb(var(--matter-helper-ontheme));
44 | transition: opacity 0.2s;
45 | }
46 |
47 | .matter-button-unelevated::after {
48 | background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat;
49 | transition: opacity 1s, background-size 0.5s;
50 | }
51 |
52 | /* Hover, Focus */
53 | .matter-button-unelevated:hover::before {
54 | opacity: 0.08;
55 | }
56 |
57 | .matter-button-unelevated:focus::before {
58 | opacity: 0.24;
59 | }
60 |
61 | .matter-button-unelevated:hover:focus::before {
62 | opacity: 0.32;
63 | }
64 |
65 | /* Active */
66 | .matter-button-unelevated:active::after {
67 | opacity: 0.32;
68 | background-size: 100% 100%;
69 | transition: background-size 0s;
70 | }
71 |
72 | /* Disabled */
73 | .matter-button-unelevated:disabled {
74 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
75 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.12);
76 | cursor: initial;
77 | }
78 |
79 | .matter-button-unelevated:disabled::before,
80 | .matter-button-unelevated:disabled::after {
81 | opacity: 0;
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/progress/linear/progress-linear.css:
--------------------------------------------------------------------------------
1 | .matter-progress-linear {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | -webkit-appearance: none;
4 | -moz-appearance: none;
5 | appearance: none;
6 | border: none;
7 | width: 160px;
8 | height: 4px;
9 | vertical-align: middle;
10 | color: rgb(var(--matter-helper-theme));
11 | background-color: rgba(var(--matter-helper-theme), 0.12);
12 | }
13 |
14 | .matter-progress-linear::-webkit-progress-bar {
15 | background-color: transparent;
16 | }
17 |
18 | /* Determinate */
19 | .matter-progress-linear::-webkit-progress-value {
20 | background-color: currentColor;
21 | transition: all 0.2s;
22 | }
23 |
24 | .matter-progress-linear::-moz-progress-bar {
25 | background-color: currentColor;
26 | transition: all 0.2s;
27 | }
28 |
29 | .matter-progress-linear::-ms-fill {
30 | border: none;
31 | background-color: currentColor;
32 | transition: all 0.2s;
33 | }
34 |
35 | /* Indeterminate */
36 | .matter-progress-linear:indeterminate {
37 | background-size: 200% 100%;
38 | background-image:
39 | linear-gradient(to right, currentColor 16%, transparent 16%),
40 | linear-gradient(to right, currentColor 16%, transparent 16%),
41 | linear-gradient(to right, currentColor 25%, transparent 25%);
42 | animation: matter-progress-linear 1.8s infinite linear;
43 | }
44 |
45 | .matter-progress-linear:indeterminate::-webkit-progress-value {
46 | background-color: transparent;
47 | }
48 |
49 | .matter-progress-linear:indeterminate::-moz-progress-bar {
50 | background-color: transparent;
51 | }
52 |
53 | .matter-progress-linear:indeterminate::-ms-fill {
54 | animation-name: none;
55 | }
56 |
57 | @keyframes matter-progress-linear {
58 | 0% {
59 | background-position: 32% 0, 32% 0, 50% 0;
60 | }
61 | 2% {
62 | background-position: 32% 0, 32% 0, 50% 0;
63 | }
64 | 21% {
65 | background-position: 32% 0, -18% 0, 0 0;
66 | }
67 | 42% {
68 | background-position: 32% 0, -68% 0, -27% 0;
69 | }
70 | 50% {
71 | background-position: 32% 0, -93% 0, -46% 0;
72 | }
73 | 56% {
74 | background-position: 32% 0, -118% 0, -68% 0;
75 | }
76 | 66% {
77 | background-position: -11% 0, -200% 0, -100% 0;
78 | }
79 | 71% {
80 | background-position: -32% 0, -200% 0, -100% 0;
81 | }
82 | 79% {
83 | background-position: -54% 0, -242% 0, -100% 0;
84 | }
85 | 86% {
86 | background-position: -68% 0, -268% 0, -100% 0;
87 | }
88 | 100% {
89 | background-position: -100% 0, -300% 0, -100% 0;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/buttons/contained/button-contained.css:
--------------------------------------------------------------------------------
1 | .matter-button-contained {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255));
4 | position: relative;
5 | display: inline-block;
6 | box-sizing: border-box;
7 | border: none;
8 | border-radius: 4px;
9 | padding: 0 16px;
10 | min-width: 64px;
11 | height: 36px;
12 | vertical-align: middle;
13 | text-align: center;
14 | text-overflow: ellipsis;
15 | color: rgb(var(--matter-helper-ontheme));
16 | background-color: rgb(var(--matter-helper-theme));
17 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
18 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
19 | font-size: 14px;
20 | font-weight: 500;
21 | line-height: 36px;
22 | outline: none;
23 | cursor: pointer;
24 | transition: box-shadow 0.2s;
25 | }
26 |
27 | .matter-button-contained::-moz-focus-inner {
28 | border: none;
29 | }
30 |
31 | /* Highlight, Ripple */
32 | .matter-button-contained::before,
33 | .matter-button-contained::after {
34 | content: "";
35 | position: absolute;
36 | top: 0;
37 | left: 0;
38 | right: 0;
39 | bottom: 0;
40 | border-radius: inherit;
41 | opacity: 0;
42 | }
43 |
44 | .matter-button-contained::before {
45 | background-color: rgb(var(--matter-helper-ontheme));
46 | transition: opacity 0.2s;
47 | }
48 |
49 | .matter-button-contained::after {
50 | background: radial-gradient(circle at center, currentColor 1%, transparent 1%) center/10000% 10000% no-repeat;
51 | transition: opacity 1s, background-size 0.5s;
52 | }
53 |
54 | /* Hover, Focus */
55 | .matter-button-contained:hover,
56 | .matter-button-contained:focus {
57 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
58 | }
59 |
60 | .matter-button-contained:hover::before {
61 | opacity: 0.08;
62 | }
63 |
64 | .matter-button-contained:focus::before {
65 | opacity: 0.24;
66 | }
67 |
68 | .matter-button-contained:hover:focus::before {
69 | opacity: 0.32;
70 | }
71 |
72 | /* Active */
73 | .matter-button-contained:active {
74 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
75 | }
76 |
77 | .matter-button-contained:active::after {
78 | opacity: 0.32;
79 | background-size: 100% 100%;
80 | transition: background-size 0s;
81 | }
82 |
83 | /* Disabled */
84 | .matter-button-contained:disabled {
85 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
86 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.12);
87 | box-shadow: none;
88 | cursor: initial;
89 | }
90 |
91 | .matter-button-contained:disabled::before,
92 | .matter-button-contained:disabled::after {
93 | opacity: 0;
94 | }
95 |
--------------------------------------------------------------------------------
/test/helpers/capture.js:
--------------------------------------------------------------------------------
1 | import { isBrowser } from './browser.js';
2 |
3 | const IS_SAFARI = isBrowser('Safari');
4 |
5 | const capture = (element, style, spacing = 0, pixelDensity) => {
6 | const { width, height } = element.getBoundingClientRect();
7 | const xhtml = new XMLSerializer().serializeToString(element);
8 |
9 | const svg = svgify(xhtml, style, width, height, spacing, pixelDensity);
10 |
11 | const canvas = createCanvas(width + 2 * spacing, height + 2 * spacing, pixelDensity);
12 | const context = canvas.getContext('2d');
13 |
14 |
15 | if (pixelDensity === 1) {
16 | context.getImageData1x =
17 | (sx, sy, sw, sh) => context.getImageData(spacing + sx, spacing + sy, sw, sh);
18 | } else if (pixelDensity === 2) {
19 | context.getImageData2x =
20 | (sx, sy, sw, sh) => context.getImageData(2 * (spacing + sx), 2 * (spacing + sy), 2 * sw, 2 * sh);
21 | } else if (pixelDensity === 3) {
22 | context.getImageData3x =
23 | (sx, sy, sw, sh) => context.getImageData(3 * (spacing + sx), 3 * (spacing + sy), 3 * sw, 3 * sh);
24 | }
25 |
26 | return new Promise((resolve, reject) => {
27 | const image = new Image();
28 | image.onload = () => {
29 | context.drawImage(image, 0, 0);
30 | resolve(context);
31 | };
32 | image.onerror = (event) => {
33 | reject(event);
34 | };
35 |
36 | image.src = `data:image/svg+xml;utf8,${encodeURI(svg)}`;
37 | });
38 | };
39 |
40 | const svgify = (xhtml, style, width, height, spacing, pixelDensity) => {
41 | const svgWidth = (width + 2 * spacing) * pixelDensity;
42 | const svgHeight = (height + 2 * spacing) * pixelDensity;
43 |
44 | const svgViewBox = IS_SAFARI ? `0 0 ${svgWidth} ${svgHeight}` : `0 0 ${width + 2 * spacing} ${height + 2 * spacing}`;
45 | const divPadding = IS_SAFARI ? spacing * pixelDensity : spacing;
46 |
47 | if (IS_SAFARI) {
48 | style = style.replace(/(\d+)px/g, (match, number) => `${number * pixelDensity}px`);
49 | xhtml = xhtml.replace(/(\d+)px/g, (match, number) => `${number * pixelDensity}px`);
50 | }
51 |
52 | return `
53 |
56 |
57 |
58 | ${xhtml}
59 |
60 |
61 | `;
62 | };
63 |
64 | const createCanvas = (width, height, pixelDensity) => {
65 | const canvas = document.createElement('canvas');
66 | canvas.width = width * pixelDensity;
67 | canvas.height = height * pixelDensity;
68 | return canvas;
69 | };
70 |
71 | export const capture1x = (element, style, spacing) => capture(element, style, spacing, 1);
72 | export const capture2x = (element, style, spacing) => capture(element, style, spacing, 2);
73 | export const capture3x = (element, style, spacing) => capture(element, style, spacing, 3);
74 |
--------------------------------------------------------------------------------
/src/utilities/typography/typography.css:
--------------------------------------------------------------------------------
1 | .matter-h1 {
2 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
3 | font-size: 96px;
4 | font-weight: 300;
5 | letter-spacing: -1.5px;
6 | line-height: 120px;
7 | }
8 |
9 | .matter-h2 {
10 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
11 | font-size: 60px;
12 | font-weight: 300;
13 | letter-spacing: -0.5px;
14 | line-height: 80px;
15 | }
16 |
17 | .matter-h3 {
18 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
19 | font-size: 48px;
20 | font-weight: 400;
21 | letter-spacing: 0;
22 | line-height: 64px;
23 | }
24 |
25 | .matter-h4 {
26 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
27 | font-size: 34px;
28 | font-weight: 400;
29 | letter-spacing: 0.25px;
30 | line-height: 48px;
31 | }
32 |
33 | .matter-h5 {
34 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
35 | font-size: 24px;
36 | font-weight: 400;
37 | letter-spacing: 0;
38 | line-height: 36px;
39 | }
40 |
41 | .matter-h6 {
42 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
43 | font-size: 20px;
44 | font-weight: 500;
45 | letter-spacing: 0.15px;
46 | line-height: 28px;
47 | }
48 |
49 | .matter-subtitle1 {
50 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
51 | font-size: 16px;
52 | font-weight: 400;
53 | letter-spacing: 0.15px;
54 | line-height: 24px;
55 | }
56 |
57 | .matter-subtitle2 {
58 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
59 | font-size: 14px;
60 | font-weight: 500;
61 | letter-spacing: 0.1px;
62 | line-height: 20px;
63 | }
64 |
65 | .matter-body1 {
66 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
67 | font-size: 16px;
68 | font-weight: 400;
69 | letter-spacing: 0.5px;
70 | line-height: 24px;
71 | }
72 |
73 | .matter-body2 {
74 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
75 | font-size: 14px;
76 | font-weight: 400;
77 | letter-spacing: 0.25px;
78 | line-height: 20px;
79 | }
80 |
81 | .matter-button {
82 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
83 | font-size: 14px;
84 | font-weight: 500;
85 | letter-spacing: 1.25px;
86 | text-transform: uppercase;
87 | line-height: 20px;
88 | }
89 |
90 | .matter-caption {
91 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
92 | font-size: 12px;
93 | font-weight: 400;
94 | letter-spacing: 0.4px;
95 | line-height: 20px;
96 | }
97 |
98 | .matter-overline {
99 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
100 | font-size: 10px;
101 | font-weight: 400;
102 | letter-spacing: 1.5px;
103 | text-transform: uppercase;
104 | line-height: 16px;
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/selection/radio/radio.css:
--------------------------------------------------------------------------------
1 | .matter-radio {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | z-index: 0;
4 | position: relative;
5 | display: inline-block;
6 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
7 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
8 | font-size: 16px;
9 | line-height: 1.5;
10 | }
11 |
12 | /* Circle */
13 | .matter-radio > input {
14 | appearance: none;
15 | -moz-appearance: none;
16 | -webkit-appearance: none;
17 | z-index: 1;
18 | position: absolute;
19 | display: block;
20 | box-sizing: border-box;
21 | margin: 2px 0;
22 | border: solid 2px; /* Safari */
23 | border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
24 | border-radius: 50%;
25 | width: 20px;
26 | height: 20px;
27 | outline: none;
28 | cursor: pointer;
29 | transition: border-color 0.2s;
30 | }
31 |
32 | /* Span */
33 | .matter-radio > input + span {
34 | display: inline-block;
35 | box-sizing: border-box;
36 | padding-left: 30px;
37 | width: inherit;
38 | cursor: pointer;
39 | }
40 |
41 | /* Highlight */
42 | .matter-radio > input + span::before {
43 | content: "";
44 | position: absolute;
45 | left: -10px;
46 | top: -8px;
47 | display: block;
48 | border-radius: 50%;
49 | width: 40px;
50 | height: 40px;
51 | background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0));
52 | opacity: 0;
53 | transform: scale(0);
54 | pointer-events: none;
55 | transition: opacity 0.3s, transform 0.2s;
56 | }
57 |
58 | /* Check Mark */
59 | .matter-radio > input + span::after {
60 | content: "";
61 | display: block;
62 | position: absolute;
63 | top: 2px;
64 | left: 0;
65 | border-radius: 50%;
66 | width: 10px;
67 | height: 10px;
68 | background-color: rgb(var(--matter-helper-theme));
69 | transform: translate(5px, 5px) scale(0);
70 | transition: transform 0.2s;
71 | }
72 |
73 | /* Checked */
74 | .matter-radio > input:checked {
75 | border-color: rgb(var(--matter-helper-theme));
76 | }
77 |
78 | .matter-radio > input:checked + span::before {
79 | background-color: rgb(var(--matter-helper-theme));
80 | }
81 |
82 | .matter-radio > input:checked + span::after {
83 | transform: translate(5px, 5px) scale(1);
84 | }
85 |
86 | /* Hover, Focus */
87 | .matter-radio:hover > input + span::before {
88 | transform: scale(1);
89 | opacity: 0.04;
90 | }
91 |
92 | .matter-radio > input:focus + span::before {
93 | transform: scale(1);
94 | opacity: 0.12;
95 | }
96 |
97 | .matter-radio:hover > input:focus + span::before {
98 | transform: scale(1);
99 | opacity: 0.16;
100 | }
101 |
102 | /* Active */
103 | .matter-radio:active > input {
104 | border-color: rgb(var(--matter-helper-theme));
105 | }
106 |
107 | .matter-radio:active > input + span::before,
108 | .matter-radio:active:hover > input + span::before {
109 | opacity: 1;
110 | transform: scale(0);
111 | transition: transform 0s, opacity 0s;
112 | }
113 |
114 | /* Disabled */
115 | .matter-radio > input:disabled {
116 | border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
117 | cursor: initial;
118 | }
119 |
120 | .matter-radio > input:disabled + span {
121 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
122 | cursor: initial;
123 | }
124 |
125 | .matter-radio > input:disabled + span::before {
126 | opacity: 0;
127 | transform: scale(0);
128 | }
129 |
130 | .matter-radio > input:disabled + span::after {
131 | background-color: currentColor;
132 | }
133 |
--------------------------------------------------------------------------------
/src/components/progress/circular/progress-circular.css:
--------------------------------------------------------------------------------
1 | .matter-progress-circular {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | -webkit-appearance: none;
4 | -moz-appearance: none;
5 | appearance: none;
6 | box-sizing: border-box;
7 | border: none;
8 | border-radius: 50%;
9 | padding: 0.25em;
10 | width: 3em;
11 | height: 3em;
12 | color: rgb(var(--matter-helper-theme));
13 | background-color: transparent;
14 | font-size: 16px;
15 | overflow: hidden;
16 | }
17 |
18 | .matter-progress-circular::-webkit-progress-bar {
19 | background-color: transparent;
20 | }
21 |
22 | /* Indeterminate */
23 | .matter-progress-circular:indeterminate {
24 | animation: matter-progress-circular 6s infinite cubic-bezier(0.3, 0.6, 1, 1);
25 | }
26 |
27 | :-ms-lang(x), .matter-progress-circular:indeterminate {
28 | animation: none;
29 | }
30 |
31 | .matter-progress-circular:indeterminate::before,
32 | .matter-progress-circular:indeterminate::-webkit-progress-value {
33 | content: "";
34 | display: block;
35 | box-sizing: border-box;
36 | margin-bottom: 0.25em;
37 | border: solid 0.25em currentColor;
38 | border-radius: 50%;
39 | width: 100% !important;
40 | height: 100%;
41 | background-color: transparent;
42 | -webkit-clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0);
43 | clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0);
44 | animation: matter-progress-circular-pseudo 0.75s infinite linear alternate;
45 | animation-play-state: inherit;
46 | animation-delay: inherit;
47 | }
48 |
49 | .matter-progress-circular:indeterminate::-moz-progress-bar {
50 | box-sizing: border-box;
51 | border: solid 0.25em currentColor;
52 | border-radius: 50%;
53 | width: 100%;
54 | height: 100%;
55 | background-color: transparent;
56 | clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0);
57 | animation: matter-progress-circular-pseudo 0.75s infinite linear alternate;
58 | animation-play-state: inherit;
59 | animation-delay: inherit;
60 | }
61 |
62 | .matter-progress-circular:indeterminate::-ms-fill {
63 | animation-name: -ms-ring;
64 | }
65 |
66 | @keyframes matter-progress-circular {
67 | 0% {
68 | transform: rotate(0deg);
69 | }
70 | 12.5% {
71 | transform: rotate(180deg);
72 | animation-timing-function: linear;
73 | }
74 | 25% {
75 | transform: rotate(630deg);
76 | }
77 | 37.5% {
78 | transform: rotate(810deg);
79 | animation-timing-function: linear;
80 | }
81 | 50% {
82 | transform: rotate(1260deg);
83 | }
84 | 62.5% {
85 | transform: rotate(1440deg);
86 | animation-timing-function: linear;
87 | }
88 | 75% {
89 | transform: rotate(1890deg);
90 | }
91 | 87.5% {
92 | transform: rotate(2070deg);
93 | animation-timing-function: linear;
94 | }
95 | 100% {
96 | transform: rotate(2520deg);
97 | }
98 | }
99 |
100 | @keyframes matter-progress-circular-pseudo {
101 | 0% {
102 | -webkit-clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0);
103 | clip-path: polygon(50% 50%, 37% 0, 50% 0, 50% 0, 50% 0, 50% 0);
104 | }
105 | 18% {
106 | -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 0, 100% 0, 100% 0);
107 | clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 0, 100% 0, 100% 0);
108 | }
109 | 53% {
110 | -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 100% 100%, 100% 100%);
111 | clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 100% 100%, 100% 100%);
112 | }
113 | 88% {
114 | -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 100%);
115 | clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 100%);
116 | }
117 | 100% {
118 | -webkit-clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 63%);
119 | clip-path: polygon(50% 50%, 37% 0, 100% 0, 100% 100%, 0 100%, 0 63%);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/textfields/standard/textfield-standard.css:
--------------------------------------------------------------------------------
1 | .matter-textfield-standard {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | position: relative;
4 | display: inline-block;
5 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
6 | font-size: 16px;
7 | line-height: 1.5;
8 | }
9 |
10 | /* Input, Textarea */
11 | .matter-textfield-standard > input,
12 | .matter-textfield-standard > textarea {
13 | display: block;
14 | box-sizing: border-box;
15 | margin: 0;
16 | border: none;
17 | border-top: solid 24px transparent;
18 | border-bottom: solid 1px rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
19 | padding: 0 0 7px;
20 | width: 100%;
21 | height: inherit;
22 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
23 | -webkit-text-fill-color: currentColor; /* Safari */
24 | background-color: transparent;
25 | box-shadow: none; /* Firefox */
26 | font-family: inherit;
27 | font-size: inherit;
28 | line-height: inherit;
29 | caret-color: rgb(var(--matter-helper-theme));
30 | transition: border-bottom 0.2s, background-color 0.2s;
31 | }
32 |
33 | /* Span */
34 | .matter-textfield-standard > input + span,
35 | .matter-textfield-standard > textarea + span {
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | right: 0;
40 | bottom: 0;
41 | display: block;
42 | box-sizing: border-box;
43 | padding: 7px 0 0;
44 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
45 | font-size: 75%;
46 | line-height: 18px;
47 | pointer-events: none;
48 | transition: color 0.2s, font-size 0.2s, line-height 0.2s;
49 | }
50 |
51 | /* Underline */
52 | .matter-textfield-standard > input + span::after,
53 | .matter-textfield-standard > textarea + span::after {
54 | content: "";
55 | position: absolute;
56 | left: 0;
57 | bottom: 0;
58 | display: block;
59 | width: 100%;
60 | height: 2px;
61 | background-color: rgb(var(--matter-helper-theme));
62 | transform-origin: bottom center;
63 | transform: scaleX(0);
64 | transition: transform 0.2s;
65 | }
66 |
67 | /* Hover */
68 | .matter-textfield-standard:hover > input,
69 | .matter-textfield-standard:hover > textarea {
70 | border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
71 | }
72 |
73 | /* Placeholder-shown */
74 | .matter-textfield-standard > input:not(:focus):placeholder-shown + span,
75 | .matter-textfield-standard > textarea:not(:focus):placeholder-shown + span {
76 | font-size: inherit;
77 | line-height: 56px;
78 | }
79 |
80 | /* Focus */
81 | .matter-textfield-standard > input:focus,
82 | .matter-textfield-standard > textarea:focus {
83 | outline: none;
84 | }
85 |
86 | .matter-textfield-standard > input:focus + span,
87 | .matter-textfield-standard > textarea:focus + span {
88 | color: rgb(var(--matter-helper-theme));
89 | }
90 |
91 | .matter-textfield-standard > input:focus + span::after,
92 | .matter-textfield-standard > textarea:focus + span::after {
93 | transform: scale(1);
94 | }
95 |
96 | /* Disabled */
97 | .matter-textfield-standard > input:disabled,
98 | .matter-textfield-standard > textarea:disabled {
99 | border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
100 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
101 | }
102 |
103 | .matter-textfield-standard > input:disabled + span,
104 | .matter-textfield-standard > textarea:disabled + span {
105 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
106 | }
107 |
108 | /* Faster transition in Safari for less noticable fractional font-size issue */
109 | @media not all and (min-resolution:.001dpcm) {
110 | @supports (-webkit-appearance:none) {
111 | .matter-textfield-standard > input,
112 | .matter-textfield-standard > input + span,
113 | .matter-textfield-standard > input + span::after,
114 | .matter-textfield-standard > textarea,
115 | .matter-textfield-standard > textarea + span,
116 | .matter-textfield-standard > textarea + span::after {
117 | transition-duration: 0.1s;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/selection/switch/switch.css:
--------------------------------------------------------------------------------
1 | .matter-switch {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | z-index: 0;
4 | position: relative;
5 | display: inline-block;
6 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
7 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
8 | font-size: 16px;
9 | line-height: 1.5;
10 | }
11 |
12 | /* Track */
13 | .matter-switch > input {
14 | appearance: none;
15 | -moz-appearance: none;
16 | -webkit-appearance: none;
17 | z-index: 1;
18 | position: relative;
19 | float: right;
20 | display: inline-block;
21 | margin: 0 0 0 5px;
22 | border: solid 5px transparent;
23 | border-radius: 12px;
24 | width: 46px;
25 | height: 24px;
26 | background-clip: padding-box;
27 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
28 | outline: none;
29 | cursor: pointer;
30 | transition: background-color 0.2s, opacity 0.2s;
31 | }
32 |
33 | /* Span */
34 | .matter-switch > input + span {
35 | display: inline-block;
36 | box-sizing: border-box;
37 | margin-right: -51px;
38 | padding-right: 51px;
39 | width: inherit;
40 | cursor: pointer;
41 | }
42 |
43 | /* Highlight */
44 | .matter-switch > input + span::before {
45 | content: "";
46 | position: absolute;
47 | right: 11px;
48 | top: -8px;
49 | display: block;
50 | border-radius: 50%;
51 | width: 40px;
52 | height: 40px;
53 | background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0));
54 | opacity: 0;
55 | transform: scale(1);
56 | pointer-events: none;
57 | transition: opacity 0.3s 0.1s, transform 0.2s 0.1s;
58 | }
59 |
60 | /* Thumb */
61 | .matter-switch > input + span::after {
62 | content: "";
63 | z-index: 1;
64 | position: absolute;
65 | top: 2px;
66 | right: 21px;
67 | border-radius: 50%;
68 | width: 20px;
69 | height: 20px;
70 | background-color: rgb(var(--matter-surface-rgb, 255, 255, 255));
71 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
72 | pointer-events: none;
73 | transition: background-color 0.2s, transform 0.2s;
74 | }
75 |
76 | /* Checked */
77 | .matter-switch > input:checked {
78 | background-color: rgba(var(--matter-helper-theme), 0.6);
79 | }
80 |
81 | .matter-switch > input:checked + span::before {
82 | right: -5px;
83 | background-color: rgb(var(--matter-helper-theme));
84 | }
85 |
86 | .matter-switch > input:checked + span::after {
87 | background-color: rgb(var(--matter-helper-theme));
88 | transform: translateX(16px);
89 | }
90 |
91 | /* Hover, Focus */
92 | .matter-switch:hover > input + span::before {
93 | opacity: 0.04;
94 | }
95 |
96 | .matter-switch > input:focus + span::before {
97 | opacity: 0.12;
98 | }
99 |
100 | .matter-switch:hover > input:focus + span::before {
101 | opacity: 0.16;
102 | }
103 |
104 | /* Active */
105 | .matter-switch:active > input {
106 | background-color: rgba(var(--matter-helper-theme), 0.6);
107 | }
108 |
109 | .matter-switch:active > input:checked {
110 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
111 | }
112 |
113 | .matter-switch:active > input + span::before {
114 | opacity: 1;
115 | transform: scale(0);
116 | transition: transform 0s, opacity 0s;
117 | }
118 |
119 | /* Disabled */
120 | .matter-switch > input:disabled {
121 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
122 | opacity: 0.38;
123 | cursor: default;
124 | }
125 |
126 | .matter-switch > input:checked:disabled {
127 | background-color: rgba(var(--matter-helper-theme), 0.6);
128 | }
129 |
130 | .matter-switch > input:disabled + span {
131 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0, 0.38));
132 | cursor: default;
133 | }
134 |
135 | .matter-switch > input:disabled + span::before {
136 | z-index: 1;
137 | margin: 10px;
138 | width: 20px;
139 | height: 20px;
140 | background-color: rgb(var(--matter-surface-rgb, 255, 255, 255));
141 | transform: scale(1);
142 | opacity: 1;
143 | transition: none;
144 | }
145 |
146 | .matter-switch > input:disabled + span::after {
147 | opacity: 0.38;
148 | }
149 |
--------------------------------------------------------------------------------
/src/components/textfields/filled/textfield-filled.css:
--------------------------------------------------------------------------------
1 | .matter-textfield-filled {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | position: relative;
4 | display: inline-block;
5 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
6 | font-size: 16px;
7 | line-height: 1.5;
8 | }
9 |
10 | /* Input, Textarea */
11 | .matter-textfield-filled > input,
12 | .matter-textfield-filled > textarea {
13 | display: block;
14 | box-sizing: border-box;
15 | margin: 0;
16 | border: none;
17 | border-top: solid 24px transparent;
18 | border-bottom: solid 1px rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
19 | border-radius: 4px 4px 0 0;
20 | padding: 0 12px 7px;
21 | width: 100%;
22 | height: inherit;
23 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
24 | -webkit-text-fill-color: currentColor; /* Safari */
25 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.04);
26 | box-shadow: none; /* Firefox */
27 | font-family: inherit;
28 | font-size: inherit;
29 | line-height: inherit;
30 | caret-color: rgb(var(--matter-helper-theme));
31 | transition: border-bottom 0.2s, background-color 0.2s;
32 | }
33 |
34 | /* Span */
35 | .matter-textfield-filled > input + span,
36 | .matter-textfield-filled > textarea + span {
37 | position: absolute;
38 | top: 0;
39 | left: 0;
40 | right: 0;
41 | bottom: 0;
42 | display: block;
43 | box-sizing: border-box;
44 | padding: 7px 12px 0;
45 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
46 | font-size: 75%;
47 | line-height: 18px;
48 | pointer-events: none;
49 | transition: color 0.2s, font-size 0.2s, line-height 0.2s;
50 | }
51 |
52 | /* Underline */
53 | .matter-textfield-filled > input + span::after,
54 | .matter-textfield-filled > textarea + span::after {
55 | content: "";
56 | position: absolute;
57 | left: 0;
58 | bottom: 0;
59 | display: block;
60 | width: 100%;
61 | height: 2px;
62 | background-color: rgb(var(--matter-helper-theme));
63 | transform-origin: bottom center;
64 | transform: scaleX(0);
65 | transition: transform 0.3s;
66 | }
67 |
68 | /* Hover */
69 | .matter-textfield-filled:hover > input,
70 | .matter-textfield-filled:hover > textarea {
71 | border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
72 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.08);
73 | }
74 |
75 | /* Placeholder-shown */
76 | .matter-textfield-filled > input:not(:focus):placeholder-shown + span,
77 | .matter-textfield-filled > textarea:not(:focus):placeholder-shown + span {
78 | font-size: inherit;
79 | line-height: 48px;
80 | }
81 |
82 | /* Focus */
83 | .matter-textfield-filled > input:focus,
84 | .matter-textfield-filled > textarea:focus {
85 | outline: none;
86 | }
87 |
88 | .matter-textfield-filled > input:focus + span,
89 | .matter-textfield-filled > textarea:focus + span {
90 | color: rgb(var(--matter-helper-theme));
91 | }
92 |
93 | .matter-textfield-filled > input:focus + span::after,
94 | .matter-textfield-filled > textarea:focus + span::after {
95 | transform: scale(1);
96 | }
97 |
98 | /* Disabled */
99 | .matter-textfield-filled > input:disabled,
100 | .matter-textfield-filled > textarea:disabled {
101 | border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
102 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
103 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.24);
104 | }
105 |
106 | .matter-textfield-filled > input:disabled + span,
107 | .matter-textfield-filled > textarea:disabled + span {
108 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
109 | }
110 |
111 | /* Faster transition in Safari for less noticable fractional font-size issue */
112 | @media not all and (min-resolution:.001dpcm) {
113 | @supports (-webkit-appearance:none) {
114 | .matter-textfield-filled > input,
115 | .matter-textfield-filled > input + span,
116 | .matter-textfield-filled > input + span::after,
117 | .matter-textfield-filled > textarea,
118 | .matter-textfield-filled > textarea + span,
119 | .matter-textfield-filled > textarea + span::after {
120 | transition-duration: 0.1s;
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/selection/checkbox/checkbox.css:
--------------------------------------------------------------------------------
1 | .matter-checkbox {
2 | --matter-helper-theme: var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243));
3 | --matter-helper-ontheme: var(--matter-ontheme-rgb, var(--matter-onprimary-rgb, 255, 255, 255));
4 | z-index: 0;
5 | position: relative;
6 | display: inline-block;
7 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
8 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
9 | font-size: 16px;
10 | line-height: 1.5;
11 | }
12 |
13 | /* Box */
14 | .matter-checkbox > input {
15 | appearance: none;
16 | -moz-appearance: none;
17 | -webkit-appearance: none;
18 | z-index: 1;
19 | position: absolute;
20 | display: block;
21 | box-sizing: border-box;
22 | margin: 3px 1px;
23 | border: solid 2px; /* Safari */
24 | border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
25 | border-radius: 2px;
26 | width: 18px;
27 | height: 18px;
28 | outline: none;
29 | cursor: pointer;
30 | transition: border-color 0.2s, background-color 0.2s;
31 | }
32 |
33 | /* Span */
34 | .matter-checkbox > input + span {
35 | display: inline-block;
36 | box-sizing: border-box;
37 | padding-left: 30px;
38 | width: inherit;
39 | cursor: pointer;
40 | }
41 |
42 | /* Highlight */
43 | .matter-checkbox > input + span::before {
44 | content: "";
45 | position: absolute;
46 | left: -10px;
47 | top: -8px;
48 | display: block;
49 | border-radius: 50%;
50 | width: 40px;
51 | height: 40px;
52 | background-color: rgb(var(--matter-onsurface-rgb, 0, 0, 0));
53 | opacity: 0;
54 | transform: scale(1);
55 | pointer-events: none;
56 | transition: opacity 0.3s, transform 0.2s;
57 | }
58 |
59 | /* Check Mark */
60 | .matter-checkbox > input + span::after {
61 | content: "";
62 | z-index: 1;
63 | display: block;
64 | position: absolute;
65 | top: 3px;
66 | left: 1px;
67 | box-sizing: content-box;
68 | width: 10px;
69 | height: 5px;
70 | border: solid 2px transparent;
71 | border-right-width: 0;
72 | border-top-width: 0;
73 | pointer-events: none;
74 | transform: translate(3px, 4px) rotate(-45deg);
75 | transition: border-color 0.2s;
76 | }
77 |
78 | /* Checked, Indeterminate */
79 | .matter-checkbox > input:checked,
80 | .matter-checkbox > input:indeterminate {
81 | border-color: rgb(var(--matter-helper-theme));
82 | background-color: rgb(var(--matter-helper-theme));
83 | }
84 |
85 | .matter-checkbox > input:checked + span::before,
86 | .matter-checkbox > input:indeterminate + span::before {
87 | background-color: rgb(var(--matter-helper-theme));
88 | }
89 |
90 | .matter-checkbox > input:checked + span::after,
91 | .matter-checkbox > input:indeterminate + span::after {
92 | border-color: rgb(var(--matter-helper-ontheme, 255, 255, 255));
93 | }
94 |
95 | .matter-checkbox > input:indeterminate + span::after {
96 | border-left-width: 0;
97 | transform: translate(4px, 3px);
98 | }
99 |
100 | /* Hover, Focus */
101 | .matter-checkbox:hover > input + span::before {
102 | opacity: 0.04;
103 | }
104 |
105 | .matter-checkbox > input:focus + span::before {
106 | opacity: 0.12;
107 | }
108 |
109 | .matter-checkbox:hover > input:focus + span::before {
110 | opacity: 0.16;
111 | }
112 |
113 | /* Active */
114 | .matter-checkbox:active > input,
115 | .matter-checkbox:active:hover > input {
116 | border-color: rgb(var(--matter-helper-theme));
117 | }
118 |
119 | .matter-checkbox:active > input:checked {
120 | border-color: transparent;
121 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
122 | }
123 |
124 | .matter-checkbox:active > input + span::before {
125 | opacity: 1;
126 | transform: scale(0);
127 | transition: transform 0s, opacity 0s;
128 | }
129 |
130 | /* Disabled */
131 | .matter-checkbox > input:disabled {
132 | border-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
133 | cursor: initial;
134 | }
135 |
136 | .matter-checkbox > input:checked:disabled,
137 | .matter-checkbox > input:indeterminate:disabled {
138 | border-color: transparent;
139 | background-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
140 | }
141 |
142 | .matter-checkbox > input:disabled + span {
143 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
144 | cursor: initial;
145 | }
146 |
147 | .matter-checkbox > input:disabled + span::before {
148 | opacity: 0;
149 | transform: scale(0);
150 | }
151 |
--------------------------------------------------------------------------------
/src/components/links/link.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../test/helpers/capture.js';
3 | import { isBrowser, isBrowserNot } from '../../../test/helpers/browser.js';
4 |
5 | const SPACING = 4;
6 |
7 | // transparent
8 | const tp = { a: 0 };
9 |
10 | describe('Link', () => {
11 |
12 | [
13 | {
14 | label: 'normal',
15 | states: {},
16 | textColor: { r: 33, g: 150, b: 243, a: 255 },
17 | bodyColor: tp
18 | },
19 | {
20 | label: 'hover',
21 | states: {
22 | '#xmas.matter-link': [ 'hover' ]
23 | },
24 | textColor: { r: 33, g: 150, b: 243, a: 255 },
25 | bodyColor: tp,
26 | underline: true
27 | },
28 | {
29 | label: 'focus',
30 | states: {
31 | '#xmas.matter-link': [ 'focus' ]
32 | },
33 | textColor: { r: 33, g: 150, b: 243, a: 255 },
34 | bodyColor: { r: [17, 41], g: [136, 162], b: [237, 255], a: [28, 34] }
35 | },
36 | {
37 | label: 'active',
38 | states: {
39 | '#xmas.matter-link': [ 'active' ]
40 | },
41 | textColor: { r: 33, g: 150, b: 243, a: 255 },
42 | bodyColor: tp,
43 | },
44 | {
45 | label: 'hover & focus',
46 | states: {
47 | '#xmas.matter-link': [ 'hover', 'focus' ]
48 | },
49 | textColor: { r: 33, g: 150, b: 243, a: 255 },
50 | bodyColor: { r: [17, 41], g: [136, 162], b: [237, 255], a: [28, 34] },
51 | underline: true
52 | },
53 | {
54 | label: 'hover & active',
55 | states: {
56 | '#xmas.matter-link': [ 'hover', 'active' ]
57 | },
58 | textColor: { r: 33, g: 150, b: 243, a: 255 },
59 | bodyColor: tp,
60 | underline: true
61 | },
62 | {
63 | label: 'focus & active',
64 | states: {
65 | '#xmas.matter-link': [ 'focus', 'active' ]
66 | },
67 | textColor: { r: 33, g: 150, b: 243, a: 255 },
68 | bodyColor: tp
69 | },
70 | {
71 | label: 'hover, focus & active',
72 | states: {
73 | '#xmas.matter-link': [ 'hover', 'focus', 'active' ]
74 | },
75 | textColor: { r: 33, g: 150, b: 243, a: 255 },
76 | bodyColor: tp,
77 | underline: true
78 | },
79 | {
80 | label: 'customized',
81 | states: {
82 | '#xmas.matter-link': {
83 | style: '--matter-primary-rgb: 255, 0, 0;display: inline-block; width: 80px; font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system; font-size: 16px;'
84 | }
85 | },
86 | textColor: { r: 255, g: 0, b: 0, a: 255 },
87 | bodyColor: tp
88 | },
89 | {
90 | label: 'customized & focus',
91 | states: {
92 | '#xmas.matter-link': {
93 | style: '--matter-primary-rgb: 255, 0, 0;display: inline-block; width: 80px; font-family: "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system; font-size: 16px;',
94 | focus: ''
95 | }
96 | },
97 | textColor: { r: 255, g: 0, b: 0, a: 255 },
98 | bodyColor: { r: [254, 255], g: 0, b: 0, a: [28, 34] },
99 | }
100 | ].forEach((suite) => {
101 |
102 | describe(`in ${suite.label} state`, () => {
103 |
104 | let style;
105 | let link;
106 | let width;
107 | let height;
108 | let context;
109 |
110 | beforeAll(async () => {
111 | style = setUp('src/components/links/link', suite.states);
112 |
113 | link = document.querySelector('#xmas');
114 | const rect = link.getBoundingClientRect();
115 | width = rect.width;
116 | height = rect.height;
117 | context = await capture3x(link, style, SPACING);
118 | });
119 |
120 | afterAll(() => {
121 | tearDown();
122 | });
123 |
124 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
125 | const component = context.getImageData3x(-4, -4, width + 8, height + 8);
126 |
127 | expect(component).toResembleColor(suite.bodyColor);
128 | });
129 |
130 | it('should have caption text', () => {
131 | const caption = context.getImageData3x(0, 1, width, 15);
132 |
133 | expect(link.innerText).toBe('Xmas Tree');
134 | expect(caption).toResembleText('Xmas Tree', suite.textColor, suite.bodyColor);
135 | });
136 |
137 | it(`should${suite.underline ? '' : ' not'} have underline`, () => {
138 | const underline = context.getImageData3x(0, 16, width, 1);
139 | const underlineFF = context.getImageData3x(0, 18, width, 1);
140 |
141 | isBrowserNot('Firefox') && expect(underline).toResembleColor(suite.underline ? suite.textColor : suite.bodyColor);
142 | isBrowser('Firefox') && expect(underlineFF).toResembleColor(suite.underline ? suite.textColor : suite.bodyColor);
143 | });
144 |
145 | });
146 |
147 | });
148 |
149 | });
150 |
--------------------------------------------------------------------------------
/src/components/progress/linear/progress-linear.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 4;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Linear Progress', () => {
10 |
11 | const baseColor = { r: [31, 34], g: [145, 153], b: [242, 247] };
12 |
13 | [
14 | {
15 | label: 'determinate 0% progress',
16 | states: {
17 | '#xmas': {
18 | value: 0
19 | }
20 | },
21 | fill: [ 0, 480 ],
22 | barColor: { ...baseColor, a: [30, 31]},
23 | fillColor: { ...baseColor, a: 255}
24 | },
25 | {
26 | label: 'determinate 25% progress',
27 | states: {
28 | '#xmas': {
29 | value: 25
30 | }
31 | },
32 | fill: [ 120, 360 ],
33 | barColor: { ...baseColor, a: [30, 31]},
34 | fillColor: { ...baseColor, a: 255}
35 | },
36 | {
37 | label: 'determinate 50% progress',
38 | states: {
39 | '#xmas': {
40 | value: 50
41 | }
42 | },
43 | fill: [ 240, 240 ],
44 | barColor: { ...baseColor, a: [30, 31]},
45 | fillColor: { ...baseColor, a: 255}
46 | },
47 | {
48 | label: 'determinate 75% progress',
49 | states: {
50 | '#xmas': {
51 | value: 75
52 | }
53 | },
54 | fill: [ 360, 120 ],
55 | barColor: { ...baseColor, a: [30, 31]},
56 | fillColor: { ...baseColor, a: 255}
57 | },
58 | {
59 | label: 'determinate 100% progress',
60 | states: {
61 | '#xmas': {
62 | value: 100
63 | }
64 | },
65 | fill: [ 480, 0 ],
66 | barColor: { ...baseColor, a: [30, 31]},
67 | fillColor: { ...baseColor, a: 255}
68 | },
69 | {
70 | label: 'indeterminate 0% animation',
71 | states: {
72 | '#xmas': {
73 | indeterminate: '',
74 | style: 'animation-delay: 0s; animation-play-state: paused; vertical-align: top;'
75 | }
76 | },
77 | fill: [ 0, 480 ],
78 | barColor: { ...baseColor, a: [30, 31]},
79 | fillColor: { ...baseColor, a: 255}
80 | },
81 | {
82 | label: 'indeterminate 25% animation',
83 | states: {
84 | '#xmas': {
85 | indeterminate: '',
86 | style: 'animation-delay: -0.45s; animation-play-state: paused; vertical-align: top;'
87 | }
88 | },
89 | fill: [ 0, 24, 262, 194 ],
90 | barColor: { ...baseColor, a: [30, 31]},
91 | fillColor: { ...baseColor, a: 255}
92 | },
93 | {
94 | label: 'indeterminate 50% animation',
95 | states: {
96 | '#xmas': {
97 | indeterminate: '',
98 | style: 'animation-delay: -0.9s; animation-play-state: paused; vertical-align: top;'
99 | }
100 | },
101 | fill: [ 0, 221, 259 ],
102 | barColor: { ...baseColor, a: [30, 31]},
103 | fillColor: { ...baseColor, a: 255}
104 | },
105 | {
106 | label: 'indeterminate 75% animation',
107 | states: {
108 | '#xmas': {
109 | indeterminate: '',
110 | style: 'animation-delay: -1.35s; animation-play-state: paused; vertical-align: top;'
111 | }
112 | },
113 | fill: [ 0, 100, 260, 120 ],
114 | barColor: { ...baseColor, a: [30, 31]},
115 | fillColor: { ...baseColor, a: 255}
116 | },
117 | {
118 | label: 'indeterminate 100% animation',
119 | states: {
120 | '#xmas': {
121 | indeterminate: '',
122 | style: 'animation-delay: -1.8s; animation-play-state: paused; vertical-align: top;'
123 | }
124 | },
125 | fill: [ 0, 480 ],
126 | barColor: { ...baseColor, a: [30, 31]},
127 | fillColor: { ...baseColor, a: 255}
128 | },
129 | {
130 | label: 'customized & determinate 50% progress',
131 | states: {
132 | '#xmas': {
133 | value: 50,
134 | style: 'vertical-align: top;--matter-primary-rgb: 255, 0, 0;'
135 | }
136 | },
137 | fill: [ 240, 240 ],
138 | barColor: {r: [254, 255], g: 0, b: 0, a: [30, 31]},
139 | fillColor: {r: 255, g: 0, b: 0, a: 255}
140 | },
141 |
142 | ].forEach((suite) => {
143 |
144 | describe(`in ${suite.label} state`, () => {
145 |
146 | let style;
147 | let progress;
148 | let width;
149 | let height;
150 | let context;
151 |
152 | beforeAll(async () => {
153 | style = setUp('src/components/progress/linear/progress-linear', suite.states);
154 |
155 | progress = document.querySelector('#xmas');
156 | const rect = progress.getBoundingClientRect();
157 | width = rect.width;
158 | height = rect.height;
159 | context = await capture3x(progress, style, SPACING);
160 | });
161 |
162 | afterAll(() => {
163 | tearDown();
164 | });
165 |
166 | it(`should have corresponding fill`, () => {
167 | const im = { ...suite.barColor, a: [ suite.barColor.a[0], suite.fillColor.a ] };
168 |
169 | const row = suite.fill.reduce((array, length, index) => {
170 | const segment = new Array(length).fill(index % 2 ? suite.barColor : suite.fillColor);
171 | if (segment.length) {
172 | segment[0] = im;
173 | segment[length - 1] = im;
174 | }
175 |
176 | return array.concat(segment);
177 | }, []);
178 |
179 | const shape = context.getImageData3x(0, 0, width, 4);
180 |
181 | expect(shape).toResembleOblongShape(row, 270);
182 | });
183 |
184 | it('should have no shadow', () => {
185 | const shadow = [ tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp ];
186 |
187 | const topShadow = context.getImageData3x(-4, -4, width + 4, 4);
188 | const rightShadow = context.getImageData3x(width, -4, 4, height + 4);
189 | const bottomShadow = context.getImageData3x(0, 4, width + 4, 4);
190 | const leftShadow = context.getImageData3x(-4, 0, 4, height + 4);
191 |
192 | expect(topShadow).toResembleOblongShape(shadow, 0);
193 | expect(rightShadow).toResembleOblongShape(shadow, 90);
194 | expect(bottomShadow).toResembleOblongShape(shadow, 180);
195 | expect(leftShadow).toResembleOblongShape(shadow, 270);
196 | });
197 |
198 | });
199 |
200 | });
201 |
202 | });
203 |
--------------------------------------------------------------------------------
/src/components/textfields/outlined/textfield-outlined.css:
--------------------------------------------------------------------------------
1 | .matter-textfield-outlined {
2 | --matter-helper-theme: rgb(var(--matter-theme-rgb, var(--matter-primary-rgb, 33, 150, 243)));
3 | --matter-helper-safari1: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
4 | --matter-helper-safari2: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
5 | --matter-helper-safari3: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
6 | position: relative;
7 | display: inline-block;
8 | padding-top: 6px;
9 | font-family: var(--matter-font-family, "Roboto", "Segoe UI", BlinkMacSystemFont, system-ui, -apple-system);
10 | font-size: 16px;
11 | line-height: 1.5;
12 | }
13 |
14 | /* Input, Textarea */
15 | .matter-textfield-outlined > input,
16 | .matter-textfield-outlined > textarea {
17 | box-sizing: border-box;
18 | margin: 0;
19 | border-style: solid;
20 | border-width: 1px;
21 | border-color: transparent var(--matter-helper-safari2) var(--matter-helper-safari2);
22 | border-radius: 4px;
23 | padding: 15px 13px 15px;
24 | width: 100%;
25 | height: inherit;
26 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.87);
27 | -webkit-text-fill-color: currentColor; /* Safari */
28 | background-color: transparent;
29 | box-shadow: inset 1px 0 transparent, inset -1px 0 transparent, inset 0 -1px transparent;
30 | font-family: inherit;
31 | font-size: inherit;
32 | line-height: inherit;
33 | caret-color: var(--matter-helper-theme);
34 | transition: border 0.2s, box-shadow 0.2s;
35 | }
36 |
37 | .matter-textfield-outlined > input:not(:focus):placeholder-shown,
38 | .matter-textfield-outlined > textarea:not(:focus):placeholder-shown {
39 | border-top-color: var(--matter-helper-safari2);
40 | }
41 |
42 | /* Span */
43 | .matter-textfield-outlined > input + span,
44 | .matter-textfield-outlined > textarea + span {
45 | position: absolute;
46 | top: 0;
47 | left: 0;
48 | display: flex;
49 | width: 100%;
50 | max-height: 100%;
51 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6);
52 | font-size: 75%;
53 | line-height: 15px;
54 | cursor: text;
55 | transition: color 0.2s, font-size 0.2s, line-height 0.2s;
56 | }
57 |
58 | .matter-textfield-outlined > input:not(:focus):placeholder-shown + span,
59 | .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span {
60 | font-size: inherit;
61 | line-height: 68px;
62 | }
63 |
64 | /* Corners */
65 | .matter-textfield-outlined > input + span::before,
66 | .matter-textfield-outlined > input + span::after,
67 | .matter-textfield-outlined > textarea + span::before,
68 | .matter-textfield-outlined > textarea + span::after {
69 | content: "";
70 | display: block;
71 | box-sizing: border-box;
72 | margin-top: 6px;
73 | border-top: solid 1px var(--matter-helper-safari2);
74 | min-width: 10px;
75 | height: 8px;
76 | pointer-events: none;
77 | box-shadow: inset 0 1px transparent;
78 | transition: border 0.2s, box-shadow 0.2s;
79 | }
80 |
81 | .matter-textfield-outlined > input + span::before,
82 | .matter-textfield-outlined > textarea + span::before {
83 | margin-right: 4px;
84 | border-left: solid 1px transparent;
85 | border-radius: 4px 0;
86 | }
87 |
88 | .matter-textfield-outlined > input + span::after,
89 | .matter-textfield-outlined > textarea + span::after {
90 | flex-grow: 1;
91 | margin-left: 4px;
92 | border-right: solid 1px transparent;
93 | border-radius: 0 4px;
94 | }
95 |
96 | .matter-textfield-outlined > input:not(:focus):placeholder-shown + span::before,
97 | .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span::before,
98 | .matter-textfield-outlined > input:not(:focus):placeholder-shown + span::after,
99 | .matter-textfield-outlined > textarea:not(:focus):placeholder-shown + span::after {
100 | border-top-color: transparent;
101 | }
102 |
103 | /* Hover */
104 | .matter-textfield-outlined:hover > input,
105 | .matter-textfield-outlined:hover > textarea {
106 | border-color: transparent var(--matter-helper-safari3) var(--matter-helper-safari3);
107 | }
108 |
109 | .matter-textfield-outlined:hover > input + span::before,
110 | .matter-textfield-outlined:hover > textarea + span::before,
111 | .matter-textfield-outlined:hover > input + span::after,
112 | .matter-textfield-outlined:hover > textarea + span::after {
113 | border-top-color: var(--matter-helper-safari3);
114 | }
115 |
116 | .matter-textfield-outlined:hover > input:not(:focus):placeholder-shown,
117 | .matter-textfield-outlined:hover > textarea:not(:focus):placeholder-shown {
118 | border-color: var(--matter-helper-safari3);
119 | }
120 |
121 | /* Focus */
122 | .matter-textfield-outlined > input:focus,
123 | .matter-textfield-outlined > textarea:focus {
124 | border-color: transparent var(--matter-helper-theme) var(--matter-helper-theme);
125 | box-shadow: inset 1px 0 var(--matter-helper-theme), inset -1px 0 var(--matter-helper-theme), inset 0 -1px var(--matter-helper-theme);
126 | outline: none;
127 | }
128 |
129 | .matter-textfield-outlined > input:focus + span,
130 | .matter-textfield-outlined > textarea:focus + span {
131 | color: var(--matter-helper-theme);
132 | }
133 |
134 | .matter-textfield-outlined > input:focus + span::before,
135 | .matter-textfield-outlined > input:focus + span::after,
136 | .matter-textfield-outlined > textarea:focus + span::before,
137 | .matter-textfield-outlined > textarea:focus + span::after {
138 | border-top-color: var(--matter-helper-theme) !important;
139 | box-shadow: inset 0 1px var(--matter-helper-theme);
140 | }
141 |
142 | /* Disabled */
143 | .matter-textfield-outlined > input:disabled,
144 | .matter-textfield-outlined > input:disabled + span,
145 | .matter-textfield-outlined > textarea:disabled,
146 | .matter-textfield-outlined > textarea:disabled + span {
147 | border-color: transparent var(--matter-helper-safari1) var(--matter-helper-safari1) !important;
148 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.38);
149 | pointer-events: none;
150 | }
151 |
152 | .matter-textfield-outlined > input:disabled + span::before,
153 | .matter-textfield-outlined > input:disabled + span::after,
154 | .matter-textfield-outlined > textarea:disabled + span::before,
155 | .matter-textfield-outlined > textarea:disabled + span::after {
156 | border-top-color: var(--matter-helper-safari1) !important;
157 | }
158 |
159 | .matter-textfield-outlined > input:disabled:placeholder-shown,
160 | .matter-textfield-outlined > input:disabled:placeholder-shown + span,
161 | .matter-textfield-outlined > textarea:disabled:placeholder-shown,
162 | .matter-textfield-outlined > textarea:disabled:placeholder-shown + span {
163 | border-top-color: var(--matter-helper-safari1) !important;
164 | }
165 |
166 | .matter-textfield-outlined > input:disabled:placeholder-shown + span::before,
167 | .matter-textfield-outlined > input:disabled:placeholder-shown + span::after,
168 | .matter-textfield-outlined > textarea:disabled:placeholder-shown + span::before,
169 | .matter-textfield-outlined > textarea:disabled:placeholder-shown + span::after {
170 | border-top-color: transparent !important;
171 | }
172 |
173 | /* Faster transition in Safari for less noticable fractional font-size issue */
174 | @media not all and (min-resolution:.001dpcm) {
175 | @supports (-webkit-appearance:none) {
176 | .matter-textfield-outlined > input,
177 | .matter-textfield-outlined > input + span,
178 | .matter-textfield-outlined > textarea,
179 | .matter-textfield-outlined > textarea + span,
180 | .matter-textfield-outlined > input + span::before,
181 | .matter-textfield-outlined > input + span::after,
182 | .matter-textfield-outlined > textarea + span::before,
183 | .matter-textfield-outlined > textarea + span::after {
184 | transition-duration: 0.1s;
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/components/selection/radio/radio.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 10;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Radio', () => {
10 |
11 | [
12 | {
13 | label: 'normal',
14 | states: {},
15 | textColor: { r: 0, g: 0, b: 0, a: 222},
16 | radioColor: { r: 0, g: 0, b: 0, a: 153 },
17 | highlightColor: tp
18 | },
19 | {
20 | label: 'hover',
21 | states: {
22 | '#xmas': [ 'hover' ]
23 | },
24 | textColor: { r: 0, g: 0, b: 0, a: 222},
25 | radioColor: { r: 0, g: 0, b: 0, a: 157 },
26 | highlightColor: { r: 0, g: 0, b: 0, a: 10 }
27 | },
28 | {
29 | label: 'focus',
30 | states: {
31 | '#xmas > input': [ 'focus' ]
32 | },
33 | textColor: { r: 0, g: 0, b: 0, a: 222},
34 | radioColor: { r: 0, g: 0, b: 0, a: 165 },
35 | highlightColor: { r: 0, g: 0, b: 0, a: [30, 31] }
36 | },
37 | {
38 | label: 'focus & active',
39 | states: {
40 | '#xmas': [ 'active' ],
41 | '#xmas > input': [ 'focus' ]
42 | },
43 | textColor: { r: 0, g: 0, b: 0, a: 222},
44 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
45 | highlightColor: tp
46 | },
47 | {
48 | label: 'checked',
49 | states: {
50 | '#xmas > input': [ 'checked' ]
51 | },
52 | checked: true,
53 | textColor: { r: 0, g: 0, b: 0, a: 222},
54 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
55 | highlightColor: tp
56 | },
57 | {
58 | label: 'hover & focus',
59 | states: {
60 | '#xmas': [ 'hover' ],
61 | '#xmas > input': [ 'focus' ]
62 | },
63 | textColor: { r: 0, g: 0, b: 0, a: 222},
64 | radioColor: { r: 0, g: 0, b: 0, a: 169 },
65 | highlightColor: { r: 0, g: 0, b: 0, a: [40, 41] }
66 | },
67 | {
68 | label: 'hover, focus & active',
69 | states: {
70 | '#xmas': [ 'active', 'hover' ],
71 | '#xmas > input': [ 'focus' ]
72 | },
73 | textColor: { r: 0, g: 0, b: 0, a: 222 },
74 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
75 | highlightColor: tp
76 | },
77 | {
78 | label: 'hover & checked',
79 | states: {
80 | '#xmas': [ 'hover' ],
81 | '#xmas > input': [ 'checked' ]
82 | },
83 | checked: true,
84 | textColor: { r: 0, g: 0, b: 0, a: 222},
85 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
86 | highlightColor: { r: [25, 26], g: 153, b: 255, a: 10 }
87 | },
88 | {
89 | label: 'focus & checked',
90 | states: {
91 | '#xmas > input': [ 'checked', 'focus' ]
92 | },
93 | checked: true,
94 | textColor: { r: 0, g: 0, b: 0, a: 222},
95 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
96 | highlightColor: { r: [26, 33], g: [148, 153], b: [246, 247], a: [30, 31] }
97 | },
98 | {
99 | label: 'focus, active & checked',
100 | states: {
101 | '#xmas': [ 'active' ],
102 | '#xmas > input': [ 'focus', 'checked' ]
103 | },
104 | checked: true,
105 | textColor: { r: 0, g: 0, b: 0, a: 222},
106 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
107 | highlightColor: tp
108 | },
109 | {
110 | label: 'hover, focus & checked',
111 | states: {
112 | '#xmas': [ 'hover' ],
113 | '#xmas > input': [ 'checked', 'focus' ]
114 | },
115 | checked: true,
116 | textColor: { r: 0, g: 0, b: 0, a: 222},
117 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
118 | highlightColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [ 40, 41] },
119 | },
120 | {
121 | label: 'hover, focus, active & checked',
122 | states: {
123 | '#xmas': [ 'active', 'hover' ],
124 | '#xmas > input': [ 'checked', 'focus' ]
125 | },
126 | checked: true,
127 | textColor: { r: 0, g: 0, b: 0, a: 222},
128 | radioColor: { r: 33, g: 150, b: 243, a: 255 },
129 | highlightColor: tp
130 | },
131 | {
132 | label: 'disabled',
133 | states: {
134 | '#xmas > input': [ 'disabled' ]
135 | },
136 | textColor: { r: 0, g: 0, b: 0, a: 97},
137 | radioColor: { r: 0, g: 0, b: 0, a: 97},
138 | highlightColor: tp
139 | },
140 | {
141 | label: 'disabled & checked',
142 | states: {
143 | '#xmas > input': [ 'checked', 'disabled' ]
144 | },
145 | checked: true,
146 | textColor: { r: 0, g: 0, b: 0, a: 97},
147 | radioColor: { r: 0, g: 0, b: 0, a: 97},
148 | highlightColor: tp
149 | },
150 | {
151 | label: 'customized',
152 | states: {
153 | '#xmas': {
154 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onsurface-rgb: 255, 255, 255;'
155 | }
156 | },
157 | textColor: { r: 255, g: 255, b: 255, a: 222},
158 | radioColor: { r: [254, 255], g: [254, 255], b: [254, 255], a: 153 },
159 | highlightColor: tp
160 | },
161 | {
162 | label: 'customized & checked',
163 | states: {
164 | '#xmas': {
165 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onsurface-rgb: 255, 255, 255;'
166 | },
167 | '#xmas > input': [ 'checked' ]
168 | },
169 | checked: true,
170 | textColor: { r: 255, g: 255, b: 255, a: 222},
171 | radioColor: { r: 255, g: 0, b: 0, a: 255 },
172 | highlightColor: tp
173 | }
174 | ].forEach((suite) => {
175 |
176 | describe(`in ${suite.label} state`, () => {
177 |
178 | let style;
179 | let radio;
180 | let width;
181 | let height;
182 | let context;
183 |
184 | beforeAll(async () => {
185 | style = setUp('src/components/selection/radio/radio', suite.states);
186 |
187 | radio = document.querySelector('#xmas');
188 | const rect = radio.getBoundingClientRect();
189 | width = rect.width;
190 | height = rect.height;
191 | context = await capture3x(radio, style, SPACING);
192 | });
193 |
194 | afterAll(() => {
195 | tearDown();
196 | });
197 |
198 | it('should have dominant transparent color', () => {
199 | const component = context.getImageData3x(0, 0, width, height);
200 |
201 | expect(component).toResembleColor(tp);
202 | });
203 |
204 | it('should have text', () => {
205 | const caption = context.getImageData3x(30, 0, width - 30, height);
206 |
207 | expect(radio.innerText).toBe('Xmas Tree');
208 | expect(caption).toResembleText('Xmas Tree', suite.textColor, tp);
209 | });
210 |
211 | it('should have a circular indicator representing state', () => {
212 | // radio
213 | const rd = suite.radioColor;
214 |
215 | // highlight
216 | const hl = suite.highlightColor;
217 |
218 | // check
219 | const ch = suite.checked ? rd : hl;
220 |
221 | // intermediate
222 | const im = { a: [ 0, rd.a ] };
223 |
224 | const slice = [
225 | ch, ch, ch, ch, ch, ch, ch, ch, ch, ch,
226 | ch, ch, ch, ch, im, im, im, hl, hl, hl,
227 | hl, hl, hl, im, im, im, rd, rd, rd, im,
228 | im, im, hl, hl, hl, hl, hl, hl, hl, hl,
229 | hl, hl, hl, hl, hl, hl, hl, hl, hl, hl,
230 | hl, hl, hl, hl, hl, hl, hl, hl, hl, im,
231 | im, im, tp, tp, tp, tp, tp, tp, tp, tp,
232 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
233 | tp, tp, tp, tp, tp
234 | ];
235 |
236 | const indicator = context.getImageData3x(-10, -8, 40, 40);
237 |
238 | expect(indicator).toResembleCircularShape(slice);
239 | });
240 |
241 | });
242 |
243 | });
244 |
245 | });
246 |
--------------------------------------------------------------------------------
/src/components/tooltips/tooltip.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../test/helpers/capture.js';
3 |
4 | const SPACING = 40;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Tooltip', () => {
10 |
11 | [
12 | {
13 | label: 'hover',
14 | states: {
15 | '#xmas': [ 'hover' ]
16 | },
17 | textColor: { r: 255, g: 255, b: 255, a: 255 },
18 | bodyColor: { r: [95, 96], g: [95, 96], b: [95, 96], a: 230}
19 | },
20 | {
21 | label: 'focus-within',
22 | states: {
23 | '#xmas': [ 'focus-within' ]
24 | },
25 | textColor: { r: 255, g: 255, b: 255, a: 255 },
26 | bodyColor: { r: [95, 96], g: [95, 96], b: [95, 96], a: 230}
27 | },
28 | {
29 | label: 'hover & focus-within',
30 | states: {
31 | '#xmas': [ 'hover', 'focus-within' ]
32 | },
33 | textColor: { r: 255, g: 255, b: 255, a: 255 },
34 | bodyColor: { r: [95, 96], g: [95, 96], b: [95, 96], a: 230}
35 | },
36 | {
37 | label: 'customized & hover',
38 | states: {
39 | '#xmas': {
40 | style: '--matter-surface-rgb: 0, 0, 0;--matter-onsurface-rgb: 255, 255, 255;position: relative;width: 120px;height: 20px;',
41 | hover: ''
42 | }
43 | },
44 | textColor: { r: [0, 15], g: [0, 15], b: [0, 15], a: 255 },
45 | bodyColor: { r: [157, 160], g: [157, 160], b: [157, 160], a: 230}
46 | }
47 | ].forEach((suite) => {
48 |
49 | describe(`in ${suite.label} state`, () => {
50 |
51 | let style;
52 | let tooltipParent;
53 | let width;
54 | let height;
55 | let context;
56 |
57 | beforeAll(async () => {
58 | style = setUp('src/components/tooltips/tooltip', suite.states);
59 |
60 | /* Snapping to exact pixels, and resetting sticky for Chrome */
61 | style += '.matter-tooltip > span, .matter-tooltip-top > span {min-width: 56px; position: relative;}';
62 |
63 | tooltipParent = document.querySelector('#xmas');
64 | width = 72;
65 | height = 24;
66 | context = await capture3x(tooltipParent, style, SPACING);
67 | });
68 |
69 | afterAll(() => {
70 | tearDown();
71 | });
72 |
73 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
74 | const component = context.getImageData3x(24, 28, width, height);
75 |
76 | expect(component).toResembleColor(suite.bodyColor);
77 | });
78 |
79 | it('should have caption text', () => {
80 | const caption = context.getImageData3x(28, 32, width - 8, height - 8);
81 | const tooltip = document.querySelector('#xmas > .matter-tooltip');
82 |
83 | expect(tooltip.innerText).toBe('Small Help');
84 | expect(caption).toResembleText('Small Help', suite.textColor, suite.bodyColor);
85 | });
86 |
87 | it(`should have 4px round corners`, () => {
88 | // intermediate
89 | const im = {
90 | a: [ 0, typeof suite.bodyColor.a === 'number' ? suite.bodyColor.a : suite.bodyColor.a[1] ]
91 | };
92 |
93 | // body
94 | const bd = suite.bodyColor;
95 |
96 | const corner = [
97 | [ tp, tp, tp, tp, tp, tp, tp, im, im, im, im, im],
98 | [ tp, tp, tp, tp, tp, im, im, im, bd, bd, bd, bd],
99 | [ tp, tp, tp, tp, im, im, bd, bd, bd, bd, bd, bd],
100 | [ tp, tp, tp, im, im, bd, bd, bd, bd, bd, bd, bd],
101 | [ tp, tp, im, im, bd, bd, bd, bd, bd, bd, bd, bd],
102 | [ tp, im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd],
103 | [ tp, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
104 | [ im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
105 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
106 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
107 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
108 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd]
109 | ];
110 |
111 | const topLeft = context.getImageData3x(24, 28, 4, 4);
112 | const topRight = context.getImageData3x(20 + width, 28, 4, 4);
113 | const bottomRight = context.getImageData3x(20 + width, 24 + height, 4, 4);
114 | const bottomLeft = context.getImageData3x(24, 24 + height, 4, 4);
115 |
116 | expect(topLeft).toResembleShape(corner, 0);
117 | expect(topRight).toResembleShape(corner, 90);
118 | expect(bottomRight).toResembleShape(corner, 180);
119 | expect(bottomLeft).toResembleShape(corner, 270);
120 | });
121 |
122 | it('should have no outline', () => {
123 | // body
124 | const bd = suite.bodyColor;
125 |
126 | const edge = [ bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd ];
127 |
128 | const top = context.getImageData3x(28, 28, width - 8, 4);
129 | const right = context.getImageData3x(20 + width, 32, 4, height - 8);
130 | const bottom = context.getImageData3x(28, 24 + height, width - 8, 4);
131 | const left = context.getImageData3x(24, 32, 4, height - 8);
132 |
133 | expect(top).toResembleOblongShape(edge, 0);
134 | expect(right).toResembleOblongShape(edge, 90);
135 | expect(bottom).toResembleOblongShape(edge, 180);
136 | expect(left).toResembleOblongShape(edge, 270);
137 | });
138 |
139 | it('should have no shadow', () => {
140 | const shadow = [ tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp ];
141 |
142 | const topShadow = context.getImageData3x(24, 24, width, 4);
143 | const rightShadow = context.getImageData3x(24 + width, 28, 4, height);
144 | const bottomShadow = context.getImageData3x(24, 28 + height, width, 4);
145 | const leftShadow = context.getImageData3x(20, 28, 4, height);
146 |
147 | expect(topShadow).toResembleOblongShape(shadow, 0);
148 | expect(rightShadow).toResembleOblongShape(shadow, 90);
149 | expect(bottomShadow).toResembleOblongShape(shadow, 180);
150 | expect(leftShadow).toResembleOblongShape(shadow, 270);
151 | });
152 |
153 | });
154 |
155 | });
156 |
157 | describe('in normal state', () => {
158 |
159 | let style;
160 | let tooltipParent;
161 | let width;
162 | let height;
163 | let context;
164 |
165 | beforeAll(async () => {
166 | style = setUp('src/components/tooltips/tooltip');
167 |
168 | /* Snapping to exact pixels, and resetting sticky for Chrome */
169 | style += '.matter-tooltip > span, .matter-tooltip-top > span {min-width: 56px; position: relative;}';
170 |
171 | tooltipParent = document.querySelector('#xmas');
172 | width = 72;
173 | height = 24;
174 | context = await capture3x(tooltipParent, style, SPACING);
175 | });
176 |
177 | afterAll(() => {
178 | tearDown();
179 | });
180 |
181 | it('should be hidden (have dominant transparent color)', () => {
182 | const component = context.getImageData3x(24, 28, width, height);
183 |
184 | expect(component).toResembleColor(tp);
185 | });
186 |
187 | });
188 |
189 | describe('top variant in hover state', () => {
190 |
191 | let style;
192 | let tooltipParent;
193 | let width;
194 | let height;
195 | let context;
196 |
197 | beforeAll(async () => {
198 | style = setUp('src/components/tooltips/tooltip', { '#top': [ 'hover' ] });
199 |
200 | /* Snapping to exact pixels, and resetting sticky for Chrome */
201 | style += '.matter-tooltip > span, .matter-tooltip-top > span {min-width: 56px; position: relative;}';
202 |
203 | tooltipParent = document.querySelector('#top');
204 | width = 72;
205 | height = 24;
206 | context = await capture3x(tooltipParent, style, SPACING);
207 | });
208 |
209 | afterAll(() => {
210 | tearDown();
211 | });
212 |
213 | it('should have dominant { r: [95, 96], g: [95, 96], b: [95, 96], a: 230} color', () => {
214 | const component = context.getImageData3x(24, -40, width, height);
215 |
216 | expect(component).toResembleColor( { r: [95, 96], g: [95, 96], b: [95, 96], a: 230});
217 | });
218 |
219 | });
220 |
221 | });
222 |
--------------------------------------------------------------------------------
/src/components/buttons/text/button-text.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 4;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Text Button', () => {
10 |
11 | [
12 | {
13 | label: 'normal',
14 | states: {},
15 | textColor: { r: 33, g: 150, b: 243, a: 255 },
16 | bodyColor: { a: 0 }
17 | },
18 | {
19 | label: 'hover',
20 | states: {
21 | '#xmas.matter-button-text': [ 'hover' ]
22 | },
23 | textColor: { r: 33, g: 150, b: 243, a: 255 },
24 | bodyColor: { r: [25, 26], g: 153, b: 255, a: 10 }
25 | },
26 | {
27 | label: 'focus',
28 | states: {
29 | '#xmas.matter-button-text': [ 'focus' ]
30 | },
31 | textColor: { r: 33, g: 150, b: 243, a: 255 },
32 | bodyColor: { r: [26, 34], g: [148, 153], b: [246, 247], a: [30, 31] }
33 | },
34 | {
35 | label: 'active',
36 | states: {
37 | '#xmas.matter-button-text': [ 'active' ]
38 | },
39 | textColor: { r: 33, g: 150, b: 243, a: 255 },
40 | bodyColor: { a: 0 },
41 | },
42 | {
43 | label: 'hover & focus',
44 | states: {
45 | '#xmas.matter-button-text': [ 'hover', 'focus' ]
46 | },
47 | textColor: { r: 33, g: 150, b: 243, a: 255 },
48 | bodyColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [40, 41] }
49 | },
50 | {
51 | label: 'hover & active',
52 | states: {
53 | '#xmas.matter-button-text': [ 'hover', 'active' ]
54 | },
55 | textColor: { r: 33, g: 150, b: 243, a: 255 },
56 | bodyColor: { r: [25, 26], g: 153, b: 255, a: 10 }
57 | },
58 | {
59 | label: 'focus & active',
60 | states: {
61 | '#xmas.matter-button-text': [ 'focus', 'active' ]
62 | },
63 | textColor: { r: 33, g: 150, b: 243, a: 255 },
64 | bodyColor: { r: [26, 34], g: [148, 153], b: [246, 247], a: [30, 31] }
65 | },
66 | {
67 | label: 'hover, focus & active',
68 | states: {
69 | '#xmas.matter-button-text': [ 'hover', 'focus', 'active' ]
70 | },
71 | textColor: { r: 33, g: 150, b: 243, a: 255 },
72 | bodyColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [40, 41] }
73 | },
74 | {
75 | label: 'disabled',
76 | states: {
77 | '#xmas.matter-button-text': [ 'disabled' ]
78 | },
79 | textColor: { r: 0, g: 0, b: 0, a: 97 },
80 | bodyColor: { a: 0 }
81 | },
82 | {
83 | label: 'customized',
84 | states: {
85 | '#xmas.matter-button-text': {
86 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onsurface-rgb: 255, 255, 255;width: 120px'
87 | }
88 | },
89 | textColor: { r: 255, g: 0, b: 0, a: 255 },
90 | bodyColor: { a: 0 }
91 | }
92 | ].forEach((suite) => {
93 |
94 | describe(`in ${suite.label} state`, () => {
95 |
96 | let style;
97 | let button;
98 | let width;
99 | let height;
100 | let context;
101 |
102 | beforeAll(async () => {
103 | style = setUp('src/components/buttons/text/button-text', suite.states);
104 |
105 | button = document.querySelector('#xmas');
106 | const rect = button.getBoundingClientRect();
107 | width = rect.width;
108 | height = rect.height;
109 | context = await capture3x(button, style, SPACING);
110 | });
111 |
112 | afterAll(() => {
113 | tearDown();
114 | });
115 |
116 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
117 | const component = context.getImageData3x(0, 0, width, height);
118 |
119 | expect(component).toResembleColor(suite.bodyColor);
120 | });
121 |
122 | it('should have caption text', () => {
123 | const caption = context.getImageData3x(4, 4, width - 8, height - 8);
124 |
125 | expect(button.innerText).toBe('XMAS TREE');
126 | expect(caption).toResembleText('XMAS TREE', suite.textColor, suite.bodyColor);
127 | });
128 |
129 | it(`should have 4px round corners`, () => {
130 | // intermediate
131 | const im = {
132 | a: [ 0, typeof suite.bodyColor.a === 'number' ? suite.bodyColor.a : suite.bodyColor.a[1] ]
133 | };
134 |
135 | // body
136 | const bd = suite.bodyColor;
137 |
138 | const corner = [
139 | [ tp, tp, tp, tp, tp, tp, tp, im, im, im, im, im],
140 | [ tp, tp, tp, tp, tp, im, im, im, bd, bd, bd, bd],
141 | [ tp, tp, tp, tp, im, im, bd, bd, bd, bd, bd, bd],
142 | [ tp, tp, tp, im, im, bd, bd, bd, bd, bd, bd, bd],
143 | [ tp, tp, im, im, bd, bd, bd, bd, bd, bd, bd, bd],
144 | [ tp, im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd],
145 | [ tp, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
146 | [ im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
147 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
148 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
149 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
150 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd]
151 | ];
152 |
153 | const topLeft = context.getImageData3x(0, 0, 4, 4);
154 | const topRight = context.getImageData3x(width - 4, 0, 4, 4);
155 | const bottomRight = context.getImageData3x(width - 4, height - 4, 4, 4);
156 | const bottomLeft = context.getImageData3x(0, height - 4, 4, 4);
157 |
158 | expect(topLeft).toResembleShape(corner, 0);
159 | expect(topRight).toResembleShape(corner, 90);
160 | expect(bottomRight).toResembleShape(corner, 180);
161 | expect(bottomLeft).toResembleShape(corner, 270);
162 | });
163 |
164 | it('should have no outline', () => {
165 | // body
166 | const bd = suite.bodyColor;
167 |
168 | const edge = [ bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd ];
169 |
170 | const top = context.getImageData3x(4, 0, width - 8, 4);
171 | const right = context.getImageData3x(width - 4, 4, 4, height - 8);
172 | const bottom = context.getImageData3x(4, height - 4, width - 8, 4);
173 | const left = context.getImageData3x(0, 4, 4, height - 8);
174 |
175 | expect(top).toResembleOblongShape(edge, 0);
176 | expect(right).toResembleOblongShape(edge, 90);
177 | expect(bottom).toResembleOblongShape(edge, 180);
178 | expect(left).toResembleOblongShape(edge, 270);
179 | });
180 |
181 | it('should have no shadow', () => {
182 | const shadow = [ tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp ];
183 |
184 | const topShadow = context.getImageData3x(-SPACING, -SPACING, width + SPACING, SPACING);
185 | const rightShadow = context.getImageData3x(width, -SPACING, SPACING, height + SPACING);
186 | const bottomShadow = context.getImageData3x(0, height, width + SPACING, SPACING);
187 | const leftShadow = context.getImageData3x(-SPACING, 0, SPACING, height + SPACING);
188 |
189 | expect(topShadow).toResembleOblongShape(shadow, 0);
190 | expect(rightShadow).toResembleOblongShape(shadow, 90);
191 | expect(bottomShadow).toResembleOblongShape(shadow, 180);
192 | expect(leftShadow).toResembleOblongShape(shadow, 270);
193 | });
194 |
195 | });
196 |
197 | });
198 |
199 | describe('in normal state', () => {
200 |
201 | beforeAll(() => {
202 | setUp('src/components/buttons/text/button-text');
203 | });
204 |
205 | afterAll(() => {
206 | tearDown();
207 | });
208 |
209 | it('should have a height of 36px', () => {
210 | const { height } = document.querySelector('#normal').getBoundingClientRect();
211 |
212 | expect(height).toBe(36);
213 | });
214 |
215 | it('should have a minimum width of 64px', () => {
216 | const { width } = document.querySelector('#min').getBoundingClientRect();
217 |
218 | expect(width).toBe(64);
219 | });
220 |
221 | it('should have variable-width', () => {
222 | const { width } = document.querySelector('#sized').getBoundingClientRect();
223 |
224 | expect(width).toBe(120);
225 | });
226 |
227 | });
228 |
229 | });
230 |
--------------------------------------------------------------------------------
/src/components/progress/circular/progress-circular.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 | import { isBrowserNot } from '../../../../test/helpers/browser.js';
4 |
5 | const SPACING = 4;
6 |
7 | // transparent
8 | const tp = { a: 0 };
9 |
10 | describe('Circular Progress', () => {
11 |
12 | const baseColor = { r: [31, 34], g: [145, 153], b: [241, 247] };
13 |
14 | [
15 | {
16 | label: 'indeterminate 0% animation',
17 | states: {
18 | '#xmas': {
19 | indeterminate: '',
20 | style: 'animation-delay: 0s; animation-play-state: paused;'
21 | }
22 | },
23 | fill: [ 346, 0 ],
24 | fillColor: { ...baseColor, a: 255}
25 | },
26 | {
27 | label: 'indeterminate 12.5% animation',
28 | states: {
29 | '#xmas': {
30 | indeterminate: '',
31 | style: 'animation-delay: -0.75s; animation-play-state: paused;'
32 | }
33 | },
34 | fill: [ 166, 75 ],
35 | fillColor: { ...baseColor, a: [192, 255]}
36 | },
37 | {
38 | label: 'indeterminate 25% animation',
39 | states: {
40 | '#xmas': {
41 | indeterminate: '',
42 | style: 'animation-delay: -1.5s; animation-play-state: paused;'
43 | }
44 | },
45 | fill: [ 256.5, 270 ],
46 | fillColor: { ...baseColor, a: 255}
47 | },
48 | {
49 | label: 'indeterminate 50% animation',
50 | states: {
51 | '#xmas': {
52 | indeterminate: '',
53 | style: 'animation-delay: -3s; animation-play-state: paused;'
54 | }
55 | },
56 | fill: [ 166, 180 ],
57 | fillColor: { ...baseColor, a: 255}
58 | },
59 | {
60 | label: 'indeterminate 75% animation',
61 | states: {
62 | '#xmas': {
63 | indeterminate: '',
64 | style: 'animation-delay: -4.5s; animation-play-state: paused;'
65 | }
66 | },
67 | fill: [ 76.5, 90 ],
68 | fillColor: { ...baseColor, a: 255}
69 | },
70 | {
71 | label: 'indeterminate 100% animation',
72 | states: {
73 | '#xmas': {
74 | indeterminate: '',
75 | style: 'animation-delay: -6s; animation-play-state: paused;'
76 | }
77 | },
78 | fill: [ 346, 0 ],
79 | fillColor: { ...baseColor, a: 255}
80 | },
81 | {
82 | label: 'customized & indeterminate 0% animation',
83 | states: {
84 | '#xmas': {
85 | indeterminate: '',
86 | style: '--matter-primary-rgb: 255, 0, 0;animation-delay: 0s; animation-play-state: paused;'
87 | }
88 | },
89 | fill: [ 346, 0 ],
90 | fillColor: {r: 255, g: 0, b: 0, a: 255}
91 | }
92 | ].forEach((suite) => {
93 |
94 | describe(`in ${suite.label} state`, () => {
95 |
96 | let style;
97 | let progress;
98 | let width;
99 | let height;
100 | let context;
101 |
102 | beforeAll(async () => {
103 | style = setUp('src/components/progress/circular/progress-circular', suite.states);
104 |
105 | progress = document.querySelector('#xmas');
106 | const rect = progress.getBoundingClientRect();
107 | width = rect.width;
108 | height = rect.height;
109 | context = await capture3x(progress, style, SPACING);
110 | });
111 |
112 | afterAll(() => {
113 | tearDown();
114 | });
115 |
116 | it(`should have corresponding fill`, () => {
117 | // fill
118 | const fl = suite.fillColor;
119 |
120 | // intermediate
121 | const im = { };
122 |
123 | const slice = [
124 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
125 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
126 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
127 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
128 | tp, tp, tp, tp, tp, tp, tp, im, im, fl,
129 | fl, fl, fl, fl, fl, fl, fl, fl, fl, im,
130 | im, im, tp, tp, tp, tp, tp, tp, tp, tp,
131 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
132 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
133 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
134 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
135 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp
136 | ];
137 |
138 | const indicator = context.getImageData3x(-4, -4, 56, 56);
139 |
140 | isBrowserNot('Safari') && expect(indicator).toResembleCircularShape(slice, ...suite.fill);
141 | });
142 |
143 | it('should have no shadow', () => {
144 | // intermediate
145 | const im = { };
146 |
147 | const slice = [
148 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
149 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
150 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
151 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
152 | tp, tp, tp, tp, tp, tp, tp, im, im, im,
153 | im, im, im, im, im, im, im, im, im, im,
154 | im, im, tp, tp, tp, tp, tp, tp, tp, tp,
155 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
156 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
157 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
158 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
159 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp
160 | ];
161 |
162 | const indicator = context.getImageData3x(-4, -4, 56, 56);
163 |
164 | expect(indicator).toResembleCircularShape(slice);
165 | });
166 |
167 | });
168 |
169 | });
170 |
171 | describe('in smaller size', () => {
172 |
173 | let style;
174 | let progress;
175 | let width;
176 | let height;
177 | let context;
178 |
179 | beforeAll(async () => {
180 | style = setUp('src/components/progress/circular/progress-circular', {
181 | '#sized': {
182 | indeterminate: '',
183 | style: 'font-size: 12px; animation-delay: 0s; animation-play-state: paused;'
184 | }
185 | });
186 |
187 | progress = document.querySelector('#sized');
188 | const rect = progress.getBoundingClientRect();
189 | width = rect.width;
190 | height = rect.height;
191 | context = await capture3x(progress, style, SPACING);
192 | });
193 |
194 | afterAll(() => {
195 | tearDown();
196 | });
197 |
198 | it(`should have corresponding fill`, () => {
199 | // fill
200 | const fl = { ...baseColor, a: 255 };
201 |
202 | // intermediate
203 | const im = { };
204 |
205 | const slice = [
206 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
207 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
208 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
209 | tp, tp, tp, tp, tp, tp, im, im, fl, fl,
210 | fl, fl, fl, fl, fl, im, im, tp, tp, tp,
211 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
212 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
213 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
214 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
215 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
216 | ];
217 |
218 | const indicator = context.getImageData3x(-4, -4, 44, 44);
219 |
220 | isBrowserNot('Safari') && expect(indicator).toResembleCircularShape(slice, 346.5, 0);
221 | });
222 |
223 | it('should have no shadow', () => {
224 | // intermediate
225 | const im = { };
226 |
227 | const slice = [
228 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
229 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
230 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
231 | tp, tp, tp, tp, tp, tp, im, im, im, im,
232 | im, im, im, im, im, im, im, tp, tp, tp,
233 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
234 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
235 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
236 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
237 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp
238 | ];
239 |
240 | const indicator = context.getImageData3x(-4, -4, 44, 44);
241 |
242 | expect(indicator).toResembleCircularShape(slice);
243 | });
244 |
245 | });
246 |
247 | });
248 |
--------------------------------------------------------------------------------
/src/components/buttons/unelevated/button-unelevated.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 4;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | // shadows
10 | const noShadow = [ tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp ];
11 |
12 | describe('Unelevated Button', () => {
13 |
14 | [
15 | {
16 | label: 'normal',
17 | states: {},
18 | textColor: { r: 255, g: 255, b: 255, a: 255 },
19 | bodyColor: { r: 33, g: 150, b: 243, a: 255 }
20 | },
21 | {
22 | label: 'hover',
23 | states: {
24 | '#xmas.matter-button-unelevated': [ 'hover' ]
25 | },
26 | textColor: { r: 255, g: 255, b: 255, a: 255 },
27 | bodyColor: { r: 50, g: 158, b: [243, 244], a: 255 }
28 | },
29 | {
30 | label: 'focus',
31 | states: {
32 | '#xmas.matter-button-unelevated': [ 'focus' ]
33 | },
34 | textColor: { r: 255, g: 255, b: 255, a: 255 },
35 | bodyColor: { r: [85, 86], g: 175, b: [245, 246], a: 255 }
36 | },
37 | {
38 | label: 'active',
39 | states: {
40 | '#xmas.matter-button-unelevated': [ 'active' ]
41 | },
42 | textColor: { r: 255, g: 255, b: 255, a: 255 },
43 | bodyColor: { r: 33, g: 150, b: 243, a: 255 }
44 | },
45 | {
46 | label: 'hover & focus',
47 | states: {
48 | '#xmas.matter-button-unelevated': [ 'hover', 'focus' ]
49 | },
50 | textColor: { r: 255, g: 255, b: 255, a: 255 },
51 | bodyColor: { r: [99, 104], g: [181, 184], b: [246, 247], a: 255 }
52 | },
53 | {
54 | label: 'hover & active',
55 | states: {
56 | '#xmas.matter-button-unelevated': [ 'hover', 'active' ]
57 | },
58 | textColor: { r: 255, g: 255, b: 255, a: 255 },
59 | bodyColor: { r: 50, g: 158, b: [243, 244], a: 255 }
60 | },
61 | {
62 | label: 'focus & active',
63 | states: {
64 | '#xmas.matter-button-unelevated': [ 'focus', 'active' ]
65 | },
66 | textColor: { r: 255, g: 255, b: 255, a: 255 },
67 | bodyColor: { r: [85, 86], g: 175, b: [245, 246], a: 255 }
68 | },
69 | {
70 | label: 'hover, focus & active',
71 | states: {
72 | '#xmas.matter-button-unelevated': [ 'hover', 'focus', 'active' ]
73 | },
74 | textColor: { r: 255, g: 255, b: 255, a: 255 },
75 | bodyColor: { r: [99, 104], g: [181, 184], b: [246, 247], a: 255 }
76 | },
77 | {
78 | label: 'disabled',
79 | states: {
80 | '#xmas.matter-button-unelevated': [ 'disabled' ]
81 | },
82 | textColor: { r: 0, g: 0, b: 0, a: 116 },
83 | bodyColor: { r: 0, g: 0, b: 0, a: [30, 31] }
84 | },
85 | {
86 | label: 'customized',
87 | states: {
88 | '#xmas.matter-button-unelevated': {
89 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onprimary-rgb: 0, 0, 0;width: 120px'
90 | }
91 | },
92 | textColor: { r: 0, g: 0, b: 0, a: 255 },
93 | bodyColor: { r: 255, g: 0, b: 0, a: 255 }
94 | }
95 | ].forEach((suite) => {
96 |
97 | describe(`in ${suite.label} state`, () => {
98 |
99 | let style;
100 | let button;
101 | let width;
102 | let height;
103 | let context;
104 |
105 | beforeAll(async () => {
106 | style = setUp('src/components/buttons/unelevated/button-unelevated', suite.states);
107 |
108 | button = document.querySelector('#xmas');
109 | const rect = button.getBoundingClientRect();
110 | width = rect.width;
111 | height = rect.height;
112 | context = await capture3x(button, style, SPACING);
113 | });
114 |
115 | afterAll(() => {
116 | tearDown();
117 | });
118 |
119 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
120 | const component = context.getImageData3x(0, 0, width, height);
121 |
122 | expect(component).toResembleColor(suite.bodyColor);
123 | });
124 |
125 | it('should have caption text', () => {
126 | const caption = context.getImageData3x(4, 4, width - 8, height - 8);
127 |
128 | expect(button.innerText).toBe('XMAS TREE');
129 | expect(caption).toResembleText('XMAS TREE', suite.textColor, suite.bodyColor);
130 | });
131 |
132 | it(`should have 4px round corners`, () => {
133 | // shadow
134 | const sh = {a: [0, 98]};
135 |
136 | // intermediate
137 | const im = {};
138 |
139 | // body
140 | const bd = suite.bodyColor;
141 |
142 | const corner = [
143 | [ sh, sh, sh, sh, sh, sh, sh, im, im, im, im, im],
144 | [ sh, sh, sh, sh, sh, im, im, im, bd, bd, bd, bd],
145 | [ sh, sh, sh, sh, im, im, bd, bd, bd, bd, bd, bd],
146 | [ sh, sh, sh, im, im, bd, bd, bd, bd, bd, bd, bd],
147 | [ sh, sh, im, im, bd, bd, bd, bd, bd, bd, bd, bd],
148 | [ sh, im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd],
149 | [ sh, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
150 | [ im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
151 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
152 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
153 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
154 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd]
155 | ];
156 |
157 | const topLeft = context.getImageData3x(0, 0, 4, 4);
158 | const topRight = context.getImageData3x(width - 4, 0, 4, 4);
159 | const bottomRight = context.getImageData3x(width - 4, height - 4, 4, 4);
160 | const bottomLeft = context.getImageData3x(0, height - 4, 4, 4);
161 |
162 | expect(topLeft).toResembleShape(corner, 0);
163 | expect(topRight).toResembleShape(corner, 90);
164 | expect(bottomRight).toResembleShape(corner, 180);
165 | expect(bottomLeft).toResembleShape(corner, 270);
166 | });
167 |
168 | it('should have no outline', () => {
169 | // body
170 | const bd = suite.bodyColor;
171 |
172 | const edge = [ bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd ];
173 |
174 | const top = context.getImageData3x(4, 0, width - 8, 4);
175 | const right = context.getImageData3x(width - 4, 4, 4, height - 8);
176 | const bottom = context.getImageData3x(4, height - 4, width - 8, 4);
177 | const left = context.getImageData3x(0, 4, 4, height - 8);
178 |
179 | expect(top).toResembleOblongShape(edge, 0);
180 | expect(right).toResembleOblongShape(edge, 90);
181 | expect(bottom).toResembleOblongShape(edge, 180);
182 | expect(left).toResembleOblongShape(edge, 270);
183 | });
184 |
185 | it('should have no shadow', () => {
186 | const topShadow = context.getImageData3x(-SPACING, -SPACING, width + SPACING, SPACING);
187 | const rightShadow = context.getImageData3x(width, -SPACING, SPACING, height + SPACING);
188 | const bottomShadow = context.getImageData3x(0, height, width + SPACING, SPACING);
189 | const leftShadow = context.getImageData3x(-SPACING, 0, SPACING, height + SPACING);
190 |
191 | expect(topShadow).toResembleOblongShape(noShadow, 0);
192 | expect(rightShadow).toResembleOblongShape(noShadow, 90);
193 | expect(bottomShadow).toResembleOblongShape(noShadow, 180);
194 | expect(leftShadow).toResembleOblongShape(noShadow, 270);
195 | });
196 |
197 | });
198 |
199 | });
200 |
201 | describe('in normal state', () => {
202 |
203 | beforeAll(() => {
204 | setUp('src/components/buttons/unelevated/button-unelevated');
205 | });
206 |
207 | afterAll(() => {
208 | tearDown();
209 | });
210 |
211 | it('should have a height of 36px', () => {
212 | const { height } = document.querySelector('#normal').getBoundingClientRect();
213 |
214 | expect(height).toBe(36);
215 | });
216 |
217 | it('should have a minimum width of 64px', () => {
218 | const { width } = document.querySelector('#min').getBoundingClientRect();
219 |
220 | expect(width).toBe(64);
221 | });
222 |
223 | it('should have variable-width', () => {
224 | const { width } = document.querySelector('#sized').getBoundingClientRect();
225 |
226 | expect(width).toBe(120);
227 | });
228 |
229 | });
230 |
231 | });
232 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Matter
6 |
7 | Material Design Components in Pure CSS
8 |
9 | Materializing HTML at just one class per component (ex-Pure CSS Material Components )
10 |
11 | 
12 |
13 | ## 🎬 Get Started
14 |
15 | 1. Get Matter in one of the following ways:
16 | **Normal build** from CDN (include this in ``):
17 | ```html
18 |
19 | ```
20 | **Minified build** from CDN (include this in ``):
21 | ```html
22 |
23 | ```
24 | **Download a build** from the assets of a release in [Releases](https://github.com/finnhvman/matter/releases), and include it in your project
25 |
26 | 2. Use the Markup and apply the Class of your choice:
27 |
28 | ### Buttons
29 | ```html
30 |
31 |
32 | BUTTON
33 |
34 |
35 | CONTAINED
36 |
37 | OUTLINED
38 |
39 | TEXT
40 |
41 | UNELEVATED
42 |
43 | ```
44 |
45 | ### Colors
46 | ```html
47 |
48 |
49 | BUTTON
50 |
51 |
52 |
53 | I am a paragraph
54 | ```
55 |
56 | ### Links
57 | ```html
58 |
59 | Link
60 | ```
61 |
62 | ### Progress Indicators
63 | ```html
64 |
65 |
66 |
67 |
68 |
69 | ```
70 |
71 | ### Selection Controls
72 | ```html
73 |
74 |
75 |
76 | Checkbox
77 |
78 |
79 |
80 |
81 |
82 | Radio
83 |
84 |
85 |
86 |
87 |
88 | Switch
89 |
90 | ```
91 |
92 | ### Textfields
93 | ```html
94 |
95 |
96 |
97 |
98 | Textfield
99 |
100 |
101 |
102 |
103 |
104 |
105 | Textfield
106 |
107 | ```
108 |
109 | ### Tooltips
110 | ```html
111 |
112 | Tooltip
113 |
114 |
115 |
116 |
117 | Tooltip
118 |
119 | Outlined Textfield with Tooltip
120 |
121 | ```
122 |
123 | ### Typography
124 | ```html
125 |
126 |
127 | Your paragraph here
128 | ```
129 |
130 | ---
131 |
132 | Use standard HTML attributes like `autofocus`, `disabled`, `required`, etc. where applicable to further configure components.
133 |
134 | Click the link of a component in the next section to find more examples of its usage in the `.spec.html` file!
135 |
136 | ## 📦 Components & Utilities
137 |
138 | **Implemented/Planned:**
139 | * [x] Buttons
140 | * [x] [Contained](./src/components/buttons/contained)
141 | * [x] [Outlined](./src/components/buttons/outlined)
142 | * [x] [Text](./src/components/buttons/text)
143 | * [x] [Unelevated](./src/components/buttons/unelevated)
144 | * [x] [Colors](./src/utilities/colors)
145 | * [x] [Links](./src/components/links)
146 | * [x] Progress Indicators
147 | * [x] [Circular](./src/components/progress/circular)
148 | * [x] [Linear](./src/components/progress/linear)
149 | * [x] Selection Controls
150 | * [x] [Checkbox](./src/components/selection/checkbox)
151 | * [x] [Radio](./src/components/selection/radio)
152 | * [x] [Switch](./src/components/selection/switch)
153 | * [ ] Slider
154 | * [x] Textfields
155 | * [x] [Filled](./src/components/textfields/filled)
156 | * [x] [Outlined](./src/components/textfields/outlined)
157 | * [x] [Standard](./src/components/textfields/standard)
158 | * [x] [Tooltip](./src/components/tooltips)
159 | * [x] [Typography](./src/utilities/typography)
160 |
161 | ## 🌐 Browser Support
162 |
163 |
164 |
165 | Targeted Browsers : Chrome, Firefox, Safari
166 | Supported Browsers : Edge, Samsung Internet
167 |
168 |
169 | Automated tests are executed in targeted browsers and manual testing is performed in supported browsers.
170 |
171 | Matter components are well-covered with **Visual Feature Tests** (**VFTs**). Visual Feature Tests verify certain visual parts of components like: dominant color, shape of corners (rounded/sharp), types of edges (outlined or not), shadows, and more. VFTs are executed for every component in various states (like hover, focus, active, etc. and their permutations) in targeted browsers. VFTs reside in the `.spec.js` files of the components.
172 |
173 | ## 👋 Who Is This For?
174 |
175 | **People** who work on:
176 |
177 | * Simple projects
178 | * Internal facing tools
179 | * Framework-less apps
180 | * Javascript-less apps
181 | * Proof of Concept and demo projects
182 |
183 | **Newcomers** to web development who want to build nice UIs quick and easy.
184 |
185 | This is **not** for complex apps and SPAs. Rather use the following libraries in case of larger projects:
186 | * [Material-UI (React)](https://github.com/mui-org/material-ui)
187 | * [Vuetify](https://github.com/vuetifyjs/vuetify)
188 | * [Material Design for Angular](https://github.com/angular/material2)
189 | * [Material Components Web](https://github.com/material-components/material-components-web)
190 |
191 | ## 🤔 Philosophy
192 |
193 | The purpose of Matter is to provide the most easy-to-use but accurate implementation of [Material Design Components](https://material.io/design/guidelines-overview/).
194 |
195 | Matter has probably the lowest entry-barrier among Material Design Component libraries. The only technical knowledge needed to use it is basic HTML5. It doesn't rely on JavaScript, it only needs one to three HTML elements and a CSS class per component to work. The markup of the components is semantic by design.
196 |
197 | Matter is built with theming in mind. Its components can be customized by specifying certain colors and/or fonts. The granularity of customization is variable: components can be themed on global level, component level, component instance level, or on any level between.
198 |
199 | 💎 Matter is solid. All the components are tested thoroughly to ensure rock-solid quality across all targeted browsers.
200 |
201 | 💧 Matter is liquid. Components can be resized fluidly to match layout needs, otherwise they take up the size necessary.
202 |
203 | 🎈 Matter is gas. It's highly compressible so delivery can be performed in compact formats like gzip or brotli.
204 |
205 | ⚡️ Matter is plasma. It's just CSS relying almost exclusively on class selectors making it lightning fast.
206 |
207 | ## 💬 Contact
208 |
209 | If you have questions, feedback or anything to share you can get in touch via:
210 | * Twitter [@finnhvman](https://twitter.com/finnhvman)
211 | * Spectrum [@finnhvman](https://spectrum.chat/users/finnhvman)
212 | * or [submit an issue](https://github.com/finnhvman/matter/issues)
213 |
214 | ## 🙏 Special Thanks To
215 |
216 | * [Scott O'Hara](https://twitter.com/scottohara) (accessibility)
217 |
--------------------------------------------------------------------------------
/src/components/buttons/outlined/button-outlined.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 4;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Outlined Button', () => {
10 |
11 | [
12 | {
13 | label: 'normal',
14 | states: {},
15 | textColor: { r: 33, g: 150, b: 243, a: 255 },
16 | bodyColor: { a: 0 },
17 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
18 | },
19 | {
20 | label: 'hover',
21 | states: {
22 | '#xmas.matter-button-outlined': [ 'hover' ]
23 | },
24 | textColor: { r: 33, g: 150, b: 243, a: 255 },
25 | bodyColor: { r: [25, 26], g: 153, b: 255, a: 10 },
26 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
27 | },
28 | {
29 | label: 'focus',
30 | states: {
31 | '#xmas.matter-button-outlined': [ 'focus' ]
32 | },
33 | textColor: { r: 33, g: 150, b: 243, a: 255 },
34 | bodyColor: { r: [26, 34], g: [148, 153], b: [246, 247], a: [30, 31] },
35 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
36 | },
37 | {
38 | label: 'active',
39 | states: {
40 | '#xmas.matter-button-outlined': [ 'active' ]
41 | },
42 | textColor: { r: 33, g: 150, b: 243, a: 255 },
43 | bodyColor: { a: 0 },
44 | outlineColor: { r: 0, g: 0, b: 0, a: 61 }
45 | },
46 | {
47 | label: 'hover & focus',
48 | states: {
49 | '#xmas.matter-button-outlined': [ 'hover', 'focus' ]
50 | },
51 | textColor: { r: 33, g: 150, b: 243, a: 255 },
52 | bodyColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [40, 41] },
53 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
54 | },
55 | {
56 | label: 'hover & active',
57 | states: {
58 | '#xmas.matter-button-outlined': [ 'hover', 'active' ]
59 | },
60 | textColor: { r: 33, g: 150, b: 243, a: 255 },
61 | bodyColor: { r: [25, 26], g: 153, b: 255, a: 10 },
62 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
63 | },
64 | {
65 | label: 'focus & active',
66 | states: {
67 | '#xmas.matter-button-outlined': [ 'focus', 'active' ]
68 | },
69 | textColor: { r: 33, g: 150, b: 243, a: 255 },
70 | bodyColor: { r: [26, 34], g: [148, 153], b: [246, 247], a: [30, 31] },
71 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
72 | },
73 | {
74 | label: 'hover, focus & active',
75 | states: {
76 | '#xmas.matter-button-outlined': [ 'hover', 'focus', 'active' ]
77 | },
78 | textColor: { r: 33, g: 150, b: 243, a: 255 },
79 | bodyColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [40, 41] },
80 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
81 | },
82 | {
83 | label: 'disabled',
84 | states: {
85 | '#xmas.matter-button-outlined': [ 'disabled' ]
86 | },
87 | textColor: { r: 0, g: 0, b: 0, a: 97 },
88 | bodyColor: { a: 0 },
89 | outlineColor: { r: 0, g: 0, b: 0, a: [60, 61] }
90 | },
91 | {
92 | label: 'customized',
93 | states: {
94 | '#xmas.matter-button-outlined': {
95 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onsurface-rgb: 255, 255, 255;width: 120px'
96 | }
97 | },
98 | textColor: { r: 255, g: 0, b: 0, a: 255 },
99 | bodyColor: { a: 0 },
100 | outlineColor: { r: 255, g: 255, b: 255, a: [60, 61] }
101 | }
102 | ].forEach((suite) => {
103 |
104 | describe(`in ${suite.label} state`, () => {
105 |
106 | let style;
107 | let button;
108 | let width;
109 | let height;
110 | let context;
111 |
112 | beforeAll(async () => {
113 | style = setUp('src/components/buttons/outlined/button-outlined', suite.states);
114 |
115 | button = document.querySelector('#xmas');
116 | const rect = button.getBoundingClientRect();
117 | width = rect.width;
118 | height = rect.height;
119 | context = await capture3x(button, style, SPACING);
120 | });
121 |
122 | afterAll(() => {
123 | tearDown();
124 | });
125 |
126 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
127 | const component = context.getImageData3x(0, 0, width, height);
128 |
129 | expect(component).toResembleColor(suite.bodyColor);
130 | });
131 |
132 | it('should have caption text', () => {
133 | const caption = context.getImageData3x(4, 4, width - 8, height - 8);
134 |
135 | expect(button.innerText).toBe('XMAS TREE');
136 | expect(caption).toResembleText('XMAS TREE', suite.textColor, suite.bodyColor);
137 | });
138 |
139 | it(`should have 4px round outlined corners`, () => {
140 | // intermediate
141 | const im = { a: [ 0, 61 ] };
142 | // outline
143 | const ol = suite.outlineColor;
144 |
145 | // body
146 | const bd = suite.bodyColor;
147 |
148 | const corner = [
149 | [ tp, tp, tp, tp, tp, tp, tp, im, im, im, im, im],
150 | [ tp, tp, tp, tp, tp, im, im, im, ol, ol, ol, ol],
151 | [ tp, tp, tp, tp, im, im, ol, ol, ol, ol, ol, ol],
152 | [ tp, tp, tp, im, im, ol, ol, ol, im, im, im, im],
153 | [ tp, tp, im, im, ol, ol, im, im, im, bd, bd, bd],
154 | [ tp, im, im, ol, ol, im, im, bd, bd, bd, bd, bd],
155 | [ tp, im, ol, ol, im, im, bd, bd, bd, bd, bd, bd],
156 | [ im, im, ol, ol, im, bd, bd, bd, bd, bd, bd, bd],
157 | [ im, ol, ol, im, im, bd, bd, bd, bd, bd, bd, bd],
158 | [ im, ol, ol, im, bd, bd, bd, bd, bd, bd, bd, bd],
159 | [ im, ol, ol, im, bd, bd, bd, bd, bd, bd, bd, bd],
160 | [ im, ol, ol, im, bd, bd, bd, bd, bd, bd, bd, bd]
161 | ];
162 |
163 | const topLeft = context.getImageData3x(0, 0, 4, 4);
164 | const topRight = context.getImageData3x(width - 4, 0, 4, 4);
165 | const bottomRight = context.getImageData3x(width - 4, height - 4, 4, 4);
166 | const bottomLeft = context.getImageData3x(0, height - 4, 4, 4);
167 |
168 | expect(topLeft).toResembleShape(corner, 0);
169 | expect(topRight).toResembleShape(corner, 90);
170 | expect(bottomRight).toResembleShape(corner, 180);
171 | expect(bottomLeft).toResembleShape(corner, 270);
172 | });
173 |
174 | it('should have 1px outline', () => {
175 | // outline
176 | const ol = suite.outlineColor;
177 |
178 | // body
179 | const bd = suite.bodyColor;
180 |
181 | const edge = [ ol, ol, ol, bd, bd, bd, bd, bd, bd, bd, bd, bd ];
182 |
183 | const top = context.getImageData3x(4, 0, width - 8, 4);
184 | const right = context.getImageData3x(width - 4, 4, 4, height - 8);
185 | const bottom = context.getImageData3x(4, height - 4, width - 8, 4);
186 | const left = context.getImageData3x(0, 4, 4, height - 8);
187 |
188 | expect(top).toResembleOblongShape(edge, 0);
189 | expect(right).toResembleOblongShape(edge, 90);
190 | expect(bottom).toResembleOblongShape(edge, 180);
191 | expect(left).toResembleOblongShape(edge, 270);
192 | });
193 |
194 | it('should have no shadow', () => {
195 | const shadow = [ tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp, tp ];
196 |
197 | const topShadow = context.getImageData3x(-SPACING, -SPACING, width + SPACING, SPACING);
198 | const rightShadow = context.getImageData3x(width, -SPACING, SPACING, height + SPACING);
199 | const bottomShadow = context.getImageData3x(0, height, width + SPACING, SPACING);
200 | const leftShadow = context.getImageData3x(-SPACING, 0, SPACING, height + SPACING);
201 |
202 | expect(topShadow).toResembleOblongShape(shadow, 0);
203 | expect(rightShadow).toResembleOblongShape(shadow, 90);
204 | expect(bottomShadow).toResembleOblongShape(shadow, 180);
205 | expect(leftShadow).toResembleOblongShape(shadow, 270);
206 | });
207 |
208 | });
209 |
210 | });
211 |
212 | describe('in normal state', () => {
213 |
214 | beforeAll(() => {
215 | setUp('src/components/buttons/outlined/button-outlined');
216 | });
217 |
218 | afterAll(() => {
219 | tearDown();
220 | });
221 |
222 | it('should have a height of 36px', () => {
223 | const { height } = document.querySelector('#normal').getBoundingClientRect();
224 |
225 | expect(height).toBe(36);
226 | });
227 |
228 | it('should have a minimum width of 64px', () => {
229 | const { width } = document.querySelector('#min').getBoundingClientRect();
230 |
231 | expect(width).toBe(64);
232 | });
233 |
234 | it('should have variable-width', () => {
235 | const { width } = document.querySelector('#sized').getBoundingClientRect();
236 |
237 | expect(width).toBe(120);
238 | });
239 |
240 | });
241 |
242 | });
243 |
--------------------------------------------------------------------------------
/src/components/selection/switch/switch.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 |
4 | const SPACING = 10;
5 |
6 | // transparent
7 | const tp = { a: 0 };
8 |
9 | describe('Switch', () => {
10 |
11 | [
12 | {
13 | label: 'normal',
14 | states: {},
15 | textColor: { r: 0, g: 0, b: 0, a: 222},
16 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
17 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
18 | highlightColor: tp
19 | },
20 | {
21 | label: 'hover',
22 | states: {
23 | '#xmas': [ 'hover' ]
24 | },
25 | textColor: { r: 0, g: 0, b: 0, a: 222},
26 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
27 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
28 | highlightColor: { r: 0, g: 0, b: 0, a: 10 }
29 | },
30 | {
31 | label: 'focus',
32 | states: {
33 | '#xmas > input': [ 'focus' ]
34 | },
35 | textColor: { r: 0, g: 0, b: 0, a: 222},
36 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
37 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
38 | highlightColor: { r: 0, g: 0, b: 0, a: [30, 31] }
39 | },
40 | {
41 | label: 'focus & active',
42 | states: {
43 | '#xmas': [ 'active' ],
44 | '#xmas > input': [ 'focus' ]
45 | },
46 | textColor: { r: 0, g: 0, b: 0, a: 222},
47 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
48 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
49 | highlightColor: tp
50 | },
51 | {
52 | label: 'checked',
53 | states: {
54 | '#xmas > input': [ 'checked' ]
55 | },
56 | checked: true,
57 | textColor: { r: 0, g: 0, b: 0, a: 222},
58 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
59 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
60 | highlightColor: tp
61 | },
62 | {
63 | label: 'hover & focus',
64 | states: {
65 | '#xmas': [ 'hover' ],
66 | '#xmas > input': [ 'focus' ]
67 | },
68 | textColor: { r: 0, g: 0, b: 0, a: 222},
69 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
70 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
71 | highlightColor: { r: 0, g: 0, b: 0, a: [40, 41] }
72 | },
73 | {
74 | label: 'hover, focus & active',
75 | states: {
76 | '#xmas': [ 'active', 'hover' ],
77 | '#xmas > input': [ 'focus' ]
78 | },
79 | textColor: { r: 0, g: 0, b: 0, a: 222 },
80 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
81 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
82 | highlightColor: tp
83 | },
84 | {
85 | label: 'hover & checked',
86 | states: {
87 | '#xmas': [ 'hover' ],
88 | '#xmas > input': [ 'checked' ]
89 | },
90 | checked: true,
91 | textColor: { r: 0, g: 0, b: 0, a: 222},
92 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
93 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
94 | highlightColor: { r: [25, 26], g: 153, b: 255, a: 10 }
95 | },
96 | {
97 | label: 'focus & checked',
98 | states: {
99 | '#xmas > input': [ 'checked', 'focus' ]
100 | },
101 | checked: true,
102 | textColor: { r: 0, g: 0, b: 0, a: 222},
103 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
104 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
105 | highlightColor: { r: [26, 34], g: [148, 153], b: [246, 247], a: [30, 31] }
106 | },
107 | {
108 | label: 'focus, active & checked',
109 | states: {
110 | '#xmas': [ 'active' ],
111 | '#xmas > input': [ 'focus', 'checked' ]
112 | },
113 | checked: true,
114 | textColor: { r: 0, g: 0, b: 0, a: 222},
115 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
116 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
117 | highlightColor: tp
118 | },
119 | {
120 | label: 'hover, focus & checked',
121 | states: {
122 | '#xmas': [ 'hover' ],
123 | '#xmas > input': [ 'checked', 'focus' ]
124 | },
125 | checked: true,
126 | textColor: { r: 0, g: 0, b: 0, a: 222},
127 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
128 | trackColor: { r: [31, 33], g: [149, 150], b: 243, a: 153 },
129 | highlightColor: { r: [31, 32], g: [149, 153], b: [242, 243], a: [ 40, 41] }
130 | },
131 | {
132 | label: 'hover, focus, active & checked',
133 | states: {
134 | '#xmas': [ 'active', 'hover' ],
135 | '#xmas > input': [ 'checked', 'focus' ]
136 | },
137 | checked: true,
138 | textColor: { r: 0, g: 0, b: 0, a: 222},
139 | thumbColor: { r: 33, g: 150, b: 243, a: 255 },
140 | trackColor: { r: 0, g: 0, b: 0, a: [95, 97] },
141 | highlightColor: tp
142 | },
143 | {
144 | label: 'disabled',
145 | states: {
146 | '#xmas > input': [ 'disabled' ]
147 | },
148 | textColor: { r: 0, g: 0, b: 0, a: 97 },
149 | thumbColor: { r: 255, g: 255, b: 255, a: 255 },
150 | trackColor: { r: 0, g: 0, b: 0, a: 37 },
151 | highlightColor: tp
152 | },
153 | {
154 | label: 'disabled & checked',
155 | states: {
156 | '#xmas > input': [ 'checked', 'disabled' ]
157 | },
158 | checked: true,
159 | textColor: { r: 0, g: 0, b: 0, a: 97},
160 | thumbColor: { r: [170, 171], g: 215, b: [250, 251], a: 255 },
161 | trackColor: { r: [30, 35], g: 149, b: [241, 246], a: 58 },
162 | highlightColor: tp
163 | },
164 | {
165 | label: 'customized',
166 | states: {
167 | '#xmas': {
168 | style: '--matter-primary-rgb: 255, 0, 0;--matter-surface-rgb: 0, 0, 0;--matter-onsurface-rgb: 255, 255, 255;width: 150px;'
169 | }
170 | },
171 | textColor: { r: 255, g: 255, b: 255, a: 222},
172 | thumbColor: { r: 0, g: 0, b: 0, a: 255 },
173 | trackColor: { r: 255, g: 255, b: 255, a: [95, 97] },
174 | highlightColor: tp
175 | },
176 | {
177 | label: 'customized & checked',
178 | states: {
179 | '#xmas': {
180 | style: '--matter-primary-rgb: 255, 0, 0;--matter-surface-rgb: 0, 0, 0;--matter-onsurface-rgb: 255, 255, 255;width: 150px;'
181 | },
182 | '#xmas > input': [ 'checked' ]
183 | },
184 | checked: true,
185 | textColor: { r: 255, g: 255, b: 255, a: 222},
186 | thumbColor: { r: 255, g: 0, b: 0, a: 255 },
187 | trackColor: { r: [254, 255], g: 0, b: 0, a: 153 },
188 | highlightColor: tp
189 | }
190 | ].forEach((suite) => {
191 | describe(`in ${suite.label} state`, () => {
192 |
193 | let style;
194 | let toggle;
195 | let width;
196 | let height;
197 | let context;
198 |
199 | beforeAll(async () => {
200 | style = setUp('src/components/selection/switch/switch', suite.states);
201 |
202 | // Remove box-shadow to help testing
203 | style += '.matter-switch > input + span::after { box-shadow: none !important; }';
204 |
205 | toggle = document.querySelector('#xmas');
206 | const rect = toggle.getBoundingClientRect();
207 | width = rect.width;
208 | height = rect.height;
209 | context = await capture3x(toggle, style, SPACING);
210 | });
211 |
212 | afterAll(() => {
213 | tearDown();
214 | });
215 |
216 | it('should have dominant transparent color', () => {
217 | const component = context.getImageData3x(0, 0, width, height);
218 |
219 | expect(component).toResembleColor(tp);
220 | });
221 |
222 | it('should have text', () => {
223 | const caption = context.getImageData3x(0, 0, width - 51, height);
224 |
225 | expect(toggle.innerText).toBe('Xmas Tree');
226 | expect(caption).toResembleText('Xmas Tree', suite.textColor, tp);
227 | });
228 |
229 | it('should have thumb', () => {
230 | // thumb
231 | const th = suite.thumbColor;
232 |
233 | // intermediate
234 | const im = {};
235 |
236 | const thumb = suite.checked
237 | ? context.getImageData3x(width - 25, 2, 20, 20)
238 | : context.getImageData3x(width - 41, 2, 20, 20);
239 |
240 | const slice = [
241 | th, th, th, th, th, th, th, th, th, th,
242 | th, th, th, th, th, th, th, th, th, th,
243 | th, th, th, th, th, th, th, th, th, im,
244 | im, im, im, im, im, im, im, im, im, im,
245 | im, im, im
246 | ];
247 |
248 | expect(thumb).toResembleCircularShape(slice);
249 | });
250 |
251 | it('should have track', () => {
252 | // track
253 | const tr = suite.trackColor;
254 |
255 | // intermediate
256 | const im = {};
257 |
258 | const track = suite.checked
259 | ? context.getImageData3x(width - 41, 5, 14, 14)
260 | : context.getImageData3x(width - 19, 5, 14, 14);
261 |
262 | const slice = [
263 | im, im, im, im, im, im, tr, tr, tr, tr,
264 | tr, tr, tr, tr, tr, tr, tr, tr, tr, tr,
265 | im, im, im, im, im, im, im, im, im, im,
266 | ];
267 |
268 | expect(track).toResembleCircularShape(slice, suite.checked ? 205 : 25, suite.checked ? 335 : 155);
269 | });
270 |
271 | it('should have a circular highlight representing state', () => {
272 | // highlight
273 | const hl = suite.highlightColor;
274 |
275 | // intermediate
276 | const im = {};
277 |
278 | const highlight = suite.checked
279 | ? context.getImageData3x(width - 35, -8, 40, 40)
280 | : context.getImageData3x(width - 51, -8, 40, 40);
281 |
282 | const slice = [
283 | im, im, im, im, im, im, im, im, im, im,
284 | im, im, im, im, im, im, im, im, im, im,
285 | im, im, im, im, im, im, im, im, im, im,
286 | im, im, hl, hl, hl, hl, hl, hl, hl, hl,
287 | hl, hl, hl, hl, hl, hl, hl, hl, hl, hl,
288 | hl, hl, hl, hl, hl, hl, hl, hl, hl, im,
289 | im, im, tp, tp, tp, tp, tp, tp, tp, tp,
290 | tp, tp, tp, tp, tp, tp, tp, tp, tp, tp,
291 | tp, tp, tp, tp, tp
292 | ];
293 |
294 | expect(highlight).toResembleCircularShape(slice, suite.checked ? 310 : 130, suite.checked ? 230 : 50);
295 | });
296 |
297 | });
298 | });
299 |
300 | describe('in normal state', () => {
301 |
302 | beforeAll(() => {
303 | setUp('src/components/selection/switch/switch');
304 | });
305 |
306 | afterAll(() => {
307 | tearDown();
308 | });
309 |
310 | it('should have variable-width', () => {
311 | const { width } = document.querySelector('#sized').getBoundingClientRect();
312 |
313 | expect(width).toBe(200);
314 | });
315 |
316 | });
317 |
318 | });
319 |
--------------------------------------------------------------------------------
/src/components/buttons/contained/button-contained.spec.js:
--------------------------------------------------------------------------------
1 | import { setUp, tearDown } from '../../../../test/helpers/fixture.js';
2 | import { capture3x } from '../../../../test/helpers/capture.js';
3 | import { isBrowserNot } from '../../../../test/helpers/browser.js';
4 |
5 | const SPACING = 6;
6 |
7 | const black = {
8 | r: 0,
9 | g: 0,
10 | b: 0
11 | };
12 |
13 | const shadow = (alphas) => {
14 | return alphas.map(alpha => ({...black, a: alpha}));
15 | };
16 |
17 | // shadows
18 | const noShadow = new Array(18).fill({ a: 0 });
19 | const topShadow2 = shadow([[0, 0], [0, 0], [0, 0], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1], [1, 2], [1, 2], [2, 3], [3, 4], [3, 4], [4, 5], [5, 6], [7, 8], [8, 10], [9, 11]]);
20 | const topShadow4 = shadow([[2, 3], [2, 3], [2, 4], [2, 4], [2, 4], [3, 5], [3, 5], [4, 6], [5, 7], [5, 7], [6, 8], [7, 9], [7, 10], [8, 12], [9, 13], [9, 14], [10, 15], [13, 18]]);
21 | const topShadow8 = shadow([[4, 5], [4, 6], [4, 6], [5, 7], [5, 8], [5, 8], [6, 9], [6, 9], [7, 10], [7, 10], [8, 11], [8, 11], [9, 13], [9, 14], [9, 14], [10, 15], [11, 16], [12, 17]]);
22 | const sideShadow2 = shadow([[0, 1], [0, 1], [0, 1], [0, 1], [0, 4], [0, 4], [0, 4], [0, 4], [0, 4], [0, 4], [4, 8], [4, 8], [6, 10], [8, 12], [12, 16], [16, 21], [20, 28], [24, 32]]);
23 | const sideShadow4 = shadow([[0, 5], [0, 5], [0, 6], [0, 8], [0, 8], [4, 9], [4, 12], [4, 12], [4, 14], [8, 16], [8, 20], [12, 21], [12, 25], [16, 28], [20, 32], [24, 36], [28, 41], [32, 46]]);
24 | const sideShadow8 = shadow([[8, 16], [8, 17], [8, 18], [12, 20], [12, 21], [12, 22], [16, 24], [16, 24], [20, 26], [20, 28], [20, 30], [24, 32], [24, 32], [24, 36], [28, 36], [28, 40], [32, 44], [32, 48]]);
25 | const bottomShadow2 = shadow([[0, 1], [0, 1], [1, 2], [1, 2], [2, 4], [3, 5], [4, 6], [6, 8], [9, 11], [14, 17], [18, 21], [23, 26], [31, 34], [42, 45], [54, 57], [68, 73], [80, 86], [87, 93]]);
26 | const bottomShadow4 = shadow([[11, 14], [12, 17], [15, 20], [17, 22], [20, 26], [24, 30], [26, 32], [30, 36], [33, 39], [38, 44], [42, 48], [46, 52], [51, 57], [55, 63], [60, 66], [63, 70], [67, 74], [71, 78]]);
27 | const bottomShadow8 = shadow([[35, 43], [36, 46], [37, 47], [40, 50], [42, 52], [45, 55], [47, 57], [51, 61], [52, 63], [56, 67], [58, 69], [62, 73], [63, 75], [65, 77], [68, 80], [70, 83], [73, 85], [76, 88]]);
28 |
29 | describe('Contained Button', () => {
30 |
31 | [
32 | {
33 | label: 'normal',
34 | states: {},
35 | textColor: { r: 255, g: 255, b: 255, a: 255 },
36 | bodyColor: { r: 33, g: 150, b: 243, a: 255 },
37 | shadow: {
38 | top: topShadow2,
39 | side: sideShadow2,
40 | bottom: bottomShadow2
41 | }
42 | },
43 | {
44 | label: 'hover',
45 | states: {
46 | '#xmas.matter-button-contained': [ 'hover' ]
47 | },
48 | textColor: { r: 255, g: 255, b: 255, a: 255 },
49 | bodyColor: { r: 50, g: 158, b: [243, 244], a: 255 },
50 | shadow: {
51 | top: topShadow4,
52 | side: sideShadow4,
53 | bottom: bottomShadow4
54 | }
55 | },
56 | {
57 | label: 'focus',
58 | states: {
59 | '#xmas.matter-button-contained': [ 'focus' ]
60 | },
61 | textColor: { r: 255, g: 255, b: 255, a: 255 },
62 | bodyColor: { r: [85, 86], g: 175, b: [245, 246], a: 255 },
63 | shadow: {
64 | top: topShadow4,
65 | side: sideShadow4,
66 | bottom: bottomShadow4
67 | }
68 | },
69 | {
70 | label: 'active',
71 | states: {
72 | '#xmas.matter-button-contained': [ 'active' ]
73 | },
74 | textColor: { r: 255, g: 255, b: 255, a: 255 },
75 | bodyColor: { r: 33, g: 150, b: 243, a: 255 },
76 | shadow: {
77 | top: topShadow8,
78 | side: sideShadow8,
79 | bottom: bottomShadow8,
80 | }
81 | },
82 | {
83 | label: 'hover & focus',
84 | states: {
85 | '#xmas.matter-button-contained': [ 'hover', 'focus' ]
86 | },
87 | textColor: { r: 255, g: 255, b: 255, a: 255 },
88 | bodyColor: { r: [99, 104], g: [181, 184], b: [246, 247], a: 255 },
89 | shadow: {
90 | top: topShadow4,
91 | side: sideShadow4,
92 | bottom: bottomShadow4
93 | }
94 | },
95 | {
96 | label: 'hover & active',
97 | states: {
98 | '#xmas.matter-button-contained': [ 'hover', 'active' ]
99 | },
100 | textColor: { r: 255, g: 255, b: 255, a: 255 },
101 | bodyColor: { r: 50, g: 158, b: [243, 244], a: 255 },
102 | shadow: {
103 | top: topShadow8,
104 | side: sideShadow8,
105 | bottom: bottomShadow8,
106 | }
107 | },
108 | {
109 | label: 'focus & active',
110 | states: {
111 | '#xmas.matter-button-contained': [ 'focus', 'active' ]
112 | },
113 | textColor: { r: 255, g: 255, b: 255, a: 255 },
114 | bodyColor: { r: [85, 86], g: 175, b: [245, 246], a: 255 },
115 | shadow: {
116 | top: topShadow8,
117 | side: sideShadow8,
118 | bottom: bottomShadow8,
119 | }
120 | },
121 | {
122 | label: 'hover, focus & active',
123 | states: {
124 | '#xmas.matter-button-contained': [ 'hover', 'focus', 'active' ]
125 | },
126 | textColor: { r: 255, g: 255, b: 255, a: 255 },
127 | bodyColor: { r: [99, 104], g: [181, 184], b: [246, 247], a: 255 },
128 | shadow: {
129 | top: topShadow8,
130 | side: sideShadow8,
131 | bottom: bottomShadow8,
132 | }
133 | },
134 | {
135 | label: 'disabled',
136 | states: {
137 | '#xmas.matter-button-contained': [ 'disabled' ]
138 | },
139 | textColor: { r: 0, g: 0, b: 0, a: 116 },
140 | bodyColor: { r: 0, g: 0, b: 0, a: [30, 31] },
141 | shadow: {
142 | top: noShadow,
143 | side: noShadow,
144 | bottom: noShadow
145 | }
146 | },
147 | {
148 | label: 'customized',
149 | states: {
150 | '#xmas.matter-button-contained': {
151 | style: '--matter-primary-rgb: 255, 0, 0;--matter-onprimary-rgb: 0, 0, 0;width: 120px'
152 | }
153 | },
154 | textColor: { r: 0, g: 0, b: 0, a: 255 },
155 | bodyColor: { r: 255, g: 0, b: 0, a: 255 },
156 | shadow: {
157 | top: topShadow2,
158 | side: sideShadow2,
159 | bottom: bottomShadow2
160 | }
161 | }
162 | ].forEach((suite) => {
163 |
164 | describe(`in ${suite.label} state`, () => {
165 |
166 | let style;
167 | let button;
168 | let width;
169 | let height;
170 | let context;
171 |
172 | beforeAll(async () => {
173 | style = setUp('src/components/buttons/contained/button-contained', suite.states);
174 |
175 | button = document.querySelector('#xmas');
176 | const rect = button.getBoundingClientRect();
177 | width = rect.width;
178 | height = rect.height;
179 | context = await capture3x(button, style, SPACING);
180 | });
181 |
182 | afterAll(() => {
183 | tearDown();
184 | });
185 |
186 | it(`should have dominant ${JSON.stringify(suite.bodyColor).replace(/"/g, '')} color`, () => {
187 | const component = context.getImageData3x(0, 0, width, height);
188 |
189 | expect(component).toResembleColor(suite.bodyColor);
190 | });
191 |
192 | it('should have caption text', () => {
193 | const caption = context.getImageData3x(4, 4, width - 8, height - 8);
194 |
195 | expect(button.innerText).toBe('XMAS TREE');
196 | expect(caption).toResembleText('XMAS TREE', suite.textColor, suite.bodyColor);
197 | });
198 |
199 | it(`should have 4px round corners`, () => {
200 | // shadow
201 | const sh = {a: [0, 98]};
202 |
203 | // intermediate
204 | const im = {};
205 |
206 | // body
207 | const bd = suite.bodyColor;
208 |
209 | const corner = [
210 | [ sh, sh, sh, sh, sh, sh, sh, im, im, im, im, im],
211 | [ sh, sh, sh, sh, sh, im, im, im, bd, bd, bd, bd],
212 | [ sh, sh, sh, sh, im, im, bd, bd, bd, bd, bd, bd],
213 | [ sh, sh, sh, im, im, bd, bd, bd, bd, bd, bd, bd],
214 | [ sh, sh, im, im, bd, bd, bd, bd, bd, bd, bd, bd],
215 | [ sh, im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd],
216 | [ sh, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
217 | [ im, im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
218 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
219 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
220 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd],
221 | [ im, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd]
222 | ];
223 |
224 | const topLeft = context.getImageData3x(0, 0, 4, 4);
225 | const topRight = context.getImageData3x(width - 4, 0, 4, 4);
226 | const bottomRight = context.getImageData3x(width - 4, height - 4, 4, 4);
227 | const bottomLeft = context.getImageData3x(0, height - 4, 4, 4);
228 |
229 | expect(topLeft).toResembleShape(corner, 0);
230 | expect(topRight).toResembleShape(corner, 90);
231 | expect(bottomRight).toResembleShape(corner, 180);
232 | expect(bottomLeft).toResembleShape(corner, 270);
233 | });
234 |
235 | it('should have no outline', () => {
236 | // body
237 | const bd = suite.bodyColor;
238 |
239 | const edge = [ bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd, bd ];
240 |
241 | const top = context.getImageData3x(4, 0, width - 8, 4);
242 | const right = context.getImageData3x(width - 4, 4, 4, height - 8);
243 | const bottom = context.getImageData3x(4, height - 4, width - 8, 4);
244 | const left = context.getImageData3x(0, 4, 4, height - 8);
245 |
246 | expect(top).toResembleOblongShape(edge, 0);
247 | expect(right).toResembleOblongShape(edge, 90);
248 | expect(bottom).toResembleOblongShape(edge, 180);
249 | expect(left).toResembleOblongShape(edge, 270);
250 | });
251 |
252 | it('should have shadow', () => {
253 | const top = context.getImageData3x(SPACING, -SPACING, width - 2 * SPACING, SPACING);
254 | const right = context.getImageData3x(width, 12, SPACING, height - 24);
255 | const bottom = context.getImageData3x(SPACING, height, width - 2 * SPACING, SPACING);
256 | const left = context.getImageData3x(-SPACING, 12, SPACING, height - 24);
257 |
258 | isBrowserNot('Safari') && expect(top).toResembleOblongShape(suite.shadow.top, 0);
259 | expect(right).toResembleOblongShape(suite.shadow.side, 90);
260 | isBrowserNot('Safari') && expect(bottom).toResembleOblongShape(suite.shadow.bottom, 180);
261 | expect(left).toResembleOblongShape(suite.shadow.side, 270);
262 | });
263 |
264 | });
265 |
266 | });
267 |
268 | describe('in normal state', () => {
269 |
270 | beforeAll(() => {
271 | setUp('src/components/buttons/contained/button-contained');
272 | });
273 |
274 | afterAll(() => {
275 | tearDown();
276 | });
277 |
278 | it('should have a height of 36px', () => {
279 | const { height } = document.querySelector('#normal').getBoundingClientRect();
280 |
281 | expect(height).toBe(36);
282 | });
283 |
284 | it('should have a minimum width of 64px', () => {
285 | const { width } = document.querySelector('#min').getBoundingClientRect();
286 |
287 | expect(width).toBe(64);
288 | });
289 |
290 | it('should have variable-width', () => {
291 | const { width } = document.querySelector('#sized').getBoundingClientRect();
292 |
293 | expect(width).toBe(120);
294 | });
295 |
296 | });
297 |
298 | });
299 |
--------------------------------------------------------------------------------
/test/matchers/resemble.js:
--------------------------------------------------------------------------------
1 | beforeEach(() => {
2 | const TEXT_TOLERANCE = 25;
3 | const ERROR_LIMIT = 20;
4 |
5 | const match = (expected, actual) => {
6 | if (typeof expected === 'number') {
7 | return expected === actual;
8 | } else if (expected instanceof Array) {
9 | return expected[0] <= actual && actual <= expected[1];
10 | } else {
11 | return true;
12 | }
13 | };
14 |
15 | /**
16 | * @return
17 | * - (space) match
18 | *
19 | * r - red mismatch
20 | * g - green mismatch
21 | * b - blue mismatch
22 | * a - alpha mismatch
23 | *
24 | * c - at least two of r, g, or b
25 | * x - at least one of r, g, or b and a
26 | */
27 | const matchPixel = (expected, r, g, b, a) => {
28 | let errors = match(expected.a, a) ? '' : 'a';
29 | errors += match(expected.r, r) ? '' : 'r';
30 | errors += match(expected.g, g) ? '' : 'g';
31 | errors += match(expected.b, b) ? '' : 'b';
32 | if (errors === '') {
33 | return ' ';
34 | } else if (errors.length === 1) {
35 | return errors;
36 | } else {
37 | return errors.includes('a') ? 'x' : 'c';
38 | }
39 | };
40 |
41 | const tolerate = (value, tolerance) => {
42 | if (typeof value === 'number') {
43 | return [ value - tolerance, value + tolerance ];
44 | } else if (value instanceof Array) {
45 | return [ value[0] - tolerance, value[1] + tolerance ];
46 | }
47 | };
48 |
49 | const tolerateColor = (color, tolerance) => {
50 | return Object.keys(color).reduce((toleratedColor, channel) => {
51 | toleratedColor[channel] = tolerate(color[channel], tolerance);
52 | return toleratedColor;
53 | }, {});
54 | };
55 |
56 | const segment = (data, width, height, bodyColor) => {
57 | const segments = new Array(height).fill(0).map(() => new Array(width));
58 | const regions = [];
59 | let index = 0;
60 | for (let x = 0; x < width; x++) {
61 | for (let y = 0; y < height; y++) {
62 | const pixelIndex = width * y * 4 + x * 4;
63 | const current = matchPixel(bodyColor,
64 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]);
65 | if (current === ' ') {
66 | segments[y][x] = 0;
67 | } else {
68 | const neighbours = [];
69 | if (0 < y) {
70 | if (segments[y - 1][x]) { // check top
71 | neighbours.push(segments[y - 1][x]);
72 | }
73 | }
74 | if (0 < x) {
75 | if (segments[y][x - 1]) { // check left
76 | neighbours.push(segments[y][x - 1]);
77 | }
78 | }
79 |
80 | if (neighbours.length) {
81 | neighbours.sort();
82 | segments[y][x] = neighbours[0];
83 | if (1 < neighbours.length) {
84 | neighbours.forEach(neighbour => { // connect regions
85 | if (neighbour !== neighbours[0]) {
86 | const foundReference = regions.find(region => region.includes(neighbours[0]));
87 | const foundCurrent = regions.find(region => region.includes(neighbour));
88 | if (foundCurrent === foundReference) {
89 | // Nothing to do
90 | } else if (foundReference && foundCurrent) {
91 | regions.push(foundReference.concat(foundCurrent));
92 | foundReference.length = 0;
93 | foundCurrent.length = 0;
94 | } else {
95 | foundReference.push(neighbour);
96 | }
97 | }
98 | });
99 | }
100 | } else {
101 | index++;
102 | segments[y][x] = index;
103 | regions.push([index]);
104 | }
105 | }
106 | }
107 | }
108 |
109 | return regions.filter(region => region.length).length;
110 | };
111 |
112 | jasmine.addMatchers({
113 | toResembleColor: () => ({
114 | compare: (actual, expected) => {
115 | const { data, width, height } = actual;
116 | const area = width * height;
117 |
118 | let count = 0;
119 | for (let x = 0; x < width; x++) {
120 | for (let y = 0; y < height; y++) {
121 | const pixelIndex = width * y * 4 + x * 4;
122 | const currentPassing = ' ' === matchPixel(expected,
123 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]);
124 | if (currentPassing) {
125 | count++;
126 | }
127 | }
128 | }
129 |
130 | const passing = area / 2 < count;
131 |
132 | return {
133 | pass: passing,
134 | message: `Matched ${count} out of ${area}`
135 | };
136 | }
137 | }),
138 | toResembleText: () => ({
139 | compare: (actual, expected, textColor, bodyColor) => {
140 | const { data, width, height } = actual;
141 |
142 | const segments = segment(data, width, height, bodyColor);
143 |
144 | const words = expected.replace(/\s/g, '').length;
145 |
146 | let passing = segments === words;
147 |
148 | const toleratedColor = tolerateColor(textColor, TEXT_TOLERANCE);
149 |
150 | const colors = {
151 | body: 0,
152 | text: 0,
153 | misc: 0
154 | };
155 | for (let x = 0; x < width; x++) {
156 | for (let y = 0; y < height; y++) {
157 | const pixelIndex = width * y * 4 + x * 4;
158 | if (' ' === matchPixel(bodyColor,
159 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3])) {
160 | colors.body++;
161 | } else if (' ' === matchPixel(toleratedColor,
162 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3])) {
163 | colors.text++;
164 | } else {
165 | colors.misc++
166 | }
167 | }
168 | }
169 |
170 | passing = passing && colors.misc <= colors.text && colors.text <= colors.body;
171 |
172 | return {
173 | pass: passing,
174 | message: `Expected: ${words} characters (${expected}), Actual: ${segments} characters. Colors: ${colors.misc}, ${colors.text}, ${colors.body}`
175 | }
176 | }
177 | }),
178 | toResembleShape: () => ({
179 | compare: (actual, expected, rotateCW) => {
180 | const { data, width, height } = actual;
181 | let passing = true;
182 | let matrix = `|${'-'.repeat(width)}|\n|`;
183 | let errors = '';
184 | let errorCount = 0;
185 |
186 | for (let y = 0; y < height; y++) {
187 | for (let x = 0; x < width; x++) {
188 | const pixelIndex = width * y * 4 + x * 4;
189 | let mappedX;
190 | let mappedY;
191 | switch (rotateCW) {
192 | case 90:
193 | mappedX = y;
194 | mappedY = width - x - 1;
195 | break;
196 | case 180:
197 | mappedX = width - x - 1;
198 | mappedY = height - y - 1;
199 | break;
200 | case 270:
201 | mappedX = height - y - 1;
202 | mappedY = x;
203 | break;
204 | default:
205 | mappedX = x;
206 | mappedY = y;
207 | break;
208 | }
209 |
210 | const current = matchPixel(expected[mappedY][mappedX],
211 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]);
212 | matrix += current;
213 | passing = passing && current === ' ';
214 |
215 | if (current !== ' ') {
216 | if (errorCount < ERROR_LIMIT) {
217 | errors += `(${mappedX}, ${mappedY}): ${data[pixelIndex]}, ${data[pixelIndex + 1]}, ${data[pixelIndex + 2]}, ${data[pixelIndex + 3]})}\n`;
218 | }
219 | errorCount++;
220 | }
221 | }
222 | matrix += '|\n|';
223 | }
224 | matrix += `${'-'.repeat(width)}|`;
225 |
226 | return {
227 | pass: passing,
228 | message: `Problems at ${rotateCW}deg:\n${matrix}\n${errors}\n${errorCount - ERROR_LIMIT} more errors.`
229 | };
230 | }
231 | }),
232 | toResembleOblongShape: () => ({
233 | compare: (actual, expected, rotateCW) => {
234 | const { data, width, height } = actual;
235 | let passing = true;
236 | let errors = '';
237 | let errorCount = 0;
238 |
239 | for (let x = 0; x < width; x++) {
240 | for (let y = 0; y < height; y++) {
241 | const pixelIndex = width * y * 4 + x * 4;
242 | let mapped;
243 | if (rotateCW === 90 || rotateCW === 270) {
244 | mapped = rotateCW === 90 ? width - x - 1 : x;
245 | } else {
246 | mapped = rotateCW === 180 ? height - y - 1 : y;
247 | }
248 |
249 | const current = matchPixel(expected[mapped],
250 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]);
251 | passing = passing && (current === ' ');
252 |
253 | if (current !== ' ') {
254 | if (errorCount < ERROR_LIMIT) {
255 | errors += `(${x}, ${y}): ${data[pixelIndex]}, ${data[pixelIndex + 1]}, ${data[pixelIndex + 2]}, ${data[pixelIndex + 3]})}\n`;
256 | }
257 | errorCount++;
258 | }
259 | }
260 | }
261 |
262 | return {
263 | pass: passing,
264 | message: `Problems at ${rotateCW}deg (first ${ERROR_LIMIT}):\n${errors}\n${errorCount - ERROR_LIMIT} more errors.`
265 | };
266 | }
267 | }),
268 | toResembleCircularShape: () => ({
269 | compare: (actual, expected, from = 0, to = 360) => {
270 | const { data, width, height } = actual;
271 | let passing = true;
272 | let errors = '';
273 | let errorCount = 0;
274 |
275 | const center = {
276 | x: width / 2,
277 | y: height / 2
278 | };
279 |
280 | for (let x = 0; x < width; x++) {
281 | for (let y = 0; y < height; y++) {
282 | const pixelIndex = width * y * 4 + x * 4;
283 | const dx = x - center.x + 0.5;
284 | const dy = y - center.y + 0.5;
285 | const mapped = Math.round(Math.sqrt(dx ** 2 + dy ** 2));
286 | const angle = Math.atan(dy / dx) / Math.PI * 180 + (0 < dx ? 90 : 270);
287 |
288 | const inSector = from < to ? (from <= angle && angle <= to) : (from <= angle || angle <= to);
289 | const current = !inSector ? ' ' : matchPixel(expected[mapped],
290 | data[pixelIndex], data[pixelIndex + 1], data[pixelIndex + 2], data[pixelIndex + 3]);
291 | passing = passing && (current === ' ');
292 |
293 | if (current !== ' ') {
294 | if (errorCount < ERROR_LIMIT) {
295 | errors += `(${x}, ${y}), mapped (${mapped}, ${angle.toFixed(2)}°): ${data[pixelIndex]}, ${data[pixelIndex + 1]}, ${data[pixelIndex + 2]}, ${data[pixelIndex + 3]}}\n`;
296 | }
297 | errorCount++;
298 | }
299 | }
300 | }
301 |
302 | return {
303 | pass: passing,
304 | message: `Problems (first ${ERROR_LIMIT}):\n${errors}\n${errorCount - ERROR_LIMIT} more errors.`
305 | };
306 | }
307 | })
308 | });
309 | });
310 |
--------------------------------------------------------------------------------