Skip to content

Commit

Permalink
Feat(web-react): Introduce Item component #DS-1049
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelklibani committed Nov 28, 2023
1 parent 4d98af2 commit be86547
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 5 deletions.
1 change: 1 addition & 0 deletions .storybook/assets/stylesheets/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@forward '../../../packages/web/src/scss/components/FileUploader';
@forward '../../../packages/web/src/scss/components/Grid';
@forward '../../../packages/web/src/scss/components/Header';
@forward '../../../packages/web/src/scss/components/Item';
@forward '../../../packages/web/src/scss/components/Modal';
@forward '../../../packages/web/src/scss/components/Pagination';
@forward '../../../packages/web/src/scss/components/Pill';
Expand Down
2 changes: 1 addition & 1 deletion packages/web-react/config/vite/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import handlebars from 'vite-plugin-handlebars';
import react from '@vitejs/plugin-react';
import { getNestedDirs } from '../../scripts/build';

const hiddenDemoComponents = ['Field', 'Dialog', 'Icon', 'TextFieldBase', 'VisuallyHidden', 'Item'];
const hiddenDemoComponents = ['Field', 'Dialog', 'Icon', 'TextFieldBase', 'VisuallyHidden'];

export default defineConfig({
plugins: [
Expand Down
1 change: 1 addition & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const entryPoints = [
{ dirs: ['components', 'Header'] },
{ dirs: ['components', 'Heading'] },
{ dirs: ['components', 'Icon'] },
{ dirs: ['components', 'Item'] },
{ dirs: ['components', 'Link'] },
{ dirs: ['components', 'Modal'] },
{ dirs: ['components', 'Pagination'] },
Expand Down
37 changes: 37 additions & 0 deletions packages/web-react/src/components/Item/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { ElementType } from 'react';
import classNames from 'classnames';
import { SpiritItemProps } from '../../types';
import { useStyleProps } from '../../hooks';
import { HelperText } from '../Field';
import { Icon } from '../Icon';
import { useItemStyleProps } from './useItemStyleProps';

const Item = <T extends ElementType = 'button'>(props: SpiritItemProps<T>): JSX.Element => {
const { label, elementType: ElementTag = 'button', iconName, helperText, isSelected, ...restProps } = props;
const { classProps, props: modifiedProps } = useItemStyleProps({ isSelected, ...restProps });
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

return (
<ElementTag
{...otherProps}
{...styleProps}
className={classNames(classProps.root, styleProps.className)}
aria-selected={!!isSelected}
>
{iconName && (
<span className={classNames(classProps.icon.root, classProps.icon.start)}>
<Icon name={iconName} />
</span>
)}
<span className={classProps.label}>{label}</span>
<HelperText className={classProps.helperText} elementType="span" helperText={helperText} />
{isSelected && (
<span className={classNames(classProps.icon.root, classProps.icon.end)}>
<Icon name="check-plain" />
</span>
)}
</ElementTag>
);
};

export default Item;
94 changes: 90 additions & 4 deletions packages/web-react/src/components/Item/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,94 @@
# Item

To implement Item component with content of `Radio` or `Checkbox`,
you need to use directly these components with prop `isItem`.
The Item component is used to display a single item in a list. It can be used in Dropdown or similar.

See `web` package [Item documentation][item-documentation] for more info.
To use Item with checkbox or radio please use components [Checkbox][checkbox] or [Radio][radio]
with `isItem` property. We do this to avoid repeating the same code and to simplify the API.

[item-documentation]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web/src/scss/components/Item/README.md
Simple Item example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" />;
```

Item with icon example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" iconName="search" />;
```

Item in selected state example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" isSelected />;
```

Item with Helper text example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" helperText="Helper text" />;
```

Item in disabled state example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" isDisabled />;
```

Item with icon and helper text in selected state example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" iconName="search" helperText="Helper text" isSelected />;
```

Item as a link example:

```jsx
import { Item } from '@lmc-eu/spirit-web-react';

<Item label="Item" elementType="a" href="#" />;
```

Radio as Item:

```jsx
import { Radio } from '@lmc-eu/spirit-web-react';

<Radio id="radioItem" name="example" label="Radio Label" isItem />;
```

Checkbox as Item:

```jsx
import { Checkbox } from '@lmc-eu/spirit-web-react';

<Checkbox id="checkboxItem" name="example" label="Checkbox Label" isItem />;
```

## API

| Name | Type | Default | Required | Description |
| ------------------ | ------------------------- | -------- | -------- | ------------------------------- |
| `elementType` | `ElementType` | `button` || Type of element used as wrapper |
| `helperText` | `string` ||| Custom helper text |
| `iconName` | `string` ||| Icon used in item |
| `isDisabled` | `bool` | `false` || Whether is the item disabled |
| `isSelected` | `bool` | `false` || Whether is the item selected |
| `label` | [`string` \| `ReactNode`] | - || Label of the item |
| `UNSAFE_className` | `string` ||| Wrapper custom class name |
| `UNSAFE_style` | `CSSProperties` ||| Wrapper custom style |

[checkbox]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Checkbox/README.md
[radio]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Radio/README.md
60 changes: 60 additions & 0 deletions packages/web-react/src/components/Item/__tests__/Item.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { SpiritItemProps } from '../../../types';
import Item from '../Item';

describe('Item', () => {
stylePropsTest(Item);

restPropsTest((props: SpiritItemProps) => <Item {...props} />, 'button');

it('should render label', () => {
const label = 'Item label';
const dom = render(<Item label={label} />);

const element = dom.container.querySelector('.Item > .Item__label') as HTMLElement;
expect(element).toHaveTextContent(label);
});

it('should render helperText', () => {
const helperText = 'Helper text';
const dom = render(<Item label="Item label" helperText={helperText} />);

const element = dom.container.querySelector('.Item > .Item__helperText') as HTMLElement;
expect(element).toHaveTextContent(helperText);
});

it('should render icon', () => {
const iconName = 'search';
const dom = render(<Item label="Item label" iconName={iconName} />);

const element = dom.container.querySelector('.Item > .Item__icon') as HTMLElement;
expect(element).toHaveClass('Item__icon--start');
});

it('should be selected and render end icon', () => {
const dom = render(<Item label="Item label" isSelected />);

const element = dom.container.querySelector('.Item') as HTMLElement;
const iconElement = dom.container.querySelector('.Item > .Item__icon') as HTMLElement;
expect(element).toHaveClass('Item--selected');
expect(iconElement).toHaveClass('Item__icon--end');
});

it('should be disabled', () => {
const dom = render(<Item label="Item label" isDisabled />);

const element = dom.container.querySelector('.Item') as HTMLElement;
expect(element).toHaveClass('Item--disabled');
});

it('should render as anchor', () => {
const dom = render(<Item label="Item label" elementType="a" />);

const element = dom.container.querySelector('a') as HTMLElement;
expect(element).toHaveClass('Item');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { renderHook } from '@testing-library/react-hooks';
import { SpiritItemProps } from '../../../types';
import { useItemStyleProps } from '../useItemStyleProps';

describe('useItemStyleProps', () => {
it('should return defaults', () => {
const props = {};
const { result } = renderHook(() => useItemStyleProps(props));

expect(result.current.classProps).toEqual({
helperText: 'Item__helperText',
icon: {
root: 'Item__icon',
start: 'Item__icon--start',
end: 'Item__icon--end',
},
label: 'Item__label',
root: 'Item',
});
});

it('should return selected item', () => {
const props = { isSelected: true } as SpiritItemProps;
const { result } = renderHook(() => useItemStyleProps(props));

expect(result.current.classProps.root).toBe('Item Item--selected');
});

it('should return disabled item', () => {
const props = { isDisabled: true } as SpiritItemProps;
const { result } = renderHook(() => useItemStyleProps(props));

expect(result.current.classProps.root).toBe('Item Item--disabled');
});
});
8 changes: 8 additions & 0 deletions packages/web-react/src/components/Item/demo/CheckboxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import { Checkbox } from '../../Checkbox';

const CheckboxItem = () => (
<Checkbox id="checkboxItemDefault" name="checkboxItemDefault" label="Checkbox Item" isItem />
);

export default CheckboxItem;
12 changes: 12 additions & 0 deletions packages/web-react/src/components/Item/demo/ItemDefault.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import Item from '../Item';

const ItemDefault = () => (
<>
<Item label="Item label" />
<Item elementType="a" label="Item label" href="https://www.example.com/" />
<Item elementType="div" label="Item label" />
</>
);

export default ItemDefault;
13 changes: 13 additions & 0 deletions packages/web-react/src/components/Item/demo/ItemDisabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import Item from '../Item';

const ItemDisabled = () => {
return (
<>
<Item label="Item label" isDisabled />
<Item label="Item label" isDisabled isSelected />
</>
);
};

export default ItemDisabled;
15 changes: 15 additions & 0 deletions packages/web-react/src/components/Item/demo/ItemHelperText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import Item from '../Item';

const ItemHelperText = () => {
return (
<>
<Item label="Item label" helperText="Helper text" />
<Item label="Item label" helperText="Helper text" isSelected />
<Item label="Item label" helperText="Helper text" isDisabled />
<Item label="Item label" helperText="Helper text" isSelected isDisabled />
</>
);
};

export default ItemHelperText;
13 changes: 13 additions & 0 deletions packages/web-react/src/components/Item/demo/ItemIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import Item from '../Item';

const ItemIcon = () => {
return (
<>
<Item label="Item label" iconName="search" />
<Item label="Item label" iconName="search" isSelected />
</>
);
};

export default ItemIcon;
6 changes: 6 additions & 0 deletions packages/web-react/src/components/Item/demo/ItemSelected.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import Item from '../Item';

const ItemSelected = () => <Item label="Item label" isSelected />;

export default ItemSelected;
6 changes: 6 additions & 0 deletions packages/web-react/src/components/Item/demo/RadioItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { Radio } from '../../Radio';

const RadioItem = () => <Radio id="radioItemDefault" isItem label="Radio Item" name="item" />;

export default RadioItem;
42 changes: 42 additions & 0 deletions packages/web-react/src/components/Item/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No declaration file
import icons from '@lmc-eu/spirit-icons/dist/icons';
import DocsSection from '../../../../docs/DocsSections';
import { IconsProvider } from '../../../context';
import ItemDefault from './ItemDefault';
import ItemSelected from './ItemSelected';
import ItemDisabled from './ItemDisabled';
import ItemHelperText from './ItemHelperText';
import ItemIcon from './ItemIcon';
import CheckboxItem from './CheckboxItem';
import RadioItem from './RadioItem';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<IconsProvider value={icons}>
<DocsSection title="Default">
<ItemDefault />
</DocsSection>
<DocsSection title="Selected">
<ItemSelected />
</DocsSection>
<DocsSection title="Disabled">
<ItemDisabled />
</DocsSection>
<DocsSection title="Helper Text">
<ItemHelperText />
</DocsSection>
<DocsSection title="Icon">
<ItemIcon />
</DocsSection>
<DocsSection title="Checkbox Item">
<CheckboxItem />
</DocsSection>
<DocsSection title="Radio Item">
<RadioItem />
</DocsSection>
</IconsProvider>
</React.StrictMode>,
);
1 change: 1 addition & 0 deletions packages/web-react/src/components/Item/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{> demo}}
3 changes: 3 additions & 0 deletions packages/web-react/src/components/Item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Item';
export * from './useItemStyleProps';
export { default as Item } from './Item';
Loading

0 comments on commit be86547

Please sign in to comment.