├── .gitignore ├── tests ├── README.md ├── visual │ ├── badge_upstream.html │ ├── badge.html │ ├── button.html │ ├── button_upstream.html │ ├── progress.html │ ├── alert.html │ ├── alert_upstream.html │ └── progress_upstream.html └── visual.js ├── elements.js ├── helpers └── helpers.js ├── .editorconfig ├── elements ├── button-group.js ├── badge.js ├── progress.js ├── alert.js └── button.js ├── rollup.config.js ├── .circleci └── config.yml ├── LICENSE ├── package.json ├── styles ├── type.js └── colors.js ├── README.md ├── CONTRIBUTING.md └── demo └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test-results 3 | dist 4 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Test overview 2 | 3 | ## Visual 4 | 5 | * based on https://github.com/Polymer/pwa-starter-kit/blob/master/test/integration/visual.js 6 | -------------------------------------------------------------------------------- /elements.js: -------------------------------------------------------------------------------- 1 | export * from './elements/alert.js'; 2 | export * from './elements/badge.js'; 3 | export * from './elements/button-group.js'; 4 | export * from './elements/button.js'; 5 | export * from './elements/progress.js'; 6 | -------------------------------------------------------------------------------- /helpers/helpers.js: -------------------------------------------------------------------------------- 1 | import {render} from 'lit-html/lit-html.js'; 2 | 3 | export function renderToHead(style) { 4 | let fragment = document.createDocumentFragment(); 5 | render(style, fragment); 6 | document.head.appendChild(fragment); 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = LF 5 | indent_style = tab 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,md,yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /elements/button-group.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | export default class BsButtonGroup extends LitElement { 4 | static get styles() { 5 | return css` 6 | :host { 7 | display: inline-flex; 8 | vertical-align: middle; 9 | } 10 | 11 | ::slotted(bs-button:not(:last-child)) { 12 | --bs-border-top-right-radius: 0; 13 | --bs-border-bottom-right-radius: 0; 14 | } 15 | 16 | ::slotted(bs-button:not(:first-child)) { 17 | --bs-border-top-left-radius: 0; 18 | --bs-border-bottom-left-radius: 0; 19 | } 20 | `; 21 | } 22 | 23 | render() { 24 | return html` 25 | 26 | `; 27 | } 28 | } 29 | 30 | customElements.define('bs-button-group', BsButtonGroup); 31 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import minify from 'rollup-plugin-babel-minify'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import filesize from 'rollup-plugin-filesize'; 5 | const outFolder = 'dist'; 6 | 7 | rimraf.sync(outFolder); 8 | 9 | export default [ 10 | { 11 | input: 'elements.js', 12 | output: { 13 | file: `${outFolder}/elements.bundled.js`, 14 | format: 'iife', 15 | name: 'BootstrapElements' 16 | }, 17 | plugins: [resolve()] 18 | }, 19 | { 20 | input: 'elements.js', 21 | output: { 22 | file: `${outFolder}/elements.bundled.min.js`, 23 | format: 'iife', 24 | name: 'BootstrapElements' 25 | }, 26 | plugins: [resolve(), minify({ comments: false }), filesize({showBrotliSize: true})] 27 | } 28 | ]; 29 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # https://circleci.com/docs/2.0/language-javascript/ 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8-browsers # https://hub.docker.com/r/circleci/node/ 6 | 7 | steps: 8 | - checkout 9 | 10 | - restore_cache: 11 | keys: 12 | - dependencies-{{ checksum "package.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - dependencies- 15 | 16 | - run: npm install 17 | 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: dependencies-{{ checksum "package.json" }} 22 | 23 | - run: 24 | name: visual 25 | command: npm run test -- --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/visual/result.xml 26 | 27 | - store_artifacts: 28 | path: test-results 29 | 30 | - store_test_results: 31 | path: test-results 32 | 33 | - run: 34 | name: build 35 | command: npm run build 36 | 37 | - store_artifacts: 38 | path: dist 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 morbidick 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 | -------------------------------------------------------------------------------- /tests/visual/badge_upstream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | Primary 13 | Secondary 14 | Success 15 | Danger 16 | Warning 17 | Info 18 | Light 19 | Dark 20 |

21 | Primary 22 | Secondary 23 | Success 24 | Danger 25 | Warning 26 | Info 27 | Light 28 | Dark 29 |

30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/visual/badge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 20 | 21 | 22 | 23 |

24 | Primary 25 | Secondary 26 | Success 27 | Danger 28 | Warning 29 | Info 30 | Light 31 | Dark 32 |

33 | Primary 34 | Secondary 35 | Success 36 | Danger 37 | Warning 38 | Info 39 | Light 40 | Dark 41 |

42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/visual/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 20 | 21 | 22 | 23 |

24 | Primary 25 | Secondary 26 | Success 27 | Danger 28 | Warning 29 | Info 30 | Light 31 | Dark 32 |

33 | Primary 34 | Secondary 35 | Success 36 | Danger 37 | Warning 38 | Info 39 | Light 40 | Dark 41 |

