From 4d424f017d198431b297397c07b520ba1c130d54 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Thu, 2 Sep 2021 06:35:12 -0500 Subject: [PATCH] fix(styles): update notification to new token format (#9582) * docs(carbon-react): update storybook to use background theme color * fix(eslint): update to exclude *.stories.js files * docs(notification): add playground for toast * refactor(styles): update notification tests to one file * feat(themes): add support for component tokens * feat(styles): add support for CSS Custom Property in component tokens * fix(styles): update notification to new token format Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- config/eslint-config-carbon/plugins/react.js | 2 +- packages/carbon-react/.storybook/styles.scss | 8 +- .../Notification/Notification.stories.js | 44 ++++ .../__tests__/inline-notification-test.js | 27 -- .../components/__tests__/notification-test.js | 44 ++++ .../__tests__/toast-notification-test.js | 27 -- .../scss/components/notification/_index.scss | 7 +- .../notification/_inline-notification.scss | 12 +- .../notification/_toast-notification.scss | 9 +- .../scss/components/notification/_tokens.scss | 233 ++++++++++-------- .../scss/utilities/_component-tokens.scss | 28 +++ packages/themes/scss/modules/_theme.scss | 60 +++++ 12 files changed, 328 insertions(+), 173 deletions(-) delete mode 100644 packages/styles/scss/components/__tests__/inline-notification-test.js create mode 100644 packages/styles/scss/components/__tests__/notification-test.js delete mode 100644 packages/styles/scss/components/__tests__/toast-notification-test.js diff --git a/config/eslint-config-carbon/plugins/react.js b/config/eslint-config-carbon/plugins/react.js index 2dbc3da652b3..b27f7046ba58 100644 --- a/config/eslint-config-carbon/plugins/react.js +++ b/config/eslint-config-carbon/plugins/react.js @@ -49,7 +49,7 @@ module.exports = { // In these cases, we don't need to handle prop type validation like we // would for code we ship to users. { - files: ['*-story.js'], + files: ['*-story.js', '*.stories.js'], rules: { 'react/display-name': 0, 'react/prop-types': 0, diff --git a/packages/carbon-react/.storybook/styles.scss b/packages/carbon-react/.storybook/styles.scss index 8187462b27e5..23207583f534 100644 --- a/packages/carbon-react/.storybook/styles.scss +++ b/packages/carbon-react/.storybook/styles.scss @@ -5,10 +5,6 @@ // LICENSE file in the root directory of this source tree. // -$feature-flags: ( - enable-v11-release: true, -); - @use '../index.scss' as styles with ( $css--font-face: true, $css--plex-arabic: true, @@ -32,6 +28,10 @@ $feature-flags: ( @include styles.theme(styles.$g100, button.$g100, tag.$g100); } +body { + background: styles.$background; +} + html[lang='en'] body { font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif; } diff --git a/packages/carbon-react/src/components/Notification/Notification.stories.js b/packages/carbon-react/src/components/Notification/Notification.stories.js index 97a5e3a6eb4b..443866f0117c 100644 --- a/packages/carbon-react/src/components/Notification/Notification.stories.js +++ b/packages/carbon-react/src/components/Notification/Notification.stories.js @@ -25,6 +25,11 @@ const toastNotificationProps = () => ({ export default { title: 'Components/Notifications', + parameters: { + controls: { + hideNoControlsWarning: true, + }, + }, }; export const Toast = () => ( @@ -35,6 +40,45 @@ export const Toast = () => ( /> ); +export const ToastPlayground = ({ + kind = 'info', + title = 'Notification title', + subtitle = 'Notification subtitle', + caption = '00:00:00 AM', + lowContrast = false, +}) => { + return ( + + ); +}; +ToastPlayground.argTypes = { + kind: { + options: [ + 'error', + 'info', + 'info-square', + 'success', + 'warning', + 'warning-alt', + ], + control: { + type: 'select', + }, + }, + lowContrast: { + value: false, + control: { + type: 'boolean', + }, + }, +}; + export const Inline = () => ( { - test('Public API', async () => { - const { unwrap } = await render(` - @use 'sass:map'; - @use 'sass:meta'; - @use '../notification'; - - $_: get('mixin', meta.mixin-exists('inline-notification', 'notification')); - `); - expect(unwrap('mixin')).toBe(true); - }); -}); diff --git a/packages/styles/scss/components/__tests__/notification-test.js b/packages/styles/scss/components/__tests__/notification-test.js new file mode 100644 index 000000000000..0205796c3155 --- /dev/null +++ b/packages/styles/scss/components/__tests__/notification-test.js @@ -0,0 +1,44 @@ +/** + * Copyright IBM Corp. 2018, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +'use strict'; + +const { SassRenderer } = require('@carbon/test-utils/scss'); + +const { render } = SassRenderer.create(__dirname); + +describe('scss/components/notification', () => { + test('Public API', async () => { + const { unwrap } = await render(` + @use 'sass:map'; + @use 'sass:meta'; + @use '../notification'; + + $_: get('mixins', ( + toast-notification: meta.mixin-exists('toast-notification', 'notification'), + inline-notification: meta.mixin-exists('inline-notification', 'notification'), + )); + + $_: get('tokens', map.keys(meta.module-variables('notification'))); + `); + + expect(unwrap('mixins')).toEqual({ + 'toast-notification': true, + 'inline-notification': true, + }); + expect(unwrap('tokens')).toEqual([ + 'notification-background-error', + 'notification-background-success', + 'notification-background-info', + 'notification-background-warning', + 'notification-action-hover', + 'notification-tokens', + ]); + }); +}); diff --git a/packages/styles/scss/components/__tests__/toast-notification-test.js b/packages/styles/scss/components/__tests__/toast-notification-test.js deleted file mode 100644 index f688a3ad9bf7..000000000000 --- a/packages/styles/scss/components/__tests__/toast-notification-test.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright IBM Corp. 2018, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - * - * @jest-environment node - */ - -'use strict'; - -const { SassRenderer } = require('@carbon/test-utils/scss'); - -const { render } = SassRenderer.create(__dirname); - -describe('scss/components/notification', () => { - test('Public API', async () => { - const { unwrap } = await render(` - @use 'sass:map'; - @use 'sass:meta'; - @use '../notification'; - - $_: get('mixin', meta.mixin-exists('toast-notification', 'notification')); - `); - expect(unwrap('mixin')).toBe(true); - }); -}); diff --git a/packages/styles/scss/components/notification/_index.scss b/packages/styles/scss/components/notification/_index.scss index d281b0723465..83f68110c448 100644 --- a/packages/styles/scss/components/notification/_index.scss +++ b/packages/styles/scss/components/notification/_index.scss @@ -9,8 +9,11 @@ @forward 'inline-notification'; @forward 'toast-notification'; -@use 'inline-notification'; -@use 'toast-notification'; +@use '../../theme'; +@use './tokens'; +@use './inline-notification'; +@use './toast-notification'; +@include theme.add-component-tokens(tokens.$notification-tokens); @include inline-notification.inline-notification; @include toast-notification.toast-notification; diff --git a/packages/styles/scss/components/notification/_inline-notification.scss b/packages/styles/scss/components/notification/_inline-notification.scss index fc57355905a3..33cc79cebdb5 100644 --- a/packages/styles/scss/components/notification/_inline-notification.scss +++ b/packages/styles/scss/components/notification/_inline-notification.scss @@ -15,9 +15,9 @@ @use '../../themes' as *; @use '../../type' as *; @use '../../utilities/convert' as *; -@use '../../utilities/component-tokens' as *; @use '../../utilities/high-contrast-mode' as *; @use '../../utilities/focus-outline' as *; +@use './tokens' as *; /// Inline notification styles /// @access public @@ -102,7 +102,7 @@ .#{$prefix}--inline-notification--low-contrast.#{$prefix}--inline-notification--error { @include notification--experimental( $support-error, - get-token(var(--cds-background-error)) + $notification-background-error ); &::before { @@ -120,7 +120,7 @@ .#{$prefix}--inline-notification--low-contrast.#{$prefix}--inline-notification--success { @include notification--experimental( $support-success, - get-token-value(var(--cds-background-success)) + $notification-background-success ); &::before { @@ -140,7 +140,7 @@ .#{$prefix}--inline-notification--low-contrast.#{$prefix}--inline-notification--info-square { @include notification--experimental( $support-info, - get-token-value(var(--cds-background-info)) + $notification-background-info ); &::before { @@ -160,7 +160,7 @@ .#{$prefix}--inline-notification--low-contrast.#{$prefix}--inline-notification--warning-alt { @include notification--experimental( $support-warning, - get-token-value(var(--cds-background-warning)) + $notification-background-warning ); &::before { @@ -241,7 +241,7 @@ .#{$prefix}--inline-notification__action-button.#{$prefix}--btn--ghost:active, .#{$prefix}--inline-notification--low-contrast .#{$prefix}--inline-notification__action-button.#{$prefix}--btn--ghost:hover { - background-color: get-token-value(var(--cds-action-hover)); + background-color: $notification-action-hover; } .#{$prefix}--inline-notification__action-button.#{$prefix}--btn--ghost:focus { diff --git a/packages/styles/scss/components/notification/_toast-notification.scss b/packages/styles/scss/components/notification/_toast-notification.scss index 552a79879cda..6a187e55dbc5 100644 --- a/packages/styles/scss/components/notification/_toast-notification.scss +++ b/packages/styles/scss/components/notification/_toast-notification.scss @@ -19,6 +19,7 @@ @use '../../utilities/convert' as *; @use '../../utilities/high-contrast-mode' as *; @use '../../utilities/focus-outline' as *; +@use './tokens' as *; /// Toast notification styles /// @access public @@ -82,7 +83,7 @@ .#{$prefix}--toast-notification--low-contrast.#{$prefix}--toast-notification--error { @include notification--experimental( $support-error, - get-token(var(--cds-background-error)) + $notification-background-error ); } @@ -96,7 +97,7 @@ .#{$prefix}--toast-notification--low-contrast.#{$prefix}--toast-notification--success { @include notification--experimental( $support-success, - get-token(var(--cds-background-success)) + $notification-background-success ); } @@ -112,7 +113,7 @@ .#{$prefix}--toast-notification--low-contrast.#{$prefix}--toast-notification--info-square { @include notification--experimental( $support-info, - get-token(var(--cds-background-info)) + $notification-background-info ); } @@ -128,7 +129,7 @@ .#{$prefix}--toast-notification--low-contrast.#{$prefix}--toast-notification--warning-alt { @include notification--experimental( $support-warning, - get-token(var(--cds-background-warning)) + $notification-background-warning ); } diff --git a/packages/styles/scss/components/notification/_tokens.scss b/packages/styles/scss/components/notification/_tokens.scss index d172d84ad1f2..b9985e3a2c0d 100644 --- a/packages/styles/scss/components/notification/_tokens.scss +++ b/packages/styles/scss/components/notification/_tokens.scss @@ -7,120 +7,149 @@ @use 'sass:color'; @use '../../colors'; -@use '../../theme' as *; @use '../../themes'; +@use '../../theme'; @use '../../utilities/component-tokens'; -// prettier-ignore -$-tokens: ( - 'notification-background-error': ( - fallback: colors.$red-10, - values: ( - ( - theme: themes.$white, - value: colors.$red-10, - ), - ( - theme: themes.$g10, - value: colors.$red-10, - ), - ( - theme: themes.$g90, - value: $layer, - ), - ( - theme: themes.$g100, - value: $layer, - ), +$notification-background-error: ( + fallback: colors.$red-10, + values: ( + ( + theme: themes.$white, + value: colors.$red-10, + ), + ( + theme: themes.$g10, + value: colors.$red-10, + ), + ( + theme: themes.$g90, + value: theme.$layer, + ), + ( + theme: themes.$g100, + value: theme.$layer, ), ), - 'notification-background-success': ( - fallback: colors.$green-10, - values: ( - ( - theme: themes.$white, - value: colors.$green-10, - ), - ( - theme: themes.$g10, - value: colors.$green-10, - ), - ( - theme: themes.$g90, - value: $layer, - ), - ( - theme: themes.$g100, - value: $layer, - ), +) !default; + +$notification-background-success: ( + fallback: colors.$green-10, + values: ( + ( + theme: themes.$white, + value: colors.$green-10, + ), + ( + theme: themes.$g10, + value: colors.$green-10, + ), + ( + theme: themes.$g90, + value: theme.$layer, + ), + ( + theme: themes.$g100, + value: theme.$layer, ), ), - 'notification-background-info': ( - fallback: colors.$blue-10, - values: ( - ( - theme: themes.$white, - value: colors.$blue-10, - ), - ( - theme: themes.$g10, - value: colors.$blue-10, - ), - ( - theme: themes.$g90, - value: $layer, - ), - ( - theme: themes.$g100, - value: $layer, - ), +) !default; + +$notification-background-info: ( + fallback: colors.$blue-10, + values: ( + ( + theme: themes.$white, + value: colors.$blue-10, + ), + ( + theme: themes.$g10, + value: colors.$blue-10, + ), + ( + theme: themes.$g90, + value: theme.$layer, + ), + ( + theme: themes.$g100, + value: theme.$layer, ), ), - 'notification-background-warning': ( - fallback: mix(colors.$yellow-30, colors.$white-0, 15%), - values: ( - ( - theme: themes.$white, - value: mix(colors.$yellow-30, colors.$white-0, 15%), - ), - ( - theme: themes.$g10, - value: mix(colors.$yellow-30, colors.$white-0, 15%), - ), - ( - theme: themes.$g90, - value: $layer, - ), - ( - theme: themes.$g100, - value: $layer, - ), +) !default; + +$notification-background-warning: ( + fallback: color.mix(colors.$yellow-30, colors.$white-0, 15%), + values: ( + ( + theme: themes.$white, + value: color.mix(colors.$yellow-30, colors.$white-0, 15%), + ), + ( + theme: themes.$g10, + value: color.mix(colors.$yellow-30, colors.$white-0, 15%), + ), + ( + theme: themes.$g90, + value: theme.$layer, + ), + ( + theme: themes.$g100, + value: theme.$layer, ), ), - 'notification-action-hover': ( - fallback: colors.$white-0, - values: ( - ( - theme: themes.$white, - value: colors.$white-0, - ), - ( - theme: themes.$g10, - value: colors.$white-0, - ), - ( - theme: themes.$g90, - value: $layer-hover, - ), - ( - theme: themes.$g100, - value: $layer-hover, - ), +) !default; + +$notification-action-hover: ( + fallback: colors.$white-0, + values: ( + ( + theme: themes.$white, + value: colors.$white-0, + ), + ( + theme: themes.$g10, + value: colors.$white-0, + ), + ( + theme: themes.$g90, + value: theme.$layer-hover, + ), + ( + theme: themes.$g100, + value: theme.$layer-hover, ), ), +) !default; + +$notification-tokens: ( + notification-background-error: $notification-background-error, + notification-background-success: $notification-background-success, + notification-background-info: $notification-background-info, + notification-background-warning: $notification-background-warning, + notification-action-hover: $notification-action-hover, +); + +$notification-background-error: component-tokens.get-var( + $notification-background-error, + 'notification-background-error' ); -$white: component-tokens.get-tokens($-tokens, themes.$white); -$g10: component-tokens.get-tokens($-tokens, themes.$g10); -$g90: component-tokens.get-tokens($-tokens, themes.$g90); -$g100: component-tokens.get-tokens($-tokens, themes.$g100); +$notification-background-success: component-tokens.get-var( + $notification-background-success, + 'notification-background-success' +); + +$notification-background-info: component-tokens.get-var( + $notification-background-info, + 'notification-background-info' +); + +$notification-background-warning: component-tokens.get-var( + $notification-background-warning, + 'notification-background-warning' +); + +$notification-action-hover: component-tokens.get-var( + $notification-action-hover, + 'notification-action-hover' +); diff --git a/packages/styles/scss/utilities/_component-tokens.scss b/packages/styles/scss/utilities/_component-tokens.scss index bc578c570792..bc6f50e2a198 100644 --- a/packages/styles/scss/utilities/_component-tokens.scss +++ b/packages/styles/scss/utilities/_component-tokens.scss @@ -6,6 +6,10 @@ // @use 'sass:map'; +@use 'sass:meta'; +@use '../themes'; +@use '../theme'; +@use './custom-property'; /// Extract the component tokens from a given theme /// @param {SassMap} $tokens @@ -25,3 +29,27 @@ @return $result; } + +/// Get the CSS Custom Property value for a given token map +/// @param {any} $token-map The possible values for the token, this value can +/// be a plain value used as a CSS value or a Sass Map +/// @param {String} $name The name of the CSS Custom Property +@function get-var($token-map, $name) { + @if meta.type-of($token-map) == map { + $fallback: map.get($token-map, fallback); + $theme-values: map.get($token-map, values); + + @each $theme-value in $theme-values { + $theme: map.get($theme-value, theme); + $value: map.get($theme-value, value); + + @if theme.matches($theme, theme.$theme) { + @return custom-property.get-var($name, $value); + } + } + + @return custom-property.get-var($name, $fallback); + } + + @return custom-property.get-var($name, $token-map); +} diff --git a/packages/themes/scss/modules/_theme.scss b/packages/themes/scss/modules/_theme.scss index 5b475d412879..f56a411bcd0f 100644 --- a/packages/themes/scss/modules/_theme.scss +++ b/packages/themes/scss/modules/_theme.scss @@ -20,6 +20,9 @@ $fallback: themes.$white !default; $theme: () !default; $theme: map.merge($fallback, $theme); +/// Local component tokens that can be updated with `@mixin add-component-tokens`. +$-component-tokens: (); + /// Include the CSS Custom Properties for the active theme or a given theme on /// a selector @mixin theme($active-theme: $theme, $component-tokens...) { @@ -32,6 +35,13 @@ $theme: map.merge($fallback, $theme); @include -custom-property($token, $value); } } + + @each $token, $value in $-component-tokens { + @include -custom-property( + $token, + -resolve-token-value($active-theme, $value) + ); + } } /// Retrieve the value for the given $token from the current $theme @@ -43,6 +53,56 @@ $theme: map.merge($fallback, $theme); @error "Unable to find token: #{$token} in current $theme"; } +/// Compare two themes to see if the second theme is a superset of the first +/// theme. In other words, this function will return true if every token in the +/// first theme is available in the second theme and has the same value. The +/// second theme is allowed to have additional values and it will still match. +/// @param {Map} $a +/// @param {Map} $b +/// @returns {Boolean} +@function matches($a, $b) { + @each $key, $value in $a { + @if map.has-key($b, $key) == false { + @return false; + } + + @if map.get($b, $key) != $value { + @return false; + } + } + + @return true; +} + +/// Add component tokens which will be included any time the theme mixin is +/// called +@mixin add-component-tokens($token-map) { + $-component-tokens: map.merge($-component-tokens, $token-map) !global; +} + +/// Determine the value from a component token that matches the given theme. +/// This is helpful when a component token may change depending on what theme the +/// component is being rendered in. +@function -resolve-token-value($active-theme: $theme, $token-value) { + @if meta.type-of($token-value) == map and map.has-key($token-value, values) { + $fallback: map.get($token-value, fallback); + $theme-values: map.get($token-value, values); + + @each $theme-value in $theme-values { + $theme-to-match: map.get($theme-value, theme); + $value: map.get($theme-value, value); + + @if matches($theme-to-match, $active-theme) { + @return $value; + } + } + + @return $fallback; + } + + @return $token-value; +} + /// @access private /// @group @carbon/themes @mixin -custom-property($name, $value) {