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

MOON-338: Add component CardSelector #460

Merged
merged 13 commits into from
Dec 6, 2024
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
4 changes: 4 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ module.exports = {
postcss: false,
},

typescript: {
reactDocgen: "react-docgen-typescript",
},

stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],

addons: [
Expand Down
Empty file.
90 changes: 90 additions & 0 deletions src/components/CardSelector/CardSelector.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.moonstone-cardSelector {
width: 100%;
padding: var(--spacing-small);

border: 1px solid var(--color-gray40);
border-radius: 2px;
background-color: var(--color-white);
cursor: pointer;

transition: all 0.2s ease-in-out;

&:hover {
border-color: var(--color-gray60);
box-shadow: 0 1px 6px var(--color-dark20);
}

&:focus {
border-color: var(--color-accent);
}
}

.moonstone-cardSelector_disabled {
opacity: 60%;

pointer-events: none;
}

.moonstone-cardSelector_dragIcon {
margin-left: calc(-1 * var(--spacing-small));
}

.moonstone-cardSelector_body {
overflow: hidden;
}

.moonstone-cardSelector_body,
.moonstone-cardSelector_actions {
gap: var(--spacing-small);

& > * {
gap: var(--spacing-small);
}
}

.moonstone-cardSelector_systemName,
.moonstone-cardSelector_information {
color: var(--color-dark_plain60);
}

.moonstone-cardSelector_thumbnail {
width: 60px;
min-width: 60px;
height: 60px;

margin-right: var(--spacing-small);
overflow: hidden;

border-radius: 2px;
background-color: var(--color-gray_light40);
}

.moonstone-cardSelector_thumbnail_preview {
width: 100%;
height: inherit;
object-fit: cover;
}

.moonstone-cardSelector_thumbnail_icon {
width: 32px;
height: 32px;
}

.moonstone-cardSelector_actions {
margin-left: auto;
}

.moonstone-cardSelector_error {
gap: var(--spacing-nano);
width: 100%;
min-height: 78px;
padding: var(--spacing-medium) 0;

color: var(--color-warning);

border: 1px solid var(--color-warning);
border-radius: 2px;
background-color: var(--color-white);

pointer-events: none;
}
98 changes: 98 additions & 0 deletions src/components/CardSelector/CardSelector.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {Chip} from '~/main';

import {CardSelector} from './index';

describe('CardSelector', () => {
it('should display additional class names', () => {
render(<CardSelector data-testid="card-selector" className="extra"/>);
expect(screen.getByTestId('card-selector')).toHaveClass('extra');
});

it('should display displayName', () => {
render(<CardSelector displayName="this displayName"/>);
expect(screen.queryByText('this displayName')).toBeInTheDocument();
});

it('should display systemName', () => {
render(<CardSelector systemName="this systemName"/>);
expect(screen.queryByText('(this systemName)')).toBeInTheDocument();
});

it('should display information', () => {
render(<CardSelector information="this information"/>);
expect(screen.queryByText('this information')).toBeInTheDocument();
});

it('should display the image with thumbnailURL', () => {
render(<CardSelector thumbnailURL="thumbnail.png"/>);
expect(screen.getByRole('img', {src: 'thumbnail.png'})).toBeInTheDocument();
});

it('should display img as icon when thumbnailType is icon', () => {
const {container} = render(<CardSelector thumbnailType="icon" thumbnailURL="thumbnail.png"/>);
expect(container.querySelector('.moonstone-cardSelector_thumbnail_icon')).toBeInTheDocument();
});

it('should display img as img when thumbnailType is preview', () => {
const {container} = render(<CardSelector thumbnailType="preview" thumbnailURL="thumbnail.png"/>);
expect(container.querySelector('.moonstone-cardSelector_thumbnail_preview')).toBeInTheDocument();
});

it('should use thumbnailAlt as img alt attribute', () => {
render(<CardSelector thumbnailAlt="thumbnail-alt" thumbnailURL="thumbnail.png"/>);
expect(screen.getByRole('img', {alt: 'thumbnail-alt'})).toBeInTheDocument();
});

it('should have attribute draggable when isDraggable', () => {
render(<CardSelector isDraggable data-testid="card-selector"/>);
expect(screen.getByTestId('card-selector')).toHaveAttribute('draggable', 'true');
});

it('should display chips', () => {
render(<CardSelector chips={[<Chip key="chip" label="chip"/>]}/>);
expect(screen.queryByText('chip')).toBeInTheDocument();
});

it('should display cardActions', () => {
render(<CardSelector cardAction={<Chip key="chip" label="action"/>}/>);
expect(screen.queryByText('action')).toBeInTheDocument();
});

it('should call onClick when clicked', async () => {
const user = userEvent.setup();
const onClick = jest.fn();

render(<CardSelector data-testid="card-selector" onClick={onClick}/>);
await user.click(screen.getByTestId('card-selector'));

expect(onClick).toHaveBeenCalled();
});

it('should be disabled', () => {
render(<CardSelector isDisabled data-testid="card-selector"/>);
expect(screen.getByTestId('card-selector')).toHaveClass('moonstone-cardSelector_disabled');
});

it('should be disabled when isReadOnly', () => {
render(<CardSelector isReadOnly data-testid="card-selector"/>);
expect(screen.getByTestId('card-selector')).toHaveClass('moonstone-cardSelector_disabled');
});

it('should not call onClick when disabled', async () => {
const user = userEvent.setup();
const onClick = jest.fn();

render(<CardSelector isDisabled data-testid="card-selector" onClick={onClick}/>);
await user.click(screen.getByTestId('card-selector'));

expect(onClick).not.toHaveBeenCalled();
});

it('should display errorCardSelector if hasError', () => {
render(<CardSelector hasError data-testid="card-selector"/>);
expect(screen.getByTestId('card-selector')).toHaveClass('moonstone-cardSelector_error');
});
});
77 changes: 77 additions & 0 deletions src/components/CardSelector/CardSelector.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import {StoryObj, Meta} from '@storybook/react';

import {CardSelector} from './index';
import {Button, Chip} from '~/main';
import {Close, FileImage, Lock} from '~/icons';
import type {CardSelectorProps} from './CardSelector.types';

const meta: Meta<typeof CardSelector> = {
title: 'Components/CardSelector',
component: CardSelector,

parameters: {
layout: 'padded',
actions: {argTypesRegex: '^on.*'}
}
};
export default meta;

type Story = StoryObj<typeof CardSelector>;
const Template = (args: CardSelectorProps) => {
return <div style={{maxWidth: '100vw'}}><CardSelector {...args}/></div>;
};

export const Default: Story = {
args: {
id: 'cardSelector',
displayName: 'Item name',
systemName: 'system name'
},
render: Template
};

export const Image: Story = {
args: {
...Default.args,
thumbnailURL: 'https://picsum.photos/100/300',
thumbnailAlt: 'preview-img',
thumbnailType: 'preview',
information: 'more information',
chips: [<Chip key="chip" label="image" icon={<FileImage/>} color="accent"/>, <Chip key="chip2" label="marked for deletion" icon={<Lock/>} color="danger"/>]
},
render: Template
};

export const Icon: Story = {
args: {
...Image.args,
thumbnailURL: 'http://www.google.com/s2/favicons?domain=www.jahia.com',
thumbnailType: 'icon'
},
render: Template
};

export const Actions: Story = {
args: {
...Image.args,
cardAction: <Button key="btn" variant="ghost" icon={<Close/>}/>
},
render: Template
};

export const NoChips: Story = {
args: {
...Image.args,
chips: null
},
render: Template
};

export const Error: Story = {
args: {
hasError: true,
errorMessage: 'Broken reference'
},
render: Template
};
Loading
Loading