-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dropwdown): added Dropdown component (#570)
* feat(dropdown): first dropdown component implementation * refactor: changed fn name * refactor: fixed newline * docs: better props * feat: danger ui * refactor: removed comment * refactor: extracted eventAdapter * refactor: innernode oneliner * rem: unused style * docs: danger doc * fix: danger ui * refactor: module css new line
- Loading branch information
1 parent
7aa0552
commit a3e3df4
Showing
9 changed files
with
593 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* Copyright 2024 Mia srl | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import type { Meta, StoryObj } from '@storybook/react' | ||
import { action } from '@storybook/addon-actions' | ||
|
||
import { DropdownProps, DropdownTrigger } from './props' | ||
import { Button } from '../Button' | ||
import { Dropdown } from '.' | ||
|
||
const defaults: Partial<DropdownProps> = { | ||
items: [{ | ||
id: 'id1', | ||
label: 'value 1', | ||
}, { | ||
id: 'id2', | ||
label: 'value 2', | ||
secondaryLabel: 'Some additional info 2', | ||
}, { | ||
id: 'id3', | ||
label: 'I am danger!', | ||
danger: true, | ||
secondaryLabel: 'Some additional info 3', | ||
}], | ||
triggers: [DropdownTrigger.Click], | ||
children: <Button >{'click me'}</Button>, | ||
onClick: action('on click'), | ||
} | ||
|
||
const meta = { | ||
component: Dropdown, | ||
args: defaults, | ||
argTypes: { | ||
children: { control: false }, | ||
}, | ||
render: (_, { args }) => <Dropdown {...args} />, | ||
} satisfies Meta<typeof Dropdown> | ||
|
||
type Story = StoryObj<typeof meta> | ||
|
||
export default meta | ||
|
||
export const BasicExample: Story = {} | ||
|
||
export const Disabled: Story = { | ||
args: { | ||
isDisabled: true, | ||
}, | ||
} | ||
|
||
export const ContextMenuTrigger: Story = { | ||
args: { | ||
children: <span>{'right-click on me'}</span>, | ||
triggers: [DropdownTrigger.ContextMenu], | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/** | ||
* Copyright 2024 Mia srl | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { DropdownItem, DropdownProps, DropdownTrigger } from './props' | ||
import { RenderResult, render, screen, userEvent, waitFor } from '../../test-utils' | ||
import { Button } from '../Button' | ||
import { Dropdown } from './Dropdown' | ||
|
||
const items: DropdownItem[] = [ | ||
{ id: '1', label: 'Label 1' }, | ||
{ id: '2', label: 'Label 2', secondaryLabel: 'Additional Info 2' }, | ||
{ id: '3', label: 'Danger Label', secondaryLabel: 'Additional Info 2', danger: true }, | ||
] | ||
const defaultProps: DropdownProps = { | ||
items, | ||
children: <Button>{'test-trigger-button'}</Button>, | ||
onClick: jest.fn(), | ||
} | ||
|
||
describe('Dropdown Component', () => { | ||
beforeEach(() => jest.clearAllMocks()) | ||
|
||
describe('triggers', () => { | ||
it('opens dropdown on click', async() => { | ||
renderDropdown() | ||
const button = screen.getByText('test-trigger-button') | ||
userEvent.click(button) | ||
await screen.findByRole('menuitem', { name: 'Label 1' }) | ||
}) | ||
|
||
it('opens dropdown on hover', async() => { | ||
renderDropdown({ props: { ...defaultProps, triggers: [DropdownTrigger.Hover] } }) | ||
const button = screen.getByText('test-trigger-button') | ||
userEvent.hover(button) | ||
await screen.findByRole('menuitem', { name: 'Label 1' }) | ||
}) | ||
}) | ||
|
||
describe('label layouts', () => { | ||
it('render labels', async() => { | ||
renderDropdown({ props: { ...defaultProps, items } }) | ||
const button = screen.getByText('test-trigger-button') | ||
userEvent.click(button) | ||
|
||
await screen.findByRole('menuitem', { name: 'Label 1' }) | ||
|
||
expect(screen.getAllByRole('menuitem')).toHaveLength(3) | ||
|
||
const [first, second, third] = screen.getAllByRole('menuitem') | ||
expect(first).toMatchSnapshot() | ||
expect(second).toMatchSnapshot() | ||
expect(third).toMatchSnapshot() | ||
}) | ||
}) | ||
|
||
describe('onClick', () => { | ||
it('invokes onClick with correct id', async() => { | ||
const onClick = jest.fn() | ||
const props = { | ||
...defaultProps, | ||
onClick, | ||
} | ||
renderDropdown({ props }) | ||
const button = screen.getByText('test-trigger-button') | ||
userEvent.click(button) | ||
|
||
const item = await screen.findByRole('menuitem', { name: 'Label 1' }) | ||
userEvent.click(item) | ||
|
||
await waitFor(() => expect(onClick).toHaveBeenCalled()) | ||
expect(onClick).toHaveBeenCalledTimes(1) | ||
const [[invocation]] = onClick.mock.calls | ||
expect(invocation.id).toEqual('1') | ||
expect(invocation.selectedPath).toEqual(['1']) | ||
expect(invocation.item).toEqual(items[0]) | ||
}) | ||
}) | ||
}) | ||
|
||
function renderDropdown( | ||
{ props }: {props: DropdownProps} = { props: defaultProps } | ||
): RenderResult { | ||
return render(<Dropdown {...props} />) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/** | ||
* Copyright 2024 Mia srl | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { Dropdown as AntdDropdown, type MenuProps as AntdMenuProps } from 'antd' | ||
import React, { ReactElement, ReactNode, useCallback, useMemo } from 'react' | ||
|
||
import { DropdownClickEvent, DropdownItem, DropdownProps, DropdownTrigger } from './props' | ||
import Label from './components/Label/Label' | ||
import styles from './dropdown.module.css' | ||
|
||
type ArrayElement<ArrayType extends readonly unknown[] | undefined> = | ||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never; | ||
|
||
type AntdMenuItems = AntdMenuProps['items'] | ||
type AntdMenuItem = ArrayElement<AntdMenuItems> | ||
|
||
type AntdMenuClickEvent = { | ||
key: string, | ||
keyPath: string[], | ||
domEvent: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement> | ||
} | ||
|
||
export const defaults = { | ||
trigger: [DropdownTrigger.Click], | ||
} | ||
|
||
const itemMatcher = (id: string) => (item: DropdownItem): boolean => item.id === id | ||
|
||
export const Dropdown = ({ | ||
autoFocus, | ||
children, | ||
isDisabled, | ||
items, | ||
onClick, | ||
triggers, | ||
}: DropdownProps): ReactElement => { | ||
const findItem = useCallback((id: string) => items.find(itemMatcher(id)), [items]) | ||
|
||
const antdItems = useMemo<AntdMenuItems>(() => itemsAdapter(items), [items]) | ||
const innerNode = useMemo(() => (children ? <span>{children}</span> : null), [children]) | ||
|
||
const onAntdMenuClick = useCallback( | ||
(antdEvent: AntdMenuClickEvent) => onClick(eventAdapter(antdEvent, findItem)), | ||
[findItem, onClick] | ||
) | ||
|
||
const dropdownRender = useCallback((menu: ReactNode): ReactNode => { | ||
return React.cloneElement(menu as ReactElement) | ||
}, []) | ||
|
||
const menu = useMemo(() => ({ | ||
items: antdItems, | ||
onClick: onAntdMenuClick, | ||
}), [antdItems, onAntdMenuClick]) | ||
|
||
return ( | ||
<AntdDropdown | ||
autoFocus={autoFocus} | ||
disabled={isDisabled} | ||
dropdownRender={dropdownRender} | ||
menu={menu} | ||
overlayClassName={styles.dropdownWrapper} | ||
trigger={triggers} | ||
> | ||
{innerNode} | ||
</AntdDropdown> | ||
) | ||
} | ||
|
||
Dropdown.Trigger = DropdownTrigger | ||
|
||
function itemsAdapter(items: DropdownItem[]): AntdMenuItems { | ||
return items.map<AntdMenuItem>((item: DropdownItem) => ({ | ||
label: <Label {...item} />, | ||
key: item.id, | ||
danger: item.danger, | ||
})) | ||
} | ||
|
||
function eventAdapter( | ||
event: AntdMenuClickEvent, | ||
itemFinder: (id: string) => DropdownItem|undefined, | ||
): DropdownClickEvent { | ||
return { | ||
id: event.key, | ||
selectedPath: event.keyPath, | ||
domEvent: event.domEvent, | ||
item: itemFinder(event.key), | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`Dropdown Component label layouts render labels 1`] = ` | ||
<li | ||
class="mia-platform-dropdown-menu-item mia-platform-dropdown-menu-item-only-child" | ||
data-menu-id="rc-menu-uuid-test-1" | ||
role="menuitem" | ||
tabindex="-1" | ||
> | ||
<span | ||
class="mia-platform-dropdown-menu-title-content" | ||
> | ||
<div | ||
class="labelContainer" | ||
> | ||
<span | ||
class="primaryLabel" | ||
> | ||
Label 1 | ||
</span> | ||
</div> | ||
</span> | ||
</li> | ||
`; | ||
|
||
exports[`Dropdown Component label layouts render labels 2`] = ` | ||
<li | ||
class="mia-platform-dropdown-menu-item mia-platform-dropdown-menu-item-only-child" | ||
data-menu-id="rc-menu-uuid-test-2" | ||
role="menuitem" | ||
tabindex="-1" | ||
> | ||
<span | ||
class="mia-platform-dropdown-menu-title-content" | ||
> | ||
<div | ||
class="labelContainer" | ||
> | ||
<span | ||
class="primaryLabel" | ||
> | ||
Label 2 | ||
</span> | ||
<span | ||
class="secondaryLabel" | ||
> | ||
· | ||
</span> | ||
<span | ||
class="secondaryLabel" | ||
> | ||
Additional Info 2 | ||
</span> | ||
</div> | ||
</span> | ||
</li> | ||
`; | ||
|
||
exports[`Dropdown Component label layouts render labels 3`] = ` | ||
<li | ||
class="mia-platform-dropdown-menu-item mia-platform-dropdown-menu-item-danger mia-platform-dropdown-menu-item-only-child" | ||
data-menu-id="rc-menu-uuid-test-3" | ||
role="menuitem" | ||
tabindex="-1" | ||
> | ||
<span | ||
class="mia-platform-dropdown-menu-title-content" | ||
> | ||
<div | ||
class="labelContainer" | ||
> | ||
<span | ||
class="primaryLabel danger" | ||
> | ||
Danger Label | ||
</span> | ||
<span | ||
class="secondaryLabel danger" | ||
> | ||
· | ||
</span> | ||
<span | ||
class="secondaryLabel danger" | ||
> | ||
Additional Info 2 | ||
</span> | ||
</div> | ||
</span> | ||
</li> | ||
`; |
Oops, something went wrong.