Skip to content

Commit

Permalink
Feat(web-react): Introduce alignment to Dropdown component #DS-1411
Browse files Browse the repository at this point in the history
  • Loading branch information
crishpeen committed Jan 8, 2025
1 parent 56c7ebc commit 1999ec5
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 32 deletions.
58 changes: 42 additions & 16 deletions packages/web-react/src/components/Dropdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,43 @@ import { UncontrolledDropdown, DropdownTrigger, DropdownPopover } from '@lmc-eu/

### Dropdown

| Name | Type | Default | Required | Description |
| ----------------- | -------------------------------------------------- | -------------- | -------- | ---------------------------------------------- |
| `enableAutoClose` | `bool` | `true` || Enables close on click outside of Dropdown |
| `fullWidthMode` | [`DropdownFullWidthMode`][dropdown-fullwidth-mode] | `off` || Full-width mode |
| `id` | `string` ||| Component id |
| `isOpen` | `bool` | `false` || Open state |
| `onAutoClose` | `(event: Event) => void` ||| Callback on close on click outside of Dropdown |
| `onToggle` | `() => void` ||| Function for toggle open state of dropdown |
| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` || Alignment of the component |
| Name | Type | Default | Required | Description |
| ----------------- | --------------------------------------------------------------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `alignmentX` | \[ [AlignmentXExtended dictionary][dictionary-alignment] \| `object`] | `null` || Apply vertical alignment to trigger, use object to set responsive values, e.g. `{ mobile: 'left', tablet: 'center', desktop: 'right' }` |
| `alignmentY` | \[ [AlignmentYExtended dictionary][dictionary-alignment] \| `object`] | `null` || Apply horizontal alignment to trigger, use object to set responsive values, e.g. `{ mobile: 'top', tablet: 'center', desktop: 'bottom' }` |
| `enableAutoClose` | `bool` | `true` || Enables close on click outside of Dropdown |
| `fullWidthMode` | [`DropdownFullWidthMode`][dropdown-fullwidth-mode] | `off` || Full-width mode |
| `id` | `string` ||| Component id |
| `isOpen` | `bool` | `false` || Open state |
| `onAutoClose` | `(event: Event) => void` ||| Callback on close on click outside of Dropdown |
| `onToggle` | `() => void` ||| Function for toggle open state of dropdown |
| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` || Alignment of the component |

On top of the API options, the components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
and [escape hatches][readme-escape-hatches].

#### Alignment

Dropdown supports the extended [Alignment Dictionary][dictionary-alignment] for alignment on both axis. To use it, set the
specific prop to the `Dropdown` component, e.g. `<Dropdown alignmentX="right" />` or `<Dropdown alignmentY="stretch" />`. Adding
any of these props will make the element display as `flex`.

We also support responsive alignment props. To use them, set the prop as an object,
e.g. `<Dropdown alignmentX={{ mobile: 'right', tablet: 'left', desktop: 'center' }} />`.

ℹ️ This controls only the alignment inside the wrapping `Dropdown` element. And even with alignment, the popover will still be positioned
at edge of the `Dropdown` element and on the place defined by the placement attribute.

```jsx
<Dropdown alignmentX={{ mobile: 'right', tablet: 'left', desktop: 'center' }} alignmentY="center" id="#dropdown-alignment">
<DropdownTrigger elementType={Button}>Button as anchor</DropdownTrigger>
<DropdownPopover>
<!-- ... -->
</DropdownPopover>
</Dropdown>
```

### DropdownTrigger

| Name | Type | Default | Required | Description |
Expand All @@ -89,18 +112,21 @@ and [escape hatches][readme-escape-hatches].

### UncontrolledDropdown

