_fab-theme.scss 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. // Copyright 2016 Google Inc.
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy
  4. // of this software and associated documentation files (the "Software"), to deal
  5. // in the Software without restriction, including without limitation the rights
  6. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7. // copies of the Software, and to permit persons to whom the Software is
  8. // furnished to do so, subject to the following conditions:
  9. //
  10. // The above copyright notice and this permission notice shall be included in
  11. // all copies or substantial portions of the Software.
  12. //
  13. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. // THE SOFTWARE.
  20. // stylelint-disable selector-class-pattern --
  21. // Selector '.mdc-*' should only be used in this project.
  22. @use '@material/elevation/elevation-theme';
  23. @use '@material/animation/functions' as animation-functions;
  24. @use '@material/feature-targeting/feature-targeting';
  25. @use '@material/ripple/ripple-theme';
  26. @use '@material/shape/mixins' as shape-mixins;
  27. @use '@material/theme/css';
  28. @use '@material/theme/custom-properties';
  29. @use '@material/theme/keys';
  30. @use '@material/theme/replace';
  31. @use '@material/theme/state';
  32. @use '@material/theme/theme';
  33. @use '@material/theme/theme-color';
  34. @use '@material/tokens/resolvers';
  35. @use './fab-custom-properties';
  36. @use 'sass:math';
  37. @use 'sass:list';
  38. @use 'sass:map';
  39. @use 'sass:meta';
  40. $height: 56px !default;
  41. $mini-height: 40px !default;
  42. $shape-radius: 50% !default;
  43. $ripple-target: '.mdc-fab__ripple';
  44. $light-theme: (
  45. container-color: secondary,
  46. container-elevation: 6,
  47. container-height: 56px,
  48. container-shadow-color: black,
  49. container-shape: $shape-radius,
  50. container-surface-tint-layer-color: null,
  51. container-width: 56px,
  52. focus-container-elevation: null,
  53. focus-icon-color: null,
  54. focus-outline-color: null,
  55. focus-outline-width: null,
  56. focus-state-layer-color: theme-color.$primary,
  57. focus-state-layer-opacity: null,
  58. hover-container-elevation: null,
  59. hover-icon-color: null,
  60. hover-state-layer-color: theme-color.$primary,
  61. hover-state-layer-opacity: null,
  62. icon-color: on-secondary,
  63. icon-size: 24px,
  64. lowered-container-elevation: null,
  65. lowered-focus-container-elevation: null,
  66. lowered-hover-container-elevation: null,
  67. lowered-pressed-container-elevation: null,
  68. pressed-container-elevation: null,
  69. pressed-icon-color: null,
  70. pressed-ripple-color: null,
  71. pressed-ripple-opacity: null,
  72. pressed-state-layer-color: theme-color.$primary,
  73. pressed-state-layer-opacity: null,
  74. );
  75. $custom-property-prefix: 'fab';
  76. ///
  77. /// Applies the given theme as custom properties without any selectors.
  78. ///
  79. @mixin theme($theme, $resolvers: resolvers.$material) {
  80. @include theme.validate-theme($light-theme, $theme);
  81. $resolved-theme: resolve-theme($theme, $resolvers);
  82. @include keys.declare-custom-properties(
  83. $resolved-theme,
  84. $prefix: $custom-property-prefix
  85. );
  86. }
  87. @mixin theme-styles($theme, $resolvers: resolvers.$material) {
  88. @include theme.validate-theme($light-theme, $theme);
  89. $theme: keys.create-theme-properties(
  90. $theme,
  91. $prefix: $custom-property-prefix
  92. );
  93. @include base-theme-styles($theme, $resolvers: $resolvers);
  94. $shape-radius: map.get($theme, container-shape);
  95. @if $shape-radius {
  96. @include shape-radius($shape-radius);
  97. }
  98. }
  99. ///
  100. /// Resolves the given theme with the given resolvers.
  101. ///
  102. @function resolve-theme($theme, $resolvers) {
  103. $elevation-resolver: map.get($resolvers, elevation);
  104. @return _resolve-theme-elevation-keys(
  105. $theme,
  106. $elevation-resolver,
  107. (
  108. container-elevation,
  109. hover-container-elevation,
  110. focus-container-elevation,
  111. pressed-container-elevation,
  112. disabled-container-elevation,
  113. lowered-container-elevation,
  114. lowered-focus-container-elevation,
  115. lowered-hover-container-elevation,
  116. lowered-pressed-container-elevation
  117. )
  118. );
  119. }
  120. ///
  121. /// Returns the theme with the elevation keys resolved.
  122. ///
  123. @function _resolve-theme-elevation-keys($theme, $resolver, $elevation-keys) {
  124. @if $resolver == null {
  125. @return $theme;
  126. }
  127. // Shadow color is universal for the component.
  128. $shadow-color: map.get($theme, container-shadow-color);
  129. @each $key in $elevation-keys {
  130. $elevation: map.get($theme, $key);
  131. @if $elevation != null {
  132. $resolved-value: meta.call(
  133. $resolver,
  134. $elevation: $elevation,
  135. $shadow-color: $shadow-color
  136. );
  137. // Update the key with the resolved value.
  138. $theme: map.set($theme, $key, $resolved-value);
  139. }
  140. }
  141. @return $theme;
  142. }
  143. @mixin base-theme-styles($theme, $resolvers: resolvers.$material) {
  144. @include container-color(map.get($theme, container-color));
  145. @include _container-elevation(
  146. map.get($resolvers, elevation),
  147. map.get($theme, container-shadow-color),
  148. map.get($theme, container-surface-tint-layer-color),
  149. (
  150. default: map.get($theme, container-elevation),
  151. hover: map.get($theme, hover-container-elevation),
  152. focus: map.get($theme, focus-container-elevation),
  153. pressed: map.get($theme, pressed-container-elevation),
  154. )
  155. );
  156. @include _container-height(map.get($theme, container-height));
  157. @include _container-width(map.get($theme, container-width));
  158. @include icon-size(map.get($theme, icon-size));
  159. @include _icon-color(
  160. (
  161. default: map.get($theme, icon-color),
  162. hover: map.get($theme, hover-icon-color),
  163. focus: map.get($theme, focus-icon-color),
  164. pressed: map.get($theme, pressed-icon-color),
  165. )
  166. );
  167. $opacity-map: (
  168. hover: map.get($theme, hover-state-layer-opacity),
  169. focus: map.get($theme, focus-state-layer-opacity),
  170. press: map.get($theme, pressed-state-layer-opacity),
  171. );
  172. $hover-state-layer-color: map.get($theme, hover-state-layer-color);
  173. @if $hover-state-layer-color {
  174. @include ripple-color($hover-state-layer-color, $opacity-map: $opacity-map);
  175. }
  176. $focus-outline-color: map.get($theme, focus-outline-color);
  177. @if $focus-outline-color {
  178. @include focus-outline-color($focus-outline-color);
  179. }
  180. $focus-outline-width: map.get($theme, focus-outline-width);
  181. @if $focus-outline-width {
  182. @include focus-outline-width($focus-outline-width);
  183. }
  184. }
  185. @mixin ripple-color($color, $opacity-map: (), $query: feature-targeting.all()) {
  186. @include ripple-theme.states(
  187. $color,
  188. $opacity-map: $opacity-map,
  189. $query: $query,
  190. $ripple-target: $ripple-target
  191. );
  192. }
  193. @mixin accessible($container-color, $query: feature-targeting.all()) {
  194. @include container-color($container-color, $query: $query);
  195. $fill-tone: theme-color.tone($container-color);
  196. @if ($fill-tone == 'dark') {
  197. @include ink-color(text-primary-on-dark, $query: $query);
  198. @include ripple-theme.states(
  199. text-primary-on-dark,
  200. $query: $query,
  201. $ripple-target: $ripple-target
  202. );
  203. } @else {
  204. @include ink-color(text-primary-on-light, $query: $query);
  205. @include ripple-theme.states(
  206. text-primary-on-light,
  207. $query: $query,
  208. $ripple-target: $ripple-target
  209. );
  210. }
  211. }
  212. @mixin container-color($color, $query: feature-targeting.all()) {
  213. $feat-color: feature-targeting.create-target($query, color);
  214. @include feature-targeting.targets($feat-color) {
  215. @include theme.property(background-color, $color);
  216. }
  217. }
  218. @mixin icon-size($width, $height: $width, $query: feature-targeting.all()) {
  219. $feat-structure: feature-targeting.create-target($query, structure);
  220. .mdc-fab__icon {
  221. @include feature-targeting.targets($feat-structure) {
  222. @include theme.property('width', $width);
  223. @include theme.property('height', $height);
  224. @include theme.property('font-size', $height);
  225. }
  226. }
  227. }
  228. @mixin ink-color($color, $query: feature-targeting.all()) {
  229. $feat-color: feature-targeting.create-target($query, color);
  230. @include feature-targeting.targets($feat-color) {
  231. &,
  232. &:not(:disabled) .mdc-fab__icon,
  233. &:not(:disabled) .mdc-fab__label,
  234. &:disabled .mdc-fab__icon,
  235. &:disabled .mdc-fab__label {
  236. @include theme.property(color, $color);
  237. }
  238. }
  239. }
  240. @mixin _container-height($height) {
  241. @include theme.property('height', $height);
  242. }
  243. @mixin _container-width($width) {
  244. @include theme.property('width', $width);
  245. }
  246. @mixin _icon-color($color-or-map) {
  247. &:not(:disabled) {
  248. @include _set-icon-color(state.get-default-state($color-or-map));
  249. &:hover {
  250. @include _set-icon-color(state.get-hover-state($color-or-map));
  251. }
  252. &:focus {
  253. @include _set-icon-color(state.get-focus-state($color-or-map));
  254. }
  255. &:active {
  256. @include _set-icon-color(state.get-pressed-state($color-or-map));
  257. }
  258. }
  259. &:disabled {
  260. @include _set-icon-color(state.get-disabled-state($color-or-map));
  261. }
  262. }
  263. @mixin _set-icon-color($color) {
  264. .mdc-fab__icon {
  265. @include theme.property(color, $color);
  266. }
  267. }
  268. @mixin _container-elevation($resolver, $shadow-color, $container-color, $map) {
  269. &:not(:disabled) {
  270. @include elevation-theme.with-resolver(
  271. $resolver,
  272. $elevation: state.get-default-state($map),
  273. $shadow-color: $shadow-color
  274. );
  275. @include elevation-theme.overlay-container-color($container-color);
  276. &:hover {
  277. @include elevation-theme.with-resolver(
  278. $resolver,
  279. $elevation: state.get-hover-state($map),
  280. $shadow-color: $shadow-color
  281. );
  282. @include elevation-theme.overlay-container-color($container-color);
  283. }
  284. &:focus {
  285. @include elevation-theme.with-resolver(
  286. $resolver,
  287. $elevation: state.get-focus-state($map),
  288. $shadow-color: $shadow-color
  289. );
  290. @include elevation-theme.overlay-container-color($container-color);
  291. }
  292. &:active {
  293. @include elevation-theme.with-resolver(
  294. $resolver,
  295. $elevation: state.get-pressed-state($map),
  296. $shadow-color: $shadow-color
  297. );
  298. @include elevation-theme.overlay-container-color($container-color);
  299. }
  300. }
  301. &:disabled {
  302. // FAB does not have disabled state. Use default state's elevation.
  303. @include elevation-theme.with-resolver(
  304. $resolver,
  305. $elevation: state.get-default-state($map),
  306. $shadow-color: $shadow-color
  307. );
  308. }
  309. }
  310. ///
  311. /// Sets outline width only when button is in focus. Also sets padding to
  312. /// include outline on focus (Helps prevent size jump on focus).
  313. /// @param {Number} $width - Outline (border) width.
  314. /// @param {Number|List} $padding [0] - Padding when button is not in focus.
  315. /// Offsets padding based on given outline width on focus.
  316. ///
  317. @mixin focus-outline-width(
  318. $width,
  319. $padding: 0,
  320. $query: feature-targeting.all()
  321. ) {
  322. $feat-structure: feature-targeting.create-target($query, structure);
  323. $padding: css.unpack-value($padding);
  324. $padding-fallbacks: (0 0 0 0);
  325. $is-padding-custom-prop: (false false false false);
  326. $is-width-custom-prop: custom-properties.is-custom-prop($width);
  327. $width-fallback: if(
  328. custom-properties.is-custom-prop($width),
  329. custom-properties.get-fallback($width),
  330. $width
  331. );
  332. $width: if(
  333. custom-properties.is-custom-prop($width),
  334. custom-properties.get-declaration-value($width),
  335. $width
  336. );
  337. // conform padding values and extract custom property metadata from them
  338. @for $i from 1 through list.length($padding) {
  339. $value: list.nth($padding, $i);
  340. $value-is-custom-prop: custom-properties.is-custom-prop($value);
  341. // css max will fail to compare a bare 0 to a px value
  342. $value: if($value == 0, 0px, $value);
  343. $value-fallback: if(
  344. $value-is-custom-prop,
  345. custom-properties.get-fallback($value),
  346. $value
  347. );
  348. $value: if(
  349. $value-is-custom-prop,
  350. custom-properties.get-declaration-value($value),
  351. $value
  352. );
  353. $padding: list.set-nth($padding, $i, $value);
  354. $padding-fallbacks: list.set-nth($padding-fallbacks, $i, $value-fallback);
  355. $is-padding-custom-prop: list.set-nth(
  356. $is-padding-custom-prop,
  357. $i,
  358. $value-is-custom-prop
  359. );
  360. }
  361. // Padding should include outline width which will be set on focus.
  362. // sass math required for IE since IE doesn't support css max
  363. $padding-top-fallback: math.max(
  364. list.nth($padding-fallbacks, 1),
  365. $width-fallback
  366. );
  367. $padding-right-fallback: math.max(
  368. list.nth($padding-fallbacks, 2),
  369. $width-fallback
  370. );
  371. $padding-bottom-fallback: math.max(
  372. list.nth($padding-fallbacks, 3),
  373. $width-fallback
  374. );
  375. $padding-left-fallback: math.max(
  376. list.nth($padding-fallbacks, 4),
  377. $width-fallback
  378. );
  379. $padding-top: replace.replace-string(
  380. 'max(paddingval, width)',
  381. (
  382. paddingval: list.nth($padding, 1),
  383. width: $width,
  384. )
  385. );
  386. $padding-right: replace.replace-string(
  387. 'max(paddingval, width)',
  388. (
  389. paddingval: list.nth($padding, 2),
  390. width: $width,
  391. )
  392. );
  393. $padding-bottom: replace.replace-string(
  394. 'max(paddingval, width)',
  395. (
  396. paddingval: list.nth($padding, 3),
  397. width: $width,
  398. )
  399. );
  400. $padding-left: replace.replace-string(
  401. 'max(paddingval, width)',
  402. (
  403. paddingval: list.nth($padding, 4),
  404. width: $width,
  405. )
  406. );
  407. $top-has-custom-prop: $is-width-custom-prop or
  408. list.nth($is-padding-custom-prop, 1);
  409. @include css.declaration(padding-top, $padding-top-fallback);
  410. @if $top-has-custom-prop {
  411. @include css.declaration(
  412. padding-top,
  413. $padding-top,
  414. $gss: (alternate: $top-has-custom-prop)
  415. );
  416. }
  417. $right-has-custom-prop: $is-width-custom-prop or
  418. list.nth($is-padding-custom-prop, 2);
  419. @include css.declaration(padding-right, $padding-right-fallback);
  420. @if $right-has-custom-prop {
  421. @include css.declaration(
  422. padding-right,
  423. $padding-right,
  424. $gss: (alternate: $right-has-custom-prop)
  425. );
  426. }
  427. $bottom-has-custom-prop: $is-width-custom-prop or
  428. list.nth($is-padding-custom-prop, 3);
  429. @include css.declaration(padding-bottom, $padding-bottom-fallback);
  430. @if $bottom-has-custom-prop {
  431. @include css.declaration(
  432. padding-bottom,
  433. $padding-bottom,
  434. $gss: (alternate: $bottom-has-custom-prop)
  435. );
  436. }
  437. $left-has-custom-prop: $is-width-custom-prop or
  438. list.nth($is-padding-custom-prop, 4);
  439. @include css.declaration(padding-left, $padding-left-fallback);
  440. @if $left-has-custom-prop {
  441. @include css.declaration(
  442. padding-left,
  443. $padding-left,
  444. $gss: (alternate: $left-has-custom-prop)
  445. );
  446. }
  447. &:not(:disabled) {
  448. @include ripple-theme.focus() {
  449. @include feature-targeting.targets($feat-structure) {
  450. border-style: solid;
  451. @include theme.property(border-width, $width);
  452. // sass math required for IE since IE doesn't support css max
  453. $padding-top-fallback: math.abs(
  454. list.nth($padding-fallbacks, 1) - $width-fallback
  455. );
  456. $padding-right-fallback: math.abs(
  457. list.nth($padding-fallbacks, 2) - $width-fallback
  458. );
  459. $padding-bottom-fallback: math.abs(
  460. list.nth($padding-fallbacks, 3) - $width-fallback
  461. );
  462. $padding-left-fallback: math.abs(
  463. list.nth($padding-fallbacks, 4) - $width-fallback
  464. );
  465. // max(a, calc(a * -1)) is equivalent to math.abs
  466. $padding-top: replace.replace-string(
  467. 'max(paddingcalc, calc(paddingcalc * -1))',
  468. (
  469. paddingcalc: 'calc(paddingval - width)',
  470. paddingval: list.nth($padding, 1),
  471. width: $width,
  472. )
  473. );
  474. $padding-right: replace.replace-string(
  475. 'max(paddingcalc, calc(paddingcalc * -1))',
  476. (
  477. paddingcalc: 'calc(paddingval - width)',
  478. paddingval: list.nth($padding, 2),
  479. width: $width,
  480. )
  481. );
  482. $padding-bottom: replace.replace-string(
  483. 'max(paddingcalc, calc(paddingcalc * -1))',
  484. (
  485. paddingcalc: 'calc(paddingval - width)',
  486. paddingval: list.nth($padding, 3),
  487. width: $width,
  488. )
  489. );
  490. $padding-left: replace.replace-string(
  491. 'max(paddingcalc, calc(paddingcalc * -1))',
  492. (
  493. paddingcalc: 'calc(paddingval - width)',
  494. paddingval: list.nth($padding, 4),
  495. width: $width,
  496. )
  497. );
  498. @include css.declaration(padding-top, $padding-top-fallback);
  499. @if $top-has-custom-prop {
  500. @include css.declaration(
  501. padding-top,
  502. $padding-top,
  503. $gss: (alternate: $top-has-custom-prop)
  504. );
  505. }
  506. @include css.declaration(padding-right, $padding-right-fallback);
  507. @if $right-has-custom-prop {
  508. @include css.declaration(
  509. padding-right,
  510. $padding-right,
  511. $gss: (alternate: $right-has-custom-prop)
  512. );
  513. }
  514. @include css.declaration(padding-bottom, $padding-bottom-fallback);
  515. @if $bottom-has-custom-prop {
  516. @include css.declaration(
  517. padding-bottom,
  518. $padding-bottom,
  519. $gss: (alternate: $bottom-has-custom-prop)
  520. );
  521. }
  522. @include css.declaration(padding-left, $padding-left-fallback);
  523. @if $left-has-custom-prop {
  524. @include css.declaration(
  525. padding-left,
  526. $padding-left,
  527. $gss: (alternate: $left-has-custom-prop)
  528. );
  529. }
  530. }
  531. }
  532. }
  533. }
  534. ///
  535. /// Sets outline color only when button is in focus. Use `focus-outline-width()`
  536. /// to set appropriate outline width.
  537. /// @param {Color} $color - Outline (border) color.
  538. ///
  539. @mixin focus-outline-color($color, $query: feature-targeting.all()) {
  540. $feat-color: feature-targeting.create-target($query, color);
  541. &:not(:disabled) {
  542. @include ripple-theme.focus() {
  543. @include feature-targeting.targets($feat-color) {
  544. @include theme.property(border-color, $color);
  545. }
  546. }
  547. }
  548. }
  549. @mixin shape-radius(
  550. $radius,
  551. $rtl-reflexive: false,
  552. $query: feature-targeting.all()
  553. ) {
  554. &:not(.mdc-fab--extended) {
  555. // Do not specify $component-height for shape radius. FABs are circular,
  556. // which means they can use percentage border radius without resolving to
  557. // a component height.
  558. @include shape-mixins.radius($radius, $rtl-reflexive, $query: $query);
  559. #{$ripple-target} {
  560. @include shape-mixins.radius($radius, $rtl-reflexive, $query: $query);
  561. }
  562. }
  563. }