From 76ea1c5236ae8178c0849bcf59f6b32083d48ffb Mon Sep 17 00:00:00 2001 From: Andrew Musgrave Date: Wed, 16 Oct 2019 11:37:55 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Added=20pressed=20state=20to=20?= =?UTF-8?q?Button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Button/Button.scss | 131 +++++++++++++++++++- src/components/Button/Button.tsx | 25 +++- src/components/Button/README.md | 33 +++++ src/components/Button/tests/Button.test.tsx | 46 +++++++ src/styles/shared/_buttons.scss | 8 +- 5 files changed, 235 insertions(+), 8 deletions(-) diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss index 3ac1f1c7efe..4305ec0ba0c 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -7,6 +7,61 @@ $slim-vertical-padding: ($slim-min-height - line-height(body) - rem(2px)) / 2; $large-min-height: rem(44px); $large-vertical-padding: ($large-min-height - line-height(body) - rem(2px)) / 2; $spinner-size: rem(20px); +// darken(color(sky, dark), 4%) +$pressed-border-color: rgb(184, 195, 205); +// darken(color(sky), 2%) +$pressed-hover-background: rgb(217, 222, 228); +// darken(color(sky), 4%) +$pressed-active-background: rgb(211, 217, 223); +// darken(color('indigo'), 10%) +$primary-pressed-lightest: rgb(63, 78, 174); +// darken(color('indigo'), 12%) +$primary-pressed-light: rgb(60, 75, 167); +// darken(color('indigo', 'dark'), 15%) +$primary-pressed-dark: rgb(16, 23, 60); +// darken(color('indigo', 'dark'), 20%) +$primary-pressed-darkest: rgb(11, 15, 39); +// darken(color('red'), 10%) +$destructive-pressed-lightest: rgb(176, 43, 19); +// darken(color('red'), 12%) +$destructive-pressed-light: rgb(167, 41, 18); +// darken(color('red', 'dark'), 15%) +$destructive-pressed-dark: rgb(117, 4, 10); +// darken(color('red', 'dark'), 20%) +$destructive-pressed-darkest: rgb(93, 3, 8); +$partial-button-filled-pressed-box-shadow: inset 0 0 0 0 transparent, + inset 0 1px 1px 0 rgba(22, 29, 37, 0.05), inset 0 0 3px 0; + +@mixin pressed-box-shadow($color: transparent) { + box-shadow: 0 0 0 1px $color, + inset 0 1px 1px 0 rgba(color('ink', 'lighter'), 0.1), + inset 0 1px 4px 0 rgba(color('ink', 'lighter'), 0.1); +} + +@mixin button-filled-pressed( + $color-lightest, + $color-light, + $color-dark, + $color-darkest +) { + background: linear-gradient(to bottom, $color-lightest, $color-lightest); + border-color: $color-dark; + box-shadow: $partial-button-filled-pressed-box-shadow $color-dark; + + &:focus, + &:hover { + transition-duration: duration(fast); + background: linear-gradient(to bottom, $color-light, $color-light); + border-color: $color-dark; + box-shadow: $partial-button-filled-pressed-box-shadow$color-dark; + } + + &:active { + background: linear-gradient(to bottom, $color-lightest, $color-lightest); + border-color: $color-dark; + box-shadow: $partial-button-filled-pressed-box-shadow$color-darkest; + } +} .Button { @include button-base; @@ -82,6 +137,15 @@ $spinner-size: rem(20px); --p-button-color-disabled: var(--p-branded-action-disabled); @include button-filled-disabled(color('indigo')); } + + &.pressed { + @include button-filled-pressed( + $primary-pressed-lightest, + $primary-pressed-light, + $primary-pressed-dark, + $primary-pressed-darkest + ); + } } .destructive { @@ -94,6 +158,15 @@ $spinner-size: rem(20px); &.disabled { @include button-filled-disabled(color('red')); } + + &.pressed { + @include button-filled-pressed( + $destructive-pressed-lightest, + $destructive-pressed-light, + $destructive-pressed-dark, + $destructive-pressed-darkest + ); + } } .outline { @@ -107,6 +180,14 @@ $spinner-size: rem(20px); .destructive.outline { @include button-outline(color('red')); @include recolor-icon(color('red', 'dark')); + + &.pressed { + @include button-outline( + color('red', 'dark'), + rgba(color('red', 'dark'), 0.03) + ); + @include recolor-icon(color('red', 'darker')); + } } .disabled { @@ -131,6 +212,35 @@ $spinner-size: rem(20px); } } +.pressed { + background: color(sky); + border-color: $pressed-border-color; + @include pressed-box-shadow; + + &:hover { + transition-duration: duration(fast); + background: $pressed-hover-background; + border-color: $pressed-border-color; + @include pressed-box-shadow; + } + + &:focus { + border-color: color('indigo'); + @include pressed-box-shadow(color('indigo')); + } + + &:active { + background: $pressed-active-background; + border-color: $pressed-border-color; + @include pressed-box-shadow; + } + + @media (-ms-high-contrast: active) { + color: ms-high-contrast-color('button-text'); + background: ms-high-contrast-color('button-text-background'); + } +} + // The way the designs work, we need to do lots of reaching down to // target the content in pseudo-selectors, so we need higher specificity // in this case. @@ -146,6 +256,7 @@ $spinner-size: rem(20px); box-shadow: none; color: color('blue'); + &.pressed, &:hover, &:focus, &:active { @@ -157,6 +268,7 @@ $spinner-size: rem(20px); text-decoration: underline; } + &.pressed, &:focus { @include high-contrast-button-outline(none); } @@ -165,6 +277,12 @@ $spinner-size: rem(20px); @include high-contrast-button-outline; } + &.pressed > .Content { + @include plain-button-backdrop(rgba(color('ink', 'lighter'), 0.1)); + } + + &.pressed:hover:not(.iconOnly) > .Content, + &.pressed:active:not(.iconOnly) > .Content, &:focus:not(.iconOnly) > .Content { @include plain-button-backdrop; } @@ -327,13 +445,24 @@ $spinner-size: rem(20px); z-index: 0; } + &.pressed { + background: transparent; + border-color: currentColor; + box-shadow: none; + + // stylelint-disable-next-line selector-max-class + &::before { + opacity: 0.05; + } + } + &:hover, &:focus, &:active { background-color: transparent; border-color: currentColor; &::before { - opacity: 0.05; + opacity: 0.07; } } } diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index c61f2368630..1d96d6b69a4 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import {CaretDownMinor} from '@shopify/polaris-icons'; import {classNames, variationName} from '../../utilities/css'; import {handleMouseUpByBlurring} from '../../utilities/focus'; @@ -39,6 +39,8 @@ export interface ButtonProps { textAlign?: TextAlign; /** Gives the button a subtle alternative to the default button styling, appropriate for certain backdrops */ outline?: boolean; + /** Demonstrates a pressed state */ + pressed?: boolean; /** Allows the button to grow to the width of its container */ fullWidth?: boolean; /** Displays the button with a disclosure icon */ @@ -61,7 +63,10 @@ export interface ButtonProps { ariaControls?: string; /** Tells screen reader the controlled element is expanded */ ariaExpanded?: boolean; - /** Tells screen reader the element is pressed */ + /** + * @deprecated As of release 4.7.0, replaced by {@link https://polaris.shopify.com/components/structure/page#props-pressed} + * Tells screen reader the element is pressed + */ ariaPressed?: boolean; /** Callback when clicked */ onClick?(): void; @@ -108,7 +113,18 @@ export function Button({ size = DEFAULT_SIZE, textAlign, fullWidth, + pressed, }: ButtonProps) { + const hasGivenDeprecationWarning = useRef(false); + + if (ariaPressed && !hasGivenDeprecationWarning.current) { + // eslint-disable-next-line no-console + console.warn( + 'Deprecation: The ariaPressed prop has been replaced with pressed', + ); + hasGivenDeprecationWarning.current = true; + } + const i18n = useI18n(); const isDisabled = disabled || loading; @@ -121,6 +137,7 @@ export function Button({ isDisabled && styles.disabled, loading && styles.loading, plain && styles.plain, + pressed && !disabled && !url && styles.pressed, monochrome && styles.monochrome, size && size !== DEFAULT_SIZE && styles[variationName('size', size)], textAlign && styles[variationName('textAlign', textAlign)], @@ -206,6 +223,8 @@ export function Button({ ); } + const ariaPressedStatus = pressed !== undefined ? pressed : ariaPressed; + return ( ``` +### Pressed button + + + +Buttons are sometimes used as a toggle for other parts of the user interface. + +```jsx +function PressedButton() { + const [isFirstButtonActive, setIsFirstButtonActive] = useState(true); + + const handleFirstButtonClick = useCallback(() => { + if (isFirstButtonActive) return; + setIsFirstButtonActive(true); + }, [isFirstButtonActive]); + + const handleSecondButtonClick = useCallback(() => { + if (!isFirstButtonActive) return; + setIsFirstButtonActive(false); + }, [isFirstButtonActive]); + + return ( + + + + + ); +} +``` + ### Disabled state Use for actions that aren’t currently available. The surrounding interface should make it clear why the button is disabled and what needs to be done to enable it. diff --git a/src/components/Button/tests/Button.test.tsx b/src/components/Button/tests/Button.test.tsx index aac21683bd1..4aeeb3e9f22 100644 --- a/src/components/Button/tests/Button.test.tsx +++ b/src/components/Button/tests/Button.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {PlusMinor} from '@shopify/polaris-icons'; import {mountWithAppProvider, trigger} from 'test-utilities/legacy'; +import {mountWithApp} from 'test-utilities'; import {UnstyledLink, Icon, Spinner} from 'components'; import {Button, IconWrapper} from '../Button'; @@ -191,8 +192,14 @@ describe('