From f69f8876fce3993d063f10f947c5fcdfdec83e1d Mon Sep 17 00:00:00 2001 From: Jan Hassel Date: Wed, 12 May 2021 00:59:50 +0200 Subject: [PATCH 01/13] refactor(toggle): create v11 toggle (#8469) * refactor(toggle): create v11 toggle basis * fix(toggle): fix spacing for rtl layouts * test(toggle): add unit tests * feat(toggle): allow toggle state to be controlled * fix(toggle): forward correct prop types * feat(toggle): add props.hideLabel * test(toggle): add missing required props to old toggle tests * fix(toggle): remove explicit prop type assignment * fix(toggle): remove default empty function for props.onClick * fix(toggle): remove unnecessary tabindex definition * test(toggle): migrate to testing-library/react * fix(toggle): update tokens * refactor(toggle): move hcm styles to bottom of stylesheet * feat(toggle): spread other props on button element Co-authored-by: TJ Egan Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/components/toggle/_toggle.scss | 848 ++++++++++-------- .../__snapshots__/PublicAPI-test.js.snap | 55 +- .../src/components/Toggle/Toggle-test.js | 21 +- packages/react/src/components/Toggle/index.js | 19 +- .../src/components/Toggle/next/Toggle-test.js | 146 +++ .../src/components/Toggle/next/Toggle.js | 166 ++++ 6 files changed, 838 insertions(+), 417 deletions(-) create mode 100644 packages/react/src/components/Toggle/next/Toggle-test.js create mode 100644 packages/react/src/components/Toggle/next/Toggle.js diff --git a/packages/components/src/components/toggle/_toggle.scss b/packages/components/src/components/toggle/_toggle.scss index a8f5e80d8afe..113f0870a0f8 100644 --- a/packages/components/src/components/toggle/_toggle.scss +++ b/packages/components/src/components/toggle/_toggle.scss @@ -19,461 +19,597 @@ /// @access private /// @group toggle @mixin toggle { - .#{$prefix}--toggle { - @include hidden; + @if feature-flag-enabled('enable-2021-release') { + .#{$prefix}--toggle__label-text { + @include type-style('label-01'); - &:focus { - outline: none; + display: block; + margin-bottom: $spacing-05; + color: $text-secondary; } - } - .#{$prefix}--toggle__label { - @include type-style('label-01'); + .#{$prefix}--toggle__button { + @include hidden; - position: relative; - display: flex; - align-items: center; - margin: $carbon--spacing-03 0; - cursor: pointer; - } - - // V11: It looks like this block no longer applies to any element. - // May need to check with Vue/Angular implementations, - // but I do not see any `toggle__appearance` in the rendered HTML. - // There seem to be quite a few references throughout this file. - .#{$prefix}--toggle__appearance { - position: relative; - width: carbon--rem(48px); - height: carbon--rem(24px); - - // Toggle background oval - &::before { - position: absolute; - top: 0; - display: block; - width: carbon--rem(48px); - height: carbon--rem(24px); - box-sizing: border-box; - background-color: $toggle-off; - border-radius: carbon--rem(15px); - // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) - box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; - content: ''; - cursor: pointer; - transition: box-shadow $duration--fast-01 motion(exit, productive), - background-color $duration--fast-01 motion(exit, productive); - will-change: box-shadow; + &:focus { + outline: none; + } } - // Toggle circle - &::after { - position: absolute; - top: carbon--rem(3px); - left: carbon--rem(3px); - display: block; - width: carbon--rem(18px); - height: carbon--rem(18px); - box-sizing: border-box; - background-color: $icon-on-color; - border-radius: 50%; - content: ''; + .#{$prefix}--toggle__appearance { + display: grid; + align-items: center; + column-gap: $spacing-03; cursor: pointer; - transition: transform $duration--fast-01 motion(exit, productive); + grid-template-columns: max-content max-content; } - } - - .#{$prefix}--toggle__check { - position: absolute; - z-index: 1; - top: carbon--rem(6px); - left: carbon--rem(6px); - width: carbon--rem(6px); - height: carbon--rem(5px); - fill: $icon-on-color; - transform: scale(0.2); - transition: $duration--fast-01 motion(exit, productive); - } - - .#{$prefix}--toggle__text--left, - .#{$prefix}--toggle__text--right { - @include type-style('body-short-01'); - position: relative; - margin-left: $carbon--spacing-03; - } - - .#{$prefix}--toggle__text--left { - position: absolute; - left: carbon--rem(48px); - } + .#{$prefix}--toggle__switch { + position: relative; + width: rem(48px); + height: rem(24px); + background-color: $toggle-off; + border-radius: rem(12px); + transition: background-color $duration--fast-01 motion(exit, productive); - .#{$prefix}--toggle:checked - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--left, - .#{$prefix}--toggle:not(:checked) - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--right { - visibility: hidden; - } + &::before { + position: absolute; + top: rem(3px); + left: rem(3px); + width: rem(18px); + height: rem(18px); + background-color: $icon-on-color; + border-radius: 50%; + content: ''; + transition: transform $duration--fast-01 motion(exit, productive); + } + } - .#{$prefix}--toggle:checked - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--right, - .#{$prefix}--toggle:not(:checked) - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--left { - display: inline; - } + .#{$prefix}--toggle__button:focus + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__switch, + .#{$prefix}--toggle__button:active + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__switch { + box-shadow: 0 0 0 1px $focus-inset, 0 0 0 3px $focus; + } - .#{$prefix}--toggle:checked - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance { - &::before { + .#{$prefix}--toggle__switch--checked { background-color: $support-success; + + &::before { + transform: translateX(rem(24px)); + } } - &::after { - background-color: $icon-on-color; - transform: translateX(carbon--rem(24px)); + .#{$prefix}--toggle__text { + @include type-style('body-long-01'); + + color: $text-primary; } - } - //---------------------------------------------- - // Focus - // --------------------------------------------- - .#{$prefix}--toggle - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance::before { - // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) - box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; - } + .#{$prefix}--toggle__appearance--sm .#{$prefix}--toggle__switch { + width: rem(32px); + height: rem(16px); - .#{$prefix}--toggle:focus + .#{$prefix}--toggle__label, - .#{$prefix}--toggle:active - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance::before { - box-shadow: 0 0 0 1px $layer-accent, 0 0 0 3px $focus; - } + &::before { + width: rem(10px); + height: rem(10px); + } + } - //---------------------------------------------- - // Disabled - // --------------------------------------------- - .#{$prefix}--toggle:disabled + .#{$prefix}--toggle__label { - cursor: not-allowed; - } + .#{$prefix}--toggle__appearance--sm + .#{$prefix}--toggle__switch--checked::before { + transform: translateX(rem(16px)); + } - .#{$prefix}--toggle:disabled - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance { - &::before { - background-color: $layer-disabled; + .#{$prefix}--toggle__check { + position: absolute; + top: rem(6px); + right: rem(5px); + fill: $support-success; + visibility: hidden; } - &::after { - background-color: $button-disabled; + .#{$prefix}--toggle__switch--checked .#{$prefix}--toggle__check { + visibility: visible; } - &::before, - &::after { + .#{$prefix}--toggle--disabled .#{$prefix}--toggle__appearance { cursor: not-allowed; - transition: $duration--fast-01 motion(exit, productive); } - } - .#{$prefix}--toggle:disabled - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--left, - .#{$prefix}--toggle:disabled - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--right { - color: $text-disabled; - } + .#{$prefix}--toggle--disabled .#{$prefix}--toggle__label-text, + .#{$prefix}--toggle--disabled .#{$prefix}--toggle__text { + color: $text-disabled; + } - .#{$prefix}--toggle:disabled:active - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance:before { - box-shadow: none; - } + .#{$prefix}--toggle--disabled .#{$prefix}--toggle__switch { + background-color: $button-disabled; - .#{$prefix}--toggle:disabled - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__check { - fill: $icon-disabled; - } + &::before { + background-color: $icon-on-color-disabled; + } + } - //---------------------------------------------- - // Small toggle - // --------------------------------------------- + .#{$prefix}--toggle--disabled .#{$prefix}--toggle__check { + fill: $button-disabled; + } - .#{$prefix}--toggle--small - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance { - width: carbon--rem(32px); - height: carbon--rem(16px); + // HCM - &::before { - top: 0; - width: carbon--rem(32px); - height: carbon--rem(16px); - box-sizing: border-box; - border-radius: 0.9375rem; + .#{$prefix}--toggle__switch, + .#{$prefix}--toggle__switch::before { + @include high-contrast-mode('outline'); } - &::after { - top: carbon--rem(3px); - left: carbon--rem(3px); - width: carbon--rem(10px); - height: carbon--rem(10px); + // stylelint-disable-next-line no-duplicate-selectors + .#{$prefix}--toggle__button:focus + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__switch, + .#{$prefix}--toggle__button:active + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__switch { + @include high-contrast-mode('focus'); } - } + } @else { + .#{$prefix}--toggle { + @include hidden; - .#{$prefix}--toggle--small:checked - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__check { - fill: $support-success; - transform: scale(1) translateX(carbon--rem(16px)); - } + &:focus { + outline: none; + } + } - .#{$prefix}--toggle--small - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__text--left { - left: carbon--rem(32px); - } + .#{$prefix}--toggle__label { + @include type-style('label-01'); - .#{$prefix}--toggle--small:checked - + .#{$prefix}--toggle__label - .#{$prefix}--toggle__appearance { - &::after { - margin-left: 0; - transform: translateX(rem(17px)); + position: relative; + display: flex; + align-items: center; + margin: $carbon--spacing-03 0; + cursor: pointer; } - } - // ----------------------------------------------------- - // new accessible toggle - // TODO: deprecate styles above this line - // ----------------------------------------------------- + // V11: It looks like this block no longer applies to any element. + // May need to check with Vue/Angular implementations, + // but I do not see any `toggle__appearance` in the rendered HTML. + // There seem to be quite a few references throughout this file. + .#{$prefix}--toggle__appearance { + position: relative; + width: carbon--rem(48px); + height: carbon--rem(24px); - .#{$prefix}--toggle-input { - @include hidden; + // Toggle background oval + &::before { + position: absolute; + top: 0; + display: block; + width: carbon--rem(48px); + height: carbon--rem(24px); + box-sizing: border-box; + background-color: $toggle-off; + border-radius: carbon--rem(15px); + // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) + box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; + content: ''; + cursor: pointer; + transition: box-shadow $duration--fast-01 motion(exit, productive), + background-color $duration--fast-01 motion(exit, productive); + will-change: box-shadow; + } - &:focus { - outline: none; + // Toggle circle + &::after { + position: absolute; + top: carbon--rem(3px); + left: carbon--rem(3px); + display: block; + width: carbon--rem(18px); + height: carbon--rem(18px); + box-sizing: border-box; + background-color: $icon-on-color; + border-radius: 50%; + content: ''; + cursor: pointer; + transition: transform $duration--fast-01 motion(exit, productive); + } } - } - .#{$prefix}--toggle-input__label { - @include type-style('label-01'); + .#{$prefix}--toggle__check { + position: absolute; + z-index: 1; + top: carbon--rem(6px); + left: carbon--rem(6px); + width: carbon--rem(6px); + height: carbon--rem(5px); + fill: $icon-on-color; + transform: scale(0.2); + transition: $duration--fast-01 motion(exit, productive); + } - display: flex; - flex-direction: column; - align-items: flex-start; - color: $text-secondary; - cursor: pointer; - } + .#{$prefix}--toggle__text--left, + .#{$prefix}--toggle__text--right { + @include type-style('body-short-01'); - .#{$prefix}--toggle__switch { - position: relative; - display: flex; - width: carbon--rem(48px); - height: carbon--rem(24px); - align-items: center; - cursor: pointer; + position: relative; + margin-left: $carbon--spacing-03; + } - // Toggle background oval - &::before { + .#{$prefix}--toggle__text--left { position: absolute; - top: 0; - display: block; - width: carbon--rem(48px); - height: carbon--rem(24px); - box-sizing: border-box; - background-color: $toggle-off; - border-radius: carbon--rem(15px); - // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) - box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; - content: ''; - transition: box-shadow $duration--fast-01 motion(exit, productive), - background-color $duration--fast-01 motion(exit, productive); - will-change: box-shadow; + left: carbon--rem(48px); } - // Toggle circle - &::after { - position: absolute; - top: carbon--rem(3px); - left: carbon--rem(3px); - display: block; - width: carbon--rem(18px); - height: carbon--rem(18px); - box-sizing: border-box; - background-color: $icon-on-color; - border-radius: 50%; - content: ''; - transition: transform $duration--fast-01 motion(exit, productive); + .#{$prefix}--toggle:checked + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--left, + .#{$prefix}--toggle:not(:checked) + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--right { + visibility: hidden; } - .#{$prefix}--toggle-input__label & { - margin-top: $carbon--spacing-05; + .#{$prefix}--toggle:checked + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--right, + .#{$prefix}--toggle:not(:checked) + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--left { + display: inline; } - } - .#{$prefix}--toggle__text--off, - .#{$prefix}--toggle__text--on { - @include type-style('body-short-01'); - - position: absolute; - // top offset needed to vertically center absolutely positioned flex child in IE11 - top: 50%; - margin-left: rem(56px); - transform: translateY(-50%); - user-select: none; - white-space: nowrap; - } + .#{$prefix}--toggle:checked + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance { + &::before { + background-color: $support-success; + } - //---------------------------------------------- - // Checked - // --------------------------------------------- - .#{$prefix}--toggle-input:checked - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch - > .#{$prefix}--toggle__text--off, - .#{$prefix}--toggle-input:not(:checked) - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch - > .#{$prefix}--toggle__text--on { - visibility: hidden; - } + &::after { + background-color: $icon-on-color; + transform: translateX(carbon--rem(24px)); + } + } - .#{$prefix}--toggle-input:checked - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch { - &::before { - background-color: $support-success; + //---------------------------------------------- + // Focus + // --------------------------------------------- + .#{$prefix}--toggle + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance::before { + // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) + box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; } - &::after { - background-color: $icon-on-color; - transform: translateX(carbon--rem(24px)); + .#{$prefix}--toggle:focus + .#{$prefix}--toggle__label, + .#{$prefix}--toggle:active + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance::before { + box-shadow: 0 0 0 1px $layer-accent, 0 0 0 3px $focus; } - } - //---------------------------------------------- - // Focus and active - // --------------------------------------------- - .#{$prefix}--toggle-input:focus - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch::before, - .#{$prefix}--toggle-input:active - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch::before { - box-shadow: 0 0 0 1px $focus-inset, 0 0 0 3px $focus; - } + //---------------------------------------------- + // Disabled + // --------------------------------------------- + .#{$prefix}--toggle:disabled + .#{$prefix}--toggle__label { + cursor: not-allowed; + } - //---------------------------------------------- - // Disabled - // --------------------------------------------- - .#{$prefix}--toggle-input:disabled + .#{$prefix}--toggle-input__label { - color: $text-disabled; - cursor: not-allowed; - } + .#{$prefix}--toggle:disabled + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance { + &::before { + background-color: $layer-disabled; + } + + &::after { + background-color: $button-disabled; + } - .#{$prefix}--toggle-input:disabled - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch { - cursor: not-allowed; + &::before, + &::after { + cursor: not-allowed; + transition: $duration--fast-01 motion(exit, productive); + } + } - &::before { - background-color: $button-disabled; + .#{$prefix}--toggle:disabled + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--left, + .#{$prefix}--toggle:disabled + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--right { + color: $text-disabled; } - &::after { - background-color: $icon-on-color-disabled; + .#{$prefix}--toggle:disabled:active + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance:before { + box-shadow: none; } - &::before, - &::after { - cursor: not-allowed; - transition: $duration--fast-01 motion(exit, productive); + .#{$prefix}--toggle:disabled + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__check { + fill: $icon-disabled; } - } - .#{$prefix}--toggle-input:disabled:active - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch::before { - box-shadow: none; - } + //---------------------------------------------- + // Small toggle + // --------------------------------------------- - //---------------------------------------------- - // Small toggle - // --------------------------------------------- - .#{$prefix}--toggle-input--small + .#{$prefix}--toggle-input__label { - > .#{$prefix}--toggle__switch { + .#{$prefix}--toggle--small + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance { width: carbon--rem(32px); height: carbon--rem(16px); &::before { + top: 0; width: carbon--rem(32px); height: carbon--rem(16px); + box-sizing: border-box; border-radius: 0.9375rem; } &::after { + top: carbon--rem(3px); + left: carbon--rem(3px); width: carbon--rem(10px); height: carbon--rem(10px); } } + .#{$prefix}--toggle--small:checked + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__check { + fill: $support-success; + transform: scale(1) translateX(carbon--rem(16px)); + } + + .#{$prefix}--toggle--small + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__text--left { + left: carbon--rem(32px); + } + + .#{$prefix}--toggle--small:checked + + .#{$prefix}--toggle__label + .#{$prefix}--toggle__appearance { + &::after { + margin-left: 0; + transform: translateX(rem(17px)); + } + } + + // ----------------------------------------------------- + // new accessible toggle + // TODO: deprecate styles above this line + // ----------------------------------------------------- + + .#{$prefix}--toggle-input { + @include hidden; + + &:focus { + outline: none; + } + } + + .#{$prefix}--toggle-input__label { + @include type-style('label-01'); + + display: flex; + flex-direction: column; + align-items: flex-start; + color: $text-secondary; + cursor: pointer; + } + + .#{$prefix}--toggle__switch { + position: relative; + display: flex; + width: carbon--rem(48px); + height: carbon--rem(24px); + align-items: center; + cursor: pointer; + + // Toggle background oval + &::before { + position: absolute; + top: 0; + display: block; + width: carbon--rem(48px); + height: carbon--rem(24px); + box-sizing: border-box; + background-color: $toggle-off; + border-radius: carbon--rem(15px); + // Corresponds to the double-border for focused state (`0 0 0 1px $ui-02, 0 0 0 3px $focus`) + box-shadow: 0 0 0 1px transparent, 0 0 0 3px transparent; + content: ''; + transition: box-shadow $duration--fast-01 motion(exit, productive), + background-color $duration--fast-01 motion(exit, productive); + will-change: box-shadow; + } + + // Toggle circle + &::after { + position: absolute; + top: carbon--rem(3px); + left: carbon--rem(3px); + display: block; + width: carbon--rem(18px); + height: carbon--rem(18px); + box-sizing: border-box; + background-color: $icon-on-color; + border-radius: 50%; + content: ''; + transition: transform $duration--fast-01 motion(exit, productive); + } + + .#{$prefix}--toggle-input__label & { + margin-top: $carbon--spacing-05; + } + } + .#{$prefix}--toggle__text--off, .#{$prefix}--toggle__text--on { - margin-left: carbon--rem(40px); + @include type-style('body-short-01'); + + position: absolute; + // top offset needed to vertically center absolutely positioned flex child in IE11 + top: 50%; + margin-left: rem(56px); + transform: translateY(-50%); + user-select: none; + white-space: nowrap; } - } - .#{$prefix}--toggle-input--small:checked + .#{$prefix}--toggle-input__label { - > .#{$prefix}--toggle__switch::after { - transform: translateX(carbon--rem(17px)); + //---------------------------------------------- + // Checked + // --------------------------------------------- + .#{$prefix}--toggle-input:checked + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch + > .#{$prefix}--toggle__text--off, + .#{$prefix}--toggle-input:not(:checked) + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch + > .#{$prefix}--toggle__text--on { + visibility: hidden; } - .#{$prefix}--toggle__check { - fill: $support-success; - transform: scale(1) translateX(carbon--rem(16px)); + .#{$prefix}--toggle-input:checked + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch { + &::before { + background-color: $support-success; + } + + &::after { + background-color: $icon-on-color; + transform: translateX(carbon--rem(24px)); + } } - } - .#{$prefix}--toggle-input--small:disabled:checked - + .#{$prefix}--toggle-input__label - .#{$prefix}--toggle__check { - fill: $layer-disabled; - } + //---------------------------------------------- + // Focus and active + // --------------------------------------------- + .#{$prefix}--toggle-input:focus + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch::before, + .#{$prefix}--toggle-input:active + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch::before { + box-shadow: 0 0 0 1px $focus-inset, 0 0 0 3px $focus; + } - //---------------------------------------------- - // Skeleton - // --------------------------------------------- + //---------------------------------------------- + // Disabled + // --------------------------------------------- + .#{$prefix}--toggle-input:disabled + .#{$prefix}--toggle-input__label { + color: $text-disabled; + cursor: not-allowed; + } - .#{$prefix}--toggle__label.#{$prefix}--skeleton { - flex-direction: column; - align-items: flex-start; + .#{$prefix}--toggle-input:disabled + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch { + cursor: not-allowed; - .#{$prefix}--toggle__label-text { - margin-bottom: $carbon--spacing-03; + &::before { + background-color: $button-disabled; + } + + &::after { + background-color: $icon-on-color-disabled; + } + + &::before, + &::after { + cursor: not-allowed; + transition: $duration--fast-01 motion(exit, productive); + } } - } - // Windows HCM fix - .#{$prefix}--toggle__switch::after, - .#{$prefix}--toggle__switch::before { - @include high-contrast-mode('outline'); - } + .#{$prefix}--toggle-input:disabled:active + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch::before { + box-shadow: none; + } + + //---------------------------------------------- + // Small toggle + // --------------------------------------------- + .#{$prefix}--toggle-input--small + .#{$prefix}--toggle-input__label { + > .#{$prefix}--toggle__switch { + width: carbon--rem(32px); + height: carbon--rem(16px); + + &::before { + width: carbon--rem(32px); + height: carbon--rem(16px); + border-radius: 0.9375rem; + } + + &::after { + width: carbon--rem(10px); + height: carbon--rem(10px); + } + } + + .#{$prefix}--toggle__text--off, + .#{$prefix}--toggle__text--on { + margin-left: carbon--rem(40px); + } + } + + .#{$prefix}--toggle-input--small:checked + + .#{$prefix}--toggle-input__label { + > .#{$prefix}--toggle__switch::after { + transform: translateX(carbon--rem(17px)); + } - // stylelint-disable-next-line no-duplicate-selectors - .#{$prefix}--toggle-input:focus - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch::before, - .#{$prefix}--toggle-input:active - + .#{$prefix}--toggle-input__label - > .#{$prefix}--toggle__switch::before { - @include high-contrast-mode('focus'); + .#{$prefix}--toggle__check { + fill: $support-success; + transform: scale(1) translateX(carbon--rem(16px)); + } + } + + .#{$prefix}--toggle-input--small:disabled:checked + + .#{$prefix}--toggle-input__label + .#{$prefix}--toggle__check { + fill: $layer-disabled; + } + + //---------------------------------------------- + // Skeleton + // --------------------------------------------- + + .#{$prefix}--toggle__label.#{$prefix}--skeleton { + flex-direction: column; + align-items: flex-start; + + .#{$prefix}--toggle__label-text { + margin-bottom: $carbon--spacing-03; + } + } + + // Windows HCM fix + .#{$prefix}--toggle__switch::after, + .#{$prefix}--toggle__switch::before { + @include high-contrast-mode('outline'); + } + + // stylelint-disable-next-line no-duplicate-selectors + .#{$prefix}--toggle-input:focus + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch::before, + .#{$prefix}--toggle-input:active + + .#{$prefix}--toggle-input__label + > .#{$prefix}--toggle__switch::before { + @include high-contrast-mode('focus'); + } } } diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 581065ede35e..a718524c4dfe 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -6212,60 +6212,7 @@ Map { }, }, }, - "Toggle" => Object { - "defaultProps": Object { - "aria-label": "Toggle", - "defaultToggled": false, - "labelA": "Off", - "labelB": "On", - "onToggle": [Function], - }, - "propTypes": Object { - "aria-label": Object { - "isRequired": true, - "type": "string", - }, - "className": Object { - "type": "string", - }, - "defaultToggled": Object { - "type": "bool", - }, - "id": Object { - "isRequired": true, - "type": "string", - }, - "labelA": Object { - "isRequired": true, - "type": "node", - }, - "labelB": Object { - "isRequired": true, - "type": "node", - }, - "labelText": Object { - "type": "node", - }, - "onChange": Object { - "type": "func", - }, - "onToggle": Object { - "type": "func", - }, - "size": Object { - "args": Array [ - Array [ - "sm", - "md", - ], - ], - "type": "oneOf", - }, - "toggled": Object { - "type": "bool", - }, - }, - }, + "Toggle" => Object {}, "ToggleSmall" => Object { "defaultProps": Object { "defaultToggled": false, diff --git a/packages/react/src/components/Toggle/Toggle-test.js b/packages/react/src/components/Toggle/Toggle-test.js index 5e3f25d50182..a1cb2a8f498e 100644 --- a/packages/react/src/components/Toggle/Toggle-test.js +++ b/packages/react/src/components/Toggle/Toggle-test.js @@ -12,8 +12,15 @@ import { settings } from 'carbon-components'; const { prefix } = settings; describe('Toggle', () => { + const commonProps = { + 'aria-label': 'Toggle label', + labelA: 'Off', + labelB: 'On', + labelText: 'Toggle label', + }; + describe('Renders as expected', () => { - const wrapper = mount(); + const wrapper = mount(); const input = wrapper.find('input'); @@ -63,7 +70,7 @@ describe('Toggle', () => { }); it('toggled prop sets checked prop on input', () => { - const wrapper = mount(); + const wrapper = mount(); const input = () => wrapper.find('input'); expect(input().props().checked).toEqual(true); @@ -76,7 +83,9 @@ describe('Toggle', () => { it('passes along onChange to ', () => { const onChange = jest.fn(); const id = 'test-input'; - const wrapper = mount(); + const wrapper = mount( + + ); const input = wrapper.find('input'); const inputElement = input.instance(); @@ -94,7 +103,9 @@ describe('Toggle', () => { it('should invoke onToggle with expected arguments', () => { const onToggle = jest.fn(); const id = 'test-input'; - const wrapper = mount(); + const wrapper = mount( + + ); const input = wrapper.find('input'); const inputElement = input.instance(); @@ -111,7 +122,7 @@ describe('Toggle', () => { }); describe('ToggleSmall', () => { - const wrapper = mount(); + const wrapper = mount(); it('Sets the `ToggleSmall` className', () => { const input = wrapper.find('input'); diff --git a/packages/react/src/components/Toggle/index.js b/packages/react/src/components/Toggle/index.js index fd4e275c234e..8c9a766eab95 100644 --- a/packages/react/src/components/Toggle/index.js +++ b/packages/react/src/components/Toggle/index.js @@ -5,5 +5,20 @@ * LICENSE file in the root directory of this source tree. */ -export * from './Toggle.Skeleton'; -export default from './Toggle'; +import React from 'react'; + +import ToggleNext from './next/Toggle'; +import ToggleClassic from './Toggle'; + +import { useFeatureFlag } from '../FeatureFlags'; + +function Toggle(props) { + const enabled = useFeatureFlag('enable-2021-release'); + if (enabled) { + return ; + } + return ; +} + +export { default as ToggleSkeleton } from './Toggle.Skeleton'; +export default Toggle; diff --git a/packages/react/src/components/Toggle/next/Toggle-test.js b/packages/react/src/components/Toggle/next/Toggle-test.js new file mode 100644 index 000000000000..739d5e40917e --- /dev/null +++ b/packages/react/src/components/Toggle/next/Toggle-test.js @@ -0,0 +1,146 @@ +/** + * Copyright IBM Corp. 2021 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import Toggle from './Toggle'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +describe('Toggle', () => { + const props = { + id: 'toggle-id', + labelA: 'labelA-unchecked', + labelB: 'labelB-checked', + labelText: 'Toggle label', + toggled: false, + onToggle: () => {}, + }; + let wrapper; + + beforeEach(() => { + wrapper = render(); + }); + + describe('renders as expected', () => { + it('button and label ids should match', () => { + const button = wrapper.getByRole('switch'); + const label = wrapper.container.querySelector('label'); + expect(button.id).toBe(label.htmlFor); + }); + + it('renders labelA when unchecked', () => { + wrapper.rerender(); + expect(wrapper.queryByText(props.labelA)).toBeTruthy(); + expect(wrapper.queryByText(props.labelB)).toBeNull(); + }); + + it('renders labelB when checked', () => { + wrapper.rerender(); + expect(wrapper.queryByText(props.labelA)).toBeNull(); + expect(wrapper.queryByText(props.labelB)).toBeTruthy(); + }); + + it('supports additional css class names', () => { + const className = 'some-additional-class'; + wrapper.rerender(); + + expect( + wrapper.container + .querySelector(`.${prefix}--toggle`) + .classList.contains(className) + ).toBe(true); + }); + + it('supports sm size', () => { + expect( + wrapper.container + .querySelector(`.${prefix}--toggle__appearance`) + .classList.contains(`${prefix}--toggle__appearance--sm`) + ).toBe(false); + expect( + wrapper.container.querySelector(`.${prefix}--toggle__check`) + ).toBeNull(); + + wrapper.rerender(); + + expect( + wrapper.container + .querySelector(`.${prefix}--toggle__appearance`) + .classList.contains(`${prefix}--toggle__appearance--sm`) + ).toBe(true); + expect( + wrapper.container.querySelector(`.${prefix}--toggle__check`) + ).toBeTruthy(); + }); + + it('supports to use top label as side label', () => { + wrapper.rerender(); + + expect( + wrapper.container + .querySelector(`.${prefix}--toggle__label-text`) + .classList.contains(`${prefix}--visually-hidden`) + ).toBe(true); + expect( + wrapper.container.querySelector(`.${prefix}--toggle__label-text`) + .textContent + ).toBe(props.labelText); + }); + }); + + describe('behaves as expected', () => { + it('supports to be disabled', () => { + expect(wrapper.getByRole('switch').disabled).toBe(false); + wrapper.rerender(); + expect(wrapper.getByRole('switch').disabled).toBe(true); + }); + + it('can be controlled with props.toggled', () => { + wrapper.rerender(); + expect(wrapper.getByRole('switch').getAttribute('aria-checked')).toBe( + 'false' + ); + wrapper.rerender(); + expect(wrapper.getByRole('switch').getAttribute('aria-checked')).toBe( + 'true' + ); + }); + }); + + describe('emits events as expected', () => { + it('passes along props.onClick to button', () => { + const onClick = jest.fn(); + wrapper.rerender(); + + expect(onClick).not.toHaveBeenCalled(); + userEvent.click(wrapper.getByRole('switch')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('emits props.onToggle when toggled and passes current state', () => { + const onToggle = jest.fn(); + + wrapper.rerender( + + ); + expect(onToggle).not.toHaveBeenCalled(); + userEvent.click(wrapper.getByRole('switch')); + expect(onToggle).toHaveBeenCalledTimes(1); + expect(onToggle.mock.calls[0][0]).toBe(true); + + wrapper.rerender( + + ); + userEvent.click(wrapper.getByRole('switch')); + expect(onToggle).toHaveBeenCalledTimes(2); + expect(onToggle.mock.calls[1][0]).toBe(false); + }); + }); +}); diff --git a/packages/react/src/components/Toggle/next/Toggle.js b/packages/react/src/components/Toggle/next/Toggle.js new file mode 100644 index 000000000000..755a4495a9e2 --- /dev/null +++ b/packages/react/src/components/Toggle/next/Toggle.js @@ -0,0 +1,166 @@ +/** + * Copyright IBM Corp. 2021 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { settings } from 'carbon-components'; +import { useControllableState } from '../../../internal/useControllableState'; + +const { prefix } = settings; + +export function Toggle({ + className, + defaultToggled = false, + disabled = false, + hideLabel = false, + id, + labelA = 'Off', + labelB = 'On', + labelText, + onClick, + onToggle, + size = 'md', + toggled, + ...other +}) { + const [checked, setChecked] = useControllableState( + toggled, + onToggle, + defaultToggled + ); + + function handleClick(e) { + setChecked(!checked); + + if (onClick) { + onClick(e); + } + } + + const isSm = size === 'sm'; + const sideLabel = hideLabel ? labelText : checked ? labelB : labelA; + + const wrapperClasses = classNames( + `${prefix}--toggle`, + { + [`${prefix}--toggle--disabled`]: disabled, + }, + className + ); + + const labelTextClasses = classNames(`${prefix}--toggle__label-text`, { + [`${prefix}--visually-hidden`]: hideLabel, + }); + + const appearanceClasses = classNames(`${prefix}--toggle__appearance`, { + [`${prefix}--toggle__appearance--sm`]: isSm, + }); + + const switchClasses = classNames(`${prefix}--toggle__switch`, { + [`${prefix}--toggle__switch--checked`]: checked, + }); + + return ( +
+
+ ); +} + +Toggle.propTypes = { + /** + * Specify a custom className to apply to the form-item node + */ + className: PropTypes.string, + + /** + * Specify whether the toggle should be on by default + */ + defaultToggled: PropTypes.bool, + + /** + * Whether this control should be disabled + */ + disabled: PropTypes.bool, + + /** + * Specify whether the label should be hidden, or not + */ + hideLabel: PropTypes.bool, + + /** + * Provide an id that unique represents the underlying ` +   {!regularProps.kind.includes('danger') && ( - + <> + +   + + )}
{ ); }; +export const ExpressiveButtons = () => { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + +
+ + ); +}; + export const Skeleton = () => (
diff --git a/packages/react/src/components/Button/Button.js b/packages/react/src/components/Button/Button.js index fe0ed52f354a..f45c2299c035 100644 --- a/packages/react/src/components/Button/Button.js +++ b/packages/react/src/components/Button/Button.js @@ -28,6 +28,7 @@ const Button = React.forwardRef(function Button( size, kind, href, + isExpressive, isSelected, tabIndex, type, @@ -122,14 +123,19 @@ const Button = React.forwardRef(function Button( const buttonClasses = classNames(className, { [`${prefix}--btn`]: true, - [`${prefix}--btn--sm`]: size === 'small' || size === 'sm' || small, - [`${prefix}--btn--md`]: size === 'field' || size === 'md', + [`${prefix}--btn--sm`]: + (size === 'small' && !isExpressive) || + (size === 'sm' && !isExpressive) || + (small && !isExpressive), + [`${prefix}--btn--md`]: + (size === 'field' && !isExpressive) || (size === 'md' && !isExpressive), // V11: change lg to xl [`${prefix}--btn--lg`]: enabled ? size === 'xl' : size === 'lg', // V11: change xl to 2xl [`${prefix}--btn--xl`]: enabled ? size === '2xl' : size === 'xl', [`${prefix}--btn--${kind}`]: kind, [`${prefix}--btn--disabled`]: disabled, + [`${prefix}--btn--expressive`]: isExpressive, [`${prefix}--tooltip--hidden`]: hasIconOnly && !allowTooltipVisibility, [`${prefix}--tooltip--visible`]: isHovered, [`${prefix}--btn--icon-only`]: hasIconOnly, @@ -275,6 +281,11 @@ Button.propTypes = { return undefined; }, + /** + * Specify whether the Button is expressive, or not + */ + isExpressive: PropTypes.bool, + /** * Specify whether the Button is currently selected */ @@ -383,6 +394,7 @@ Button.defaultProps = { dangerDescription: 'danger', tooltipAlignment: 'center', tooltipPosition: 'top', + isExpressive: false, }; export default Button; diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap index dc5628a821a3..3b97bed5e5cc 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap @@ -1887,6 +1887,7 @@ exports[`DataTable should render 1`] = `