| Name | Type | Default | Required | Description |
| ----------------- | -------------------------------------------------- | -------------- | -------- | ---------------------------------------------- |
| `enableAutoClose` | `bool` | `true` || Enables close on click outside of Dropdown |
| `fullWidthMode` | [`DropdownFullWidthMode`][dropdown-fullwidth-mode] | `off` || Full-width mode |
| `id` | `string` | `<random>` || Component id |
| `onAutoClose` | `(event: Event) => void` ||| Callback on close on click outside of Dropdown |
| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` || Alignment of the component |
| Name | Type | Default | Required | Description |
| ----------------- | --------------------------------------------------------------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `alignmentX` | \[ [AlignmentXExtended dictionary][dictionary-alignment] \| `object`] | `null` || Apply vertical alignment to trigger, use object to set responsive values, e.g. `{ mobile: 'left', tablet: 'center', desktop: 'right' }` |
| `alignmentY` | \[ [AlignmentYExtended dictionary][dictionary-alignment] \| `object`] | `null` || Apply horizontal alignment to trigger, use object to set responsive values, e.g. `{ mobile: 'top', tablet: 'center', desktop: 'bottom' }` |
| `enableAutoClose` | `bool` | `true` || Enables close on click outside of Dropdown |
| `fullWidthMode` | [`DropdownFullWidthMode`][dropdown-fullwidth-mode] | `off` || Full-width mode |
| `id` | `string` | `<random>` || Component id |
| `onAutoClose` | `(event: Event) => void` ||| Callback on close on click outside of Dropdown |
| `placement` | [Placement dictionary][dictionary-placement] | `bottom-start` || Alignment of the component |

On top of the API options, the components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
and [escape hatches][readme-escape-hatches].

[dictionary-alignment]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#alignment
[dictionary-placement]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#placement
[dropdown-fullwidth-mode]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/types/dropdown.ts#L19
[item]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Item/README.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ describe('Dropdown', () => {
restPropsTest(Dropdown, '.Dropdown');

it('should render text children', () => {
const dom = render(
render(
<Dropdown id="dropdown" isOpen={false} onToggle={() => {}}>
<DropdownTrigger>Trigger</DropdownTrigger>
<DropdownPopover>Hello World</DropdownPopover>
<DropdownPopover data-testid="dropdown-popover">Hello World</DropdownPopover>
</Dropdown>,
);
const trigger = screen.getByRole('button');
const element = dom.container.querySelector('.DropdownPopover') as HTMLElement;
const element = screen.getByTestId('dropdown-popover') as HTMLElement;

expect(trigger).toHaveTextContent('Trigger');
expect(element).toHaveTextContent('Hello World');
Expand All @@ -37,13 +37,13 @@ describe('Dropdown', () => {
it('should be opened', () => {
const onToggle = jest.fn();

const dom = render(
render(
<Dropdown id="dropdown" isOpen onToggle={onToggle}>
<DropdownTrigger>trigger</DropdownTrigger>
<DropdownPopover>Hello World</DropdownPopover>
<DropdownPopover data-testid="dropdown-popover">Hello World</DropdownPopover>
</Dropdown>,
);
const element = dom.container.querySelector('.DropdownPopover') as HTMLElement;
const element = screen.getByTestId('dropdown-popover') as HTMLElement;
const trigger = screen.getByRole('button');

expect(element).toHaveClass('is-open');
Expand All @@ -53,16 +53,86 @@ describe('Dropdown', () => {
it('should call toggle function', () => {
const onToggle = jest.fn();

const dom = render(
render(
<Dropdown id="dropdown" isOpen={false} onToggle={onToggle}>
<DropdownTrigger>trigger</DropdownTrigger>
<DropdownPopover>Hello World</DropdownPopover>
</Dropdown>,
);
const trigger = dom.container.querySelector('button') as HTMLElement;
const trigger = screen.getByRole('button') as HTMLElement;

fireEvent.click(trigger);

expect(onToggle).toHaveBeenCalled();
});

it('should render with horizontal alignment', () => {
render(<Dropdown alignmentX="center" data-testid="dropdown" id="dropdown" isOpen={false} onToggle={() => {}} />);

expect(screen.getByTestId('dropdown')).toHaveClass('Dropdown--alignmentXCenter');
});

it('should render with horizontal and vertical alignment', () => {
render(
<Dropdown
alignmentX="center"
alignmentY="center"
data-testid="dropdown"
id="dropdown"
isOpen={false}
onToggle={() => {}}
/>,
);

expect(screen.getByTestId('dropdown')).toHaveClass('Dropdown--alignmentXCenter Dropdown--alignmentYCenter');
});

it('should render with some responsive horizontal alignments', () => {
render(
<Dropdown
alignmentX={{ tablet: 'center', desktop: 'right' }}
data-testid="dropdown"
id="dropdown"
isOpen={false}
onToggle={() => {}}
/>,
);

expect(screen.getByTestId('dropdown')).toHaveClass(
'Dropdown Dropdown--tablet--alignmentXCenter Dropdown--desktop--alignmentXRight',
);
});

it('should render with all responsive horizontal alignments', () => {
render(
<Dropdown
alignmentX={{ mobile: 'left', tablet: 'center', desktop: 'right' }}
data-testid="dropdown"
id="dropdown"
isOpen={false}
onToggle={() => {}}
/>,
);

expect(screen.getByTestId('dropdown')).toHaveClass(
'Dropdown Dropdown--alignmentXLeft Dropdown--tablet--alignmentXCenter Dropdown--desktop--alignmentXRight',
);
});

it('should render with responsive horizontal and vertical alignment', () => {
render(
<Dropdown
alignmentX={{ mobile: 'left', tablet: 'center', desktop: 'right' }}
alignmentY={{ mobile: 'top', tablet: 'center', desktop: 'bottom' }}
data-testid="dropdown"
id="dropdown"
isOpen={false}
onToggle={() => {}}
/>,
);

expect(screen.getByTestId('dropdown')).toHaveClass(
'Dropdown Dropdown--alignmentXLeft Dropdown--tablet--alignmentXCenter Dropdown--desktop--alignmentXRight Dropdown--alignmentYTop Dropdown--tablet--alignmentYCenter Dropdown--desktop--alignmentYBottom',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderHook } from '@testing-library/react';
import { SpiritDropdownProps } from '../../../types';
import { useDropdownStyleProps } from '../useDropdownStyleProps';

describe('useDropdownStyleProps', () => {
Expand Down Expand Up @@ -29,4 +30,31 @@ describe('useDropdownStyleProps', () => {
expect(result.current.classProps.popover).toBe('DropdownPopover');
expect(result.current.props).toEqual({ transferProp: 'test' });
});

it.each([
// alignmentX, alignmentY, expectedClasses
[undefined, undefined, 'Dropdown'],
['left', undefined, 'Dropdown Dropdown--alignmentXLeft'],
['left', 'top', 'Dropdown Dropdown--alignmentXLeft Dropdown--alignmentYTop'],
[
{ mobile: 'left', tablet: 'center', desktop: 'right' },
undefined,
'Dropdown Dropdown--alignmentXLeft Dropdown--tablet--alignmentXCenter Dropdown--desktop--alignmentXRight',
],
[
{ mobile: 'left', tablet: 'center', desktop: 'right' },
{ mobile: 'top', tablet: 'center', desktop: 'bottom' },
'Dropdown Dropdown--alignmentXLeft Dropdown--tablet--alignmentXCenter Dropdown--desktop--alignmentXRight Dropdown--alignmentYTop Dropdown--tablet--alignmentYCenter Dropdown--desktop--alignmentYBottom',
],
[
'left',
{ mobile: 'top', tablet: 'center', desktop: 'bottom' },
'Dropdown Dropdown--alignmentXLeft Dropdown--alignmentYTop Dropdown--tablet--alignmentYCenter Dropdown--desktop--alignmentYBottom',
],
])('should return alignment CSS classes', (alignmentX, alignmentY, expectedClasses) => {
const props: SpiritDropdownProps = { alignmentX, alignmentY } as SpiritDropdownProps;
const { result } = renderHook(() => useDropdownStyleProps(props));

expect(result.current.classProps.root).toBe(expectedClasses);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { Button } from '../../Button';
import { Grid } from '../../Grid';
import { Item } from '../../Item';
import Dropdown from '../Dropdown';
import DropdownPopover from '../DropdownPopover';
import DropdownTrigger from '../DropdownTrigger';

const DropdownAlignment = () => {
const [isOpen, setIsOpen] = React.useState(false);
const onToggle = () => setIsOpen(!isOpen);

return (
<Grid cols={2}>
<Dropdown
alignmentX={{ mobile: 'right', tablet: 'left', desktop: 'center' }}
alignmentY="center"
id="dropdown-alignment"
isOpen={isOpen}
onToggle={onToggle}
placement="top-start"
>
<DropdownTrigger elementType={Button}>Button as anchor</DropdownTrigger>
<DropdownPopover>
<Item elementType="a" href="#" label="Action" />
<Item elementType="a" href="#" label="Another action" />
<Item elementType="a" href="#" label="Something else here" />
</DropdownPopover>
</Dropdown>
<div className="px-800 py-1700 bg-tertiary text-center">This a big unrelated box</div>
</Grid>
);
};

export default DropdownAlignment;
4 changes: 4 additions & 0 deletions packages/web-react/src/components/Dropdown/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import DocsSection from '../../../../docs/DocsSections';
import { IconsProvider } from '../../../context';
import DropdownAlignment from './DropdownAlignment';
import DropdownDisabledAutoclose from './DropdownDisabledAutoclose';
import DropdownFullwidthAll from './DropdownFullwidthAll';
import DropdownFullwidthMobileOnly from './DropdownFullwidthMobileOnly';
Expand Down Expand Up @@ -35,6 +36,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<DocsSection title="Full-width mode 'mobile-only'">
<DropdownFullwidthMobileOnly />
</DocsSection>
<DocsSection title="Alignment" stackAlignment="stretch">
<DropdownAlignment />
</DocsSection>
</IconsProvider>
</React.StrictMode>,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Markdown } from '@storybook/blocks';
import type { Meta, StoryObj } from '@storybook/react';
import React, { useState } from 'react';
import { Button, Icon, Text } from '../..';
import { Placements } from '../../../constants';
import { AlignmentXExtended, AlignmentYExtended, Placements } from '../../../constants';
import { DropdownFullWidthModes, SpiritDropdownProps } from '../../../types';
import ReadMe from '../README.md';
import { Dropdown, DropdownTrigger, DropdownPopover } from '..';
Expand All @@ -17,6 +17,20 @@ const meta: Meta<typeof Dropdown> = {
layout: 'centered',
},
argTypes: {
alignmentX: {
control: 'select',
options: [undefined, ...Object.values(AlignmentXExtended)],
table: {
defaultValue: { summary: undefined },
},
},
alignmentY: {
control: 'select',
options: [undefined, ...Object.values(AlignmentYExtended)],
table: {
defaultValue: { summary: undefined },
},
},
children: {
control: 'object',
},
Expand Down Expand Up @@ -45,6 +59,8 @@ const meta: Meta<typeof Dropdown> = {
},
},
args: {
alignmentX: undefined,
alignmentY: undefined,
children: (
<>
<a href="#info" className="d-flex mb-400">
Expand Down
Loading

0 comments on commit 1999ec5

Please sign in to comment.