Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions packages/lido-app-ui/src/button/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
font-family: var(--lido-app-ui-font-family);
font-weight: var(--lido-app-ui-font-weight-semibold);
letter-spacing: var(--lido-app-ui-letter-spacing-button);
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition:
Expand All @@ -28,7 +29,7 @@
color 0.2s ease;
}

.button:disabled:not(.loading) {
.button:is(:disabled, [aria-disabled='true']):not(.loading) {
--local-color: var(--lido-app-ui-color-text-disabled);
--local-spinner-color: var(--lido-app-ui-color-icons-disabled);

Expand Down Expand Up @@ -110,11 +111,11 @@
--local-spinner-color: var(--lido-app-ui-color-icons-inverse);
}

.primary:not(:disabled):hover {
.primary:not(:disabled, [aria-disabled='true']):hover {
--local-bg: var(--lido-app-ui-color-background-action-hover);
}

.primary:not(:disabled):active {
.primary:not(:disabled, [aria-disabled='true']):active {
--local-bg: var(--lido-app-ui-color-background-action-pressed);
}

Expand All @@ -125,11 +126,11 @@
--local-spinner-color: var(--lido-app-ui-color-icons-secondary);
}

.outline:not(:disabled):hover {
.outline:not(:disabled, [aria-disabled='true']):hover {
--local-bg: var(--lido-app-ui-color-background-transparent-hover);
}

.outline:not(:disabled):active {
.outline:not(:disabled, [aria-disabled='true']):active {
--local-bg: var(--lido-app-ui-color-background-transparent-pressed);
}

Expand All @@ -140,12 +141,12 @@
--local-spinner-color: var(--lido-app-ui-color-icons-tertiary);
}

.subtle:not(:disabled):hover {
.subtle:not(:disabled, [aria-disabled='true']):hover {
--local-bg: var(--lido-app-ui-color-background-transparent-hover);
--local-color: var(--lido-app-ui-color-text-default);
}

.subtle:not(:disabled):active {
.subtle:not(:disabled, [aria-disabled='true']):active {
--local-bg: var(--lido-app-ui-color-background-transparent-pressed);
--local-color: var(--lido-app-ui-color-text-default);
}
Expand All @@ -155,11 +156,11 @@
--local-spinner-color: var(--lido-app-ui-color-icons-secondary);
}

.ghost:not(:disabled):hover {
.ghost:not(:disabled, [aria-disabled='true']):hover {
--local-bg: var(--lido-app-ui-color-background-transparent-hover);
}

.ghost:not(:disabled):active {
.ghost:not(:disabled, [aria-disabled='true']):active {
--local-bg: var(--lido-app-ui-color-background-transparent-pressed);
}

Expand All @@ -170,23 +171,23 @@
--local-spinner-color: var(--lido-app-ui-color-icons-error);
}

.error:not(:disabled):hover {
.error:not(:disabled, [aria-disabled='true']):hover {
--local-bg: var(--lido-app-ui-color-tint-error-hover);
}

.error:not(:disabled):active {
.error:not(:disabled, [aria-disabled='true']):active {
--local-bg: var(--lido-app-ui-color-tint-error);
}

/* Disabled look; plain loading keeps the variant's default look. */

.primary:disabled:not(.loading) {
.primary:is(:disabled, [aria-disabled='true']):not(.loading) {
--local-bg: var(--lido-app-ui-color-background-disabled);
}

.outline:disabled:not(.loading),
.subtle:disabled:not(.loading),
.error:disabled:not(.loading) {
.outline:is(:disabled, [aria-disabled='true']):not(.loading),
.subtle:is(:disabled, [aria-disabled='true']):not(.loading),
.error:is(:disabled, [aria-disabled='true']):not(.loading) {
--local-bg: transparent;
--local-border-color: var(--lido-app-ui-color-border-default);
}
50 changes: 35 additions & 15 deletions packages/lido-app-ui/src/button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Button, type ButtonVariant } from '.'
import { Button, LinkButton, type ButtonVariant } from '.'

import { StoryContainer, StorySection } from '../../.storybook/components'
import { Counter, type CounterVariant } from '../counter'
Expand All @@ -8,12 +8,11 @@ import { IconInfo } from '../icons'
const meta: Meta<typeof Button> = {
title: 'Buttons/Button',
component: Button,
tags: ['autodocs'],
args: {
children: 'Button',
},
argTypes: {
children: { description: 'Button label', control: { type: 'text' } },
children: { control: { type: 'text' } },
variant: {
control: { type: 'select' },
options: ['primary', 'outline', 'subtle', 'ghost', 'error'],
Expand All @@ -26,15 +25,6 @@ const meta: Meta<typeof Button> = {
loading: { control: { type: 'boolean' } },
type: { table: { disable: true } },
},
parameters: {
controls: { exclude: ['type'] },
docs: {
description: {
component:
'Fully rounded call to action. `variant` picks the fill/border treatment, `size` picks big (52px) or small (36px), `loading` swaps the icon slot for a spinner and disables the button.',
},
},
},
}

export default meta
Expand Down Expand Up @@ -166,7 +156,7 @@ export const WithCounter: Story = {
variant={variant}
size={size}
counter={<Counter variant='error'>3</Counter>}
aria-label={`${label}, 3 unread`}
counterLabel='3 unread'
{...state}
>
{label}
Expand All @@ -181,7 +171,7 @@ export const WithCounter: Story = {
key={counterVariant}
variant='outline'
counter={<Counter variant={counterVariant}>3</Counter>}
aria-label={`${counterVariant}, 3 unread`}
counterLabel='3 unread'
>
{counterVariant}
</Button>
Expand All @@ -194,7 +184,37 @@ export const WithCounter: Story = {
docs: {
description: {
story:
'Corner `counter` slot, typically a `Counter`. It is aria-hidden, so include the count in `aria-label`.',
'Corner `counter` slot, typically a `Counter`. It is aria-hidden; `counterLabel` adds the count to the accessible name.',
},
},
},
}

export const AsLink: Story = {
render: () => (
<StoryContainer>
<StorySection title='LinkButton'>
{variants.map((variant) => (
<LinkButton key={variant} variant={variant} href='#section'>
{variant}
</LinkButton>
))}
</StorySection>
<StorySection title='disabled'>
{variants.map((variant) => (
<LinkButton key={variant} variant={variant} href='#section' disabled>
{variant}
</LinkButton>
))}
</StorySection>
</StoryContainer>
),
parameters: {
controls: { disable: true },
docs: {
description: {
story:
'`LinkButton` is the same button as an `<a>`, for navigation: it keeps link semantics and routes through `LinkProvider`. `disabled` drops the `href` and sets `aria-disabled`. It takes `external` and the anchor attributes instead of `type` and `loading`.',
},
},
},
Expand Down
83 changes: 80 additions & 3 deletions packages/lido-app-ui/src/button/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRef } from 'react'
import { describe, expect, it } from 'vitest'

import { Counter } from '../counter'
import counterStyles from '../counter/Counter.module.css'
import { Button } from './Button'

describe('Button', () => {
Expand Down Expand Up @@ -45,6 +46,38 @@ describe('Button', () => {
})
})

describe('Button loading', () => {
it('disables, marks busy and hides icons while loading', () => {
render(
<Button
loading
iconLeft={<i data-testid='left' />}
iconRight={<i data-testid='right' />}
>
A
</Button>,
)
const button = screen.getByRole('button')
expect(button).toBeDisabled()
expect(button).toHaveAttribute('aria-busy', 'true')
expect(screen.queryByTestId('left')).not.toBeInTheDocument()
expect(screen.queryByTestId('right')).not.toBeInTheDocument()
})

it('shows icons when not loading', () => {
render(
<Button
iconLeft={<i data-testid='left' />}
iconRight={<i data-testid='right' />}
>
A
</Button>,
)
expect(screen.getByTestId('left')).toBeInTheDocument()
expect(screen.getByTestId('right')).toBeInTheDocument()
})
})

describe('Button counter', () => {
it('renders the counter inside an aria-hidden wrapper', () => {
render(
Expand All @@ -56,13 +89,13 @@ describe('Button counter', () => {
</Button>,
)
const wrapper = screen.getByTestId('counter')
expect(wrapper.getAttribute('aria-hidden')).toBe('true')
expect(wrapper).toHaveAttribute('aria-hidden', 'true')
expect(wrapper.textContent).toBe('5')
})

it('renders no counter by default', () => {
render(<Button dataTestId={{ counter: 'counter' }}>Inbox</Button>)
expect(screen.queryByTestId('counter')).toBeNull()
expect(screen.queryByTestId('counter')).not.toBeInTheDocument()
})

it('renders no wrapper for counter={false}', () => {
Expand All @@ -71,6 +104,50 @@ describe('Button counter', () => {
Inbox
</Button>,
)
expect(screen.queryByTestId('counter')).toBeNull()
expect(screen.queryByTestId('counter')).not.toBeInTheDocument()
})

it('wraps a numeric counter in a default Counter', () => {
render(
<Button counter={3} dataTestId={{ counter: 'counter' }}>
Inbox
</Button>,
)
const pill = screen.getByTestId('counter').firstElementChild
expect(pill).toHaveTextContent('3')
expect(pill).toHaveClass(counterStyles.counter)
})

it('renders a zero counter as a pill', () => {
render(
<Button counter={0} dataTestId={{ counter: 'counter' }}>
Inbox
</Button>,
)
const pill = screen.getByTestId('counter').firstElementChild
expect(pill).toHaveClass(counterStyles.counter)
expect(pill).toHaveTextContent('0')
})

it('appends counterLabel to the accessible name', () => {
render(
<Button counter={3} counterLabel='3 unread'>
Inbox
</Button>,
)
expect(
screen.getByRole('button', { name: 'Inbox 3 unread' }),
).toBeInTheDocument()
})

it('joins aria-label and counterLabel', () => {
render(
<Button aria-label='Inbox' counterLabel='3 unread'>
X
</Button>,
)
expect(
screen.getByRole('button', { name: 'Inbox, 3 unread' }),
).toBeInTheDocument()
})
})
Loading
Loading