From 8546cc3e9866e040708628a10e62243cd037bd5d Mon Sep 17 00:00:00 2001 From: Natalie Pina <34781875+nataliepina@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:16:03 -0500 Subject: [PATCH] feat: new outline button components (#1068) Introduces a new OutlineButton component, along with an OutlineDropdownButton. Closes D2IQ-99363 --- packages/button/README.md | 1 + packages/button/components/ButtonBase.tsx | 3 +- packages/button/components/OutlineButton.tsx | 16 ++ .../components/OutlineDropdownButton.tsx | 14 + packages/button/index.ts | 2 + .../button/stories/DefaultButton.stories.tsx | 8 +- .../button/stories/DropdownButton.stories.tsx | 9 +- packages/button/style.ts | 72 ++++- .../__snapshots__/ButtonBase.test.tsx.snap | 246 ++++++++++++++++++ packages/index.ts | 2 + 10 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 packages/button/components/OutlineButton.tsx create mode 100644 packages/button/components/OutlineDropdownButton.tsx diff --git a/packages/button/README.md b/packages/button/README.md index 936b443fa..db7391c0b 100644 --- a/packages/button/README.md +++ b/packages/button/README.md @@ -10,6 +10,7 @@ There are several types of buttons to choose from based on the intended action: | --------------- | ------------------------------------------------------------------------------- | | Primary | Used for the most important call-to-action on a page, such as Save or Deploy. | | Secondary | Used primarily for a call-to-action that is a link. | +| Outline | Used as an alternative secondary button with an outline/ghost style. | | Warning | Used to advise caution. | | Danger | Used to highlight something dangerous that can't be undone, such as "Delete". | | Icon only | Use when an icon is enough to represent the action, such as "Create a Service". | diff --git a/packages/button/components/ButtonBase.tsx b/packages/button/components/ButtonBase.tsx index 8e341acea..3e024329a 100644 --- a/packages/button/components/ButtonBase.tsx +++ b/packages/button/components/ButtonBase.tsx @@ -30,7 +30,8 @@ export enum ButtonAppearances { Standard = "standard", Danger = "danger", Success = "success", - Warning = "warning" + Warning = "warning", + Outline = "outline" } export interface ButtonProps extends LinkProps { diff --git a/packages/button/components/OutlineButton.tsx b/packages/button/components/OutlineButton.tsx new file mode 100644 index 000000000..3b49b1df9 --- /dev/null +++ b/packages/button/components/OutlineButton.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { + default as ButtonBase, + ButtonProps, + ButtonAppearances +} from "./ButtonBase"; + +const OutlineButton = (props: ButtonProps) => ( + +); + +export default OutlineButton; diff --git a/packages/button/components/OutlineDropdownButton.tsx b/packages/button/components/OutlineDropdownButton.tsx new file mode 100644 index 000000000..0a1274e2c --- /dev/null +++ b/packages/button/components/OutlineDropdownButton.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import OutlineButton from "./OutlineButton"; +import { ButtonProps } from "./ButtonBase"; +import { SystemIcons } from "../../icons/dist/system-icons-enum"; + +const OutlineDropdownButton = (props: ButtonProps) => ( + +); + +export default OutlineDropdownButton; diff --git a/packages/button/index.ts b/packages/button/index.ts index 0b7062318..864c7d141 100644 --- a/packages/button/index.ts +++ b/packages/button/index.ts @@ -4,6 +4,7 @@ export { default as StandardButton } from "./components/StandardButton"; export { default as SuccessButton } from "./components/SuccessButton"; export { default as DangerButton } from "./components/DangerButton"; export { default as WarningButton } from "./components/WarningButton"; +export { default as OutlineButton } from "./components/OutlineButton"; export { default as PrimaryDropdownButton } from "./components/PrimaryDropdownButton"; export { default as SecondaryDropdownButton } from "./components/SecondaryDropdownButton"; @@ -11,5 +12,6 @@ export { default as StandardDropdownButton } from "./components/StandardDropdown export { default as SuccessDropdownButton } from "./components/SuccessDropdownButton"; export { default as WarningDropdownButton } from "./components/WarningDropdownButton"; export { default as DangerDropdownButton } from "./components/DangerDropdownButton"; +export { default as OutlineDropdownButton } from "./components/OutlineDropdownButton"; export { default as ResetButton } from "./components/ResetButton"; diff --git a/packages/button/stories/DefaultButton.stories.tsx b/packages/button/stories/DefaultButton.stories.tsx index 752f48d8d..25c40aa7f 100644 --- a/packages/button/stories/DefaultButton.stories.tsx +++ b/packages/button/stories/DefaultButton.stories.tsx @@ -6,7 +6,8 @@ import { StandardButton, SuccessButton, DangerButton, - WarningButton + WarningButton, + OutlineButton } from "../.."; import { action } from "@storybook/addon-actions"; import { SystemIcons } from "../../icons/dist/system-icons-enum"; @@ -17,6 +18,7 @@ export default { component: StandardButton, subComponents: { PrimaryButton, + OutlineButton, SecondaryButton, SuccessButton, DangerButton, @@ -59,6 +61,10 @@ export const _SecondaryButton = { render: args => {args.children} }; +export const _OutlineButton = { + render: args => {args.children} +}; + export const _SuccessButton = { render: args => {args.children} }; diff --git a/packages/button/stories/DropdownButton.stories.tsx b/packages/button/stories/DropdownButton.stories.tsx index 198e1f21a..b9fe25475 100644 --- a/packages/button/stories/DropdownButton.stories.tsx +++ b/packages/button/stories/DropdownButton.stories.tsx @@ -6,7 +6,8 @@ import { StandardDropdownButton, SuccessDropdownButton, WarningDropdownButton, - DangerDropdownButton + DangerDropdownButton, + OutlineDropdownButton } from "../../index"; import { SystemIcons } from "../../icons/dist/system-icons-enum"; @@ -49,6 +50,12 @@ export const _SecondaryDropdownButton = { ) }; +export const _OutlineDropdownButton = { + render: args => ( + {args.children} + ) +}; + export const _SuccessDropdownButton = { render: args => ( {args.children} diff --git a/packages/button/style.ts b/packages/button/style.ts index dc9ca2b9e..fba3ff3e3 100644 --- a/packages/button/style.ts +++ b/packages/button/style.ts @@ -18,7 +18,10 @@ import { themeErrorInverted, themeTextColorDisabledInverted, themeWarningInverted, - themeWarning + themeWarning, + purpleLighten5, + purpleLighten4, + themeBgPrimary } from "../design-tokens/build/js/designTokens"; import { darken, getTextColor } from "../shared/styles/color"; import { ButtonAppearances } from "./components/ButtonBase"; @@ -70,6 +73,7 @@ export const filledButton = ( const mutedButton = css` ${tintContent(themeTextColorDisabled)}; + border-color: ${themeBgDisabled}; cursor: default; pointer-events: none; @@ -196,6 +200,26 @@ export const focusStyleByAppearance = (appearance, isInverse) => { return isInverse ? focusStyles(getHoverColor(getCSSVarValue(themeWarningInverted))) : focusStyles(getHoverColor(getCSSVarValue(themeWarning))); + case "outline": + return css` + ${(isInverse + ? focusStyles( + getCSSVarValue(themeBgPrimary), + getHoverColor(getCSSVarValue(themeTextColorInteractiveInverted)) + ) + : getCSSVarValue(themeBgPrimary), + focusStyles(getHoverColor(getCSSVarValue(themeTextColorInteractive))))}; + &:focus { + background-color: ${purpleLighten5}; + + &:after { + bottom: -4px; + left: -4px; + right: -4px; + top: -4px; + } + } + `; default: return ""; } @@ -251,6 +275,32 @@ export const button = appearance => { getHoverColor(getCSSVarValue(themeWarning)), getActiveColor(getCSSVarValue(themeWarning)) ); + case "outline": + return css` + border: 1px solid ${getCSSVarValue(themeTextColorInteractive)}; + border-radius: ${borderRadiusDefault}; + padding: ${buttonPadding.vert} ${buttonPadding.horiz}; + + ${tintContent(themeTextColorInteractive)}; + + &:hover { + background-color: ${purpleLighten5}; + ${tintContent( + getHoverColor(getCSSVarValue(themeTextColorInteractive)) + )}; + } + &:active { + background-color: ${purpleLighten4}; + ${tintContent( + getActiveColor(getCSSVarValue(themeTextColorInteractive)) + )}; + } + &[href], + &[href]:visited { + background-color: ${purpleLighten4}; + ${tintContent(themeTextColorInteractive)}; + } + `; default: return ""; } @@ -306,6 +356,26 @@ export const buttonInverse = appearance => { getHoverColor(getCSSVarValue(themeWarningInverted)), getActiveColor(getCSSVarValue(themeWarningInverted)) ); + case "outline": + return css` + border: 1px solid ${getCSSVarValue(themeTextColorInteractiveInverted)}; + ${tintContent(themeTextColorInteractiveInverted)}; + + &:hover { + ${tintContent( + getHoverColor(getCSSVarValue(themeTextColorInteractiveInverted)) + )}; + } + &:active { + ${tintContent( + getActiveColor(getCSSVarValue(themeTextColorInteractiveInverted)) + )}; + } + &[href], + &[href]:visited { + ${tintContent(themeTextColorInteractiveInverted)}; + } + `; default: return ""; } diff --git a/packages/button/tests/__snapshots__/ButtonBase.test.tsx.snap b/packages/button/tests/__snapshots__/ButtonBase.test.tsx.snap index 824681fa8..9a22f357e 100644 --- a/packages/button/tests/__snapshots__/ButtonBase.test.tsx.snap +++ b/packages/button/tests/__snapshots__/ButtonBase.test.tsx.snap @@ -46,6 +46,7 @@ exports[`ButtonBase renders all appearances with props 1`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; background-color: var(--themeBgDisabled, #E8EAED); @@ -273,6 +274,7 @@ exports[`ButtonBase renders all appearances with props 2`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; } @@ -491,6 +493,7 @@ exports[`ButtonBase renders all appearances with props 3`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; background-color: var(--themeBgDisabled, #E8EAED); @@ -714,6 +717,7 @@ exports[`ButtonBase renders all appearances with props 4`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; background-color: var(--themeBgDisabled, #E8EAED); @@ -937,6 +941,7 @@ exports[`ButtonBase renders all appearances with props 5`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; background-color: var(--themeBgDisabled, #E8EAED); @@ -1160,6 +1165,7 @@ exports[`ButtonBase renders all appearances with props 6`] = ` width: 100%; color: var(--themeTextColorDisabled, #AEB0B4); fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); cursor: default; pointer-events: none; background-color: var(--themeBgDisabled, #E8EAED); @@ -1336,3 +1342,243 @@ exports[`ButtonBase renders all appearances with props 6`] = ` `; + +exports[`ButtonBase renders all appearances with props 7`] = ` + + .emotion-0 { + outline: none; + position: relative; +} + +.emotion-0:focus { + background-color: #704fe5; +} + +.emotion-0:focus:after { + border: 2px solid #704fe5; + border-radius: 4px; + bottom: -3px; + content: ""; + left: -3px; + position: absolute; + right: -3px; + top: -3px; +} + +.emotion-0:focus { + background-color: #F8F6FF; +} + +.emotion-0:focus:after { + bottom: -4px; + left: -4px; + right: -4px; + top: -4px; +} + +.emotion-1 { + background: none; + border: 0; + color: inherit; + font: inherit; + line-height: normal; + overflow: visible; + padding: 0; + text-align: inherit; + border: 1px solid #7D58FF; + border-radius: 4px; + padding: 10px 18px; + color: var(--themeTextColorInteractive, #7D58FF); + fill: var(--themeTextColorInteractive, #7D58FF); + cursor: pointer; + display: inline-block; + outline: none; + -webkit-text-decoration: none; + text-decoration: none; + font-weight: 500; + text-align: center; + width: 100%; + color: var(--themeTextColorDisabled, #AEB0B4); + fill: var(--themeTextColorDisabled, #AEB0B4); + border-color: var(--themeBgDisabled, #E8EAED); + cursor: default; + pointer-events: none; + background-color: var(--themeBgDisabled, #E8EAED); +} + +.emotion-1::-moz-focus-inner { + border: 0; + padding: 0; +} + +.emotion-1:hover { + background-color: #F8F6FF; + color: #704fe5; + fill: #704fe5; +} + +.emotion-1:active { + background-color: #E5DDFF; + color: #6446cc; + fill: #6446cc; +} + +.emotion-1[href], +.emotion-1[href]:visited { + background-color: #E5DDFF; + color: var(--themeTextColorInteractive, #7D58FF); + fill: var(--themeTextColorInteractive, #7D58FF); +} + +.emotion-1:hover, +.emotion-1:focus, +.emotion-1:active { + color: var(--themeTextColorDisabled, #AEB0B4); + fill: var(--themeTextColorDisabled, #AEB0B4); +} + +.emotion-1:hover, +.emotion-1:focus, +.emotion-1:active { + background-color: var(--themeBgDisabled, #E8EAED); +} + +.emotion-2 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: auto; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + box-sizing: border-box; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 0; +} + +.emotion-2>div { + width: auto; +} + +.emotion-3 { + box-sizing: border-box; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: initial; + display: inherit; +} + +.emotion-4 { + vertical-align: middle; + fill: inherit; +} + +.emotion-4 use { + pointer-events: none; +} + +.emotion-5 { + box-sizing: border-box; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: initial; + padding-left: 8px; +} + +.emotion-5:after { + content: "..."; +} + +.emotion-6 { + box-sizing: border-box; + -webkit-flex-basis: auto; + -ms-flex-preferred-size: auto; + flex-basis: auto; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: initial; + display: inherit; + padding-left: 4px; +} + + + +`; diff --git a/packages/index.ts b/packages/index.ts index 912cea1cb..a51dc5c92 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -30,12 +30,14 @@ export { SuccessButton, DangerButton, WarningButton, + OutlineButton, PrimaryDropdownButton, SecondaryDropdownButton, StandardDropdownButton, SuccessDropdownButton, WarningDropdownButton, DangerDropdownButton, + OutlineDropdownButton, ResetButton } from "./button"; export { ButtonCard, Card, LinkCard } from "./card";