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: Introduce alignment to Dropdown component #DS-1411 #1837

Open
wants to merge 3 commits into
base: integration/header
Choose a base branch
from
Open
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
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' }` |
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the props intentionally nullable? Do we have it like this in other components with alignment?

| `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 @@ -19,14 +19,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 @@ -35,13 +35,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 @@ -51,16 +51,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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be more efficient via dataProvider and it.each?

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Box component? :D

</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
Loading