Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SelectableCards): new component thumbnail variant #1445

Merged
merged 10 commits into from
Dec 10, 2024
4 changes: 4 additions & 0 deletions packages/react-components/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export interface AvatarProps extends ComponentCoreProps {
* Displays rim
*/
withRim?: boolean;
/**
* Custom style for the avatar
*/
style?: React.CSSProperties;
}

const baseClass = 'avatar';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Canvas, ArgTypes, Meta, Title } from '@storybook/blocks';

import * as SelectableCardStories from './SelectableCard.stories';
import * as ThumbnailSelectableCard from './components/ThumbnailSelectableCard/ThumbnailSelectableCard.stories';
import * as GallerySelectableCard from './components/GallerySelectableCard/GallerySelectableCard.stories';
import * as InteractiveSelectableCard from './components/InteractiveSelectableCard/InteractiveSelectableCard.stories';

<Meta of={SelectableCardStories} />

<Title>SelectableCard</Title>

[Intro](#Intro) | [SelectableCard](#Default) | [ThumbnailSelectableCard](#Thumbnail) | [GallerySelectableCard](#Gallery) | [InteractiveSelectableCard](#Interactive) | [InteractiveGrid](#InteractiveGrid) | [Component API](#ComponentAPI)

## Intro <a id="Intro" />

`SelectableCard` is an interactive component used to create forms with card-based inputs. It supports radio and checkbox modes, making it ideal for scenarios where you want to provide visually engaging options for users to select.

### Default SelectableCard <a id="Default" />

The `SelectableCard` component is mainly used as a base for other card types ([Thumbnail](#Thumbnail), [Gallery](#Gallery), [Interactive](#Interactive)), containing the main logic responsible for interacting with the card, possibility to set the radio or checkbox type of card, basic styles and keyboard interaction.

<Canvas of={SelectableCardStories.Default} sourceState="none" />

#### Example implementation

```jsx
import { SelectableCard } from '@livechat/design-system-react-components';

<SelectableCard
selectionType="radio"
isSelected
onClick={() => {}}
>
Default accordion content
</SelectableCard>
```

### ThumbnailSelectableCard <a id="Thumbnail" />

The `ThumbnailSelectableCard` is a component built on top of `SelectableCard`, containing its own interface aimed at easily building components for a specific use case.

##### Important

`label` prop is required, hovewer it will not be displayed if `customElement` is provided. (this rule also applies to the `description` and `icon`)

<Canvas of={ThumbnailSelectableCard.RadioTypeExamples} sourceState="none" />

#### Example implementation

```jsx
import { Palette } from '@livechat/design-system-icons';
import { ThumbnailSelectableCard } from '@livechat/design-system-react-components';

<ThumbnailSelectableCard
label="Card label"
selectionType="radio"
description="Card description"
icon={<Icon source={Palette} />}
isSelected
onClick={() => {}}
/>
```


### GallerySelectableCard <a id="Gallery" />

The `GallerySelectableCard` is a component built on top of `SelectableCard`, containing its own interface aimed at easily building components for a specific use case.

##### Important

Since the interface does not assume mandatory props related to the UI for this type of card, you must remember to provide an `icon` or `customElement`.

<Canvas of={GallerySelectableCard.RadioTypeExamples} sourceState="none" />

#### Example implementation

```jsx
import { Palette } from '@livechat/design-system-icons';
import { GallerySelectableCard } from '@livechat/design-system-react-components';

<GallerySelectableCard
label="Card label"
selectionType="radio"
icon={<Icon source={Palette} />}
isSelected
onClick={() => {}}
/>
```

### InteractiveSelectableCard <a id="Interactive" />

The `InteractiveSelectableCard` is a component built on top of `SelectableCard`.
Unlike other components, the interface is simple, and building the content is not limited to interface props.
Content is fully open to your own implementation, but for faster arrangement of elements inside, a small grid system is available in the form of components, called `InteractiveGrid`.

<Canvas of={InteractiveSelectableCard.RadioTypeExamples} sourceState="none" />

#### Example implementation

```jsx
import { Palette } from '@livechat/design-system-icons';
import { InteractiveSelectableCard, GridWrapper, GridCol } from '@livechat/design-system-react-components';

<InteractiveSelectableCard
selectionType="radio"
isSelected
onClick={() => {}}
>
<GridWrapper>
<GridCol>
<div>
<Heading size="xs" className="interactive-heading">
Default custom page event
</Heading>
<Text className="interactive-text">
Goal is achieved when a specific condition set in your website’s
code. This can be filling out a registration form.
</Text>
</div>
</GridCol>
<GridCol>
<div className="interactive-custom-element">
<Icon source={Palette} />
<div>Replace me with Image, animation, video</div>
</div>
</GridCol>
</GridWrapper>
</InteractiveSelectableCard>
```

## InteractiveGrid <a id="InteractiveGrid" />

The grid components were created with the idea of ​​faster arrangement of elements inside the `InteractiveSelectableCard` and are intended exclusively for this. They allow you to quickly create a column layout, or arrange content in rows, all in accordance with the UI assumptions.

The grid components consists of:
- `GridWrapper` - necessary to use other components
- `GridCol` - for column layout, it will move to row layout in smaller screens
- `GridRow` - for row layout

## Component API <a id="ComponentAPI" />

<ArgTypes of={SelectableCardStories.Default} sort="requiredFirst" />


Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
$base-class: 'selectable-card';

.#{$base-class} {
display: flex;
position: relative;
transition:
border-color var(--transition-duration-fast-2) ease-in-out,
box-shadow var(--transition-duration-fast-2) ease-in-out;
border: 1px solid var(--border-basic-secondary);
border-radius: var(--radius-3);
box-shadow: none;
background: var(--surface-primary-default);
width: max-content;

&:hover {
border-color: var(--border-basic-hover);
box-shadow: var(--shadow-float);
cursor: pointer;
}

&:focus-visible {
outline: 0;
box-shadow: var(--shadow-focus);
}

&--thumbnail {
padding: var(--spacing-3) var(--spacing-5);

.#{$base-class}__select-indicator {
margin-right: var(--spacing-2);
height: 21px;
}
}

&--gallery {
padding: var(--spacing-5);

.#{$base-class}__select-indicator {
position: absolute;
top: var(--spacing-3);
left: var(--spacing-3);
}
}

&--interactive {
border-radius: var(--radius-4);
padding: var(--spacing-8);

.#{$base-class}__select-indicator {
position: absolute;
top: var(--spacing-4);
left: var(--spacing-4);
}
}

