_ripple-theme.scss 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. //
  2. // Copyright 2016 Google Inc.
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. //
  22. // stylelint-disable selector-class-pattern --
  23. // Selector '.mdc-*' should only be used in this project.
  24. @use 'sass:color';
  25. @use 'sass:map';
  26. @use '@material/animation/functions' as functions2;
  27. @use '@material/animation/variables' as variables2;
  28. @use '@material/base/mixins' as base-mixins;
  29. @use '@material/feature-targeting/feature-targeting';
  30. @use '@material/theme/css';
  31. @use '@material/theme/custom-properties';
  32. @use '@material/theme/theme';
  33. @use '@material/theme/keys';
  34. @use '@material/theme/shadow-dom';
  35. @use '@material/theme/theme-color';
  36. $custom-property-prefix: 'ripple';
  37. $fade-in-duration: 75ms !default;
  38. $fade-out-duration: 150ms !default;
  39. $translate-duration: 225ms !default;
  40. $states-wash-duration: 15ms !default;
  41. // Notes on states:
  42. // * focus takes precedence over hover (i.e. if an element is both focused and hovered, only focus value applies)
  43. // * press state applies to a separate pseudo-element, so it has an additive effect on top of other states
  44. // * selected/activated are applied additively to hover/focus via calculations at preprocessing time
  45. $dark-ink-opacities: (
  46. hover: 0.04,
  47. focus: 0.12,
  48. press: 0.12,
  49. selected: 0.08,
  50. activated: 0.12,
  51. ) !default;
  52. $light-ink-opacities: (
  53. hover: 0.08,
  54. focus: 0.24,
  55. press: 0.24,
  56. selected: 0.16,
  57. activated: 0.24,
  58. ) !default;
  59. // Legacy
  60. $pressed-dark-ink-opacity: 0.16 !default;
  61. $pressed-light-ink-opacity: 0.32 !default;
  62. // State selector variables used for state selector mixins below.
  63. $_hover-selector: '&:hover';
  64. $_focus-selector: '&.mdc-ripple-upgraded--background-focused, &:not(.mdc-ripple-upgraded):focus';
  65. $_active-selector: '&:not(:disabled):active';
  66. $light-theme: (
  67. focus-state-layer-color: theme-color.$on-surface,
  68. focus-state-layer-opacity: map.get($dark-ink-opacities, focus),
  69. hover-state-layer-color: theme-color.$on-surface,
  70. hover-state-layer-opacity: map.get($dark-ink-opacities, hover),
  71. pressed-state-layer-color: theme-color.$on-surface,
  72. pressed-state-layer-opacity: map.get($dark-ink-opacities, press),
  73. );
  74. @mixin theme($theme) {
  75. @include keys.declare-custom-properties(
  76. $theme,
  77. $prefix: $custom-property-prefix
  78. );
  79. @if shadow-dom.$css-selector-fallback-declarations {
  80. .mdc-ripple-surface {
  81. @include theme-styles($theme);
  82. }
  83. }
  84. }
  85. $_ripple-theme: (
  86. hover-state-layer-color: null,
  87. focus-state-layer-color: null,
  88. pressed-state-layer-color: null,
  89. hover-state-layer-opacity: null,
  90. focus-state-layer-opacity: null,
  91. pressed-state-layer-opacity: null,
  92. );
  93. @mixin theme-styles($theme, $ripple-target: '&') {
  94. $theme: keys.create-theme-properties(
  95. $theme,
  96. $prefix: $custom-property-prefix
  97. );
  98. // TODO(b/191298796): Support states layer color for every interactive states.
  99. // Use only hover state layer color, ignoring focus and pressed color.
  100. @include internal-theme-styles($theme, $ripple-target);
  101. }
  102. @mixin internal-theme-styles($theme, $ripple-target: '&') {
  103. @include theme.validate-theme-styles($_ripple-theme, $theme);
  104. @include states-base-color(
  105. map.get($theme, hover-state-layer-color),
  106. $ripple-target: $ripple-target
  107. );
  108. @include states-hover-opacity(
  109. map.get($theme, hover-state-layer-opacity),
  110. $ripple-target: $ripple-target
  111. );
  112. @include states-focus-opacity(
  113. map.get($theme, focus-state-layer-opacity),
  114. $ripple-target: $ripple-target
  115. );
  116. @include states-press-opacity(
  117. map.get($theme, pressed-state-layer-opacity),
  118. $ripple-target: $ripple-target
  119. );
  120. }
  121. @mixin states-base-color(
  122. $color,
  123. $query: feature-targeting.all(),
  124. $ripple-target: '&'
  125. ) {
  126. $feat-color: feature-targeting.create-target($query, color);
  127. @if $color {
  128. @if not custom-properties.is-custom-prop($color) {
  129. $color: custom-properties.create(
  130. ripple-color,
  131. theme-color.get-custom-property($color)
  132. );
  133. }
  134. #{$ripple-target}::before,
  135. #{$ripple-target}::after {
  136. @include feature-targeting.targets($feat-color) {
  137. @include theme.property(background-color, $color);
  138. }
  139. }
  140. }
  141. }
  142. ///
  143. /// Customizes ripple opacities in `hover`, `focus`, or `press` states
  144. /// @param {map} $opacity-map - map specifying custom opacity of zero or more states
  145. /// @param {bool} $has-nested-focusable-element - whether the component contains a focusable element in the root
  146. /// @param {string} $ripple-target - the optional selector for the ripple element
  147. ///
  148. @mixin states-opacities(
  149. $opacity-map: (),
  150. $has-nested-focusable-element: false,
  151. $ripple-target: '&',
  152. $query: feature-targeting.all()
  153. ) {
  154. // Ensure sufficient specificity to override base state opacities
  155. @if map.get($opacity-map, hover) {
  156. @include states-hover-opacity(
  157. map.get($opacity-map, hover),
  158. $ripple-target: $ripple-target,
  159. $query: $query
  160. );
  161. }
  162. @if map.get($opacity-map, focus) {
  163. @include states-focus-opacity(
  164. map.get($opacity-map, focus),
  165. $ripple-target: $ripple-target,
  166. $has-nested-focusable-element: $has-nested-focusable-element,
  167. $query: $query
  168. );
  169. }
  170. @if map.get($opacity-map, press) {
  171. @include states-press-opacity(
  172. map.get($opacity-map, press),
  173. $ripple-target: $ripple-target,
  174. $query: $query
  175. );
  176. }
  177. }
  178. @mixin states-hover-opacity(
  179. $opacity,
  180. $query: feature-targeting.all(),
  181. $ripple-target: '&'
  182. ) {
  183. $feat-color: feature-targeting.create-target($query, color);
  184. @if $opacity and not custom-properties.is-custom-prop($opacity) {
  185. $opacity: custom-properties.create(ripple-hover-opacity, $opacity);
  186. }
  187. // Background wash styles, for both CSS-only and upgraded stateful surfaces
  188. &:hover,
  189. &.mdc-ripple-surface--hover {
  190. @include states-background-selector($ripple-target) {
  191. // Opacity falls under color because the chosen opacity is color-dependent in typical usage
  192. @include feature-targeting.targets($feat-color) {
  193. @include theme.property(opacity, $opacity);
  194. }
  195. }
  196. }
  197. }
  198. @mixin states-focus-opacity(
  199. $opacity,
  200. $has-nested-focusable-element: false,
  201. $query: feature-targeting.all(),
  202. $ripple-target: '&'
  203. ) {
  204. // Focus overrides hover by reusing the ::before pseudo-element.
  205. // :focus-within generally works on non-MS browsers and matches when a *child* of the element has focus.
  206. // It is useful for cases where a component has a focusable element within the root node, e.g. text field,
  207. // but undesirable in general in case of nested stateful components.
  208. // We use a modifier class for JS-enabled surfaces to support all use cases in all browsers.
  209. @if $has-nested-focusable-element {
  210. // JS-enabled selectors.
  211. &.mdc-ripple-upgraded--background-focused,
  212. // CSS-only selectors.
  213. &:not(.mdc-ripple-upgraded):focus,
  214. &:focus-within {
  215. @include states-background-selector($ripple-target) {
  216. @include states-focus-opacity-properties_(
  217. $opacity: $opacity,
  218. $query: $query
  219. );
  220. }
  221. }
  222. } @else {
  223. // JS-enabled selectors.
  224. &.mdc-ripple-upgraded--background-focused,
  225. // CSS-only selectors.
  226. &:not(.mdc-ripple-upgraded):focus {
  227. @include states-background-selector($ripple-target) {
  228. @include states-focus-opacity-properties_(
  229. $opacity: $opacity,
  230. $query: $query
  231. );
  232. }
  233. }
  234. }
  235. }
  236. @mixin states-focus-opacity-properties_($opacity, $query) {
  237. $feat-animation: feature-targeting.create-target($query, animation);
  238. // Opacity falls under color because the chosen opacity is color-dependent in typical usage
  239. $feat-color: feature-targeting.create-target($query, color);
  240. @if $opacity {
  241. @if not custom-properties.is-custom-prop($opacity) {
  242. $opacity: custom-properties.create(ripple-focus-opacity, $opacity);
  243. }
  244. // Note that this duration is only effective on focus, not blur
  245. @include feature-targeting.targets($feat-animation) {
  246. transition-duration: 75ms;
  247. }
  248. @include feature-targeting.targets($feat-color) {
  249. @include theme.property(opacity, $opacity);
  250. }
  251. }
  252. }
  253. @mixin states-press-opacity(
  254. $opacity,
  255. $query: feature-targeting.all(),
  256. $ripple-target: '&'
  257. ) {
  258. $feat-animation: feature-targeting.create-target($query, animation);
  259. $feat-color: feature-targeting.create-target($query, color);
  260. // Styles for non-upgraded (CSS-only) stateful surfaces
  261. @if $opacity {
  262. @if not custom-properties.is-custom-prop($opacity) {
  263. $opacity: custom-properties.create(ripple-press-opacity, $opacity);
  264. }
  265. &:not(.mdc-ripple-upgraded) {
  266. // Apply press additively by using the ::after pseudo-element
  267. #{$ripple-target}::after {
  268. @include feature-targeting.targets($feat-animation) {
  269. transition: opacity $fade-out-duration linear;
  270. }
  271. }
  272. &:active {
  273. #{$ripple-target}::after {
  274. @include feature-targeting.targets($feat-animation) {
  275. transition-duration: $fade-in-duration;
  276. }
  277. // Opacity falls under color because the chosen opacity is color-dependent in typical usage
  278. @include feature-targeting.targets($feat-color) {
  279. @include theme.property(opacity, $opacity);
  280. }
  281. }
  282. }
  283. }
  284. &.mdc-ripple-upgraded {
  285. @include feature-targeting.targets($feat-color) {
  286. // Upgraded ripple should always emit custom property, regardless of
  287. // configuration, since ripple itself feature detects custom property
  288. // support at runtime.
  289. @include custom-properties.configure($emit-custom-properties: true) {
  290. @include theme.property(
  291. custom-properties.create(ripple-fg-opacity, $opacity)
  292. );
  293. }
  294. }
  295. }
  296. }
  297. }
  298. // Simple mixin for base states which automatically selects opacity values based on whether the ink color is
  299. // light or dark.
  300. @mixin states(
  301. $color: theme-color.prop-value(on-surface),
  302. $has-nested-focusable-element: false,
  303. $query: feature-targeting.all(),
  304. $ripple-target: '&',
  305. $opacity-map: null
  306. ) {
  307. @include states-interactions_(
  308. $color: $color,
  309. $has-nested-focusable-element: $has-nested-focusable-element,
  310. $query: $query,
  311. $ripple-target: $ripple-target,
  312. $opacity-map: $opacity-map
  313. );
  314. }
  315. // Simple mixin for activated states which automatically selects opacity values based on whether the ink color is
  316. // light or dark.
  317. @mixin states-activated(
  318. $color,
  319. $has-nested-focusable-element: false,
  320. $query: feature-targeting.all(),
  321. $ripple-target: '&'
  322. ) {
  323. $feat-color: feature-targeting.create-target($query, color);
  324. $activated-opacity: states-opacity($color, activated);
  325. &--activated {
  326. // Stylelint seems to think that '&' qualifies as a type selector here?
  327. @include states-background-selector($ripple-target) {
  328. // Opacity falls under color because the chosen opacity is color-dependent.
  329. @include feature-targeting.targets($feat-color) {
  330. @include theme.property(
  331. opacity,
  332. custom-properties.create(
  333. --mdc-ripple-activated-opacity,
  334. $activated-opacity
  335. )
  336. );
  337. }
  338. }
  339. @include states-interactions_(
  340. $color: $color,
  341. $has-nested-focusable-element: $has-nested-focusable-element,
  342. $opacity-modifier: $activated-opacity,
  343. $query: $query,
  344. $ripple-target: $ripple-target
  345. );
  346. }
  347. }
  348. // Simple mixin for selected states which automatically selects opacity values based on whether the ink color is
  349. // light or dark.
  350. @mixin states-selected(
  351. $color,
  352. $has-nested-focusable-element: false,
  353. $query: feature-targeting.all(),
  354. $ripple-target: '&'
  355. ) {
  356. $feat-color: feature-targeting.create-target($query, color);
  357. $selected-opacity: states-opacity($color, selected);
  358. &--selected {
  359. @include states-background-selector($ripple-target) {
  360. // Opacity falls under color because the chosen opacity is color-dependent.
  361. @include feature-targeting.targets($feat-color) {
  362. @include theme.property(
  363. opacity,
  364. custom-properties.create(
  365. --mdc-ripple-selected-opacity,
  366. $selected-opacity
  367. )
  368. );
  369. }
  370. }
  371. @include states-interactions_(
  372. $color: $color,
  373. $has-nested-focusable-element: $has-nested-focusable-element,
  374. $opacity-modifier: $selected-opacity,
  375. $query: $query,
  376. $ripple-target: $ripple-target
  377. );
  378. }
  379. }
  380. @mixin states-interactions_(
  381. $color,
  382. $has-nested-focusable-element,
  383. $opacity-modifier: 0,
  384. $query: feature-targeting.all(),
  385. $ripple-target: '&',
  386. $opacity-map: null
  387. ) {
  388. @include target-selector($ripple-target) {
  389. @include states-base-color($color, $query);
  390. }
  391. @if $opacity-map == null {
  392. $opacity-map: (
  393. hover: states-opacity($color, hover) + $opacity-modifier,
  394. focus: states-opacity($color, focus) + $opacity-modifier,
  395. press: states-opacity($color, press) + $opacity-modifier,
  396. );
  397. }
  398. @include states-opacities(
  399. $opacity-map,
  400. $has-nested-focusable-element: $has-nested-focusable-element,
  401. $ripple-target: $ripple-target,
  402. $query: $query
  403. );
  404. }
  405. // Wraps content in the `ripple-target` selector if it exists.
  406. @mixin target-selector($ripple-target: '&') {
  407. @if $ripple-target == '&' {
  408. @content;
  409. } @else {
  410. #{$ripple-target} {
  411. @content;
  412. }
  413. }
  414. }
  415. /// Selector for hover, active and focus states.
  416. @mixin states-selector() {
  417. #{$_hover-selector},
  418. #{$_focus-selector},
  419. #{$_active-selector} {
  420. @content;
  421. }
  422. }
  423. @mixin hover() {
  424. #{$_hover-selector} {
  425. @content;
  426. }
  427. }
  428. // Selector for focus state. Using ':not(.mdc-ripple-upgraded)' to continue
  429. // applying focus styles on JS-disabled components, and control focus
  430. // on JS-enabled components with '.mdc-ripple-upgraded--background-focused'.
  431. @mixin focus() {
  432. #{$_focus-selector} {
  433. @content;
  434. }
  435. }
  436. // Selector for active state. Using `:active:active` to override focus styles.
  437. @mixin pressed() {
  438. #{$_active-selector} {
  439. @content;
  440. }
  441. }
  442. // @deprecated Use `pressed()` mixin - renamed for consistency.
  443. @mixin active() {
  444. @include pressed() {
  445. @content;
  446. }
  447. }
  448. /// Keep the ripple (State overlay) behind the content.
  449. @mixin behind-content(
  450. $ripple-target,
  451. $content-root-selector: '&',
  452. $query: feature-targeting.all()
  453. ) {
  454. // Needed for IE11. Without this, IE11 renders the state layer completely
  455. // underneath the container, making it invisible.
  456. $feat-structure: feature-targeting.create-target($query, structure);
  457. #{$content-root-selector} {
  458. @include feature-targeting.targets($feat-structure) {
  459. z-index: 0;
  460. }
  461. }
  462. #{$ripple-target}::before,
  463. #{$ripple-target}::after {
  464. @include feature-targeting.targets($feat-structure) {
  465. @include theme.property(
  466. z-index,
  467. custom-properties.create(--mdc-ripple-z-index, -1)
  468. );
  469. }
  470. }
  471. }
  472. @function states-opacity($color, $state) {
  473. $color-value: theme-color.prop-value($color);
  474. $opacity-map: if(
  475. theme-color.tone($color-value) == 'light',
  476. $light-ink-opacities,
  477. $dark-ink-opacities
  478. );
  479. @if not map.has-key($opacity-map, $state) {
  480. @error "Invalid state: '#{$state}'. Choose one of: #{map.keys($opacity-map)}";
  481. }
  482. @return map.get($opacity-map, $state);
  483. }
  484. @mixin states-background-selector($ripple-target) {
  485. #{$ripple-target}::before {
  486. @content;
  487. }
  488. }