Last active
October 17, 2024 20:55
-
-
Save asmyshlyaev177/4d99427fe58cb1a1fb24fc119fbe1400 to your computer and use it in GitHub Desktop.
Custom MaterialUI style Select written with TDD.
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
/* eslint-disable cypress/no-unnecessary-waiting */ | |
import React from 'react'; | |
import { ThemeProvider } from '@material-ui/core/styles'; | |
import CssBaseline from '@material-ui/core/CssBaseline'; | |
import { Popper, Paper } from '@material-ui/core'; | |
import { styled } from '@material-ui/core/styles'; | |
import { theme } from 'components/theme'; | |
import { | |
CustomSelect, | |
omitCustomProps, | |
CustomNumberInput, | |
} from 'roles/member/pages/WhatsMyCopaySearch/components/Select'; | |
const options = [ | |
{ label: '100mi', value: '100' }, | |
{ label: '200mi', value: '200' }, | |
{ label: '300mi', value: '300' }, | |
{ label: '456mi', value: '456' }, | |
]; | |
const SELECTORS = { | |
wrapper: '[data-testid="Select"]', | |
textField: '[data-testid="CustomSelect-text-field"]', | |
textFieldInput: `[data-testid="CustomSelect-text-field"] input`, | |
paper: '[data-testid="CustomSelect-paper"]', | |
listItem: '.option', | |
options: '[data-testid="CustomSelect-options-wrapper"]', | |
otherInput: '[data-testid="test"] input', | |
otherElement: '[data-testid="other"]', | |
customInputWrapper: '[data-testid="customInput-wrapper"]', | |
customInput: '[data-testid="customInput-wrapper"] input', | |
plusIconSelector: '[data-testid="plus-icon"]', | |
minusIconSelector: '[data-testid="minus-icon"]', | |
noOptions: '[data-testid="CustomSelect-no-options"]', | |
listbox: '[data-testid="CustomSelect-options-wrapper"]', | |
toggle: '[data-testid="CustomSelect-toggle"]', | |
close: '[data-testid="CustomSelect-close"]', | |
}; | |
describe('CustomSelect', () => { | |
it('should display passed value', () => { | |
const value = options[0]; | |
cy.mount(<Wrapper value={value} />); | |
assertInputValue(value.label); | |
}); | |
it('should display selected value', () => { | |
const value = options[0]; | |
cy.mount(<Wrapper value={value} options={options} />); | |
// open | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
// selected and focused | |
cy.get(SELECTORS.listItem) | |
.filter('[aria-selected="true"]') | |
.filter('[data-focus="true"]') | |
.filter('.selected') | |
.should('have.length', 1) | |
.contains(value.label); | |
}); | |
}); | |
describe('disabled', () => { | |
it('input should be disabled', () => { | |
const value = options[0]; | |
cy.mount(<Wrapper disabled={true} value={value} options={options} />); | |
cy.get(SELECTORS.textFieldInput).should('be.disabled'); | |
}); | |
it('options list should be disabled', () => { | |
const value = options[0]; | |
cy.mount(<Wrapper disabled={true} value={value} options={options} />); | |
// eslint-disable-next-line cypress/no-force | |
cy.get(SELECTORS.textFieldInput).click({ | |
force: true, | |
}); | |
assertDropdownClosed(); | |
}); | |
it('icons should not react', () => { | |
const value = options[0]; | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
onChange={onChangeSpy} | |
disabled={true} | |
value={value} | |
options={options} | |
showClearIcon={true} | |
/> | |
); | |
assertDropdownClosed(); | |
// eslint-disable-next-line cypress/no-force | |
cy.get(SELECTORS.toggle).click({ force: true }); | |
assertDropdownClosed(); | |
// eslint-disable-next-line cypress/no-force | |
cy.get(SELECTORS.close).click({ force: true }); | |
assertDropdownClosed(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('should close', () => { | |
it('on click outside the list', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper onChange={onChangeSpy} options={options} />); | |
assertDropdownClosed(); | |
// open | |
openDropdown(); | |
assertDropdownOpen(); | |
// click inside the list | |
// eslint-disable-next-line cypress/no-force | |
cy.get(SELECTORS.options).click({ | |
force: true, | |
}); | |
assertDropdownOpen(); | |
// close | |
closeDropdown(); | |
cy.wait(100); | |
assertDropdownClosed(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('by Esc', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
assertDropdownOpen(); | |
closeDropdownByEsc(); | |
cy.wait(100); | |
assertDropdownClosed(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('when lose focus', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
assertDropdownOpen(); | |
cy.get(SELECTORS.otherInput).focus(); | |
cy.wait(100); | |
assertDropdownClosed(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('should open', () => { | |
it('on click', () => { | |
cy.mount(<Wrapper options={options} />); | |
assertDropdownClosed(); | |
// open | |
openDropdown(); | |
assertDropdownOpen(); | |
}); | |
it('when typing', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
const value = String(options[0].value).slice(0, 1); | |
openDropdown(); | |
assertDropdownOpen(); | |
closeDropdownByEsc(); | |
assertDropdownClosed(); | |
clearInput(); | |
type(value); | |
assertInputValue(value); | |
assertDropdownOpen(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
describe('on focus', () => { | |
it('open by default', () => { | |
cy.mount(<Wrapper options={options} />); | |
assertDropdownClosed(); | |
cy.get(SELECTORS.textFieldInput).focus(); | |
assertDropdownOpen(); | |
}); | |
it('when openOnFocus=false should not open', () => { | |
cy.mount(<Wrapper openOnFocus={false} options={options} />); | |
assertDropdownClosed(); | |
cy.get(SELECTORS.textFieldInput).focus(); | |
assertDropdownClosed(); | |
}); | |
}); | |
}); | |
describe('Options list', () => { | |
describe('should display all options', () => { | |
it('when have selected value', () => { | |
const value = options[0]; | |
cy.mount(<Wrapper value={value} options={options} />); | |
// open | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length); | |
}); | |
}); | |
it('when do not have selected value', () => { | |
cy.mount(<Wrapper options={options} />); | |
// open | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length); | |
cy.get(SELECTORS.listItem) | |
.filter('[aria-selected="true"]') | |
.should('have.length', 0); | |
}); | |
}); | |
}); | |
describe('headerTitle', () => { | |
it('should display when passed', () => { | |
cy.mount(<Wrapper options={options} headerTitle="Title123" />); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains('Title123'); | |
}); | |
}); | |
it('should not display when empty', () => { | |
cy.mount(<Wrapper options={options} />); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get('[data-testid="CustomSelect-title"]').should('not.exist'); | |
}); | |
}); | |
}); | |
it('should toggle open/close by icon', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
toggeDropdown(); | |
assertDropdownOpen(); | |
toggeDropdown(); | |
assertDropdownClosed(); | |
toggeDropdown(); | |
assertDropdownOpen(); | |
toggeDropdown(); | |
assertDropdownClosed(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
describe('should scroll to selected item', () => { | |
it('first item', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const longOptions = Array(50) | |
.fill(0) | |
.map((_el, ind) => ({ | |
id: ind, | |
value: ind, | |
label: ind, | |
})); | |
const selected = longOptions[0]; | |
cy.mount( | |
<Wrapper | |
value={selected} | |
options={longOptions} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).then($el => { | |
cy.wrap($el) | |
.invoke('scrollTop') | |
.should('be.closeTo', 1, 13); | |
}); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('middle item', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const longOptions = Array(50) | |
.fill(0) | |
.map((_el, ind) => ({ | |
id: ind, | |
value: ind, | |
label: ind, | |
})); | |
const selected = longOptions[24]; | |
cy.mount( | |
<Wrapper | |
value={selected} | |
options={longOptions} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).then($el => { | |
cy.wrap($el) | |
.invoke('scrollTop') | |
.should('be.closeTo', 1160, 1166); | |
}); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('last item', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const longOptions = Array(50) | |
.fill(0) | |
.map((_el, ind) => ({ | |
id: ind, | |
value: ind, | |
label: ind, | |
})); | |
const selected = longOptions.slice(-1)[0]; | |
cy.mount( | |
<Wrapper | |
value={selected} | |
options={longOptions} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).then($el => { | |
cy.wrap($el) | |
.invoke('scrollTop') | |
.should('be.closeTo', 2188, 2195); | |
}); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('long lists of options', () => { | |
it('should scroll to focused item', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const longOptions = Array(20) | |
.fill(0) | |
.map((_el, ind) => ({ | |
id: ind, | |
value: ind, | |
label: ind, | |
})); | |
const selected = longOptions.slice(-1)[0]; | |
cy.mount( | |
<Wrapper | |
value={selected} | |
options={longOptions} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
let scrollPos = 0; | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).then($el => { | |
cy.wrap($el) | |
.invoke('scrollTop') | |
.then(scrollOffset => { | |
scrollPos = scrollOffset; | |
return scrollOffset; | |
}) | |
.should('be.greaterThan', 100); | |
}); | |
}); | |
type('{upArrow}{upArrow}{upArrow}{upArrow}{upArrow}{upArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).then($el => { | |
cy.wrap($el) | |
.invoke('scrollTop') | |
.should('lte', scrollPos); | |
}); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should be possible scroll to first/last options', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const longOptions = Array(30) | |
.fill(0) | |
.map((_el, ind) => ({ | |
id: ind, | |
value: ind, | |
label: `_${ind}abc`, | |
})); | |
const selected = longOptions[14]; | |
cy.mount( | |
<Wrapper | |
value={selected} | |
options={longOptions} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
const firstItem = `${SELECTORS.listItem}:contains("_0abc")`; | |
const lastItem = `${SELECTORS.listItem}:contains("_29abc")`; | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listbox).scrollTo('top'); | |
cy.get(firstItem).should('be.visible'); | |
cy.get(SELECTORS.listbox).scrollTo('bottom'); | |
cy.get(lastItem).should('be.visible'); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('Select option', () => { | |
it('by click', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
const value = options[0]; | |
const value2 = options[1]; | |
// open | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains(value.label).click(); | |
}); | |
cy.get('@onChangeSpy').should('have.been.calledWith', value); | |
assertInputValue(value.label); | |
assertDropdownClosed(); | |
// second time | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains(value2.label).click(); | |
}); | |
cy.get('@onChangeSpy').should('have.been.calledWith', value2); | |
assertInputValue(value2.label); | |
assertDropdownClosed(); | |
}); | |
}); | |
describe('no options stub', () => { | |
describe('hideNoOptions', () => { | |
it('true by default', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
clearInput(); | |
type('111'); | |
cy.get(SELECTORS.noOptions).should('not.exist'); | |
clearInput(); | |
type('1'); | |
cy.get(SELECTORS.noOptions).should('not.exist'); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('false - should display', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
hideNoOptions={false} | |
/> | |
); | |
assertDropdownClosed(); | |
clearInput(); | |
type('111'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.noOptions).should('be.visible'); | |
}); | |
clearInput(); | |
type('1'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.noOptions).should('not.exist'); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
}); | |
describe('filter', () => { | |
describe('default filter by opt.label', () => { | |
const optionsWithDifferentLabelValue = options.map(opt => ({ | |
...opt, | |
label: +opt.value * 2 + 'mi', | |
})); | |
it('by partial match', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={optionsWithDifferentLabelValue} | |
onChange={onChangeSpy} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = optionsWithDifferentLabelValue[0]; | |
clearInput(); | |
type(value.label.slice(0, 2)); | |
assertInputValue(value.label.slice(0, 2)); | |
assertDropdownOpen().within(() => { | |
assertListLength(1).contains(value.label); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('by full match', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={optionsWithDifferentLabelValue} | |
onChange={onChangeSpy} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = optionsWithDifferentLabelValue[0]; | |
const value2 = optionsWithDifferentLabelValue[1]; | |
// first value | |
clearInput(); | |
type(value.label); | |
assertInputValue(value.label); | |
assertDropdownOpen().within(() => { | |
assertListLength(1).contains(value.label); | |
}); | |
clearInput(); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length); | |
}); | |
// second value | |
clearInput(); | |
type(value2.label); | |
assertInputValue(value2.label); | |
assertDropdownOpen().within(() => { | |
assertListLength(1).contains(value2.label); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('when there are no options', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
hideNoOptions={false} | |
/> | |
); | |
assertDropdownClosed(); | |
clearInput(); | |
type('111'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.noOptions).should('be.visible'); | |
}); | |
clearInput(); | |
type('1'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.noOptions).should('not.exist'); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('filterOptions prop', () => { | |
it('by partial match', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const filterOptions = ({ opt }) => !opt?.label?.includes('456'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
filterOptions={filterOptions} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[0]; | |
clearInput(); | |
type(value.label.slice(0, 2)); | |
assertInputValue(value.label.slice(0, 2)); | |
assertDropdownOpen().within(() => { | |
assertListLength(3); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('without filter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const filterOptions = ({ opt }) => opt; | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
filterOptions={filterOptions} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[0]; | |
clearInput(); | |
type(value.label.slice(0, 2)); | |
assertInputValue(value.label.slice(0, 2)); | |
assertDropdownOpen().within(() => { | |
assertListLength(4); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('fuzzy search', () => { | |
describe('numbers', () => { | |
it('should fuzzy filter to closest number', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
fuzzy="numbers" | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[0]; | |
clearInput(); | |
type(+value.value + 3); | |
assertDropdownOpen().within(() => { | |
assertListLength(1).contains(value.label); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('no options stub', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
fuzzy="numbers" | |
hideNoOptions={false} | |
/> | |
); | |
assertDropdownClosed(); | |
clearInput(); | |
type('abcdef'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.noOptions).should('be.visible'); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
}); | |
}); | |
describe('reset text to selected option', () => { | |
it('on close', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
const value = options[0]; | |
// open | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains(value.label).click(); | |
}); | |
cy.get('@onChangeSpy').should('have.been.calledWith', value); | |
assertInputValue(value.label); | |
assertDropdownClosed(); | |
// edit text and close | |
type('{backspace}'); | |
cy.get(SELECTORS.otherInput).click(); | |
assertDropdownClosed(); | |
assertInputValue(value.label); | |
cy.get('@onChangeSpy').should('have.been.calledOnce'); | |
// edit text and close by esc | |
type('{backspace}{esc}'); | |
assertDropdownClosed(); | |
assertInputValue(value.label); | |
cy.get('@onChangeSpy').should('have.been.calledOnce'); | |
}); | |
}); | |
describe('keyboard navigation', () => { | |
it('no focused items on open', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.should('have.length', 0); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('selected item should be focused', () => { | |
const value = options[1]; | |
cy.mount(<Wrapper options={options} value={value} />); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length) | |
.filter('[data-focus="true"]') | |
.should('have.length', 1) | |
.contains(value.label); | |
}); | |
}); | |
it('should focus item by arrow up/down', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
assertDropdownOpen(); | |
// select down | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[0].value); | |
}); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[1].value); | |
}); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[2].value); | |
}); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[3].value); | |
}); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[3].value); | |
}); | |
type('{upArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[2].value); | |
}); | |
type('{upArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[1].value); | |
}); | |
// select up | |
type('{upArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[0].value); | |
}); | |
type('{upArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[0].value); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should reset focused after close dropdown', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
// select down | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[0].value); | |
}); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.contains(options[1].value); | |
}); | |
closeDropdown(); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.should('have.length', 0); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should select item by enter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />); | |
assertDropdownClosed(); | |
openDropdown(); | |
assertDropdownOpen(); | |
// first item | |
type('{downArrow}{enter}'); | |
cy.get('@onChangeSpy').should('have.been.calledWith', options[0]); | |
assertDropdownClosed(); | |
// second item | |
openDropdown(); | |
type('{downArrow}{enter}'); | |
cy.get('@onChangeSpy').should('have.been.calledWith', options[1]); | |
assertDropdownClosed(); | |
}); | |
}); | |
// For DrugSelect on Drug-table | |
describe('Custom onKeyDown handler', () => { | |
const onKeyDown = (_ev, { key, focused, setFocused }) => { | |
if (key === 'ArrowUp' || key === 'ArrowDown') { | |
return false; | |
} | |
if (key === 'Enter' && !!focused) { | |
return false; | |
} | |
if (key === 'Enter') { | |
return true; | |
} | |
if (key === 'Escape') { | |
return false; | |
} | |
setFocused(null); | |
}; | |
it('should skip default ev handler if custom onKeyDown returns positive result', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
onKeyDown={onKeyDown} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[1]; | |
type(`${value?.label.slice(0, 3)}{enter}`); | |
assertDropdownOpen().within(() => { | |
assertListLength(1).contains(value.label); | |
assertListLength(1) | |
.filter('[data-focus="true"]') | |
.should('have.length', 0); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('Arrow Down and select by enter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
onKeyDown={onKeyDown} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[0]; | |
type(`00{downArrow}`); | |
assertDropdownOpen().within(() => { | |
assertListLength(3) | |
.filter('[data-focus="true"]') | |
.contains(value.label); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
type(`{downArrow}{downArrow}{upArrow}{enter}`); | |
cy.get('@onChangeSpy').should('have.been.calledOnceWith', options[1]); | |
}); | |
it('Arrow Up and select by enter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
onKeyDown={onKeyDown} | |
/> | |
); | |
assertDropdownClosed(); | |
const value = options[0]; | |
type(`00{upArrow}`); | |
assertDropdownOpen().within(() => { | |
assertListLength(3) | |
.filter('[data-focus="true"]') | |
.contains(value.label); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
type(`{downArrow}{downArrow}{upArrow}{enter}`); | |
cy.get('@onChangeSpy').should('have.been.calledOnceWith', options[1]); | |
}); | |
it('Should close by Esc', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
options={options} | |
onChange={onChangeSpy} | |
onKeyDown={onKeyDown} | |
/> | |
); | |
assertDropdownClosed(); | |
type(`00{upArrow}{esc}`); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
assertDropdownClosed(); | |
}); | |
}); | |
describe('custom component as listItem', () => { | |
const customOption = { | |
label: '', | |
value: '', | |
Component: CustomNumberInput, | |
custom: true, | |
id: 'custom1', | |
headerTitle: 'Select a custom value', | |
}; | |
const optionsWithCustom = options.slice(0, 1).concat(customOption); | |
it('should render', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(optionsWithCustom.length); | |
assertCustomComponentVisible(); | |
assertCustomComponentValue('0'); | |
cy.get('[data-testid="customInput-title"]') | |
.should('be.visible') | |
.should('contain.text', customOption.headerTitle); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should display custom value', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = { | |
...optionsWithCustom[1], | |
value: '555', | |
}; | |
cy.mount( | |
<Wrapper | |
onChange={onChangeSpy} | |
value={value} | |
options={optionsWithCustom} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(optionsWithCustom.length); | |
cy.get(SELECTORS.listItem) | |
.filter('[aria-selected="true"]') | |
.should('have.length', 1); | |
assertCustomComponentVisible(); | |
assertCustomComponentValue('555'); | |
}); | |
closeDropdown(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should update value on click outside', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = '123'; | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
typeCustom(value); | |
closeDropdown(); | |
assertInputValue(value); | |
cy.get('@onChangeSpy').should('have.been.calledWith', { | |
...omitCustomProps(optionsWithCustom[1]), | |
label: value, | |
value: value, | |
}); | |
assertDropdownClosed(); | |
}); | |
it.skip('should not update value when dropdown still open(click outside CustomInput but inside dropdown)', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
onChange={onChangeSpy} | |
options={optionsWithCustom} | |
fuzzy="numbers" | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentVisible().within(() => { | |
cy.get(SELECTORS.plusIconSelector).click(); | |
cy.get(SELECTORS.plusIconSelector).click(); | |
}); | |
}); | |
// TODO: Can't click precisely between items :( | |
cy.get(SELECTORS.listbox).click(30, 1); | |
assertDropdownOpen(); | |
assertCustomComponentVisible(); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should format label by labelFormatter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = '123'; | |
const labelFormatter = value => `${value} units`; | |
const optionWithFormatter = { | |
...customOption, | |
labelFormatter, | |
}; | |
const optionsCustom = options.slice(0, 1).concat(optionWithFormatter); | |
cy.mount(<Wrapper onChange={onChangeSpy} options={optionsCustom} />); | |
openDropdown(); | |
typeCustom(value); | |
closeDropdown(); | |
assertInputValue(labelFormatter(value)); | |
cy.get('@onChangeSpy').should('have.been.calledWith', { | |
...omitCustomProps(optionsWithCustom[1]), | |
label: labelFormatter(value), | |
value: value, | |
}); | |
assertDropdownClosed(); | |
}); | |
it('should update value on click plus/minus icons', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentVisible().within(() => { | |
cy.get(SELECTORS.plusIconSelector).click(); | |
cy.get(SELECTORS.plusIconSelector).click(); | |
}); | |
assertCustomComponentValue('2'); | |
assertCustomComponentVisible().within(() => { | |
cy.get(SELECTORS.minusIconSelector).click(); | |
cy.get(SELECTORS.minusIconSelector).click(); | |
}); | |
assertCustomComponentValue('0'); | |
assertCustomComponentVisible().within(() => { | |
cy.get(SELECTORS.plusIconSelector).click(); | |
cy.get(SELECTORS.plusIconSelector).click(); | |
cy.get(SELECTORS.plusIconSelector).click(); | |
}); | |
}); | |
closeDropdown(); | |
cy.get('@onChangeSpy').should('have.been.calledWith', { | |
...omitCustomProps(optionsWithCustom[1]), | |
label: '3', | |
value: '3', | |
}); | |
assertDropdownClosed(); | |
}); | |
it('should return focus to select after entering custom value', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = '123'; | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
typeCustom(`${value}{enter}`); | |
}); | |
assertDropdownClosed(); | |
cy.get(SELECTORS.textFieldInput).should('be.focused'); | |
}); | |
describe('keyboard navigation', () => { | |
it('should focus on input', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
type('{downArrow}{downArrow}'); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentFocused(); | |
}); | |
cy.get(SELECTORS.customInputWrapper).type('{upArrow}'); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentNotFocused(); | |
}); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('should update value on enter', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = '123'; | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
cy.wait(100); | |
type('{downArrow}{enter}'); | |
cy.get('@onChangeSpy').should( | |
'have.been.calledWith', | |
optionsWithCustom[0] | |
); | |
openDropdown(); | |
cy.wait(100); | |
type(`{downArrow}`); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentFocused().type(`${value}{enter}`); | |
}); | |
assertInputValue(value); | |
cy.get('@onChangeSpy').should('have.been.calledWith', { | |
...omitCustomProps(optionsWithCustom[1]), | |
label: value, | |
value: value, | |
}); | |
assertDropdownClosed(); | |
}); | |
it('should update value after loose/gain focus', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = '123'; | |
cy.mount( | |
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} /> | |
); | |
openDropdown(); | |
type(`{downArrow}{downArrow}`); | |
assertDropdownOpen().within(() => { | |
assertCustomComponentFocused().type(`${value}{upArrow}`); | |
}); | |
type(`{downArrow}{enter}`); | |
assertInputValue(value); | |
cy.get('@onChangeSpy').should('have.been.calledWith', { | |
...omitCustomProps(optionsWithCustom[1]), | |
label: value, | |
value: value, | |
}); | |
assertDropdownClosed(); | |
}); | |
it('custom item should be focused when it selected', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const customValue = '777'; | |
cy.mount( | |
<Wrapper options={optionsWithCustom} onChange={onChangeSpy} /> | |
); | |
// Select custom | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.customInput).clear(); | |
cy.get(SELECTORS.customInput).type(customValue); | |
}); | |
// blur | |
closeDropdown(); | |
assertDropdownClosed(); | |
// open again | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[aria-selected="true"]') | |
.eq(0) | |
.within(() => { | |
cy.get(`input`) | |
.invoke('val') | |
.should('eq', customValue); | |
}); | |
}); | |
// move up/down | |
openDropdown(); | |
// focus on first item | |
type(`{upArrow}`); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.eq(0) | |
.contains(optionsWithCustom[0].value); | |
}); | |
// focus on custom | |
type(`{downArrow}`); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.eq(0) | |
.within(() => { | |
cy.get(`input`) | |
.invoke('val') | |
.should('eq', customValue); | |
}); | |
}); | |
}); | |
}); | |
}); | |
}); | |
describe('Input text', () => { | |
describe('onTextChange', () => { | |
it('should fire', () => { | |
const onTextChangeSpy = cy.spy().as('onTextChangeSpy'); | |
cy.mount(<Wrapper options={options} onTextChange={onTextChangeSpy} />); | |
const text = 'abc'; | |
type(text); | |
assertInputValue(text); | |
cy.get('@onTextChangeSpy').should('have.been.calledWith', text); | |
}); | |
}); | |
describe('inputText', () => { | |
it('should call onTextChange on mount', () => { | |
const text = ''; | |
const onTextChangeSpy = cy.spy().as('onTextChangeSpy'); | |
const value = options[1]; | |
cy.mount( | |
<Wrapper | |
options={options} | |
inputText={text} | |
value={value} | |
onTextChange={onTextChangeSpy} | |
/> | |
); | |
assertInputValue(value.label); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
}); | |
describe('default value', () => { | |
it('should set as default value', () => { | |
const text = 'abc'; | |
cy.mount(<Wrapper options={options} inputText={text} />); | |
assertInputValue(text); | |
}); | |
it('should prefer inputText over value.label', () => { | |
const text = 'abc'; | |
const value = options[1]; | |
cy.mount( | |
<Wrapper options={options} inputText={text} value={value} /> | |
); | |
assertInputValue(text); | |
}); | |
it('should use value.label if inputText empty', () => { | |
const text = ''; | |
const value = options[1]; | |
cy.mount( | |
<Wrapper options={options} inputText={text} value={value} /> | |
); | |
assertInputValue(value.label); | |
}); | |
}); | |
it('should select value from list', () => { | |
const onTextChangeSpy = cy.spy().as('onTextChangeSpy'); | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = options[1]; | |
const newValue = options[2]; | |
const text = value.label; | |
cy.mount( | |
<Wrapper | |
options={options} | |
inputText={text} | |
value={value} | |
onTextChange={onTextChangeSpy} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains(newValue.label).click(); | |
}); | |
assertInputValue(newValue.label); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
cy.get('@onChangeSpy').should('have.been.calledWith', newValue); | |
}); | |
it.skip('onBlur, can not properly test it for now', () => {}); | |
}); | |
}); | |
describe('should update Select input if prop changed', () => { | |
it('inputText', () => { | |
const onTextChangeSpy = cy.spy().as('onTextChangeSpy'); | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = options[1]; | |
const text = value.label; | |
cy.mount( | |
<Wrapper | |
options={options} | |
inputText={text} | |
value={value} | |
onTextChange={onTextChangeSpy} | |
onChange={onChangeSpy} | |
/> | |
); | |
getInputText().should('have.value', text); | |
assertInputValue(text); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
clearInputText(); | |
changeInputText('12345'); | |
assertInputValue('12345'); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
it('value', () => { | |
const onTextChangeSpy = cy.spy().as('onTextChangeSpy'); | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
const value = options[1]; | |
const text = value.label; | |
cy.mount( | |
<Wrapper | |
options={options} | |
inputText={text} | |
value={value} | |
onTextChange={onTextChangeSpy} | |
onChange={onChangeSpy} | |
/> | |
); | |
getInputText().should('have.value', text); | |
assertInputValue(text); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
const newValue = options[2]; | |
changeValue(newValue); | |
assertInputValue(newValue.label); | |
cy.get('@onTextChangeSpy').should( | |
'have.been.calledOnceWith', | |
value.label | |
); | |
cy.get('@onChangeSpy').should('not.have.been.called'); | |
}); | |
}); | |
describe('autoFocus', () => { | |
it('focused by default', () => { | |
cy.mount(<Wrapper options={options} renderInput={renderInput} />); | |
assertDropdownClosed(); | |
cy.get('[data-testid="props"]').within(() => { | |
cy.get('[data-testid="autoFocus"]').contains('true'); | |
}); | |
}); | |
it('should be false when passed', () => { | |
cy.mount( | |
<Wrapper | |
autoFocus={false} | |
options={options} | |
renderInput={renderInput} | |
/> | |
); | |
cy.get('[data-testid="props"]').within(() => { | |
cy.get('[data-testid="autoFocus"]').contains('false'); | |
}); | |
}); | |
}); | |
describe('clear icon', () => { | |
const value = options[0]; | |
it('hidden by default', () => { | |
cy.mount(<Wrapper options={options} value={value} />); | |
assertDropdownClosed(); | |
cy.get(SELECTORS.close).should('not.exist'); | |
}); | |
it('should work', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
showClearIcon={true} | |
options={options} | |
value={value} | |
onChange={onChangeSpy} | |
/> | |
); | |
cy.get(SELECTORS.close).should('exist'); | |
cy.get(SELECTORS.wrapper).realHover(); | |
cy.get(SELECTORS.close) | |
.should('be.visible') | |
.click(); | |
cy.get('@onChangeSpy').should('have.been.calledOnceWith', undefined); | |
}); | |
}); | |
describe('Toggle icon', () => { | |
it('visible by default', () => { | |
cy.mount(<Wrapper options={options} />); | |
assertDropdownClosed(); | |
cy.get(SELECTORS.toggle).should('be.visible'); | |
}); | |
it('should be hidden when showToggleIcon=false', () => { | |
cy.mount(<Wrapper options={options} showToggleIcon={false} />); | |
cy.get(SELECTORS.toggle).should('not.exist'); | |
}); | |
}); | |
describe('Custom components', () => { | |
const backgroundColor = 'rgb(255, 0, 0)'; | |
const customId = 'myCustomId123'; | |
describe('renderInput - custom text input', () => { | |
it('should pass props', () => { | |
const value = options[0]; | |
cy.mount( | |
<Wrapper | |
options={options} | |
value={value} | |
name="test name" | |
label="test label" | |
disabled={false} | |
renderInput={renderInput} | |
/> | |
); | |
cy.get('[data-testid="CustomSelect-text-field"]').should('not.exist'); | |
cy.get('[data-testid="customInput1"]').should('be.visible'); | |
cy.get('[data-testid="props"]').within(() => { | |
cy.get('[data-testid="value"]').contains(value.label); | |
cy.get('[data-testid="onChange"]').contains('func'); | |
cy.get('[data-testid="onClick"]').contains('func'); | |
cy.get('[data-testid="inputRef"]').contains('{}'); | |
cy.get('[data-testid="onFocus"]').contains('func'); | |
cy.get('[data-testid="autoFocus"]').contains('true'); | |
cy.get('[data-testid="onKeyDown"]').contains('func'); | |
cy.get('[data-testid="name"]').contains('test name'); | |
cy.get('[data-testid="label"]').contains('test label'); | |
cy.get('[data-testid="disabled"]').contains('false'); | |
cy.get('[data-testid="className"]').contains('textField'); | |
}); | |
}); | |
}); | |
describe('renderItem - custom listItem component', () => { | |
const renderItem = ({ selected, focused, label }) => { | |
return ( | |
<div className="custom"> | |
<div className="selected">{JSON.stringify(selected)}</div> | |
<div className="focused">{JSON.stringify(focused)}</div> | |
<div className="label">{JSON.stringify(label)}</div> | |
</div> | |
); | |
}; | |
it('should render', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
renderItem={renderItem} | |
options={options} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length); | |
assertFocusedSelected(); | |
}); | |
}); | |
it('should focus', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
renderItem={renderItem} | |
options={options} | |
onChange={onChangeSpy} | |
/> | |
); | |
openDropdown(); | |
type('{downArrow}'); | |
assertDropdownOpen().within(() => { | |
assertListLength(options.length); | |
assertFocusedSelected({ | |
focused: 0, | |
}); | |
}); | |
type('{downArrow}'); | |
assertFocusedSelected({ | |
focused: 1, | |
}); | |
type('{downArrow}'); | |
assertFocusedSelected({ | |
focused: 2, | |
}); | |
type('{downArrow}'); | |
assertFocusedSelected({ | |
focused: 3, | |
}); | |
type('{upArrow}'); | |
assertFocusedSelected({ | |
focused: 2, | |
}); | |
type('{upArrow}'); | |
assertFocusedSelected({ | |
focused: 1, | |
}); | |
type('{upArrow}'); | |
assertFocusedSelected({ | |
focused: 0, | |
}); | |
}); | |
it('should select', () => { | |
const onChangeSpy = cy.spy().as('onChangeSpy'); | |
cy.mount( | |
<Wrapper | |
renderItem={renderItem} | |
options={options} | |
onChange={onChangeSpy} | |
/> | |
); | |
const value = options[1]; | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.contains(value.label).click(); | |
}); | |
cy.get('@onChangeSpy').should('have.been.calledWith', value); | |
assertInputValue(value.label); | |
assertDropdownClosed(); | |
}); | |
}); | |
describe('PopperComponent - custom Popper', () => { | |
it('should work', () => { | |
const StyledPopper = styled(Popper)(() => ({ | |
backgroundColor, | |
})); | |
const CustomPopper = (props = {}) => { | |
return <StyledPopper {...props} data-testid={customId} />; | |
}; | |
cy.mount(<Wrapper options={options} PopperComponent={CustomPopper} />); | |
openDropdown(); | |
assertDropdownOpen(); | |
cy.get(`[data-testid="${customId}"]`) | |
.should('be.visible') | |
.should('have.css', 'background-color', backgroundColor); | |
}); | |
}); | |
describe('PaperComponent - custom Paper', () => { | |
it('should work', () => { | |
const StyledPaper = styled(Paper)(() => ({ | |
backgroundColor, | |
})); | |
const CustomPaper = (props = {}) => { | |
return <StyledPaper {...props} className={customId} />; | |
}; | |
cy.mount(<Wrapper options={options} PaperComponent={CustomPaper} />); | |
openDropdown(); | |
assertDropdownOpen(); | |
cy.get(`.${customId}`) | |
.should('be.visible') | |
.should('have.css', 'background-color', backgroundColor); | |
}); | |
}); | |
describe('renderListTitle - custom Title', () => { | |
it('should work', () => { | |
const StyledTitle = styled('div')(() => ({ | |
backgroundColor, | |
})); | |
const text = 'TestTitle12222'; | |
const CustomTitle = ({ headerTitle }) => { | |
return <StyledTitle className={customId}>{headerTitle}</StyledTitle>; | |
}; | |
cy.mount( | |
<Wrapper | |
options={options} | |
renderListTitle={CustomTitle} | |
headerTitle={text} | |
/> | |
); | |
openDropdown(); | |
assertDropdownOpen(); | |
cy.get(`.${customId}`) | |
.should('be.visible') | |
.should('have.css', 'background-color', backgroundColor) | |
.should('contain', text); | |
}); | |
}); | |
}); | |
describe('onUpdate', () => { | |
it('should update state', () => { | |
const onUpdateSpy = cy.spy().as('onUpdateSpy'); | |
cy.mount(<Wrapper options={options} onUpdate={onUpdateSpy} />); | |
cy.wrap(onUpdateSpy?.args).then(calls => { | |
cy.wrap( | |
calls.map(call => ({ | |
isOpen: call?.[0]?.isOpen, | |
focused: call?.[0]?.focused, | |
})) | |
).should('deep.equal', [{ isOpen: false, focused: null }]); | |
}); | |
openDropdown(); | |
assertDropdownOpen(); | |
cy.wrap(onUpdateSpy?.args).then(calls => { | |
cy.wrap( | |
calls.map(call => ({ | |
isOpen: call?.[0]?.isOpen, | |
focused: call?.[0]?.focused, | |
})) | |
).should('deep.equal', [ | |
{ isOpen: false, focused: null }, | |
{ isOpen: true, focused: null }, | |
]); | |
}); | |
type('{downArrow}'); | |
cy.wrap(onUpdateSpy?.args).then(calls => { | |
cy.wrap( | |
calls.map(call => ({ | |
isOpen: call?.[0]?.isOpen, | |
focused: call?.[0]?.focused, | |
})) | |
).should('deep.equal', [ | |
{ isOpen: false, focused: null }, | |
{ isOpen: true, focused: null }, | |
{ isOpen: true, focused: { label: '100mi', value: '100' } }, | |
]); | |
}); | |
closeDropdown(); | |
assertDropdownClosed(); | |
cy.wrap(onUpdateSpy?.args).then(calls => { | |
cy.wrap( | |
calls.map(call => ({ | |
isOpen: call?.[0]?.isOpen, | |
focused: call?.[0]?.focused, | |
})) | |
).should('deep.equal', [ | |
{ isOpen: false, focused: null }, | |
{ isOpen: true, focused: null }, | |
{ | |
isOpen: true, | |
focused: { label: '100mi', value: '100' }, | |
}, | |
{ | |
isOpen: false, | |
focused: { label: '100mi', value: '100' }, | |
}, | |
{ isOpen: false, focused: null }, | |
]); | |
}); | |
}); | |
}); | |
describe('getInitialFocused', () => { | |
it('should return which item focused', () => { | |
const focused = options[1]; | |
const getInitialFocused = ({ options }) => focused; | |
cy.mount( | |
<Wrapper options={options} getInitialFocused={getInitialFocused} /> | |
); | |
openDropdown(); | |
assertDropdownOpen().within(() => { | |
cy.get(SELECTORS.listItem) | |
.filter('[data-focus="true"]') | |
.should('have.length', 1) | |
.should('contain', focused.label); | |
}); | |
}); | |
}); | |
}); | |
const renderInput = props => ( | |
<div data-testid="props"> | |
{Object.entries(props).map(([name, value], ind) => ( | |
<div data-testid={name} key={ind}> | |
{name}: {typeof value === 'function' ? 'func' : JSON.stringify(value)} | |
</div> | |
))} | |
<input data-testid="customInput1" /> | |
</div> | |
); | |
function Wrapper({ | |
value: _value, | |
onChange = () => false, | |
inputText: _inputText, | |
...rest | |
}) { | |
const [value, setValue] = React.useState(_value); | |
const [inputText, setInputText] = React.useState(_inputText); | |
return ( | |
<ThemeProvider theme={theme}> | |
<CssBaseline /> | |
<div | |
style={{ | |
padding: '20px', | |
backgroundColor: 'grey', | |
display: 'flex', | |
flexDirection: 'column', | |
}} | |
> | |
<div | |
style={{ | |
padding: '10px', | |
width: '200px', | |
backgroundColor: 'white', | |
}} | |
> | |
<div data-testid="test"> | |
<input autoFocus={false} /> | |
</div> | |
<div data-testid="other">other</div> | |
<CustomSelect | |
{...rest} | |
onChange={val => { | |
setValue(val); | |
onChange(val); | |
}} | |
value={value} | |
inputText={inputText} | |
/> | |
<div style={{ margin: '20px 0' }}> | |
<input | |
data-testid="inputText" | |
onChange={ev => setInputText(ev?.target?.value)} | |
value={inputText} | |
autoFocus={false} | |
/> | |
</div> | |
<div style={{ margin: '20px 0' }}> | |
<input | |
data-testid="value" | |
onChange={ev => { | |
let isValidJson = false; | |
let jsonValue = ''; | |
try { | |
jsonValue = JSON.parse(ev?.target?.value); | |
isValidJson = true; | |
} catch (err) {} | |
if (isValidJson) { | |
setValue(jsonValue); | |
} | |
}} | |
autoFocus={false} | |
/> | |
</div> | |
</div> | |
</div> | |
</ThemeProvider> | |
); | |
} | |
const changeValue = (valueObj = {}) => { | |
cy.log('changeValue'); | |
const text = JSON.stringify(valueObj); | |
return getValueText() | |
.clear() | |
.type(text, { parseSpecialCharSequences: false }); | |
}; | |
const changeInputText = (text = '') => { | |
cy.log('changeInputText'); | |
return getInputText().type(text); | |
}; | |
const clearInputText = () => { | |
cy.log('clearInputText'); | |
getInputText().clear(); | |
}; | |
const getInputText = () => { | |
return cy.get('[data-testid="inputText"]'); | |
}; | |
const getValueText = () => { | |
return cy.get('[data-testid="value"]'); | |
}; | |
const assertFocusedSelected = ({ focused = -1, selected = -1 } = {}) => { | |
cy.log(`assertFocusedSelected focused:${focused}, selected:${selected}`); | |
return cy.wrap(options).each((opt, ind) => { | |
assertListItem({ | |
num: ind, | |
focused: ind === focused, | |
selected: selected === ind, | |
label: opt.label, | |
}); | |
}); | |
}; | |
const assertListItem = ({ num = 0, focused, selected, label }) => { | |
cy.log(`assertListItem numb:${num}`); | |
return cy | |
.get(SELECTORS.listItem) | |
.eq(num) | |
.within(() => { | |
cy.get('.selected').contains(String(selected)); | |
cy.get('.focused').contains(String(focused)); | |
cy.get('.label').contains(label); | |
}); | |
}; | |
const assertDropdownOpen = () => { | |
cy.log('assertDropdownOpen'); | |
return cy.get(SELECTORS.paper).should('be.visible'); | |
}; | |
const assertDropdownClosed = () => { | |
cy.log('assertDropdownClosed'); | |
return cy.get(SELECTORS.paper).should('not.exist'); | |
}; | |
const assertCustomComponentVisible = () => { | |
cy.log('assertCustomComponentVisible'); | |
return cy.get(SELECTORS.customInputWrapper).should('be.visible'); | |
}; | |
const assertCustomComponentFocused = () => { | |
cy.log('assertCustomComponentFocused'); | |
return cy.get(SELECTORS.customInput).should('be.focused'); | |
}; | |
const assertCustomComponentNotFocused = () => { | |
cy.log('assertCustomComponentNotFocused'); | |
return cy.get(SELECTORS.customInput).should('not.be.focused'); | |
}; | |
const assertListLength = (length = 0) => { | |
cy.log(`assertListLength ${0}`); | |
return cy.get(SELECTORS.listItem).should('have.length', length); | |
}; | |
const assertCustomComponentValue = (value = '') => { | |
cy.log(`assertCustomComponentValue ${value}`); | |
return cy | |
.get(SELECTORS.customInput) | |
.invoke('val') | |
.should('eq', value); | |
}; | |
const assertInputValue = (value = '') => { | |
cy.log(`assertInputValue ${value}`); | |
return cy | |
.get(SELECTORS.textFieldInput) | |
.invoke('val') | |
.should('eq', value); | |
}; | |
const clearInput = () => { | |
cy.log('clearInput'); | |
return cy.get(SELECTORS.textFieldInput).clear(); | |
}; | |
const type = (text = '') => { | |
cy.log(`type ${text}`); | |
return cy.get(SELECTORS.textFieldInput).type(text); | |
}; | |
const typeCustom = (text = '') => { | |
cy.log(`typeCustomInput ${text}`); | |
cy.get(SELECTORS.customInput).clear(); | |
return cy.get(SELECTORS.customInput).type(text); | |
}; | |
const closeDropdown = () => { | |
cy.log('closeDropdown'); | |
return cy.get(SELECTORS.otherElement).click(); | |
}; | |
const closeDropdownByEsc = () => { | |
cy.log('closeDropdownByEsc'); | |
return cy.get(SELECTORS.textFieldInput).type(`{esc}`); | |
}; | |
const openDropdown = () => { | |
cy.log('openDropdown'); | |
return cy.get(SELECTORS.textFieldInput).click(); | |
}; | |
const toggeDropdown = () => { | |
cy.log('toggeDropdown'); | |
return cy.get(SELECTORS.toggle).click(); | |
}; |
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
import React from 'react'; | |
import _ from 'lodash/fp'; | |
import clsx from 'clsx'; | |
import { sortClosestTo } from './common'; | |
import { useClickOutside, useFocusOutside } from 'hooks/useClickOutside'; | |
import { usePrevious } from './usePrevious'; | |
import { BoundariesContext } from './boundaryContext'; | |
import { | |
Wrapper, | |
ListItem, | |
Listbox, | |
StyledItemWrapper, | |
NoOptionsStub, | |
Buttons, | |
renderInputComponent, | |
CustomPaperComponent, | |
CustomPopperComponent, | |
CustomListTitleComponent, | |
} from './CustomSelect.styles'; | |
export const CustomSelect = ({ | |
value, | |
inputText, | |
onChange = _.noop, | |
onBlur = _.noop, | |
onTextChange = _.noop, | |
onKeyDown: onKeyDownProp, | |
className, | |
options = [], | |
fuzzy, | |
label = '', | |
name = '', | |
headerTitle = '', | |
disabled, | |
hasBorder = true, | |
hideNoOptions = true, | |
openOnFocus = true, | |
autoFocus = true, | |
showToggleIcon = true, | |
showClearIcon = false, | |
filterOptions = defaultFilterOptions, | |
StartIcon, | |
renderItem, | |
renderInput, | |
renderListTitle, | |
PopperComponent, | |
PaperComponent, | |
dataTestId, | |
onUpdate = _.noop, | |
getInitialFocused, | |
}) => { | |
const [isOpen, setIsOpen] = React.useState(false); | |
const { text, setText } = useText({ | |
textProp: inputText, | |
value, | |
}); | |
useTextUpdate({ | |
inputText, | |
value, | |
isOpen, | |
setText, | |
}); | |
const handleOpen = React.useCallback(() => setIsOpen(true), []); | |
const handleClose = React.useCallback(() => { | |
setIsOpen(false); | |
}, []); | |
const handleToggle = React.useCallback(ev => { | |
ev.preventDefault(); | |
ev.stopPropagation(); | |
setIsOpen(v => !v); | |
}, []); | |
const wrapperRef = React.useRef(); | |
const inputRef = React.useRef(); | |
const paperRef = React.useRef(); | |
const listboxRef = React.useRef(); | |
const refs = React.useMemo(() => [wrapperRef, inputRef, paperRef], []); | |
useClickOutside(handleClose, refs, 50); | |
useFocusOutside(handleClose, refs, 50); | |
const handleTextChange = React.useCallback( | |
ev => { | |
const value = ev?.target?.value || ''; | |
setText({ value, reason: 'type' }); | |
onTextChange(value); | |
handleOpen(); | |
}, | |
[handleOpen, onTextChange, setText] | |
); | |
useOnMountTextChange(text?.value, onTextChange); | |
const handleFocus = React.useCallback( | |
ev => { | |
ev.target.select(); | |
if (openOnFocus) { | |
handleOpen(); | |
} | |
}, | |
[handleOpen, openOnFocus] | |
); | |
const returnFocus = React.useCallback(() => { | |
inputRef.current?.focus?.(); | |
}, []); | |
const { optionsList } = useOptions({ | |
text: text?.value, | |
options, | |
isOpen, | |
fuzzy, | |
filterOptions, | |
}); | |
const handleSelect = React.useCallback( | |
opt => { | |
setText({ value: opt?.label || '', reason: 'select' }); | |
onChange(opt); | |
handleClose(); | |
}, | |
[handleClose, onChange, setText] | |
); | |
const handleClear = React.useCallback(() => { | |
setText({ value: '', reason: 'clear' }); | |
onChange(); | |
}, [onChange, setText]); | |
const { onKeyDown, focused: focusedItem } = useKeys({ | |
handleClose, | |
options: optionsList, | |
onKeyDownProp, | |
isOpen, | |
value, | |
onSelect: handleSelect, | |
getInitialFocused, | |
}); | |
const selectedItem = optionsList.find(opt => isEqual(opt, value)); | |
const boundariesElement = React.useContext(BoundariesContext); | |
const haveTitle = !!headerTitle; | |
const haveOptions = !!optionsList.length; | |
useEmitState({ | |
onUpdate, | |
isOpen, | |
focused: focusedItem, | |
handleClose, | |
}); | |
return ( | |
<Wrapper | |
ref={wrapperRef} | |
hasborder={+hasBorder} | |
className={`${className} wrapper`} | |
data-testid={dataTestId || 'Select'} | |
aria-expanded={isOpen} | |
> | |
{renderInputComponent({ | |
renderInput, | |
onBlur, | |
value: text?.value, | |
autoFocus, | |
onChange: handleTextChange, | |
onClick: handleOpen, | |
inputRef, | |
onFocus: handleFocus, | |
onKeyDown, | |
name, | |
label, | |
disabled, | |
StartIcon, | |
})} | |
<Buttons | |
isOpen={isOpen} | |
onToggle={showToggleIcon && handleToggle} | |
onClear={showClearIcon && handleClear} | |
disabled={disabled} | |
/> | |
<CustomPopperComponent | |
PopperComponent={PopperComponent} | |
open={!disabled && isOpen} | |
boundariesElement={boundariesElement} | |
wrapperRef={wrapperRef} | |
> | |
<CustomPaperComponent | |
PaperComponent={PaperComponent} | |
innerRef={paperRef} | |
> | |
<CustomListTitleComponent | |
renderListTitle={renderListTitle} | |
headerTitle={headerTitle} | |
/> | |
{!hideNoOptions && !haveOptions && <NoOptionsStub />} | |
{haveOptions && ( | |
<Listbox | |
role="listbox" | |
data-testid="CustomSelect-options-wrapper" | |
className={`listbox ${haveTitle ? 'haveTitle' : ''}`} | |
ref={listboxRef} | |
> | |
{optionsList.map(opt => { | |
const isFocused = isEqual(focusedItem, opt); | |
const isSelected = isEqual(selectedItem, opt); | |
const isCustom = isCustomOption(opt); | |
const key = opt?.id || opt?.value || opt?.label; | |
return ( | |
<ItemWrapper | |
key={key} | |
selected={isSelected} | |
focused={isFocused} | |
isCustom={isCustom} | |
listboxRef={listboxRef} | |
onClick={() => { | |
if (!isCustom) { | |
onChange(opt); | |
handleClose(); | |
} | |
}} | |
> | |
{isCustom ? ( | |
<opt.Component | |
headerTitle={opt?.headerTitle} | |
selected={isSelected} | |
focused={isFocused} | |
returnFocus={returnFocus} | |
value={isSelected && value} | |
wrapperRef={wrapperRef} | |
onChange={value => { | |
onChange({ | |
...omitCustomProps(opt), | |
label: opt?.labelFormatter?.(value) || value, | |
value, | |
}); | |
}} | |
close={handleClose} | |
onKeyDown={onKeyDown} | |
className="listItem customListItem" | |
/> | |
) : ( | |
<ListItem | |
selected={isSelected} | |
focused={isFocused} | |
label={opt?.label} | |
renderItem={renderItem} | |
text={text?.value} | |
opt={opt} | |
/> | |
)} | |
</ItemWrapper> | |
); | |
})} | |
</Listbox> | |
)} | |
</CustomPaperComponent> | |
</CustomPopperComponent> | |
</Wrapper> | |
); | |
}; | |
export const omitCustomProps = _.omit([ | |
'Component', | |
'headerTitle', | |
'labelFormatter', | |
]); | |
export const isCustomOption = opt => | |
String(opt?.id).includes('custom') || !!opt?.Component; | |
const isEqual = (opt1, opt2) => | |
!!opt1?.id && !!opt2?.id | |
? opt1?.id === opt2?.id | |
: opt1?.value === opt2?.value; | |
const useText = ({ textProp, value }) => { | |
const [text, setText] = React.useState({ | |
value: textProp || value?.label || '', | |
reason: 'prop-initial', | |
}); | |
return { text, setText }; | |
}; | |
const useEmitState = ({ onUpdate, ...state }) => { | |
const statePrev = usePrevious(state); | |
if (typeof onUpdate !== 'function') { | |
return false; | |
} | |
if (JSON.stringify(state) !== JSON.stringify(statePrev)) { | |
onUpdate(state); | |
} | |
}; | |
const useTextUpdate = ({ inputText, value, isOpen, setText }) => { | |
const prevInputText = usePrevious(inputText); | |
const inputTextChanged = | |
typeof prevInputText === 'string' && | |
typeof inputText === 'string' && | |
inputText !== prevInputText; | |
React.useEffect(() => { | |
if (inputTextChanged) { | |
setText({ value: inputText, reason: 'prop-inputText' }); | |
} | |
}, [inputText, inputTextChanged, setText]); | |
const isOpenPrev = usePrevious(isOpen); | |
React.useEffect(() => { | |
if (!!value && !isOpen && isOpenPrev) { | |
setText({ value: value?.label || '', reason: 'close' }); | |
} | |
}, [value, isOpen, setText, isOpenPrev]); | |
const valuePrev = usePrevious(value); | |
const valueChanged = !!value && !!valuePrev && !_.isEqual(value, valuePrev); | |
React.useEffect(() => { | |
if (valueChanged) { | |
setText({ value: value?.label || '', reason: 'prop-value' }); | |
} | |
}, [valueChanged, value, setText]); | |
}; | |
const useOnMountTextChange = (text, onTextChange) => { | |
React.useEffect(() => { | |
onTextChange(text); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, []); | |
}; | |
const useKeys = ({ | |
onKeyDownProp, | |
value, | |
handleClose, | |
isOpen, | |
options, | |
onSelect, | |
getInitialFocused, | |
}) => { | |
const selectedVisible = !!options.find(opt => isEqual(opt, value)); | |
const initialFocused = getInitialFocused?.({ options }) || null; | |
const defaultFocused = selectedVisible ? value : initialFocused; | |
const [focused, setFocused] = React.useState(defaultFocused); | |
React.useEffect(() => { | |
if (!isOpen || !!defaultFocused) { | |
setFocused(defaultFocused); | |
} | |
}, [defaultFocused, isOpen]); | |
const [min, max] = [0, options.length - 1]; | |
const onKeyDown = ev => { | |
const key = ev?.key; | |
const shouldSkipStandartHandler = onKeyDownProp?.(ev, { | |
key, | |
focused, | |
setFocused, | |
isOpen, | |
handleClose, | |
}); | |
if (shouldSkipStandartHandler) { | |
return false; | |
} | |
let ind = options.findIndex(opt => isEqual(opt, focused)); | |
if (key === 'Escape') { | |
ev.preventDefault(); | |
handleClose(); | |
} else if (key === 'ArrowUp') { | |
ev.preventDefault(); | |
if (ind === -1) { | |
ind = min + 1; | |
} | |
const newInd = ind - 1 >= min ? ind - 1 : min; | |
setFocused(options[newInd]); | |
} else if (key === 'ArrowDown') { | |
ev.preventDefault(); | |
const newInd = ind + 1 < max ? ind + 1 : max; | |
setFocused(options[newInd]); | |
} else if (key === 'Enter' && isOpen) { | |
ev.preventDefault(); | |
onSelect(omitCustomProps(focused)); | |
} | |
}; | |
return { onKeyDown, focused }; | |
}; | |
const useOptions = ({ text, options, isOpen, fuzzy, filterOptions }) => { | |
const isOpenPrev = usePrevious(isOpen); | |
const [shouldFilter, setShouldFilter] = React.useState(false); | |
React.useEffect(() => { | |
if (isOpen && !isOpenPrev) { | |
setShouldFilter(false); | |
return _.noop; | |
} | |
if (isOpen) { | |
setShouldFilter(!!text); | |
} else { | |
setShouldFilter(false); | |
} | |
}, [text, isOpen, isOpenPrev]); | |
const filtered = shouldFilter | |
? options.filter(opt => filterOptions({ opt, text })) | |
: options; | |
const optionsList = | |
fuzzy === 'numbers' ? sortFuzzy({ text, options, filtered }) : filtered; | |
return { optionsList }; | |
}; | |
const sortFuzzy = ({ filtered, options, text }) => { | |
const shouldSelectClosest = !filtered.length; | |
const value = parseInt(text); | |
const sorted = shouldSelectClosest | |
? sortClosestTo( | |
options.filter(opt => !isCustomOption(opt)), | |
value, | |
val => val?.value | |
) | |
: filtered; | |
const result = shouldSelectClosest ? sorted.slice(0, 1) : sorted; | |
return result; | |
}; | |
const ItemWrapper = ({ | |
children, | |
onClick, | |
isCustom, | |
selected, | |
focused, | |
listboxRef, | |
}) => { | |
const ref = React.useRef(); | |
React.useEffect(() => { | |
if (selected || focused) { | |
scrollToItem({ containerRef: listboxRef, itemRef: ref }); | |
} | |
}, [selected, focused, listboxRef]); | |
return ( | |
<StyledItemWrapper | |
ref={ref} | |
tabIndex={-1} | |
className={clsx( | |
!isCustom && 'option', | |
selected && 'selected', | |
'itemWrapper' | |
)} | |
role={isCustom ? undefined : 'option'} | |
aria-selected={selected} | |
data-focus={focused} | |
onClick={onClick} | |
> | |
{children} | |
</StyledItemWrapper> | |
); | |
}; | |
const scrollToItem = ({ containerRef, itemRef }) => { | |
const itemExist = typeof itemRef.current?.scrollTop === 'number'; | |
const listboxExist = typeof containerRef.current?.scrollTop === 'number'; | |
const noScrol = | |
containerRef.current?.scrollHeight === containerRef.current?.clientHeight; | |
if (noScrol || !itemExist || !listboxExist) { | |
return false; | |
} | |
// if have title on list | |
const listOffset = containerRef.current?.offsetTop || 0; | |
const listTop = containerRef.current?.scrollTop; | |
const listBottom = listTop + containerRef.current?.clientHeight; | |
const itemTop = itemRef.current?.offsetTop - listOffset; | |
const itemHeight = itemRef.current?.clientHeight; | |
const itemBottom = itemTop + itemHeight; | |
const itemVisible = itemBottom <= listBottom && itemTop >= listTop; | |
if (itemVisible) { | |
return false; | |
} | |
containerRef.current.scrollTop = itemTop; | |
}; | |
const defaultFilterOptions = ({ opt, text }) => | |
String(opt?.label) | |
.toLowerCase() | |
.includes(String(text).toLocaleLowerCase()); |
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
import React from 'react'; | |
import { styled } from '@material-ui/core/styles'; | |
import { | |
ArrowDropDown as ArrowDownIcon, | |
ArrowDropUp as ArrowUpIcon, | |
Close as MuiCloseIcon, | |
} from '@material-ui/icons'; | |
import { InputAdornment } from '@material-ui/core'; | |
import { IconButton } from '@material-ui/core'; | |
import { Paper } from 'roles/member/pages/WhatsMyCopaySearch/components/Paper'; | |
import { Popper as BasePopper } from 'roles/member/pages/WhatsMyCopaySearch/components/Popper'; | |
import { PADDING } from 'roles/member/pages/WhatsMyCopaySearch/components/Paper'; | |
import { TextField as MuiTextField } from 'roles/member/pages/WhatsMyCopaySearch/components/TextField'; | |
import { IconContainer } from 'roles/member/pages/WhatsMyCopaySearch/components/IconContainer'; | |
import { CheckmarkIcon } from 'roles/member/pages/WhatsMyCopaySearch/components/CheckmarkIcon'; | |
export const Wrapper = styled('div')(({ theme, hasborder }) => ({ | |
padding: '0 12px', | |
position: 'relative', | |
display: 'flex', | |
flexWrap: 'nowrap', | |
border: `1px solid ${ | |
hasborder ? theme.updPalette.border.main : 'transparent' | |
}`, | |
borderRadius: '10px', | |
'& .clear': { | |
visibility: 'hidden', | |
}, | |
'&:hover': { | |
'& .clear': { | |
visibility: 'visible', | |
}, | |
}, | |
'&:focus-within': { | |
border: `1px solid ${ | |
hasborder ? theme.updPalette.border.focused : 'transparent' | |
}`, | |
}, | |
height: '63px', | |
})); | |
const StartIconContainer = ({ StartIcon }) => { | |
if (!StartIcon) { | |
return null; | |
} | |
return ( | |
<InputAdornment | |
position="start" | |
css={{ height: '100%', alignItems: 'center', marginRight: '4px' }} | |
> | |
<IconContainer size="14"> | |
<StartIcon /> | |
</IconContainer> | |
</InputAdornment> | |
); | |
}; | |
export const Listbox = styled('ul')(() => ({ | |
overflowY: 'auto', | |
'&.haveTitle': { | |
paddingTop: 0, | |
}, | |
})); | |
const TextField = ({ | |
value, | |
onChange, | |
onClick, | |
disabled, | |
name, | |
label, | |
inputRef, | |
onFocus, | |
onKeyDown, | |
StartIcon, | |
}) => { | |
return ( | |
<StyledTextField | |
value={value} | |
onChange={onChange} | |
onClick={onClick} | |
inputRef={inputRef} | |
onFocus={onFocus} | |
onKeyDown={onKeyDown} | |
name={name} | |
label={label} | |
disabled={disabled} | |
fullWidth | |
variant="outlined" | |
InputLabelProps={inputLabelProps} | |
data-testid="CustomSelect-text-field" | |
className={`Select_component ${name}`} | |
InputProps={{ | |
startAdornment: <StartIconContainer StartIcon={StartIcon} />, | |
}} | |
/> | |
); | |
}; | |
const StyledTextField = styled(MuiTextField)(({ theme }) => ({ | |
height: '100%', | |
'& label.MuiInputLabel-outlined': { | |
color: theme.updTypography.placeholder.main.color, | |
transform: 'translateX(0px) translateY(20px)', | |
'&.MuiInputLabel-shrink.Mui-focused, &.MuiFormLabel-filled': { | |
transform: 'translateX(0px) translateY(10px) scale(0.75)', | |
}, | |
}, | |
'& .MuiInputBase-root': { | |
height: '100%', | |
padding: 0, | |
border: 'none!important', | |
'& .MuiInput-underline:before,.MuiInput-underline:after': { | |
display: 'none!important', | |
}, | |
'& [class*="NotchedOutline"], .MuiOutlinedInput-notchedOutline': { | |
display: 'none!important', | |
opacity: '0!important', | |
}, | |
'& input.MuiInputBase-input': { | |
padding: '0', | |
transform: 'translateY(12px)', | |
}, | |
}, | |
})); | |
function onDragStart(ev) { | |
ev.preventDefault(); | |
return false; | |
} | |
const defaultInputProps = { | |
autoComplete: 'off', | |
autoCapitalize: 'off', | |
className: 'textField', | |
spellCheck: false, | |
onDragStart, | |
}; | |
export const renderInputComponent = ({ | |
renderInput, | |
onBlur, | |
value, | |
autoFocus, | |
onChange, | |
onClick, | |
inputRef, | |
onFocus, | |
onKeyDown, | |
name, | |
label, | |
disabled, | |
StartIcon, | |
}) => { | |
const Props = { | |
...defaultInputProps, | |
onBlur, | |
value, | |
autoFocus, | |
onChange, | |
onClick, | |
inputRef, | |
onFocus, | |
onKeyDown, | |
name, | |
label, | |
disabled, | |
StartIcon, | |
}; | |
if (typeof renderInput === 'function') { | |
return renderInput(Props); | |
} | |
return <TextField {...Props} />; | |
}; | |
const Popper = styled(BasePopper)(() => ({ | |
zIndex: '1500', | |
'& .MuiPaper-root': { | |
maxHeight: '400px', | |
overflowY: 'auto', | |
}, | |
})); | |
export const CustomPopperComponent = ({ | |
PopperComponent, | |
open, | |
boundariesElement, | |
wrapperRef, | |
children, | |
}) => { | |
const minWidth = Math.max(wrapperRef.current?.clientWidth || 0, 150); | |
const Props = { | |
open, | |
anchorEl: wrapperRef.current, | |
boundariesElement, | |
disablePortal: true, | |
css: { minWidth }, | |
}; | |
if (typeof PopperComponent === 'function') { | |
return <PopperComponent {...Props}>{children}</PopperComponent>; | |
} | |
return <Popper {...Props}>{children}</Popper>; | |
}; | |
export const CustomPaperComponent = ({ | |
PaperComponent, | |
innerRef, | |
children, | |
}) => { | |
const Props = { | |
'data-testid': 'CustomSelect-paper', | |
innerRef, | |
className: 'paper', | |
}; | |
if (typeof PaperComponent === 'function') { | |
return <PaperComponent {...Props}>{children}</PaperComponent>; | |
} | |
return <Paper {...Props}>{children}</Paper>; | |
}; | |
export const Buttons = ({ isOpen, onToggle, onClear, disabled }) => { | |
return ( | |
<ButtonsWrapper tabIndex="-1" className="CustomSelect-toggle-container"> | |
{!!onClear && <ClearButton onClick={onClear} disabled={disabled} />} | |
{!!onToggle && ( | |
<ToggleButton onClick={onToggle} isOpen={isOpen} disabled={disabled} /> | |
)} | |
</ButtonsWrapper> | |
); | |
}; | |
const ButtonsWrapper = styled('div')(() => ({ | |
position: 'absolute', | |
right: '4px', | |
height: '100%', | |
maxWidth: '48px', | |
display: 'flex', | |
alignItems: 'center', | |
})); | |
const ButtonContainer = styled(IconButton)(() => ({ | |
'&': { | |
width: '24px', | |
height: '24px', | |
padding: 0, | |
'& svg, .MuiIconButton-label': { | |
width: '14px', | |
height: '14px', | |
}, | |
}, | |
})); | |
const ClearButton = ({ onClick, disabled }) => { | |
return ( | |
<ButtonContainer | |
onClick={onClick} | |
disabled={disabled} | |
tabIndex="-1" | |
className="clear" | |
aria-label="Clear" | |
> | |
<CloseIcon /> | |
</ButtonContainer> | |
); | |
}; | |
const CloseIcon = () => ( | |
<IconContainer | |
data-testid="CustomSelect-close" | |
size="14" | |
className="classIcon" | |
> | |
<MuiCloseIcon /> | |
</IconContainer> | |
); | |
const ToggleButton = ({ onClick, isOpen, disabled }) => { | |
return ( | |
<ButtonContainer | |
onClick={onClick} | |
disabled={disabled} | |
tabIndex="-1" | |
aria-label="Toggle" | |
> | |
<ToggleIcon isOpen={isOpen} /> | |
</ButtonContainer> | |
); | |
}; | |
const ToggleIcon = ({ isOpen }) => { | |
return ( | |
<IconContainer | |
data-testid="CustomSelect-toggle" | |
size="14" | |
className="toggleIcon" | |
> | |
{isOpen ? <ArrowUpIcon /> : <ArrowDownIcon />} | |
</IconContainer> | |
); | |
}; | |
const ListTitle = React.memo(({ text = '' }) => { | |
return ( | |
<TitleWrapper className="listTitle" data-testid="CustomSelect-title"> | |
{text} | |
</TitleWrapper> | |
); | |
}); | |
const TitleWrapper = styled('div')(({ theme }) => ({ | |
textTransform: 'uppercase', | |
marginLeft: PADDING, | |
paddingTop: PADDING, | |
paddingBottom: '12px', | |
color: theme.updTypography.h4.color, | |
fontWeight: 500, | |
fontSize: '11px', | |
lineHeight: '14px', | |
})); | |
export const CustomListTitleComponent = ({ renderListTitle, headerTitle }) => { | |
if (typeof renderListTitle === 'function') { | |
return renderListTitle({ headerTitle }); | |
} | |
return !!headerTitle && <ListTitle text={headerTitle} />; | |
}; | |
export const NoOptionsStub = () => { | |
return ( | |
<NoOptionsWrapper | |
data-testid="CustomSelect-no-options" | |
className="noOptions" | |
> | |
No options | |
</NoOptionsWrapper> | |
); | |
}; | |
const NoOptionsWrapper = styled('div')(() => ({ | |
padding: '24px', | |
})); | |
export const StyledItemWrapper = styled('li')(() => ({ | |
userSelect: 'none', | |
userDrag: 'none', | |
cursor: 'pointer', | |
display: 'flex', | |
alignItems: 'center', | |
justifyContent: 'space-between', | |
'& .checkmark': { | |
visibility: 'hidden', | |
}, | |
'&.selected .checkmark': { | |
visibility: 'visible', | |
}, | |
})); | |
export const StyledListItem = React.memo(({ label = '' }) => { | |
return ( | |
<ListItemWrapper className="listItem"> | |
{label} | |
<StyledIconContainer className="checkmark"> | |
<CheckmarkIcon /> | |
</StyledIconContainer> | |
</ListItemWrapper> | |
); | |
}); | |
export const ListItem = ({ | |
renderItem, | |
selected, | |
focused, | |
label, | |
text, | |
opt, | |
}) => { | |
if (typeof renderItem === 'function') { | |
return renderItem({ selected, focused, label, text, option: opt }); | |
} | |
return <StyledListItem label={label} />; | |
}; | |
const StyledIconContainer = styled(IconContainer)(({ theme }) => ({ | |
color: theme.updPalette.primary.main, | |
})); | |
const ListItemWrapper = styled('div')(() => ({ | |
display: 'flex', | |
flexWrap: 'nowrap', | |
alignItems: 'center', | |
justifyContent: 'space-between', | |
width: '100%', | |
})); | |
const inputLabelProps = { | |
disableAnimation: true, | |
}; |
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
import React from 'react'; | |
import _ from 'lodash/fp'; | |
import { useTimeout } from 'hooks/useTimeout'; | |
export const useClickOutside = (cb = _.noop, refs, delay = 0) => { | |
const cbHandler = useTimeout(cb, delay); | |
React.useEffect(() => { | |
const handleClick = ev => { | |
const isOutside = refs | |
.filter(r => !!r.current) | |
.every(r => !r.current.contains(ev.target)); | |
if (isOutside) { | |
cbHandler(); | |
} | |
}; | |
document.addEventListener('mousedown', handleClick); | |
document.addEventListener('touchstart', handleClick); | |
return () => { | |
document.removeEventListener('mousedown', handleClick); | |
document.removeEventListener('touchstart', handleClick); | |
}; | |
}, [refs, cbHandler]); | |
}; | |
export const useFocusOutside = (cb = _.noop, refs, delay = 0) => { | |
const cbHandler = useTimeout(cb, delay); | |
React.useEffect(() => { | |
const handleClick = ev => { | |
const isOutside = refs | |
.filter(r => !!r.current) | |
.every(r => !r.current.contains(ev.target)); | |
if (isOutside) { | |
cbHandler(); | |
} | |
}; | |
document.addEventListener('focusin', handleClick); | |
return () => { | |
document.removeEventListener('focusin', handleClick); | |
}; | |
}, [refs, cbHandler]); | |
}; |
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
import React from 'react'; | |
export const usePrevious = value => { | |
const ref = React.useRef(); | |
React.useEffect(() => { | |
ref.current = value; | |
}, [value]); | |
return ref.current; | |
}; |
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
/** | |
* Sort array of numbers by closest to value | |
* @template T array of numbers or object with numeric property | |
* @param {T[]} data Arr for sorting | |
* @param {Number} value Value to sort by | |
* @param {(arg: T) => number} getter Fn to get numeric prop of an object | |
* @param {(arg: T) => boolean} filter Fn to filter options before sort | |
* @returns {T[]} `T[]` | |
*/ | |
export const sortClosestTo = ( | |
data, | |
value, | |
getter = val => val, | |
filter = false | |
) => { | |
if (!isNumber(value) || !Array.isArray(data)) { | |
return []; | |
} | |
const filteredData = | |
typeof filter === 'function' ? data.filter(filter) : data; | |
return filteredData.sort( | |
(a, b) => Math.abs(+getter(a) - +value) - Math.abs(+getter(b) - +value) | |
); | |
}; |
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
import { sortClosestTo } from './utils'; | |
describe('sortClosestTo', () => { | |
it('should return array sorted by closest to value', () => { | |
const arr = [1, 2, 3, 4, '5', 6, 7]; | |
expect(sortClosestTo(arr, 0)).toStrictEqual([1, 2, 3, 4, '5', 6, 7]); | |
expect(sortClosestTo(arr, 1)).toStrictEqual([1, 2, 3, 4, '5', 6, 7]); | |
expect(sortClosestTo(arr, 7)).toStrictEqual([7, 6, '5', 4, 3, 2, 1]); | |
expect(sortClosestTo(arr, 4)).toStrictEqual([4, '5', 3, 6, 2, 7, 1]); | |
expect(sortClosestTo(arr, 2)).toStrictEqual([2, 3, 1, 4, '5', 6, 7]); | |
expect(sortClosestTo(arr, 6)).toStrictEqual([6, '5', 7, 4, 3, 2, 1]); | |
}); | |
it('should sort with getter', () => { | |
const arr = [ | |
1, | |
2, | |
{ label: '3', value: '3' }, | |
4, | |
'5', | |
6, | |
{ label: '7', value: 7 }, | |
]; | |
const getter = val => val?.value ?? val; | |
expect(sortClosestTo(arr, 1, getter)).toStrictEqual([ | |
1, | |
2, | |
{ label: '3', value: '3' }, | |
4, | |
'5', | |
6, | |
{ label: '7', value: 7 }, | |
]); | |
expect(sortClosestTo(arr, 7, getter)).toStrictEqual([ | |
{ label: '7', value: 7 }, | |
6, | |
'5', | |
4, | |
{ label: '3', value: '3' }, | |
2, | |
1, | |
]); | |
expect(sortClosestTo(arr, 4, getter)).toStrictEqual([ | |
4, | |
'5', | |
{ label: '3', value: '3' }, | |
6, | |
2, | |
{ label: '7', value: 7 }, | |
1, | |
]); | |
}); | |
it('should return array filtered by filter fn', () => { | |
const arr = [1, 2, 3, 4, '5', 6, 7]; | |
const isEven = val => val % 2 === 0; | |
const isOdd = val => !isEven(val); | |
expect(sortClosestTo(arr, 0, undefined, isEven)).toStrictEqual([2, 4, 6]); | |
expect(sortClosestTo(arr, 1, undefined, isOdd)).toStrictEqual([ | |
1, | |
3, | |
'5', | |
7, | |
]); | |
}); | |
it('should return result if values empty/invalid', () => { | |
expect(sortClosestTo()).toStrictEqual([]); | |
expect(sortClosestTo([])).toStrictEqual([]); | |
expect(sortClosestTo([], 1)).toStrictEqual([]); | |
expect(sortClosestTo([7, 1, 2], 'a')).toStrictEqual([]); | |
expect(sortClosestTo([7, 1, 2], '')).toStrictEqual([]); | |
expect(sortClosestTo([7, 1, 2], null)).toStrictEqual([]); | |
expect(sortClosestTo([7, 1, 2], undefined)).toStrictEqual([]); | |
expect(sortClosestTo([7, 1, 2], {})).toStrictEqual([]); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment