Skip to content

Commit

Permalink
Merge pull request #9 from GovAlta/features/CS-668
Browse files Browse the repository at this point in the history
feat: update the dropdown menu
  • Loading branch information
solittlework authored Aug 17, 2021
2 parents 58773b9 + cd803f6 commit 9fa64a4
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 45 deletions.
88 changes: 74 additions & 14 deletions libs/react-components/src/lib/dropdown/dropdown.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,59 @@ import './dropdown.component.scss';
import GoAOption from './option/option.component';
import GoAOptionGroup from './option-group/option-group.component';

interface Props {
type DropDownrops = {
/**
* Title of dropdown
*/
title: string;
subTitle: string;
/**
* SubTitle of dropdown. The subtitle is normally used to indicate the state of dropdown.
*/
subTitle?: string;
/**
* description of dropdown. Description is the message shown on the menu.
*/
description?: string;
/**
* Maximum height of the dropdown.
*/
menuHeight?: number;
/**
* If true, allowe multiple selection. Otherwise, single selection is used as default.
*/
multiple?: boolean;
/**
* If true, disable the dropdown.
*/
disabled?: boolean;
/**
* Overwrite the discription when the dropdown is disabled.
*/
display?: string;
/**
* Error messgae
*/
errorMessage?: string;
key?: string;
/**
* If true, the menu of the dropdown changes to input model. This is useful, if we need to add custom filter for the dropdown list.
*/
menuEditable?: boolean;
/**
* Callback function if dropdown menu is in input model.
*/
menuInputChanged?: (text: string) => void;
/**
* The property will overwrite the default toggle behavior.
*/
open?: boolean
/**
* Callback function for option change event.
*/
selectionChanged: (option: DropdownOption) => void;
}

export const GoADropdown: FC<Props> = ({
export const GoADropdown: FC<DropDownrops> = ({
title,
subTitle,
menuHeight,
Expand All @@ -40,6 +79,9 @@ export const GoADropdown: FC<Props> = ({
display,
selectionChanged,
key,
open,
menuEditable,
menuInputChanged,
...rest
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
Expand Down Expand Up @@ -75,13 +117,28 @@ export const GoADropdown: FC<Props> = ({
setMaxMenuHeight(height);
});

const canOpen = (isOpenState: boolean, isOpenProp?: boolean,) => {
// If open property is not provide, default menu toggle will be applied
if (isOpenProp !== undefined) {
return isOpenProp
}

return isOpenState
}

const canMenuEditable = (editableProp?: boolean) => {
if (editableProp !== undefined) {
return editableProp
}
return true
}

return (
<DropdownContext.Provider value={{
selectionChanged: selectionChanged
}}>
{isOpen &&
<div className="dropdown-overlay" ref={overlayRef} onClick={toggleOpen}></div>
}

<div className="dropdown-overlay" data-testid='dropdown-container' ref={overlayRef} onClick={toggleOpen}></div>

<div className={rootDropDownCss()} {...rest}>
<label className="dropdown-label" htmlFor={`input-for-${title}`}>
Expand All @@ -96,18 +153,21 @@ export const GoADropdown: FC<Props> = ({
role="searchbox"
className="dropdown-textbox margin-override"
type="text"
data-testid="menu-input"
style={{ cursor: 'default' }}
id={`input-for-${key}`}
placeholder={display ? display : description}
readOnly={!isOpen}
onChange={(e) => { menuInputChanged && menuInputChanged(e.target.value) }}
readOnly={!canMenuEditable(menuEditable)}
/>
{isOpen &&
<div className="dropdown-menu" ref={menuRef} style={{
position: 'absolute',
zIndex: 1000,
maxHeight: `${maxMenuHeight}px`,
overflow: 'auto',
}}>
{canOpen(isOpen, open) &&
<div className="dropdown-menu" data-testid='dropdown-menu'
ref={menuRef} style={{
position: 'absolute',
zIndex: 1000,
maxHeight: `${maxMenuHeight}px`,
overflow: 'auto',
}}>
{children}
</div>
}
Expand Down
114 changes: 83 additions & 31 deletions libs/react-components/src/lib/dropdown/dropdown.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import React from 'react';
import { render, cleanup, screen, fireEvent } from '@testing-library/react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import { GoADropdown } from './dropdown.component';
import { GoAOptionGroup } from './option-group/option-group.component';
import { GoAOption } from './option/option.component';
import { DropdownOption } from './dropdown.context';
import { screen } from '@testing-library/dom';

afterEach(cleanup);

describe('GoA Dropdown', () => {
const expandCollapseDropDown = (container: HTMLElement) => {
const label = container.querySelector('.dropdown-label');
// Check that the label is available
expect(label).toBeTruthy();
const expandCollapseDropDown = async () => {

const container = await screen.findByTestId('dropdown-container');
// Check that the label is available
expect(container).toBeTruthy();
// Click the label to expand the dropdown
fireEvent.click(label);
fireEvent.click(container);
}

test('Expands and collapses', () => {
test('Expands and collapses', async () => {
const { container } = render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
selectionChanged={() => { }}
>
Expand All @@ -33,23 +34,23 @@ describe('GoA Dropdown', () => {
expect(container.querySelector('.option')).toBeFalsy();

//Expand the dropdown
expandCollapseDropDown(container);
await expandCollapseDropDown();

// Check that the options are displayed
expect(container.querySelector('.option')).toBeTruthy();

//Collapse the dropdown
expandCollapseDropDown(container);
await expandCollapseDropDown();

// Check that the options are NOT displayed
expect(container.querySelector('.option')).toBeFalsy();

});

test('Nothing happens when disabled', () => {
test('Nothing happens when disabled', async () => {
const { container } = render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
disabled={true}
selectionChanged={() => { }}
Expand All @@ -59,16 +60,16 @@ describe('GoA Dropdown', () => {
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);

expandCollapseDropDown(container);
await expandCollapseDropDown();

// Check that the options are NOT displayed
expect(container.querySelector('.option')).toBeFalsy();
});

test('Contains all options', () => {
test('Contains all options', async () => {
const { container } = render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
selectionChanged={() => { }}
>
Expand All @@ -77,18 +78,18 @@ describe('GoA Dropdown', () => {
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);

expandCollapseDropDown(container);
await expandCollapseDropDown();

const allLabels = ['Apple', 'Pear', 'Banana']
// Find each of the options
allLabels.forEach((l) => {
expect(screen.getByText(l)).toBeTruthy();
})
});
test('Renders options and groups', () => {
test('Renders options and groups', async () => {
const { container } = render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
multiple={false}
selectionChanged={() => { }}
Expand All @@ -100,7 +101,7 @@ describe('GoA Dropdown', () => {
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);

expandCollapseDropDown(container);
await expandCollapseDropDown();

// Find each of the options
const allLabels = ['Apple', 'Pear', 'Banana']
Expand All @@ -110,12 +111,11 @@ describe('GoA Dropdown', () => {
})
});

test('Displays warning when is required and no items are selected', () => {
test('Displays warning when is required and no items are selected', async () => {
const { container } = render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
required={true}
selectionChanged={() => { }}
errorMessage="Error of the component"
>
Expand All @@ -127,9 +127,9 @@ describe('GoA Dropdown', () => {
</GoADropdown>);

// Expand...
expandCollapseDropDown(container);
await expandCollapseDropDown();
// Collapse...
expandCollapseDropDown(container);
await expandCollapseDropDown();

// Check that the dropdown has the 'has-error' class
const dropdownElement = container.querySelector('.goa-dropdown.has-error');
Expand All @@ -142,13 +142,13 @@ describe('GoA Dropdown', () => {
});

test('[selectionChanges] callback is invoked', async () => {
let optionInCallback = {};
let optionInCallback: DropdownOption;
const selectHandler = (option: DropdownOption) => {
optionInCallback = option;
}
const { container } = render(
render(
<GoADropdown
label='Fruits'
title='Fruits'
description='Choose your favourite friut.'
multiple={true}
selectionChanged={selectHandler}
Expand All @@ -160,7 +160,7 @@ describe('GoA Dropdown', () => {
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);

expandCollapseDropDown(container);
await expandCollapseDropDown();

// Select all three options
const option = screen.queryByText('Apple');
Expand All @@ -169,11 +169,63 @@ describe('GoA Dropdown', () => {
expect(optionInCallback.value).toBe('apple');
});

test('Dynamic loading', () => {
test('Test open property', async () => {
let optionInCallback: DropdownOption;
const selectHandler = (option: DropdownOption) => {
optionInCallback = option;
}
const dropdown = render(
<GoADropdown
title='Fruits'
description='Choose your favourite friut.'
multiple={false}
open={true}
selectionChanged={selectHandler}
>
<GoAOptionGroup label="Group 1">
<GoAOption value="apple" label="Apple"></GoAOption>
<GoAOption value="pear" label="Pear"></GoAOption>
</GoAOptionGroup>
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);
// No further expect is required. Will throw exception, if dropdown-menu is missing
await dropdown.findByTestId('dropdown-menu');
});

test('Test menuEditable and menuInputFn properties', async () => {
let inputText: string;

const menuInputhandler = (text: string) => {
inputText = text;
}
const inputValue = 'mock-test'
render(
<GoADropdown
title='Fruits'
description='Choose your favourite friut.'
multiple={false}
open={true}
menuEditable={true}
menuInputChanged={menuInputhandler}
selectionChanged={null}
>
<GoAOptionGroup label="Group 1">
<GoAOption value="apple" label="Apple"></GoAOption>
<GoAOption value="pear" label="Pear"></GoAOption>
</GoAOptionGroup>
<GoAOption value="banana" label="Banana"></GoAOption>
</GoADropdown>);

const input = await screen.findByTestId('menu-input')
await fireEvent.change(input, { target: { value: inputValue } })
expect(inputText).toBe(inputValue)
});

test('Dynamic loading', async () => {
const items = [{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }]
const { container } = render(
<GoADropdown
label="Fruits"
title="Fruits"
description="Choose your favourite fruit!"
multiple={false}
selectionChanged={() => { }}
Expand All @@ -184,7 +236,7 @@ describe('GoA Dropdown', () => {
</GoADropdown>
)

expandCollapseDropDown(container);
await expandCollapseDropDown();

const displayedOtions = container.querySelectorAll('.goa-option');

Expand Down

0 comments on commit 9fa64a4

Please sign in to comment.