Skip to content

Commit

Permalink
Created reusable link button component (#2818)
Browse files Browse the repository at this point in the history
* Created reusable link button component

* Moved related files to design system folder

* Added conditional rendering

* Refactored className of button

* Refactored for snake case
  • Loading branch information
VKormylo authored Dec 3, 2024
1 parent f884090 commit c91b121
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 0 deletions.
129 changes: 129 additions & 0 deletions src/design-system/components/link-button/LinkButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
@use '~scss/utilities' as *;

.#{$prefix}link-button {
font-size: var(--#{$prefix}font-size-md);
font-weight: var(--#{$prefix}font-weight-medium);
line-height: var(--#{$prefix}line-height-lg);
text-transform: none;
display: flex;
justify-content: center;
transition: all 0.3s ease-out;
text-decoration: none;
position: relative;

&::after {
content: '';
position: absolute;
bottom: 2px;
left: var(--#{$prefix}space-1);
opacity: 0;
width: calc(100% - var(--#{$prefix}space-1) * 2);
height: var(--#{$prefix}border-width-xs);
transition: all 0.3s ease-out;
border-radius: var(--#{$prefix}border-radius-2xs);
}

& > &_loader {
position: absolute;
}

&_small {
@extend %size-small;
}

&_medium {
@extend %size-medium;
}

&_light {
@extend %variant-light;
}

&_dark {
@extend %variant-dark;
}

&_disabled {
pointer-events: none;
}

&_loading .#{$prefix}link-button_content {
visibility: hidden;
}
}

%size-small {
font-size: var(--#{$prefix}font-size-sm);
line-height: var(--#{$prefix}line-height-sm);
padding: 0 var(--#{$prefix}space-1);
column-gap: var(--#{$prefix}space-1);

&::after {
bottom: 0;
}
}

%size-medium {
padding: 0 var(--#{$prefix}space-1);
column-gap: var(--#{$prefix}space-1);
}

%variant-light {
color: var(--#{$prefix}blue-gray-800);

&:hover {
color: var(--#{$prefix}blue-gray-500);
}

&:active {
color: var(--#{$prefix}blue-gray-500);

&::after {
background-color: var(--#{$prefix}blue-gray-500);
opacity: 1;
}
}

&:focus-visible {
color: var(--#{$prefix}blue-gray-800);

&::after {
background-color: var(--#{$prefix}blue-gray-800);
opacity: 1;
}
}

&.#{$prefix}link-button_disabled {
color: var(--#{$prefix}blue-gray-300);
}
}

%variant-dark {
color: var(--#{$prefix}blue-gray-50);

&:hover {
color: var(--#{$prefix}blue-gray-300);
}

&:active {
color: var(--#{$prefix}blue-gray-300);

&::after {
background-color: var(--#{$prefix}blue-gray-300);
opacity: 1;
}
}

&:focus-visible {
color: var(--#{$prefix}blue-gray-50);

&::after {
background-color: var(--#{$prefix}blue-gray-50);
opacity: 1;
}
}

&.#{$prefix}link-button_disabled {
color: var(--#{$prefix}blue-gray-200);
}
}
43 changes: 43 additions & 0 deletions src/design-system/components/link-button/LinkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FC, ReactNode } from 'react'
import Loader from '~/components/loader/Loader'
import { LinkButtonVariantEnum, SizeEnum } from '~/types'
import { Link } from 'react-router-dom'
import { cn } from '~/utils/cn'
import '~scss-components/link-button/LinkButton.scss'

interface LinkButtonProps {
children: ReactNode
to: string
variant: LinkButtonVariantEnum
size?: SizeEnum | null
loading?: boolean
disabled?: boolean
}

const LinkButton: FC<LinkButtonProps> = ({
children,
to,
variant,
size = SizeEnum.Medium,
loading,
disabled
}) => {
const loader = <Loader size={20} sx={{ opacity: '0.6' }} />
return (
<Link
className={cn(
's2s-link-button',
`s2s-link-button_${variant}`,
`s2s-link-button_${size}`,
(disabled || loading) && 's2s-link-button_disabled',
loading && 's2s-link-button_loading'
)}
to={disabled ? '#' : to}
>
{loading && <div className='s2s-link-button_loader'>{loader}</div>}
<div className='s2s-link-button_content'>{children}</div>
</Link>
)
}

export default LinkButton
127 changes: 127 additions & 0 deletions src/design-system/stories/LinkButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Meta, StoryObj } from '@storybook/react'
import LinkButton from '~scss-components/link-button/LinkButton'
import { BrowserRouter as Router } from 'react-router-dom'
import { LinkButtonVariantEnum, SizeEnum } from '~/types'

const meta: Meta<typeof LinkButton> = {
title: 'Components/LinkButton',
component: LinkButton,
decorators: [
(Story) => (
<Router>
<Story />
</Router>
)
],
parameters: {
layout: 'centered',
docs: {
description: {
component: `
The \`LinkButton\` component is a versatile and customizable link button element that can be used for a variety of actions in your application. It supports different visual styles (variants), sizes, and states, making it a flexible component for many UI scenarios.
#### Key Features:
- **Variants:** Choose from two pre-defined styles light and dark to match the current theme.
- **Sizes:** Adjust the button's size to suit the context, whether you need a medium link button or a smaller, more subtle link button.
- **Loading State:** Display a loading indicator when an action is in progress, signaling to the user that something is happening.
- **Link Navigation:** Link button acts as a navigation element using the \`to\` prop.
This component is essential for navigating users throughout your application.
`
}
}
},
tags: ['autodocs'],
argTypes: {
children: {
description:
"The content to be displayed inside the link button, typically a text label that describes the button's action or route"
},
variant: {
description:
"The visual style of the button. This determines the button's appearance and behavior",
options: ['light', 'dark'],
control: { type: 'radio' }
},
disabled: {
description:
'When true, the button is disabled, preventing user interaction and applying a "disabled" style'
},
loading: {
description:
'When true, the button content is replaced with a loading indicator, signaling that an action is in progress'
},
size: {
description:
'Specifies the size of the button, affecting its padding and font size',
options: ['medium', 'small'],
control: { type: 'radio' }
},
to: {
description:
'A URL or path that the link button navigates to when clicked.',
control: { type: 'text' }
}
},
args: {
disabled: false,
loading: false,
size: SizeEnum.Small,
to: '/test-link'
}
}

export default meta
type Story = StoryObj<typeof meta>

export const All: Story = {
render: (args) => (
<div style={{ display: 'flex', gap: '10px' }}>
<LinkButton {...args} variant={LinkButtonVariantEnum.Light}>
Light
</LinkButton>
<LinkButton {...args} variant={LinkButtonVariantEnum.Dark}>
Dark
</LinkButton>
</div>
),
parameters: {
docs: {
description: {
story:
'This story showcases both link button variants in a single row for easy comparison.'
}
}
}
}

export const Light: Story = {
args: {
children: 'Light',
variant: LinkButtonVariantEnum.Light
},
parameters: {
docs: {
description: {
story:
'The "Light" variant is a solid link button with darker background for good contrast in light theme.'
}
}
}
}

export const Dark: Story = {
args: {
children: 'Dark',
variant: LinkButtonVariantEnum.Dark
},
parameters: {
docs: {
description: {
story:
'The "Dark" variant is a darker version of the light button for good contrast in dark theme.'
}
}
}
}
5 changes: 5 additions & 0 deletions src/types/common/enums/common.enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export enum ButtonVariantEnum {
Danger = 'danger'
}

export enum LinkButtonVariantEnum {
Light = 'light',
Dark = 'dark'
}

export enum ButtonTypeEnum {
Submit = 'submit',
Button = 'button'
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/design-system/components/LinkButton/LinkButton.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/react'
import { BrowserRouter as Router } from 'react-router-dom'
import LinkButton from '~scss-components/link-button/LinkButton'
import {
LinkButtonVariantEnum,
SizeEnum
} from '~/types/common/enums/common.enums.ts'

describe('LinkButton Component', () => {
const mockText = 'Link Button'
const mockLink = '/test-link'
const props = {
variant: LinkButtonVariantEnum.Light,
size: SizeEnum.Medium
}

beforeEach(() => {
render(
<Router>
<LinkButton to={mockLink} variant={props.variant} size={props.size}>
{mockText}
</LinkButton>
</Router>
)
})

it('should have correct text', () => {
const linkElement = screen.getByText(mockText)
expect(linkElement).toBeInTheDocument()
})

it('should have correct classes', () => {
const linkElement = screen.getByRole('link')
expect(linkElement).toHaveClass('s2s-link-button')
expect(linkElement).toHaveClass('s2s-link-button_light')
expect(linkElement).toHaveClass('s2s-link-button_medium')
})

it('should have correct link', () => {
const linkElement = screen.getByRole('link')
expect(linkElement).toHaveAttribute('href', mockLink)
})

it('should not have disabled class', () => {
const linkElement = screen.getByRole('link')
expect(linkElement).not.toHaveClass('s2s-link-button_disabled')
})
})

0 comments on commit c91b121

Please sign in to comment.