-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
418 additions
and
0 deletions.
There are no files selected for viewing
196 changes: 196 additions & 0 deletions
196
packages/core/stories/ghost-button/ghost-button.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import { Banner, FlowLayout, StackLayout } from "@salt-ds/core"; | ||
import { GhostButton, SystemStatus } from "@salt-ds/lab"; | ||
import { CloseIcon, SearchIcon } from "@salt-ds/icons"; | ||
import type { Meta, StoryFn } from "@storybook/react"; | ||
|
||
export default { | ||
title: "Lab/GhostButton", | ||
component: GhostButton, | ||
argTypes: { onClick: { action: "clicked" } }, | ||
} as Meta<typeof GhostButton>; | ||
|
||
export const TestingEverything: StoryFn<typeof GhostButton> = (props) => { | ||
return ( | ||
<StackLayout> | ||
<div style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</div> | ||
<Banner status="success" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner status="error" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner status="info" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner status="warning" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner variant="secondary" status="success" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner variant="secondary" status="error" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner variant="secondary" status="info" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<Banner variant="secondary" status="warning" style={{ width: 500 }}> | ||
<GhostButton aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</Banner> | ||
<div style={{ width: 500 }}> | ||
<GhostButton disabled aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</div> | ||
<SystemStatus status="success" style={{ width: 500 }}> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</SystemStatus> | ||
<SystemStatus status="error" style={{ width: 500 }}> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</SystemStatus> | ||
<SystemStatus status="info" style={{ width: 500 }}> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</SystemStatus> | ||
<SystemStatus status="warning" style={{ width: 500 }}> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" {...props}> | ||
<CloseIcon aria-hidden /> Button | ||
</GhostButton> | ||
</SystemStatus> | ||
</StackLayout> | ||
); | ||
}; | ||
|
||
const ButtonGridTemplate: StoryFn<typeof GhostButton> = (props) => { | ||
return ( | ||
<FlowLayout> | ||
<GhostButton {...props}>Submit</GhostButton> | ||
<GhostButton aria-label="Search" {...props}> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton {...props}> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
<Banner status="info"> | ||
<GhostButton {...props}>Submit</GhostButton> | ||
<GhostButton aria-label="Search" {...props}> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton {...props}> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</Banner> | ||
<SystemStatus status="info"> | ||
<GhostButton {...props}>Submit</GhostButton> | ||
<GhostButton aria-label="Search" {...props}> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton {...props}> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</SystemStatus> | ||
</FlowLayout> | ||
); | ||
}; | ||
|
||
export const Disabled: StoryFn = (props) => { | ||
return ( | ||
<StackLayout gap={3}> | ||
<FlowLayout> | ||
<GhostButton disabled>Submit</GhostButton> | ||
<GhostButton aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
<Banner status="info"> | ||
<GhostButton disabled>Submit</GhostButton> | ||
<GhostButton aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</Banner> | ||
<SystemStatus status="info"> | ||
<GhostButton disabled>Submit</GhostButton> | ||
<GhostButton aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</SystemStatus> | ||
</FlowLayout> | ||
<FlowLayout> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
Submit | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
<Banner status="info"> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
Submit | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</Banner> | ||
<SystemStatus status="info"> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
Submit | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" aria-label="Search" disabled> | ||
<SearchIcon aria-hidden /> | ||
</GhostButton> | ||
<GhostButton adaptiveAppearance="solid" disabled> | ||
<SearchIcon aria-hidden /> Search | ||
</GhostButton> | ||
</SystemStatus> | ||
</FlowLayout> | ||
</StackLayout> | ||
); | ||
}; | ||
|
||
export const Solid = ButtonGridTemplate.bind({}); | ||
Solid.args = { | ||
adaptiveAppearance: "solid", | ||
}; | ||
|
||
export const NonSolid = ButtonGridTemplate.bind({}); | ||
NonSolid.args = { | ||
adaptiveAppearance: "non-solid", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
.saltGhostButton { | ||
align-items: var(--saltGhostButton-alignItems, center); | ||
appearance: none; | ||
background: var(--saltGhostButton-background, var(--ghostButton-background)); | ||
border-color: var(--saltGhostButton-borderColor, var(--ghostButton-borderColor, transparent)); | ||
border-style: var(--saltGhostButton-borderStyle, solid); | ||
border-width: var(--saltGhostButton-borderWidth, var(--salt-size-border, 0)); | ||
border-radius: var(--saltGhostButton-borderRadius, var(--salt-palette-corner-weak, 0)); | ||
color: var(--saltGhostButton-text-color, var(--ghostButton-text-color)); | ||
cursor: var(--saltGhostButton-cursor, pointer); | ||
display: inline-flex; | ||
gap: var(--salt-spacing-50); | ||
justify-content: var(--saltGhostButton-justifyContent, center); | ||
font-size: var(--saltGhostButton-fontSize, var(--salt-text-fontSize)); | ||
font-family: var(--saltGhostButton-fontFamily, var(--salt-text-action-fontFamily)); | ||
line-height: var(--saltGhostButton-lineHeight, var(--salt-text-lineHeight)); | ||
letter-spacing: var(--saltGhostButton-letterSpacing, var(--salt-text-action-letterSpacing)); | ||
text-transform: var(--saltGhostButton-textTransform, var(--salt-text-action-textTransform)); | ||
padding: 0 var(--saltGhostButton-padding, calc(var(--salt-spacing-100) - var(--saltGhostButton-borderWidth, var(--salt-size-border, 0)))); | ||
margin: var(--saltGhostButton-margin, 0); | ||
height: var(--saltGhostButton-height, var(--salt-size-base)); | ||
min-width: var(--saltGhostButton-minWidth, unset); | ||
position: relative; | ||
text-align: var(--saltGhostButton-textAlign, var(--salt-text-action-textAlign)); | ||
text-decoration: none; | ||
transition: none; | ||
width: var(--saltGhostButton-width, auto); | ||
-webkit-appearance: none; | ||
-webkit-tap-highlight-color: transparent; | ||
font-weight: var(--saltGhostButton-fontWeight, var(--salt-text-action-fontWeight)); | ||
} | ||
|
||
/* Pseudo-class applied to the root element on focus */ | ||
.saltGhostButton:focus-visible { | ||
outline-style: var(--salt-focused-outlineStyle); | ||
outline-width: var(--salt-focused-outlineWidth); | ||
outline-color: var(--salt-focused-outlineColor); | ||
outline-offset: var(--salt-focused-outlineOffset); | ||
background: var(--saltGhostButton-background-hover, var(--ghostButton-background-hover)); | ||
color: var(--saltGhostButton-text-color-hover, var(--ghostButton-text-color-hover)); | ||
} | ||
|
||
/* Pseudo-class applied to the root element on focus when Button is active */ | ||
.saltGhostButton.saltGhostButton-active:focus-visible, | ||
.saltGhostButton:focus-visible:active { | ||
background: var(--saltGhostButton-background-active-hover, var(--ghostButton-background)); | ||
color: var(--saltGhostButton-text-color-active-hover, var(--ghostButton-text-color)); | ||
} | ||
|
||
/* Pseudo-class applied to the root element on hover when Button is not active or disabled */ | ||
.saltGhostButton:hover { | ||
background: var(--saltGhostButton-background-hover, var(--ghostButton-background-hover)); | ||
color: var(--saltGhostButton-text-color-hover, var(--ghostButton-text-color-hover)); | ||
} | ||
|
||
/* Pseudo-class applied to the root element when Button is active and not disabled */ | ||
.saltGhostButton:active, | ||
.saltGhostButton.saltGhostButton-active { | ||
background: var(--saltGhostButton-background-active, var(--ghostButton-background-active)); | ||
color: var(--saltGhostButton-text-color-active, var(--ghostButton-text-color-active)); | ||
} | ||
|
||
/* Styles applied when the button triggers a dialog or menu */ | ||
.saltGhostButton[aria-expanded="true"][aria-haspopup="menu"], | ||
.saltGhostButton[aria-expanded="true"][aria-haspopup="dialog"] { | ||
background: var(--saltGhostButton-background-active, var(--ghostButton-background-active)); | ||
color: var(--saltGhostButton-text-color-active, var(--ghostButton-text-color-active)); | ||
border-color: var(--saltGhostButton-borderColor-active, var(--ghostButton-borderColor-active)); | ||
} | ||
|
||
/* Pseudo-class applied to the root element if disabled={true} */ | ||
.saltGhostButton:disabled, | ||
.saltGhostButton-disabled, | ||
.saltGhostButton-disabled:active, | ||
.saltGhostButton-disabled:focus-visible, | ||
.saltGhostButton-disabled:focus-visible:active, | ||
.saltGhostButton-disabled:hover { | ||
background: var(--saltGhostButton-background-disabled, var(--ghostButton-background-disabled)); | ||
color: var(--saltGhostButton-text-color-disabled, var(--ghostButton-text-color-disabled)); | ||
cursor: var(--saltGhostButton-cursor-disabled, var(--salt-actionable-cursor-disabled)); | ||
} | ||
|
||
/* TODO: update tokens*/ | ||
.saltGhostButton { | ||
--ghostButton-text-color: var(--salt-content-primary-foreground); | ||
--ghostButton-text-color-hover: var(--salt-content-primary-foreground); | ||
--ghostButton-text-color-active: var(--salt-content-primary-foreground); | ||
--ghostButton-text-color-disabled: var(--salt-content-primary-foreground-disabled); | ||
|
||
--ghostButton-background-active: var(--salt-actionable-alpha-background-active); | ||
--ghostButton-background-hover: var(--salt-actionable-alpha-background-hover); | ||
} | ||
|
||
.saltGhostButton.saltGhostButton-solid { | ||
--ghostButton-text-color: var(--salt-content-bold-foreground); | ||
--ghostButton-text-color-hover: var(--salt-content-bold-foreground); | ||
--ghostButton-text-color-active: var(--salt-content-bold-foreground); | ||
--ghostButton-text-color-disabled: var(--salt-content-bold-foreground-disabled); | ||
|
||
--ghostButton-background-active: var(--salt-actionable-alpha-background-active-alt); | ||
--ghostButton-background-hover: var(--salt-actionable-alpha-background-hover-alt); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { useComponentCssInjection } from "@salt-ds/styles"; | ||
import { useWindow } from "@salt-ds/window"; | ||
import { clsx } from "clsx"; | ||
import { | ||
type ComponentPropsWithoutRef, | ||
forwardRef, | ||
type ReactElement, | ||
} from "react"; | ||
|
||
import ghostButtonCss from "./GhostButton.css"; | ||
import { makePrefixer, useButton } from "@salt-ds/core"; | ||
|
||
const withBaseName = makePrefixer("saltGhostButton"); | ||
|
||
export interface GhostButtonProps extends ComponentPropsWithoutRef<"button"> { | ||
/** | ||
* If `true`, the button will be disabled. | ||
*/ | ||
disabled?: boolean; | ||
/** | ||
* If `true`, the button will be focusable when disabled. | ||
*/ | ||
focusableWhenDisabled?: boolean; | ||
/** | ||
* The appearance of the button. | ||
*/ | ||
adaptiveAppearance?: "solid" | "non-solid"; | ||
} | ||
|
||
export const GhostButton = forwardRef<HTMLButtonElement, GhostButtonProps>( | ||
function GhostButton( | ||
{ | ||
children, | ||
className, | ||
disabled = false, | ||
adaptiveAppearance = "non-solid", | ||
focusableWhenDisabled, | ||
onKeyUp, | ||
onKeyDown, | ||
onBlur, | ||
onClick, | ||
type = "button", | ||
...restProps | ||
}, | ||
ref, | ||
): ReactElement<GhostButtonProps> { | ||
const { active, buttonProps } = useButton({ | ||
disabled, | ||
focusableWhenDisabled, | ||
onKeyUp, | ||
onKeyDown, | ||
onBlur, | ||
onClick, | ||
}); | ||
|
||
const targetWindow = useWindow(); | ||
useComponentCssInjection({ | ||
testId: "salt-ghost-button", | ||
css: ghostButtonCss, | ||
window: targetWindow, | ||
}); | ||
|
||
return ( | ||
<button | ||
{...buttonProps} | ||
className={clsx( | ||
withBaseName(), | ||
{ | ||
[withBaseName("disabled")]: disabled, | ||
[withBaseName("active")]: active, | ||
[withBaseName(adaptiveAppearance)]: adaptiveAppearance === "solid", | ||
}, | ||
className, | ||
)} | ||
{...restProps} | ||
ref={ref} | ||
type={type} | ||
> | ||
{children} | ||
</button> | ||
); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./GhostButton"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.