Skip to content

Commit

Permalink
Added Icon Dropdown (#1025)
Browse files Browse the repository at this point in the history
* Added Icon SelectGroup

* SelectGroupStories

* Icon Select

* Changelog and packageJson

* Dropdown with icon

* changes Select

* update

* Changes

* code cleaning

* DropdowTest

* Test

* Test Dropdown

* Test changes

* DropdownTest

* Test changes

---------

Co-authored-by: Kevin Mamaqi <kevinmamaqi@gmail.com>
  • Loading branch information
ursulacubilla and kevinmamaqi authored Mar 21, 2024
1 parent bae795c commit 272c6b2
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 162 deletions.
7 changes: 7 additions & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.30.0] - 2024-03-15

### Added

- Icon Dropdown

## [0.29.3] - 2024-03-13

### Fixed
Expand All @@ -22,6 +28,7 @@ All notable changes to this project will be documented in this file.
- Fixed icon position in InputGroup
- Fixed type error with Icon onClick in InputGroup


## [0.29.0] - 2024-02-27

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itacademy/ui",
"version": "0.29.3",
"version": "0.30.0",
"description": "React FE components for ITAcademy projects.",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.es.js",
Expand Down
82 changes: 37 additions & 45 deletions packages/ui/src/__tests__/atoms/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ import { expect } from 'vitest'
import userEvent from '@testing-library/user-event'
import { Dropdown } from '../../components/atoms/Dropdown'


const mockOptions = [
{
id: '1',
name: 'Option 1',
},
{
id: '2',
name: 'Option 2',
},
{
id: '3',
name: 'Option 3',
},
]