42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@morbidick/bootstrap", 3 | "version": "0.4.2", 4 | "description": "Rewrite of bootstrap components as web components", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/morbidick/bootstrap-webcomponents" 9 | }, 10 | "main": "dist/elements.bundled.js", 11 | "module": "elements.js", 12 | "dependencies": { 13 | "@morbidick/lit-element-notify": "^1.0.0", 14 | "lit-element": "^2.0.1", 15 | "lit-html": "^1.0.0", 16 | "rimraf": "^2.6.3" 17 | }, 18 | "devDependencies": { 19 | "@polymer/iron-demo-helpers": "^3.0.2", 20 | "@webcomponents/webcomponentsjs": "^2.2.6", 21 | "chai": "^4.2.0", 22 | "fs-extra": "^7.0.1", 23 | "mocha": "^5.0.0", 24 | "mocha-junit-reporter": "^1.18.0", 25 | "pixelmatch": "^4.0.2", 26 | "pngjs": "^3.3.3", 27 | "polyserve": "^0.27.15", 28 | "puppeteer": "1.6.2", 29 | "rollup": "^0.59.4", 30 | "rollup-plugin-babel-minify": "^5.0.0", 31 | "rollup-plugin-filesize": "^4.0.1", 32 | "rollup-plugin-node-resolve": "^3.4.0" 33 | }, 34 | "scripts": { 35 | "start": "npm run serve", 36 | "serve": "polyserve --npm --module-resolution=node", 37 | "test": "mocha tests/visual.js", 38 | "build": "rollup -c", 39 | "prepack": "npm test && npm run build" 40 | }, 41 | "files": [ 42 | "dist", 43 | "elements", 44 | "elements.js", 45 | "helpers", 46 | "styles" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/visual/button_upstream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /styles/type.js: -------------------------------------------------------------------------------- 1 | // https://getbootstrap.com/docs/4.1/content/typography/ 2 | import {css, unsafeCSS, html} from 'lit-element'; 3 | import {primary} from './colors.js'; 4 | 5 | export const fontFamilySansSerif = unsafeCSS`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; 6 | export const fontFamilyMonospace = unsafeCSS`SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`; 7 | export const fontStyle = css` 8 | :root { 9 | font-family: var(--bs-body-font-family, ${fontFamilySansSerif}); 10 | line-height: 1.5; 11 | color: var(--bs-body-font-color, #212529); 12 | } 13 | 14 | h1, h2, h3, h4, h5, h6 { 15 | margin-top: 1.5em; 16 | margin-bottom: 0.5em; 17 | font-weight: 500; 18 | line-height: 1.2; 19 | } 20 | 21 | h1 { font-size: 2.5rem; } 22 | h2 { font-size: 2rem; } 23 | h3 { font-size: 1.75rem; } 24 | h4 { font-size: 1.5rem; } 25 | h5 { font-size: 1.25rem; } 26 | h6 { font-size: 1rem; } 27 | 28 | p { 29 | margin-top: 0; 30 | margin-bottom: 1rem; 31 | }` 32 | 33 | export const linkStyle = css` 34 | a { 35 | color: var(--bs-link-color, ${primary.color}); 36 | text-decoration: none; 37 | } 38 | a:hover, a:focus { 39 | text-decoration: underline; 40 | }` 41 | 42 | export const hrStyle = css` 43 | hr { 44 | margin-top: 1rem; 45 | margin-bottom: 1rem; 46 | border: 0; 47 | border-top: 1px solid currentColor; 48 | opacity: 0.1; 49 | }` 50 | 51 | export default html` 52 | 57 | `; 58 | -------------------------------------------------------------------------------- /tests/visual/progress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 24 | 25 | 26 | 27 | 28 | 25% 29 | 30 | 75% 31 | 32 |
33 | primary 34 | secondary 35 | success 36 | info 37 | warning 38 | danger 39 | light 40 | dark 41 |
42 | 10% 43 | 44 | 50% 45 | 46 | 100% 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap Web Components 2 | 3 | [![CircleCI](https://circleci.com/gh/morbidick/bootstrap-webcomponents.svg?style=shield)](https://circleci.com/gh/morbidick/bootstrap-webcomponents) 4 | [![npm version](https://img.shields.io/npm/v/@morbidick/bootstrap.svg)](https://www.npmjs.com/package/@morbidick/bootstrap) 5 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/@morbidick/bootstrap.svg)](https://bundlephobia.com/result?p=@morbidick/bootstrap) 6 | 7 | > ## 🛠 Status: In Development 8 | > Components get implemented as needed. PRs welcome! 9 | 10 | Rewrite of bootstrap components as web components. See [the demo](demo/index.html) for all implemented features. 11 | 12 | ## Motivation 13 | 14 | This isn't just a webcomponents wrapper around the bootstrap css, this aims to be a clean and simple rewrite easily understandable to everyone new to the webcomponents standards. 15 | 16 | * No build chain / preprocessors 17 | * allow easy theming via css custom properties 18 | * provide a clear api to end users in the form of attributes 19 | * Use web standards over big libraries where possible (even if that means loosing compatibility eg. dialog element) 20 | * Provide support to projects to include polyfills and buildsteps for the above points 21 | 22 | ## Install 23 | 24 | ```bash 25 | npm install @morbidick/bootstrap 26 | ``` 27 | 28 | ## Usage 29 | 30 | Import into your module script: 31 | 32 | ```javascript 33 | import { BsButton, BsBadge } from "@morbidick/bootstrap/elements.js" 34 | ``` 35 | 36 | Alternatively, load a bundled version from CDN: 37 | 38 | ```html 39 | 40 | ``` 41 | 42 | Use it in your web page: 43 | 44 | ```html 45 | Click Me 46 | warning 47 | ``` 48 | 49 | ## Development 50 | 51 | View the [contributing notes](CONTRIBUTING.md) for instructions and further resources. 52 | -------------------------------------------------------------------------------- /elements/badge.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import colors from '../styles/colors.js'; 3 | 4 | 5 | export default class BsBadge extends LitElement { 6 | static get properties() { 7 | return { 8 | href: { 9 | type: String, 10 | reflect: true, 11 | }, 12 | theme: { 13 | type: String, 14 | reflect: true, 15 | }, 16 | } 17 | } 18 | 19 | constructor() { 20 | super(); 21 | this.theme = 'secondary'; 22 | } 23 | 24 | static get styles() { 25 | return [ 26 | css` 27 | :host { 28 | display: inline-block; 29 | } 30 | :host([hidden]) { 31 | display: hidden; 32 | } 33 | a, span { 34 | display: inline-block; 35 | padding: .25em .4em; 36 | border-top-left-radius: var(--bs-border-top-left-radius, .25em); 37 | border-top-right-radius: var(--bs-border-top-right-radius, .25em); 38 | border-bottom-left-radius: var(--bs-border-bottom-left-radius, .25em); 39 | border-bottom-right-radius: var(--bs-border-bottom-right-radius, .25em); 40 | 41 | transition: filter .15s ease-in-out; 42 | color: var(--bs-badge-color); 43 | background-color: var(--bs-badge-background-color); 44 | 45 | font-size: .75em; 46 | font-weight: 700; 47 | line-height: 1; 48 | text-align: center; 49 | white-space: nowrap; 50 | vertical-align: baseline; 51 | text-decoration: none; 52 | } 53 | a:hover, a:active, a:focus { 54 | text-decoration: none; 55 | background-color: var(--bs-badge-hover-background-color) 56 | } 57 | :host([theme~="pill"]) > * { 58 | padding-right: .6em; 59 | padding-left: .6em; 60 | border-radius: 10em; 61 | }`, 62 | ...colors.map(({selector, color, contrast, hoverbg}) => css` 63 | :host(${selector}) { 64 | --bs-badge-background-color: ${color}; 65 | --bs-badge-color: ${contrast}; 66 | --bs-badge-hover-background-color: ${hoverbg}; 67 | }` 68 | ) 69 | ]; 70 | }; 71 | 72 | render() { 73 | return html` 74 | ${this.href ? html``: html``} 75 | `;} 76 | } 77 | 78 | customElements.define('bs-badge', BsBadge); 79 | -------------------------------------------------------------------------------- /tests/visual/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 20 | 21 | 22 | 23 | This is a primary alert with an example link. Give it a click if you like. 24 | This is a secondary alert with an example link. Give it a click if you like. 25 | This is a success alert with an example link. Give it a click if you like. 26 | This is a danger alert with an example link. Give it a click if you like. 27 | This is a warning alert with an example link. Give it a click if you like. 28 | This is a info alert with an example link. Give it a click if you like. 29 | This is a light alert with an example link. Give it a click if you like. 30 | This is a dark alert with an example link. Give it a click if you like. 31 | 32 | This is a primary dismissable alert. 33 | This is a secondary dismissable alert. 34 | This is a success dismissable alert. 35 | This is a danger dismissable alert. 36 | This is a warning dismissable alert. 37 | This is a info dismissable alert. 38 | This is a light dismissable alert. 39 | This is a dark dismissable alert. 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /elements/progress.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import colors, { primary } from '../styles/colors.js'; 3 | 4 | export default class BsProgress extends LitElement { 5 | static get properties() { 6 | return { 7 | theme: { 8 | type: String, 9 | reflect: true, 10 | }, 11 | value: { 12 | type: Number, 13 | reflect: true, 14 | }, 15 | maxValue: { 16 | type: Number, 17 | reflect: true, 18 | }, 19 | } 20 | } 21 | 22 | constructor() { 23 | super(); 24 | this.value = 0; 25 | this.maxValue = 100; 26 | } 27 | 28 | static get styles() { 29 | return [ 30 | css` 31 | :host { 32 | display: flex; 33 | height: 1rem; 34 | overflow: hidden; 35 | font-size: .75rem; 36 | background-color: var(--bs-progress-default-color, #e9ecef); 37 | border-radius: .25rem; 38 | } 39 | :host([hidden]) { 40 | display: hidden; 41 | } 42 | #bar { 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: center; 46 | color: var(--bs-progress-color, ${primary.contrast}); 47 | text-align: center; 48 | white-space: nowrap; 49 | background-color: var(--bs-progress-background-color, ${primary.color}); 50 | transition: width .6s ease; 51 | } 52 | :host([theme~="striped"]) #bar { 53 | background-image: linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent); 54 | background-size: 1rem 1rem; 55 | } 56 | @keyframes stripes { 57 | from { background-position: 1rem 0; } 58 | to { background-position: 0 0; } 59 | } 60 | :host([theme~="animated"]) #bar { 61 | animation: stripes 1s linear infinite; 62 | } 63 | @media (prefers-reduced-motion: reduce) { 64 | :host([theme~="animated"]) #bar { 65 | animation: none; 66 | } 67 | } 68 | `, 69 | ...colors.map(({selector, color, contrast}) => css` 70 | :host(${selector}) { 71 | --bs-progress-background-color: ${color}; 72 | --bs-progress-color: ${contrast}; 73 | }` 74 | ) 75 | ]; 76 | }; 77 | 78 | get $bar() { 79 | return this.shadowRoot.querySelector('#bar'); 80 | } 81 | 82 | updated(changed) { 83 | if (changed.has('value') || changed.has('maxValue')) { 84 | this.$bar.style.width = `${this.value/this.maxValue*100}%`; 85 | } 86 | } 87 | 88 | render() { 89 | return html` 90 |
96 | 97 |
98 | `;} 99 | } 100 | 101 | customElements.define('bs-progress', BsProgress); 102 | -------------------------------------------------------------------------------- /elements/alert.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import colors from '../styles/colors.js'; 3 | 4 | export default class BsAlert extends LitElement { 5 | static get properties() { 6 | return { 7 | dismissable: { 8 | type: Boolean, 9 | reflect: true, 10 | }, 11 | theme: { 12 | type: String, 13 | reflect: true, 14 | }, 15 | } 16 | } 17 | 18 | constructor() { 19 | super(); 20 | this.theme = 'secondary'; 21 | } 22 | 23 | connectedCallback() { 24 | super.connectedCallback(); 25 | if (!this.hasAttribute('role')) { 26 | this.setAttribute('role', 'alert'); 27 | } 28 | } 29 | 30 | static get styles() { 31 | return [ 32 | css` 33 | :host { 34 | display: block; 35 | } 36 | :host([hidden]) { 37 | display: hidden; 38 | } 39 | :host { 40 | position: relative; 41 | padding: .75rem 1.25rem; 42 | margin-bottom: 1rem; 43 | border: 1px solid transparent; 44 | border-radius: .25rem; 45 | 46 | background-color: var(--bs-alert-background-color); 47 | color: var(--bs-alert-color); 48 | border-color: var(--bs-alert-border-color); 49 | } 50 | :host([dismissable]) { 51 | padding-right: 4em; 52 | } 53 | :host(:not([dismissable])) button { 54 | display: none; 55 | } 56 | button { 57 | position: absolute; 58 | top: 0; 59 | right: 0; 60 | padding: .75rem 1.25rem; 61 | border: 0; 62 | 63 | cursor: pointer; 64 | color: inherit; 65 | background-color: transparent; 66 | font-size: 1.5rem; 67 | font-weight: 700; 68 | line-height: 1; 69 | text-shadow: 0 1px 0 #fff; 70 | opacity: .5; 71 | } 72 | button:hover{ 73 | color: #000; 74 | text-decoration: none; 75 | opacity: .75; 76 | } 77 | ::slotted(a) { 78 | font-weight: 700; 79 | } 80 | /* important seems to be back to override parent scope */ 81 | ::slotted(:first-child) { 82 | margin-top: 0 !important; 83 | } 84 | ::slotted(:last-child) { 85 | margin-bottom: 0 !important; 86 | } 87 | `, 88 | ...colors.map(({selector, alertcolor, alertbg, alertborder, alertlinkcolor}) => css` 89 | :host(${selector}) { 90 | --bs-alert-background-color: ${alertbg}; 91 | --bs-alert-color: ${alertcolor}; 92 | --bs-alert-border-color: ${alertborder}; 93 | --bs-link-color: ${alertlinkcolor}; 94 | } 95 | `) 96 | ] 97 | } 98 | 99 | render() { 100 | return html` 101 | 102 | 108 | `; 109 | } 110 | 111 | remove() { 112 | super.remove(); 113 | this.dispatchEvent(new CustomEvent('dismissed', { 114 | bubbles: false, 115 | composed: true, 116 | })); 117 | } 118 | } 119 | 120 | customElements.define('bs-alert', BsAlert); 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Communicating 4 | 5 | * [polymer slack](https://bit.ly/polymerslack) `#bootstrap` 6 | 7 | ## Setup 8 | 9 | * Install dependencies `npm install` 10 | * Run dev Server with `npm start` and open your browser on [localhost:8081/demo](http://localhost:8081/demo) 11 | * Run the [tests](tests) with `npm test` 12 | * To produce a bundle run `npm run build` 13 | 14 | ## Implementing a new compoment 15 | 16 | * write down a list of all theme and functional variants in `tests/component_upstream.html` 17 | * evaluate native elements (e.g. dialog for modals or progress for the progress bar) 18 | * design a simple api surface (e.g. setting the `href` or `toggle` attribute to the button component changes the underlying element) in `tests/component.html` 19 | * open an issue (more eyes more ideas ;) ) 20 | * implement the dom and functional parts 21 | * try to keep it simple 22 | * prefer `slots` over attributes/properties 23 | * provide sane defaults (a11y and user experience wise) 24 | * implement the styles 25 | * have a look at the [upstream scss](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_buttons.scss) 26 | * have a look at the resulting css in [your browser](http://localhost:8081/tests/button_upstream.html) (`npm start` first) 27 | * write the base css first, prefix css custom proberties with `bs` 28 | * for performance reasons implement static css that selects on host attributes 29 | * for theme variants loop over `styles/colors.js` 30 | * run the screenshot tests `npm test`, see the results in [test-results](test-results) 31 | * push early and iterate often (most components so far took me about 3 implementations until it felt right) 32 | * extra goal: implement functional browser tests 33 | 34 | ## Resources 35 | 36 | ### Bootstrap 37 | 38 | * [components](https://getbootstrap.com/docs/4.1/components/) 39 | * [variables](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss) 40 | 41 | ### Web components 42 | 43 | #### Basics 44 | 45 | * [web components introduction](https://www.webcomponents.org/introduction) 46 | * [lit element docs](https://github.com/Polymer/lit-element#minimal-example) 47 | * [lit-html docs](https://polymer.github.io/lit-html/) 48 | 49 | 50 | #### Best Practices 51 | 52 | * [aria authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/) 53 | * [Google Dev Custom Element Best Practices](https://developers.google.com/web/fundamentals/web-components/best-practices) 54 | * [ChromeLabs howto components](https://github.com/GoogleChromeLabs/howto-components) 55 | * [Form Participation API](https://docs.google.com/document/d/1JO8puctCSpW-ZYGU8lF-h4FWRIDQNDVexzHoOQ2iQmY/edit?pli=1) 56 | 57 | #### Other libraries 58 | 59 | * [Elix](https://github.com/elix/elix) 60 | * [Vaadin Elements](https://github.com/search?q=topic%3Awebcomponents+org%3Avaadin&type=Repositories) 61 | * [Github Elements](https://github.com/search?q=topic%3Aweb-components+org%3Agithub&type=Repositories) 62 | * [predix ui](https://www.predix-ui.com/#/elements) 63 | 64 | #### Styling 65 | 66 | * [native css reset](https://caniuse.com/#search=appearance) 67 | * [vaadin themeable mixin](https://github.com/vaadin/vaadin-themable-mixin) 68 | * [::part and ::theme spec](https://drafts.csswg.org/css-shadow-parts-1/) 69 | * [CSS Modules](https://github.com/w3c/webcomponents/issues/759) and [constructible Stylesheets](https://github.com/WICG/construct-stylesheets/blob/gh-pages/explainer.md) 70 | * [color calculations in pure css](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/) 71 | -------------------------------------------------------------------------------- /tests/visual.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const puppeteer = require('puppeteer'); 4 | const pixelmatch = require('pixelmatch'); 5 | const expect = require('chai').expect; 6 | const {startServer} = require('polyserve'); 7 | const PNG = require('pngjs').PNG; 8 | 9 | const testDir = `tests/visual`; 10 | const screenshotDir = `test-results/visual/screenshots`; 11 | 12 | describe('implementation visually matches original bootstrap', function() { 13 | this.timeout(5000); 14 | let polyserve, browser, page; 15 | 16 | // This is ran when the suite starts up. 17 | before(async function() { 18 | // This is where you would substitute your python or Express server or whatever. 19 | polyserve = await startServer({port:4000, root:path.join(__dirname, '..'), moduleResolution:'node'}) 20 | 21 | // Create the test directory if needed. 22 | await fs.ensureDir(screenshotDir); 23 | }); 24 | 25 | // This is ran when the suite is done. Stop your server here. 26 | after((done) => polyserve.close(done)); 27 | 28 | // This is ran before every test. It's where you start a clean browser. 29 | beforeEach(async function() { 30 | browser = await puppeteer.launch(); 31 | page = await browser.newPage(); 32 | page.setViewport({width: 1024, height: 768}); 33 | }); 34 | 35 | // This is ran after every test; clean up after your browser. 36 | afterEach(() => browser.close()); 37 | 38 | it('Buttons', async function() { 39 | return takeAndCompareScreenshot(page, 'button'); 40 | }); 41 | 42 | it('Badges', async function() { 43 | return takeAndCompareScreenshot(page, 'badge'); 44 | }); 45 | 46 | it('Alerts', async function() { 47 | return takeAndCompareScreenshot(page, 'alert'); 48 | }); 49 | 50 | it('Progress bar', async function() { 51 | return takeAndCompareScreenshot(page, 'progress'); 52 | }); 53 | }); 54 | 55 | // - page is a reference to the Puppeteer page. 56 | // - route is the path you're loading 57 | async function takeAndCompareScreenshot(page, route) { 58 | const filePath = `${screenshotDir}/${route}.png`; 59 | const filePathUpstream = `${screenshotDir}/${route}_upstream.png`; 60 | const filePathDiff = `${screenshotDir}/${route}_diff.png`; 61 | 62 | // take rebuild screenshot 63 | await page.goto(`http://127.0.0.1:4000/${testDir}/${route}.html`, {waitUntil: 'networkidle2'}); 64 | await page.screenshot({path: filePath, fullPage: true}); 65 | 66 | // take upstream screenshot 67 | await page.goto(`http://127.0.0.1:4000/${testDir}/${route}_upstream.html`, {waitUntil: 'networkidle2'}); 68 | await page.screenshot({path: filePathUpstream, fullPage: true}); 69 | 70 | // Test to see if it's right. 71 | return compareScreenshots(filePath, filePathUpstream, filePathDiff); 72 | } 73 | 74 | function compareScreenshots(file1, file2, diffPath) { 75 | return new Promise((resolve, reject) => { 76 | const img1 = fs.createReadStream(file1).pipe(new PNG()).on('parsed', doneReading); 77 | const img2 = fs.createReadStream(file2).pipe(new PNG()).on('parsed', doneReading); 78 | 79 | let filesRead = 0; 80 | function doneReading() { 81 | // Wait until both files are read. 82 | if (++filesRead < 2) return; 83 | 84 | // The files should be the same size. 85 | expect(img1.width, 'image widths are the same').equal(img2.width); 86 | expect(img1.height, 'image heights are the same').equal(img2.height); 87 | 88 | // Do the visual diff. 89 | const diff = new PNG({width: img1.width, height: img1.height}); 90 | const numDiffPixels = pixelmatch( 91 | img1.data, img2.data, diff.data, img1.width, img1.height, 92 | {threshold: 0.15}); 93 | 94 | // write the diff 95 | diff.pack().pipe(fs.createWriteStream(diffPath)); 96 | 97 | // The files should look the same. 98 | expect(numDiffPixels, 'number of different pixels').equal(0); 99 | resolve(); 100 | } 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /tests/visual/alert_upstream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 42 | 48 | 54 | 60 | 66 | 72 | 78 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /tests/visual/progress_upstream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
25%
27 |
28 |
29 |
30 |
31 |
32 |
75%
33 |
34 |
35 |
36 |
37 |
38 |
39 |
primary
41 |
42 |
43 |
secondary
45 |
46 |
47 |
success
49 |
50 |
51 |
info
53 |
54 |
55 |
warning
57 |
58 |
59 |
danger
61 |
62 |
63 |
light
65 |
66 |
67 |
dark
69 |
70 |
71 |
72 |
10%
74 |
75 |
76 |
78 |
79 |
80 |
50%
82 |
83 |
84 |
86 |
87 |
88 |
100%
90 |
91 |
92 |
93 |
95 |
96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /styles/colors.js: -------------------------------------------------------------------------------- 1 | import {unsafeCSS} from 'lit-element'; 2 | 3 | export const primary = { 4 | selector: unsafeCSS`[theme~="primary"]`, 5 | color: unsafeCSS`var(--bs-primary-color, #007bff)`, 6 | focusring: unsafeCSS`var(--bs-primary-focusring-color, #007bff50)`, 7 | hoverbg: unsafeCSS`var(--bs-primary-hover-background-color, #0069d9)`, 8 | contrast: unsafeCSS`var(--bs-primary-contrast-color, #fff)`, 9 | alertcolor: unsafeCSS`var(--bs-primary-alert-color, #004085)`, 10 | alertbg: unsafeCSS`var(--bs-primary-alert-background-color, #cce5ff)`, 11 | alertborder: unsafeCSS`var(--bs-primary-alert-border-color, #b8daff)`, 12 | alertlinkcolor: unsafeCSS`var(--bs-primary-alert-link-color, #002752)`, 13 | 14 | } 15 | export const secondary = { 16 | selector: unsafeCSS`[theme~="secondary"]`, 17 | color: unsafeCSS`var(--bs-secondary-color, #6c757d)`, 18 | focusring: unsafeCSS`var(--bs-secondary-focusring-color, #6c757d50)`, 19 | hoverbg: unsafeCSS`var(--bs-secondary-hover-background-color, #5a6268)`, 20 | contrast: unsafeCSS`var(--bs-secondary-contrast-color, #fff)`, 21 | alertcolor: unsafeCSS`var(--bs-secondary-alert-color, #383d41)`, 22 | alertbg: unsafeCSS`var(--bs-secondary-alert-background-color, #e2e3e5)`, 23 | alertborder: unsafeCSS`var(--bs-secondary-alert-border-color, #d6d8db)`, 24 | alertlinkcolor: unsafeCSS`var(--bs-secondary-alert-link-color, #202326)`, 25 | } 26 | export const success = { 27 | selector: unsafeCSS`[theme~="success"]`, 28 | color: unsafeCSS`var(--bs-success-color, #28a745)`, 29 | focusring: unsafeCSS`var(--bs-success-focusring-color, #28a74550)`, 30 | hoverbg: unsafeCSS`var(--bs-success-hover-background-color, #218838)`, 31 | contrast: unsafeCSS`var(--bs-success-contrast-color, #fff)`, 32 | alertcolor: unsafeCSS`var(--bs-success-alert-color, #155724)`, 33 | alertbg: unsafeCSS`var(--bs-success-alert-background-color, #d4edda)`, 34 | alertborder: unsafeCSS`var(--bs-success-alert-border-color, #c3e6cb)`, 35 | alertlinkcolor: unsafeCSS`var(--bs-success-alert-link-color, #0b2e13)`, 36 | } 37 | export const danger = { 38 | selector: unsafeCSS`[theme~="danger"]`, 39 | color: unsafeCSS`var(--bs-danger-color, #dc3545)`, 40 | focusring: unsafeCSS`var(--bs-danger-focusring-color, #dc354550)`, 41 | hoverbg: unsafeCSS`var(--bs-danger-hover-background-color, #c82333)`, 42 | contrast: unsafeCSS`var(--bs-danger-contrast-color, #fff)`, 43 | alertcolor: unsafeCSS`var(--bs-danger-alert-color, #721c24)`, 44 | alertbg: unsafeCSS`var(--bs-danger-alert-background-color, #f8d7da)`, 45 | alertborder: unsafeCSS`var(--bs-danger-alert-border-color, #f5c6cb)`, 46 | alertlinkcolor: unsafeCSS`var(--bs-danger-alert-link-color, #491217)`, 47 | } 48 | export const warning = { 49 | selector: unsafeCSS`[theme~="warning"]`, 50 | color: unsafeCSS`var(--bs-warning-color, #ffc107)`, 51 | focusring: unsafeCSS`var(--bs-warning-focusring-color, #ffc10750)`, 52 | hoverbg: unsafeCSS`var(--bs-warning-hover-background-color, #e0a800)`, 53 | contrast: unsafeCSS`var(--bs-warning-contrast-color, #212529)`, 54 | alertcolor: unsafeCSS`var(--bs-warning-alert-color, #856404)`, 55 | alertbg: unsafeCSS`var(--bs-warning-alert-background-color, #fff3cd)`, 56 | alertborder: unsafeCSS`var(--bs-warning-alert-border-color, #ffeeba)`, 57 | alertlinkcolor: unsafeCSS`var(--bs-warning-alert-link-color, #533f03)`, 58 | } 59 | export const info = { 60 | selector: unsafeCSS`[theme~="info"]`, 61 | color: unsafeCSS`var(--bs-info-color, #17a2b8)`, 62 | focusring: unsafeCSS`var(--bs-info-focusring-color, #17a2b850)`, 63 | hoverbg: unsafeCSS`var(--bs-info-hover-background-color, #138496)`, 64 | contrast: unsafeCSS`var(--bs-info-contrast-color, #fff)`, 65 | alertcolor: unsafeCSS`var(--bs-info-alert-color, #0c5460)`, 66 | alertbg: unsafeCSS`var(--bs-info-alert-background-color, #d1ecf1)`, 67 | alertborder: unsafeCSS`var(--bs-info-alert-border-color, #bee5eb)`, 68 | alertlinkcolor: unsafeCSS`var(--bs-info-alert-link-color, #062c33)`, 69 | } 70 | export const light = { 71 | selector: unsafeCSS`[theme~="light"]`, 72 | color: unsafeCSS`var(--bs-light-color, #f8f9fa)`, 73 | focusring: unsafeCSS`var(--bs-light-focusring-color, #f8f9fa50)`, 74 | hoverbg: unsafeCSS`var(--bs-light-hover-background-color, #e2e6ea)`, 75 | contrast: unsafeCSS`var(--bs-light-contrast-color, #212529)`, 76 | alertcolor: unsafeCSS`var(--bs-light-alert-color, #818182)`, 77 | alertbg: unsafeCSS`var(--bs-light-alert-background-color, #fefefe)`, 78 | alertborder: unsafeCSS`var(--bs-light-alert-border-color, #fdfdfe)`, 79 | alertlinkcolor: unsafeCSS`var(--bs-light-alert-link-color, #686868)`, 80 | } 81 | export const dark = { 82 | selector: unsafeCSS`[theme~="dark"]`, 83 | color: unsafeCSS`var(--bs-dark-color, #343a40)`, 84 | focusring: unsafeCSS`var(--bs-dark-focusring-color, #343a4050)`, 85 | hoverbg: unsafeCSS`var(--bs-dark-hover-background-color, #23272b)`, 86 | contrast: unsafeCSS`var(--bs-dark-contrast-color, #fff)`, 87 | alertcolor: unsafeCSS`var(--bs-dark-alert-color, #1b1e21)`, 88 | alertbg: unsafeCSS`var(--bs-dark-alert-background-color, #d6d8d9)`, 89 | alertborder: unsafeCSS`var(--bs-dark-alert-border-color, #c6c8ca)`, 90 | alertlinkcolor: unsafeCSS`var(--bs-dark-alert-link-color, #040505)`, 91 | }; 92 | 93 | export default [ 94 | primary, 95 | secondary, 96 | success, 97 | info, 98 | warning, 99 | danger, 100 | light, 101 | dark, 102 | ]; 103 | -------------------------------------------------------------------------------- /elements/button.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { LitNotify } from '@morbidick/lit-element-notify'; 3 | import colors from '../styles/colors.js'; 4 | 5 | export default class BsButton extends LitNotify(LitElement) { 6 | static get properties() { 7 | return { 8 | // set button state to active 9 | active: { 10 | type: Boolean, 11 | reflect: true, 12 | notify: true, 13 | }, 14 | // disable the button 15 | disabled: { 16 | type: Boolean, 17 | reflect: true, 18 | }, 19 | // set to an url to get an anchor element with button styling 20 | href: { 21 | type: String, 22 | reflect: true, 23 | }, 24 | theme: { 25 | type: String, 26 | reflect: true, 27 | }, 28 | // set to true to act as a toggle button 29 | toggle: { 30 | type: Boolean, 31 | reflect: true, 32 | }, 33 | // the button type (default: button) 34 | type: { 35 | type: String, 36 | reflect: true, 37 | }, 38 | } 39 | } 40 | 41 | constructor() { 42 | super(); 43 | this.active = false; 44 | this.disabled = false; 45 | this.theme = 'secondary'; 46 | this.type = 'button'; 47 | } 48 | 49 | static get styles() { 50 | return [ 51 | css` 52 | :host(:not([hidden])) { 53 | display: inline-block; 54 | } 55 | :host > *:not([disabled]) { 56 | cursor: pointer; 57 | } 58 | :host > *:focus { 59 | outline: none; 60 | } 61 | a, button { 62 | box-sizing: border-box; 63 | display: inline-block; 64 | padding: .375em .75em; 65 | border: 1px solid transparent; 66 | border-top-left-radius: var(--bs-border-top-left-radius, .25em); 67 | border-top-right-radius: var(--bs-border-top-right-radius, .25em); 68 | border-bottom-left-radius: var(--bs-border-bottom-left-radius, .25em); 69 | border-bottom-right-radius: var(--bs-border-bottom-right-radius, .25em); 70 | 71 | font-size: 1rem; 72 | line-height: 1.5; 73 | text-align: center; 74 | user-select: none; 75 | vertical-align: middle; 76 | white-space: nowrap; 77 | text-decoration: none; 78 | 79 | background-color: transparent; 80 | transition: 81 | color .15s ease-in-out, 82 | background-color .15s ease-in-out, 83 | border-color .15s ease-in-out, 84 | box-shadow .15s ease-in-out, 85 | filter .15s ease-in-out; 86 | } 87 | /* reset firefox button focus style */ 88 | button::-moz-focus-inner { 89 | border: 0; 90 | } 91 | :host > [active]:not([disabled]) { 92 | filter: brightness(0.85); 93 | } 94 | :host > [disabled] { 95 | opacity: 0.65; 96 | } 97 | :host([theme~="small"]) > * { 98 | font-size: .875rem; 99 | } 100 | :host([theme~="large"]) > * { 101 | font-size: 1.25rem; 102 | } 103 | *:focus:not([disabled]) { 104 | box-shadow: 0 0 0 .2rem var(--bs-button-focusring-color); 105 | } 106 | :host(:not([theme~="outline"])) > * { 107 | background-color: var(--bs-button-background-color); 108 | color: var(--bs-button-color); 109 | } 110 | :host(:not([theme~="outline"])) > *:hover:not([disabled]) { 111 | background-color: var(--bs-button-hover-background-color); 112 | } 113 | :host([theme~="outline"]) > * { 114 | color: var(--bs-button-background-color); 115 | border-color: var(--bs-button-background-color); 116 | } 117 | :host([theme~="outline"]) > *:hover:not([disabled]), :host([theme~="outline"]) > [active] { 118 | background-color: var(--bs-button-background-color); 119 | color: var(--bs-button-color); 120 | } 121 | :host([theme~="outline"]) > *:hover:not([active]) { 122 | filter: none; 123 | } 124 | `, 125 | ...colors.map(({selector, color, contrast, focusring, hoverbg}) => css` 126 | :host(${selector}) { 127 | --bs-button-background-color: ${color}; 128 | --bs-button-color: ${contrast}; 129 | --bs-button-focusring-color: ${focusring}; 130 | --bs-button-hover-background-color: ${hoverbg}; 131 | }` 132 | ) 133 | ] 134 | } 135 | 136 | render() { 137 | return html` 138 | ${this.href ? html``: 139 | !this.toggle ? html``: 140 | html` 150 | `} 151 | `; 152 | } 153 | 154 | /** 155 | * Toggle the button state 156 | */ 157 | _toggle() { 158 | if (this.disabled) 159 | return; 160 | this.active = !this.active; 161 | } 162 | 163 | _toggleClickHandler(event) { 164 | event.preventDefault(); 165 | this._toggle(); 166 | } 167 | 168 | _toggleKeypressHandler(event) { 169 | if (event.altKey) 170 | return; 171 | 172 | switch (event.code) { 173 | case "Enter": 174 | case "Space": 175 | event.preventDefault(); 176 | this._toggle(); 177 | break; 178 | default: 179 | return; 180 | } 181 | } 182 | 183 | /** 184 | * Pass external clicks to the internal button 185 | */ 186 | click() { 187 | this.$button.click(); 188 | } 189 | 190 | /** 191 | * Pass external focuses to the internal button 192 | */ 193 | focus() { 194 | this.$button.focus(); 195 | } 196 | 197 | /** 198 | * Expose internal button 199 | */ 200 | get $button() { 201 | return this.shadowRoot.querySelector('#button'); 202 | } 203 | } 204 | 205 | customElements.define('bs-button', BsButton); 206 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bootstrap Webcomponents 9 | 10 | 11 | 12 | 24 | 33 | 34 | 35 | 36 |
37 |

Bootstrap Webcomponents

38 | 44 |

Buttons

45 | 46 | 57 | 58 | 59 |

outline

60 | 61 | 71 | 72 | 73 |

sizes

74 | 75 | 80 | 81 | 82 |

states

83 | 84 | 90 | 91 | 92 |

groups

93 | 94 | 102 | 103 | 104 |

links

105 | 106 | 114 | 115 | 116 |

custom theme

117 | 118 | 130 | 131 | 132 |

Badges

133 | 134 | 144 | 145 | 146 |

pill round

147 | 148 | 158 | 159 | 160 |

as links

161 | 162 | 166 | 167 | 168 |

Alerts

169 | 170 | 180 | 181 | 182 |

rich content

183 | 184 | 192 | 193 | 194 |

dismissable

195 | 196 | 201 | 202 | 203 |

Progress

204 | 205 | 214 | 215 |
216 | 217 | 218 | 219 | --------------------------------------------------------------------------------