├── 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 | 4 |

5 | 6 |

7 | 8 |

9 | 10 |

11 | -------------------------------------------------------------------------------- /src/components/buttons/outlined/button-outlined.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 |

7 | 8 |

9 | 10 |

11 | -------------------------------------------------------------------------------- /src/components/buttons/contained/button-contained.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 |

7 | 8 |

9 | 10 |

11 | -------------------------------------------------------------------------------- /src/components/buttons/unelevated/button-unelevated.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 6 |

7 | 8 |

9 | 10 |

11 | 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 | 11 | 12 |

13 | 14 | 15 | 16 |

17 | 18 | 19 | 20 |

21 | 22 | 23 | 24 |

25 | 26 | 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 | 7 | 8 |

9 | 10 | 14 | 15 | 16 |


17 | 18 | 19 | 23 | 24 |

25 | 26 | 30 | 31 | 32 |


33 | 34 | 35 | 39 | 40 |

41 | 42 | 46 | -------------------------------------------------------------------------------- /src/components/selection/switch/switch.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |


9 | 10 | 14 | 15 |


16 | 17 | 21 | 22 |


23 | 24 | 28 | 29 |


30 | 31 | 35 | 36 |


37 | 38 | 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 | 7 | 8 |


9 | 10 | 14 | 15 |


16 | 17 | 21 | 22 |


23 | 24 | 28 | 29 |


30 | 31 | 35 | 36 |


37 | 38 | 42 | 43 |


44 | 45 | 49 | 50 | 57 | -------------------------------------------------------------------------------- /docs/browsers.html: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 |
34 | Chrome 35 |
36 | 37 |
38 | Firefox 39 |
40 | 41 |
42 | Safari 43 |
44 | 45 |
46 | Edge 47 |
48 | 49 |
50 | Samsung Internet 51 |
52 |
53 | -------------------------------------------------------------------------------- /src/components/textfields/filled/textfield-filled.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 |

8 | 12 |

13 | 17 |

18 | 22 |

23 | 24 | 28 |

29 | 33 |

34 | 38 |

39 | 43 | -------------------------------------------------------------------------------- /src/components/textfields/outlined/textfield-outlined.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 |

8 | 12 |

13 | 17 |

18 | 22 |

23 | 24 | 28 |

29 | 33 |

34 | 38 |

39 | 43 | -------------------------------------------------------------------------------- /src/components/textfields/standard/textfield-standard.spec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 |

8 | 12 |

13 | 17 |

18 | 22 |

23 | 24 | 28 |

29 | 33 |

34 | 38 |

39 | 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 | 5 | I'm a div 6 |
7 | 8 |

9 | 10 |
11 | 12 | I'm a wider div 13 |
14 | 15 |

16 | 17 | I'm 18 | 19 |


20 | 21 |
22 | 23 | Top variant 24 |
25 | 26 |

27 | 28 | 33 | 34 |


35 | 36 |
37 | 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 | Matter M logo 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 | ![13 Matter Components](./docs/hero.png) 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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ``` 44 | 45 | ### Colors 46 | ```html 47 | 48 | 49 | 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 | 78 | 79 | 80 | 84 | 85 | 86 | 90 | ``` 91 | 92 | ### Textfields 93 | ```html 94 | 95 | 96 | 100 | 101 | 102 | 103 | 107 | ``` 108 | 109 | ### Tooltips 110 | ```html 111 | 112 | 113 | 114 | 115 | 116 | 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 | Chrome, Firefox, Safari, Edge, Samsung Internet
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 | --------------------------------------------------------------------------------