&--selected,
&--selected:hover {
border-color: var(--action-primary-default);
box-shadow: var(--shadow-float);
}

&__select-indicator {
display: flex;
align-items: center;
height: 16px;

&__checkbox {
height: 16px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { vi } from 'vitest';

import { render, userEvent } from 'test-utils';

import { SelectableCard } from './SelectableCard';
import { ISelectableCardProps } from './types';

const DEFAULT_PROPS = {
selectionType: 'radio' as ISelectableCardProps['selectionType'],
onClick: vi.fn(),
};

const renderComponent = (props: ISelectableCardProps) => {
return render(<SelectableCard {...props} />);
};

describe('<SelectableCard> component', () => {
it('should allow for custom class', () => {
const { container } = renderComponent({
...DEFAULT_PROPS,
className: 'my-class',
});

expect(container.firstChild).toHaveClass('my-class');
});

it('should allow for inline styles', () => {
const { container } = renderComponent({
...DEFAULT_PROPS,
style: { color: '#fff' },
});

expect(container.firstChild).toHaveStyle('color: #fff');
});

it('should render as not selected by default', () => {
const { getByRole } = renderComponent(DEFAULT_PROPS);

expect(getByRole('button')).toHaveAttribute('aria-selected', 'false');
});

it('should render as selected if isSelected is set true', () => {
const { getByRole } = renderComponent({
...DEFAULT_PROPS,
isSelected: true,
});

expect(getByRole('button')).toHaveAttribute('aria-selected', 'true');
});

it('should render card content', () => {
const { getByRole } = renderComponent({
...DEFAULT_PROPS,
children: 'Card content',
});

expect(getByRole('button')).toHaveTextContent('Card content');
});

it('should call onClick when user clicks the card', () => {
const onClick = vi.fn();
const { getByRole } = renderComponent({
...DEFAULT_PROPS,
onClick,
});

userEvent.click(getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});

it('should render as radio type', () => {
const { getByRole } = renderComponent({
...DEFAULT_PROPS,
});

expect(getByRole('radio')).toBeInTheDocument();
});

it('should render as checkbox type', () => {
const { getByRole } = renderComponent({
...DEFAULT_PROPS,
selectionType: 'checkbox',
});

expect(getByRole('checkbox')).toBeInTheDocument();
});

it('should allow to focus by tab press and call onClick on keyboard interaction', () => {
const onClick = vi.fn();
renderComponent({
...DEFAULT_PROPS,
onClick,
});

userEvent.tab();
userEvent.keyboard('{enter}');
expect(onClick).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.base-custom-element {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed var(--border-default);
border-radius: var(--radius-1);
background-color: var(--surface-basic-subtle);
color: var(--content-subtle);
}

.thumbnail-custom-element {
padding: var(--spacing-2);
font-size: 12px;
}

.gallery-custom-element {
padding: var(--spacing-2);
width: 100px;
height: 100px;
text-align: center;
font-size: 12px;
}

.interactive-heading {
margin: 0 0 var(--spacing-2);
}

.interactive-text {
margin: 0;
}

.interactive-custom-element {
padding: var(--spacing-8);
width: 100%;
height: 100%;
text-align: center;
font-size: 14px;
}
Loading
Loading