diff --git a/package.json b/package.json index 0e7ead41f318..eeb8a187b145 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/node": "^7.0.5", "@types/run-sequence": "^0.0.28", "@types/rx": "2.5.33", + "autoprefixer": "^6.7.6", "axe-core": "^2.1.7", "axe-webdriverjs": "^0.5.0", "conventional-changelog": "^1.1.0", @@ -62,7 +63,6 @@ "glob": "^7.1.1", "google-cloud": "^0.48.0", "gulp": "^3.9.1", - "gulp-autoprefixer": "^3.1.1", "gulp-better-rollup": "^1.0.2", "gulp-clean": "^0.3.2", "gulp-clean-css": "^3.0.3", diff --git a/src/demo-app/ripple/ripple-demo.scss b/src/demo-app/ripple/ripple-demo.scss index 326288d23013..2f8268a8361d 100644 --- a/src/demo-app/ripple/ripple-demo.scss +++ b/src/demo-app/ripple/ripple-demo.scss @@ -14,7 +14,6 @@ text-align: center; transition: all 200ms linear; width: 200px; - user-select: none; &.demo-ripple-disabled { color: rgba(32, 32, 32, 0.4); @@ -32,4 +31,4 @@ .demo-ripple-checkbox-option { margin: 10px 0; } -} \ No newline at end of file +} diff --git a/src/lib/button-toggle/button-toggle.scss b/src/lib/button-toggle/button-toggle.scss index eea47e3edaab..2cbb1334e96a 100644 --- a/src/lib/button-toggle/button-toggle.scss +++ b/src/lib/button-toggle/button-toggle.scss @@ -1,5 +1,6 @@ @import '../core/a11y/a11y'; @import '../core/style/elevation'; +@import '../core/style/vendor-prefixes'; @import '../core/style/layout-common'; $mat-button-toggle-padding: 0 16px !default; @@ -44,11 +45,11 @@ $mat-button-toggle-border-radius: 2px !default; } .mat-button-toggle-label-content { + @include user-select(none); display: inline-block; line-height: $mat-button-toggle-line-height; padding: $mat-button-toggle-padding; cursor: pointer; - user-select: none; } .mat-button-toggle-label-content > * { diff --git a/src/lib/core/option/_option.scss b/src/lib/core/option/_option.scss index 135835de866d..10cb01bbee17 100644 --- a/src/lib/core/option/_option.scss +++ b/src/lib/core/option/_option.scss @@ -1,4 +1,5 @@ @import '../style/menu-common'; +@import '../style/vendor-prefixes'; @import '../a11y/a11y'; /** @@ -13,8 +14,8 @@ outline: none; &[aria-disabled='true'] { + @include user-select(none); cursor: default; - user-select: none; } } diff --git a/src/lib/core/style/_button-common.scss b/src/lib/core/style/_button-common.scss index c2d53d4db5e7..c5a03c21df47 100644 --- a/src/lib/core/style/_button-common.scss +++ b/src/lib/core/style/_button-common.scss @@ -1,7 +1,9 @@ +@import './vendor-prefixes'; + // Mixin overriding default button styles like the gray background, the border, and the outline. @mixin mat-button-reset { + @include user-select(none); cursor: pointer; - user-select: none; outline: none; border: none; } diff --git a/src/lib/core/style/_vendor-prefixes.scss b/src/lib/core/style/_vendor-prefixes.scss new file mode 100644 index 000000000000..cd4b127802e3 --- /dev/null +++ b/src/lib/core/style/_vendor-prefixes.scss @@ -0,0 +1,31 @@ +/* stylelint-disable material/no-prefixes */ +@mixin user-select($value) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +@mixin input-placeholder { + &::placeholder { + @content; + } + + &::-moz-placeholder { + @content; + } + + &::-webkit-input-placeholder { + @content; + } + + &:-ms-input-placeholder { + @content; + } +} + +@mixin cursor-grab { + cursor: -webkit-grab; + cursor: grab; +} +/* stylelint-enable */ diff --git a/src/lib/input/input-container.scss b/src/lib/input/input-container.scss index 3ab2f7f6eeed..79c5fd579341 100644 --- a/src/lib/input/input-container.scss +++ b/src/lib/input/input-container.scss @@ -1,4 +1,5 @@ @import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; @import '../core/style/form-common'; @@ -109,16 +110,7 @@ $mat-input-underline-disabled-background-image: // Note that we can't use something like visibility: hidden or // display: none, because IE ends up preventing the user from // focusing the input altogether. - &::placeholder { - color: transparent; - } - &::-moz-placeholder { - color: transparent; - } - &::-webkit-input-placeholder { - color: transparent; - } - &:-ms-input-placeholder { + @include input-placeholder { color: transparent; } } diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index e024c72e12ec..d7c9e5a24ad6 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -2,6 +2,7 @@ @import '../core/style/list-common'; @import '../core/style/form-common'; @import '../core/style/variables'; +@import '../core/style/vendor-prefixes'; @import '../core/a11y/a11y'; $mat-select-trigger-height: 30px !default; @@ -28,8 +29,8 @@ $mat-select-trigger-font-size: 16px !default; font-size: $mat-select-trigger-font-size; [aria-disabled='true'] & { + @include user-select(none); cursor: default; - user-select: none; } } diff --git a/src/lib/slide-toggle/slide-toggle.scss b/src/lib/slide-toggle/slide-toggle.scss index 9b475dda1561..9114a259c0bb 100644 --- a/src/lib/slide-toggle/slide-toggle.scss +++ b/src/lib/slide-toggle/slide-toggle.scss @@ -1,6 +1,7 @@ @import '../core/style/variables'; @import '../core/ripple/ripple'; @import '../core/style/elevation'; +@import '../core/style/vendor-prefixes'; @import '../core/a11y/a11y'; $mat-slide-toggle-thumb-size: 20px !default; @@ -25,7 +26,7 @@ $mat-slide-toggle-bar-track-width: $mat-slide-toggle-bar-width - $mat-slide-togg // Disable user selection to ensure that dragging is smooth without grabbing // some elements accidentally. - user-select: none; + @include user-select(none); outline: none; @@ -100,7 +101,7 @@ $mat-slide-toggle-bar-track-width: $mat-slide-toggle-bar-width - $mat-slide-togg transition: $swift-linear; transition-property: transform; - cursor: grab; + @include cursor-grab; // Once the thumb container is being dragged around, we remove the transition duration to // make the drag feeling fast and not delayed. diff --git a/stylelint-config.json b/stylelint-config.json index 029018da60ef..3f0965eb1ad5 100644 --- a/stylelint-config.json +++ b/stylelint-config.json @@ -1,5 +1,9 @@ { + "plugins": [ + "./tools/stylelint/no-prefixes/no-prefixes.js" + ], "rules": { + "material/no-prefixes": [["last 2 versions", "not ie <= 10", "not ie_mob <= 10"]], "color-hex-case": "lower", "color-no-invalid-hex": true, diff --git a/tools/gulp/constants.ts b/tools/gulp/constants.ts index 5aaceed5edf3..096d983ed84c 100644 --- a/tools/gulp/constants.ts +++ b/tools/gulp/constants.ts @@ -10,15 +10,6 @@ export const DIST_COMPONENTS_ROOT = join(DIST_ROOT, '@angular/material'); export const COVERAGE_RESULT_FILE = join(DIST_ROOT, 'coverage', 'coverage-summary.json'); -export const SASS_AUTOPREFIXER_OPTIONS = { - browsers: [ - 'last 2 versions', - 'not ie <= 10', - 'not ie_mob <= 10', - ], - cascade: false, -}; - export const HTML_MINIFIER_OPTIONS = { collapseWhitespace: true, removeComments: true, diff --git a/tools/gulp/util/task_helpers.ts b/tools/gulp/util/task_helpers.ts index effc066048bf..8558d3072703 100644 --- a/tools/gulp/util/task_helpers.ts +++ b/tools/gulp/util/task_helpers.ts @@ -2,7 +2,7 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as gulp from 'gulp'; import * as path from 'path'; -import {NPM_VENDOR_FILES, PROJECT_ROOT, DIST_ROOT, SASS_AUTOPREFIXER_OPTIONS} from '../constants'; +import {NPM_VENDOR_FILES, PROJECT_ROOT, DIST_ROOT} from '../constants'; /** Those imports lack typings. */ @@ -11,7 +11,6 @@ const gulpMerge = require('merge2'); const gulpRunSequence = require('run-sequence'); const gulpSass = require('gulp-sass'); const gulpSourcemaps = require('gulp-sourcemaps'); -const gulpAutoprefixer = require('gulp-autoprefixer'); const gulpConnect = require('gulp-connect'); const resolveBin = require('resolve-bin'); @@ -44,7 +43,6 @@ export function sassBuildTask(dest: string, root: string) { return gulp.src(_globify(root, '**/*.scss')) .pipe(gulpSourcemaps.init()) .pipe(gulpSass().on('error', gulpSass.logError)) - .pipe(gulpAutoprefixer(SASS_AUTOPREFIXER_OPTIONS)) .pipe(gulpSourcemaps.write('.')) .pipe(gulp.dest(dest)); }; diff --git a/tools/stylelint/no-prefixes/needs-prefix.js b/tools/stylelint/no-prefixes/needs-prefix.js new file mode 100644 index 000000000000..ebfd262b42a3 --- /dev/null +++ b/tools/stylelint/no-prefixes/needs-prefix.js @@ -0,0 +1,61 @@ +const autoprefixer = require('autoprefixer'); +const Browsers = require('autoprefixer/lib/browsers'); +const Prefixes = require('autoprefixer/lib/prefixes'); + +/** + * Utility to be used when checking whether a CSS declaration needs to be prefixed. Based on + * Stylelint's `no-vendor-prefix` rule, but instead of checking whether a rule has a prefix, + * we check whether it needs one. + * Reference https://github.com/stylelint/stylelint/blob/master/lib/utils/isAutoprefixable.js + */ +module.exports = class NeedsPrefix { + constructor(browsers) { + this._prefixes = new Prefixes( + autoprefixer.data.prefixes, + new Browsers(autoprefixer.data.browsers, browsers) + ); + } + + /** Checks whether an @-rule needs to be prefixed. */ + atRule(identifier) { + return this._prefixes.add[`@${identifier.toLowerCase()}`]; + } + + /** Checks whether a selector needs to be prefixed. */ + selector(identifier) { + return this._prefixes.add.selectors.some(selectorObj => { + return identifier.toLowerCase() === selectorObj.name; + }); + } + + /** Checks whether a media query value needs to be prefixed. */ + mediaFeature(identifier) { + return identifier.toLowerCase().indexOf('device-pixel-ratio') > -1; + } + + /** Checks whether a property needs to be prefixed. */ + property(identifier) { + // `fill` is an edge case since it was part of a proposal that got renamed to `stretch`. + // see: https://www.w3.org/TR/css-sizing-3/#changes + if (!identifier || identifier === 'fill') return false; + + const needsPrefix = autoprefixer.data.prefixes[identifier.toLowerCase()]; + const browsersThatNeedPrefix = needsPrefix ? needsPrefix.browsers : null; + + return !!browsersThatNeedPrefix && !!this._prefixes.browsers.selected.find(browser => { + return browsersThatNeedPrefix.indexOf(browser) > -1; + }); + } + + /** Checks whether a CSS property value needs to be prefixed. */ + value(prop, value) { + if (!prop || !value) return false; + + const possiblePrefixableValues = this._prefixes.add[prop.toLowerCase()] && + this._prefixes.add[prop.toLowerCase()].values; + + return !!possiblePrefixableValues && possiblePrefixableValues.some(valueObj => { + return value.toLowerCase() === valueObj.name; + }); + } +}; diff --git a/tools/stylelint/no-prefixes/no-prefixes.js b/tools/stylelint/no-prefixes/no-prefixes.js new file mode 100644 index 000000000000..1e6e880be889 --- /dev/null +++ b/tools/stylelint/no-prefixes/no-prefixes.js @@ -0,0 +1,88 @@ +const stylelint = require('stylelint'); +const NeedsPrefix = require('./needs-prefix'); +const parseSelector = require('stylelint/lib/utils/parseSelector'); + +const ruleName = 'material/no-prefixes'; +const messages = stylelint.utils.ruleMessages(ruleName, { + property: property => `Unprefixed property "${property}".`, + value: (property, value) => `Unprefixed value in "${property}: ${value}".`, + atRule: name => `Unprefixed @rule "${name}".`, + mediaFeature: value => `Unprefixed media feature "${value}".`, + selector: selector => `Unprefixed selector "${selector}".` +}); + +/** + * Stylelint plugin that warns for unprefixed CSS. + */ +const plugin = stylelint.createPlugin(ruleName, browsers => { + return (root, result) => { + if (!stylelint.utils.validateOptions(result, ruleName, {})) return; + + const needsPrefix = new NeedsPrefix(browsers); + + // Check all of the `property: value` pairs. + root.walkDecls(decl => { + if (needsPrefix.property(decl.prop)) { + stylelint.utils.report({ + result, + ruleName, + message: messages.property(decl.prop), + node: decl, + index: (decl.raws.before || '').length + }); + } else if (needsPrefix.value(decl.prop, decl.value)) { + stylelint.utils.report({ + result, + ruleName, + message: messages.value(decl.prop, decl.value), + node: decl, + index: (decl.raws.before || '').length + }); + } + }); + + // Check all of the @-rules and their values. + root.walkAtRules(rule => { + if (needsPrefix.atRule(rule.name)) { + stylelint.utils.report({ + result, + ruleName, + message: messages.atRule(rule.name), + node: rule + }); + } else if (needsPrefix.mediaFeature(rule.params)) { + stylelint.utils.report({ + result, + ruleName, + message: messages.mediaFeature(rule.name), + node: rule + }); + } + }); + + // Walk the rules and check if the selector needs prefixes. + root.walkRules(rule => { + // Silence warnings for SASS selectors. Stylelint does this in their own rules as well: + // https://github.com/stylelint/stylelint/blob/master/lib/utils/isStandardSyntaxSelector.js + parseSelector(rule.selector, { warn: () => {} }, rule, selectorTree => { + selectorTree.walkPseudos(pseudoNode => { + if (needsPrefix.selector(pseudoNode.value)) { + stylelint.utils.report({ + result, + ruleName, + message: messages.selector(pseudoNode.value), + node: rule, + index: (rule.raws.before || '').length + pseudoNode.sourceIndex, + }); + } + }); + }); + }); + + }; +}); + + +plugin.ruleName = ruleName; +plugin.messages = messages; +module.exports = plugin;