From dc248d45f9c0e8feb26311a54de300cc252b0ec4 Mon Sep 17 00:00:00 2001 From: Vlad Ifrim Date: Wed, 22 Nov 2023 15:49:21 +0200 Subject: [PATCH 1/2] adds tests for the Dropdown component --- src/form/components/Dropdown/Dropdown.test.js | 251 +++++++++++++++++- src/form/components/Dropdown/index.js | 139 +++++----- .../components/Dropdown/js/DropdownItem.js | 143 +++++----- 3 files changed, 385 insertions(+), 148 deletions(-) diff --git a/src/form/components/Dropdown/Dropdown.test.js b/src/form/components/Dropdown/Dropdown.test.js index 1743e42d..efc02169 100644 --- a/src/form/components/Dropdown/Dropdown.test.js +++ b/src/form/components/Dropdown/Dropdown.test.js @@ -1,7 +1,254 @@ +import React from 'react'; import Dropdown from './index'; +// eslint-disable-next-line react/prop-types +function Component({ children, ...rest }) { + return ( + + {children || ( + <> + + + + + )} + + ); +} + +const classes = { + btnGroup: 'tyk-button-group', + displayBlock: 'tyk-dropdown--block', + custom: 'tyk-dropdown--custom', + relative: 'tyk-dropdown--relative', + scrollable: 'tyk-dropdown--scrollable', +}; + +const selectors = { + component: '.tyk-dropdown', + menu: '.tyk-dropdown-menu', + trigger: '.tyk-dropdown__trigger', + label: '.tyk-dropdown label', + item: '.tyk-dropdown-menu li', + checkIcon: '.fa-check', +}; + describe('Dropdown', () => { - it('TODO', () => { - expect(true).to.equal(true); + it('renders the component with a trigger that opens the dropdown menu', () => { + cy.mount() + .get(selectors.component) + .should('exist') + .get(selectors.menu) + .should('not.exist') + .get(selectors.trigger) + .click() + .get(selectors.menu) + .should('exist'); + }); + + it('can render with a label', () => { + const label = 'my label'; + cy.mount() + .get(selectors.label) + .should('exist') + .and('have.text', label); + }); + + it('the menu can be appended to a specific element', () => { + cy.mount( +
+
target
+ +
, + ); + + cy.get(selectors.trigger) + .click() + .get('#target-container') + .find(selectors.menu) + .should('exist'); + }); + + it('can change behaviour to close the menu on item select', () => { + cy.mount() + .get(selectors.item) + .eq(0) + .click() + .get(selectors.menu) + .should('not.exist'); + }); + + it('can not render the trigger', () => { + cy.mount() + .get(selectors.component) + .should('exist') + .get(selectors.trigger) + .should('not.exist'); + }); + + it('can specify a custom class for the trigger', () => { + const className = 'my-class'; + cy.mount() + .get(selectors.trigger) + .should('have.class', className); + }); + + it('the trigger can be a button group', () => { + cy.mount() + .get(selectors.component) + .should('have.class', classes.btnGroup); + }); + + it('can specify a theme for the component', () => { + const theme = 'primary'; + cy.mount() + .get(selectors.component) + .should('have.class', `theme-${theme}`); + }); + + it('can have a custom button title', () => { + const title = 'my title'; + cy.mount() + .get(selectors.trigger) + .should('have.text', title); + }); + + it('can use className and/or wrapperClassName to pass css classes to the component', () => { + const className = 'my-class'; + const wrapperClassName = 'my-wrapper-class'; + cy.mount() + .get(selectors.component) + .should('have.class', className) + .and('have.class', wrapperClassName); + }); + + it('adds the displayBlock class on the menu regardless of the value of the display prop', () => { + cy.mount() + .get(selectors.menu) + .should('have.class', classes.displayBlock); + }); + + it('the trigger button can be disabled', () => { + cy.mount() + .get(selectors.trigger) + .click({ force: true }) + .get(selectors.menu) + .should('not.exist'); + }); + + it('adds the custom content class if hasCustomContent is true', () => { + cy.mount() + .get(selectors.menu) + .should('have.class', classes.custom); + }); + + it('can use listclassnames to pass css classes to the menu', () => { + const className = 'my-class'; + cy.mount() + .get(selectors.menu) + .should('have.class', className); + }); + + it('calls the onClose callback when the dropdown has closed', () => { + const onClose = cy.stub().as('onClose'); + cy.mount() + .get(selectors.trigger) + .click() + .get('body') + .click() + .get('@onClose') + .should('be.called'); + }); + + it('calls the onSelect callback when an item is selected', () => { + const onSelect = cy.stub().as('onSelect'); + cy.mount() + .get(selectors.trigger) + .click() + .get(selectors.item) + .eq(0) + .click() + .get('@onSelect') + .should('be.called'); + }); + + it('can set the initial opened state', () => { + cy.mount() + .get(selectors.menu) + .should('be.visible'); + }); + + it('can specify the offset position', () => { + cy.mount() + .get(selectors.menu) + .should('have.css', 'top', '0px') + .and('have.css', 'left', '0px'); + }); + + it('adds the relative class if position is "relative"', () => { + cy.mount() + .get(selectors.menu) + .should('have.class', classes.relative); + }); + + it('selects the item with the key specified by selectedItem', () => { + cy.mount() + .get(selectors.item) + .eq(1) + .find(selectors.checkIcon) + .should('exist'); + }); + + it('can specify to not show the check icon when selected', () => { + cy.mount() + .get(selectors.item) + .eq(1) + .find(selectors.checkIcon) + .should('not.exist'); + }); + + it('can prevent the trigger label to change on item select', () => { + const label = 'my-label'; + cy.mount() + .get(selectors.trigger) + .should('have.text', label) + .get(selectors.item) + .eq(0) + .click() + .get(selectors.trigger) + .should('have.text', label); + }); + + it('adds the scrollabel class if maxHeight is set', () => { + cy.mount() + .get(selectors.menu) + .should('have.class', classes.scrollable); + }); + + it('items can have an onClick callback that is called when the item is clicked', () => { + const onClick = cy.stub().as('onClick'); + const itemId = 'item1'; + cy.mount( + + + , + ) + .get(selectors.item) + .eq(0) + .click() + .get('@onClick') + .should('be.calledWith', itemId); + }); + + it('items can be custom content', () => { + const text = 'my item'; + cy.mount( + + {text}} /> + , + ) + .get(selectors.item) + .eq(0) + .should('have.text', text); }); }); diff --git a/src/form/components/Dropdown/index.js b/src/form/components/Dropdown/index.js index 20eeff98..770b4a2f 100644 --- a/src/form/components/Dropdown/index.js +++ b/src/form/components/Dropdown/index.js @@ -7,7 +7,7 @@ import DropdownItem from './js/DropdownItem'; import Button from '../../../components/Button'; import { DropdownContext } from './dropdown-context'; -export default class Dropdown extends Component { +class Dropdown extends Component { static isElemInRightView(el, dropdownWidth) { const windowWidth = window.innerWidth; const offset = el.getBoundingClientRect(); @@ -25,64 +25,6 @@ export default class Dropdown extends Component { return elemBottom <= windowHeight; } - static propTypes = { - appendTo: PropTypes.string, - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.node, - PropTypes.string, - PropTypes.func, - ]), - closeOnSelect: PropTypes.bool, - btnClassName: PropTypes.string, - btnSize: PropTypes.string, - btnGroupSize: PropTypes.string, - btnTheme: PropTypes.string, - btnTitle: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.element, - PropTypes.string, - ]), - customBtnTitle: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.element, - PropTypes.string, - ]), - btnGroup: PropTypes.bool, - className: PropTypes.string, - display: PropTypes.string, - disabled: PropTypes.bool, - hasCustomContent: PropTypes.bool, - label: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.element, - PropTypes.func, - PropTypes.string, - ]), - listclassnames: PropTypes.string, - onClose: PropTypes.func, - onSelect: PropTypes.func, - open: PropTypes.bool, - offset: PropTypes.instanceOf(Object), - position: PropTypes.string, - selectedItem: PropTypes.string, - showDropdownIcon: PropTypes.bool, - stopButtonTextChange: PropTypes.bool, - showTriggerButton: PropTypes.bool, - showCheckmark: PropTypes.bool, - maxHeight: PropTypes.string, - wrapperClassName: PropTypes.string, - }; - - static defaultProps = { - closeOnSelect: false, - showDropdownIcon: true, - showTriggerButton: true, - showCheckmark: true, - open: false, - }; - constructor(props) { super(props); @@ -145,6 +87,15 @@ export default class Dropdown extends Component { document.removeEventListener('mousedown', this.handleClickOutside); } + handleClickOutside(event) { + if ( + this.dropdownListRef.current && !this.dropdownListRef.current.contains(event.target) + && this.dropdownRef.current && !this.dropdownRef.current.contains(event.target) + ) { + this.closeDropdown(); + } + } + onSelectItem(itemId, itemProps) { const { closeOnSelect, onSelect } = this.props; const changes = { @@ -205,6 +156,7 @@ export default class Dropdown extends Component { customLeft = left + el.clientWidth - dropdownWidth; } + console.log('position', position, '::', display); if (position !== 'relative') { dropdownEl.style.top = `${customTop}px`; if (display !== 'block') { @@ -318,15 +270,6 @@ export default class Dropdown extends Component { }); } - handleClickOutside(event) { - if ( - this.dropdownListRef.current && !this.dropdownListRef.current.contains(event.target) - && this.dropdownRef.current && !this.dropdownRef.current.contains(event.target) - ) { - this.closeDropdown(); - } - } - render() { const { appendTo, @@ -426,4 +369,64 @@ export default class Dropdown extends Component { } } +Dropdown.propTypes = { + appendTo: PropTypes.string, + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.node, + PropTypes.string, + PropTypes.func, + ]), + closeOnSelect: PropTypes.bool, + btnClassName: PropTypes.string, + btnSize: PropTypes.string, + btnGroupSize: PropTypes.string, + btnTheme: PropTypes.string, + btnTitle: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.element, + PropTypes.string, + ]), + customBtnTitle: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.element, + PropTypes.string, + ]), + btnGroup: PropTypes.bool, + className: PropTypes.string, + display: PropTypes.string, + disabled: PropTypes.bool, + hasCustomContent: PropTypes.bool, + label: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.element, + PropTypes.func, + PropTypes.string, + ]), + listclassnames: PropTypes.string, + onClose: PropTypes.func, + onSelect: PropTypes.func, + open: PropTypes.bool, + offset: PropTypes.instanceOf(Object), + position: PropTypes.string, + selectedItem: PropTypes.string, + showDropdownIcon: PropTypes.bool, + stopButtonTextChange: PropTypes.bool, + showTriggerButton: PropTypes.bool, + showCheckmark: PropTypes.bool, + maxHeight: PropTypes.string, + wrapperClassName: PropTypes.string, +}; + +Dropdown.defaultProps = { + closeOnSelect: false, + showDropdownIcon: true, + showTriggerButton: true, + showCheckmark: true, + open: false, +}; + Dropdown.Item = DropdownItem; + +export default Dropdown; diff --git a/src/form/components/Dropdown/js/DropdownItem.js b/src/form/components/Dropdown/js/DropdownItem.js index 48cb29a5..8a970ebe 100644 --- a/src/form/components/Dropdown/js/DropdownItem.js +++ b/src/form/components/Dropdown/js/DropdownItem.js @@ -1,87 +1,74 @@ -import React, { Component, cloneElement, Fragment } from 'react'; +import React, { cloneElement, useCallback } from 'react'; import PropTypes from 'prop-types'; import { DropdownContext } from '../dropdown-context'; import Icon from '../../../../components/Icon'; -export default class DropdownItem extends Component { - static propTypes = { - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.node, - PropTypes.string, - ]), - customdropdownitem: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.element, - ]), - eventKey: PropTypes.string, - href: PropTypes.string, - target: PropTypes.string, - onClick: PropTypes.func, - title: PropTypes.string, - }; - - constructor(props) { - super(props); - - this.dropdownItemCLick = this.dropdownItemCLick.bind(this); - } - - dropdownItemCLick(dropdownContext, itemProps) { - const { eventKey, onClick } = this.props; - +function DropdownItem(props) { + const { + customdropdownitem, + children, + eventKey, + href, + target, + title, + onClick, + } = props; + const dropdownItemCLick = useCallback((dropdownContext, itemProps) => { dropdownContext.onSelectItem(eventKey, itemProps); - if (onClick) { - onClick(eventKey); - } - } + if (onClick) onClick(eventKey); + }, []); - render() { - const { - customdropdownitem, - children, - eventKey, - href, - target, - title, - } = this.props; - return ( - - { - dropdownContext => ( -
  • {}} - > - { - customdropdownitem - ? ( - - { cloneElement(customdropdownitem, {}) } - - ) - : ( - - { - eventKey - && dropdownContext.selectedItem === eventKey - && dropdownContext.showCheckmark - ? - : null - } - - {title || children} - - - ) - } -
  • - ) - } -
    - ); - } + return ( + + { + (dropdownContext) => ( +
  • dropdownItemCLick(dropdownContext, props)} + onKeyUp={() => {}} + > + { + customdropdownitem + ? cloneElement(customdropdownitem, {}) + : ( + + { + eventKey + && dropdownContext.selectedItem === eventKey + && dropdownContext.showCheckmark + ? + : null + } + + {title || children} + + + ) + } +
  • + ) + } +
    + ); } + +DropdownItem.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.node, + PropTypes.string, + ]), + customdropdownitem: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.element, + ]), + eventKey: PropTypes.string, + href: PropTypes.string, + target: PropTypes.string, + onClick: PropTypes.func, + title: PropTypes.string, +}; + +export default DropdownItem; From dcde47adb6fdca4454f0e5f876107bb52a0f26e5 Mon Sep 17 00:00:00 2001 From: Vlad Ifrim Date: Wed, 22 Nov 2023 15:56:35 +0200 Subject: [PATCH 2/2] some cleanup --- src/form/components/Dropdown/Dropdown.test.js | 2 +- src/form/components/Dropdown/index.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/form/components/Dropdown/Dropdown.test.js b/src/form/components/Dropdown/Dropdown.test.js index efc02169..3d756f08 100644 --- a/src/form/components/Dropdown/Dropdown.test.js +++ b/src/form/components/Dropdown/Dropdown.test.js @@ -219,7 +219,7 @@ describe('Dropdown', () => { .should('have.text', label); }); - it('adds the scrollabel class if maxHeight is set', () => { + it('adds the scrollable class if maxHeight is set', () => { cy.mount() .get(selectors.menu) .should('have.class', classes.scrollable); diff --git a/src/form/components/Dropdown/index.js b/src/form/components/Dropdown/index.js index 770b4a2f..5cd13009 100644 --- a/src/form/components/Dropdown/index.js +++ b/src/form/components/Dropdown/index.js @@ -156,7 +156,6 @@ class Dropdown extends Component { customLeft = left + el.clientWidth - dropdownWidth; } - console.log('position', position, '::', display); if (position !== 'relative') { dropdownEl.style.top = `${customTop}px`; if (display !== 'block') {