describe('Dropdown', () => {
it('renders correctly', () => {
it('renders correctly', async () => {
render(
<Dropdown>
<p>Test children content</p>
</Dropdown>
<Dropdown options={mockOptions} />
)

const dropdown = screen.getByTestId('dropdown')
Expand All @@ -19,33 +33,28 @@ describe('Dropdown', () => {
expect(dropdown).toHaveStyle(`cursor: pointer;`)
expect(dropdownHeader).toHaveTextContent(/selecciona/i)
expect(screen.getByTitle('Ampliar')).toBeInTheDocument()
expect(screen.queryByText('Test children content')).not.toBeInTheDocument()
expect(screen.queryByText('Option 1')).not.toBeInTheDocument()
expect(screen.queryByTitle('Cerrar')).not.toBeInTheDocument()
})

it('renders dropdown children when user clicks on it', async () => {
render(
<Dropdown>
<p>Test children content</p>
</Dropdown>
<Dropdown options={mockOptions} />
)

const dropdownHeader = screen.getByTestId('dropdown-header')

expect(screen.queryByText('Test children content')).not.toBeInTheDocument()

expect(screen.queryByText('Option 1')).not.toBeInTheDocument()
expect(screen.getByTitle('Ampliar')).toBeInTheDocument()

await userEvent.click(dropdownHeader)

expect(screen.queryByText('Test children content')).toBeVisible()
expect(screen.queryByText('Option 1')).toBeVisible()

expect(screen.getByTitle('Cerrar')).toBeInTheDocument()
})

it('renders placeholder provided instead of default', () => {
it('renders placeholder provided instead of default', () => {
render(
<Dropdown placeholder="Test placeholder">
<p>Test children content</p>
</Dropdown>
<Dropdown placeholder="Test placeholder" options={mockOptions} />
)

const dropdownHeader = screen.getByTestId('dropdown-header')
Expand All @@ -54,45 +63,32 @@ describe('Dropdown', () => {
expect(dropdownHeader).not.toHaveTextContent(/selecciona/i)
})

it('renders value provided instead of placeholder', () => {
it('renders value provided instead of placeholder', async () => {
render(
<Dropdown defaultValue="Test selected value">
<p>Test children content</p>
</Dropdown>
<Dropdown defaultValue="Test selected value" options={mockOptions} />
)

const dropdownHeader = screen.getByTestId('dropdown-header')

expect(dropdownHeader).toHaveTextContent(/Test selected value/i)
expect(dropdownHeader).not.toHaveTextContent(/selecciona/i)

})

it('a click outside the dropdown closes its menu', async () => {
render(
<Dropdown>
<p>Test children content</p>
</Dropdown>
<Dropdown options={mockOptions} />
)

const dropdownHeader = screen.getByTestId('dropdown-header')

expect(screen.queryByText('Option 1')).not.toBeInTheDocument()

await userEvent.click(dropdownHeader)
expect(screen.getByText('Test children content')).toBeVisible()
expect(screen.getByText('Option 1')).toBeVisible()

await userEvent.click(document.body)
expect(screen.queryByText('Test children content')).not.toBeInTheDocument()
})

it('renders the selected value in the DropdownHeader on initial load', () => {
render(
<Dropdown defaultValue="Preselected Item">
<p data-value="Preselected Item">Preselected Item</p>
<p data-value="Item 2">Item 2</p>
</Dropdown>
)

const dropdownHeader = screen.getByTestId('dropdown-header')
expect(dropdownHeader).toHaveTextContent('Preselected Item')
expect(screen.queryByText('Option 1')).not.toBeInTheDocument()
})

const MockParent = () => {
Expand All @@ -101,13 +97,9 @@ describe('Dropdown', () => {
const handleChange = (value: string) => {
setSelectedOption(value)
}

return (
<div>
<Dropdown onValueChange={handleChange}>
<p data-value="Option 1">Option 1</p>
<p data-value="Option 2">Option 2</p>
</Dropdown>
<Dropdown onValueChange={handleChange} options={mockOptions} />
<p data-testid="selected-value">{selectedOption}</p>
</div>
)
Expand All @@ -118,11 +110,11 @@ describe('Dropdown', () => {
const dropdownHeader = screen.getByTestId('dropdown-header')
await userEvent.click(dropdownHeader)

const optionToSelect = screen.getByText('Option 2')
const optionToSelect = screen.getByTestId('2')
await userEvent.click(optionToSelect)

await waitFor(() => {
expect(screen.getByTestId('selected-value')).toHaveTextContent('Option 2')
expect(screen.getByTestId('selected-value')).toHaveTextContent('2')
})
})
})
114 changes: 66 additions & 48 deletions packages/ui/src/components/atoms/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, {
Children,
isValidElement,
useState,
useRef,
forwardRef,
useEffect,
HTMLAttributes,
useCallback,
useMemo,
} from 'react'
import styled from 'styled-components'
import { colors, dimensions, font } from '../../styles'
import { FlexBox, colors, dimensions, font } from '../../styles'
import { Button } from './Button'
import { Icon } from './Icon'

Expand All @@ -19,27 +18,35 @@ const StyledDropdown = styled.div`
position: relative;
`

const StyledIcon = styled(Icon)`
color: ${colors.gray.gray3};
`

const StyledImage = styled.img`
width: 24px;
height: 24px;
margin-right: 10px;
`

const DropdownHeader = styled(Button)`
justify-content: space-between;
background-color: ${colors.white};
justify-content: start;
margin: 0;
padding: ${dimensions.spacing.base};
border-radius: ${dimensions.borderRadius.base};
border: 1px solid ${colors.gray.gray4};
color: ${colors.black.black3};
font-family: ${font.fontFamily};
width: 320px;
&:hover {
transition: all 0.2s ease;
color: ${colors.white};
}
`
border: 1px solid;
const StyledIcon = styled(Icon)`
position: absolute;
top: ${dimensions.spacing.base};
right: ${dimensions.spacing.xxs};
color: ${colors.gray.gray3};
${StyledIcon} {
transition: all 0.2s ease;
color: ${colors.white};
}
}
`

const DropdownList = styled.div`
Expand All @@ -60,17 +67,26 @@ const DropdownItem = styled.div`
cursor: pointer;
font-family: ${font.fontFamily};
font-size: ${font.xss};
display: flex;
align-items: center;
&:hover {
background-color: ${colors.primary};
color: ${colors.white};
}
`

export type TDropdownOption = {
id: string
name: string
icon?: string
iconSvg?: string
}

export type TDropdown = HTMLAttributes<HTMLElement> & {
options: TDropdownOption[]
placeholder?: string
defaultValue?: string
children: React.ReactNode
onValueChange?: (value: string) => void
openText?: string
closeText?: string
Expand All @@ -79,9 +95,9 @@ export type TDropdown = HTMLAttributes<HTMLElement> & {
export const Dropdown = forwardRef<HTMLDivElement, TDropdown>(
(
{
options = [],
defaultValue = '',
placeholder = 'Selecciona',
children,
onValueChange,
openText = 'Ampliar',
closeText = 'Cerrar',
Expand All @@ -91,6 +107,7 @@ export const Dropdown = forwardRef<HTMLDivElement, TDropdown>(
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const [selectedValue, setSelectedValue] = useState(defaultValue)
const dropdownListRef = useRef<HTMLDivElement>(null)
const dropdownCloseOutsideRef = useRef<HTMLDivElement>(null)

const handleSelect = useCallback(
(value: string) => {
Expand All @@ -102,9 +119,8 @@ export const Dropdown = forwardRef<HTMLDivElement, TDropdown>(

const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement
const innerValue = target.innerText
if (dropdownListRef.current?.contains(target)) {
handleSelect(innerValue)
handleSelect(value);
}
}

Expand All @@ -121,53 +137,55 @@ export const Dropdown = forwardRef<HTMLDivElement, TDropdown>(

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (!dropdownListRef.current?.contains(event.target as Node)) {
if (!dropdownCloseOutsideRef.current?.contains(event.target as Node)) {
setIsDropdownOpen(false)
}
}

document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [dropdownListRef])
}, [dropdownListRef, dropdownCloseOutsideRef])

const selectedOption = useMemo(
() => options.find((option) => option.id === selectedValue),
[options, selectedValue]
)

return (
<div ref={ref}>
<StyledDropdown data-testid="dropdown">
<StyledDropdown data-testid="dropdown" ref={dropdownCloseOutsideRef}>
<DropdownHeader
data-testid="dropdown-header"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<span>{selectedValue || placeholder}</span>
{isDropdownOpen ? (
<StyledIcon
name="expand_less"
aria-hidden="true"
title={closeText}
/>
) : (
<StyledIcon
name="expand_more"
aria-hidden="true"
title={openText}
/>
)}
{ selectedOption ? (
<FlexBox direction='row' key={selectedOption.id}>
{selectedOption.icon && (
<StyledIcon name={selectedOption.icon} />
)}
{selectedOption.iconSvg && (
<StyledImage
src={selectedOption.iconSvg} alt={selectedOption.name} />
)}
<span>{selectedOption.name}</span>
</FlexBox>
) : ( <span>{defaultValue || placeholder}</span> )}

<StyledIcon
name={isDropdownOpen ? 'expand_less' : 'expand_more'}
aria-hidden="true"
title={isDropdownOpen ? closeText : openText}
/>
</DropdownHeader>
{isDropdownOpen && (
<DropdownList ref={dropdownListRef}>
{Children.map(children, (child) => {
if (isValidElement(child)) {
return (
<DropdownItem
onClick={() =>
handleSelect(child.props.children.toString())
}
>
{child}
</DropdownItem>
)
}
return null
})}
{options.map(({ name, id, icon, iconSvg }) => (
<DropdownItem key={id} data-testid={id} id={id} onClick={() => handleSelect(id)}>
{icon && <StyledIcon name={icon} />}
{iconSvg && <StyledImage src={iconSvg} alt={name} />}
<span>{name}</span>
</DropdownItem>
))}
</DropdownList>
)}
</StyledDropdown>
Expand Down
Loading

0 comments on commit 272c6b2

Please sign in to comment.