feat: add typeahead component (#375)
This commit is contained in:
16
package-lock.json
generated
16
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-onclickoutside": "^6.13.0",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
@@ -18826,6 +18827,19 @@
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-onclickoutside": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
|
||||
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
|
||||
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-overlays": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
|
||||
@@ -23393,4 +23407,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-onclickoutside": "^6.13.0",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
@@ -98,7 +99,7 @@
|
||||
"@edx/frontend-platform": "^4.0.0",
|
||||
"@edx/paragon": "^20.27.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.14.0 || ^17.0.0",
|
||||
"react": "^16.14.0 || ^17.0.0",
|
||||
"react-dom": "^16.14.0 || ^17.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
99
src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx
Normal file
99
src/editors/sharedComponents/TypeaheadDropdown/FormGroup.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash-es';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
const handleFocus = (e) => {
|
||||
if (props.handleFocus) { props.handleFocus(e); }
|
||||
};
|
||||
const handleClick = (e) => {
|
||||
if (props.handleClick) { props.handleClick(e); }
|
||||
};
|
||||
const handleOnBlur = (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Form.Group
|
||||
isInvalid={!!props.errorMessage}
|
||||
onBlur={handleOnBlur}
|
||||
className={props.className}
|
||||
>
|
||||
<Form.Control
|
||||
data-testid="formControl"
|
||||
aria-invalid={props.errorMessage}
|
||||
autoComplete={props.autoComplete ? 'on' : 'off'}
|
||||
onChange={props.handleChange}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{props.options ? props.options() : null}
|
||||
</Form.Control>
|
||||
|
||||
{props.children}
|
||||
|
||||
{props.helpText && _.isEmpty(props.errorMessage) && (
|
||||
<Form.Control.Feedback type="default" key="help-text">
|
||||
{props.helpText}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
|
||||
{!_.isEmpty(props.errorMessage) && (
|
||||
<Form.Control.Feedback
|
||||
type="invalid"
|
||||
key="error"
|
||||
feedback-for={props.name}
|
||||
data-testid="errorMessage"
|
||||
>
|
||||
{props.errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
FormGroup.defaultProps = {
|
||||
as: 'input',
|
||||
errorMessage: '',
|
||||
autoComplete: null,
|
||||
readOnly: false,
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleFocus: null,
|
||||
handleClick: null,
|
||||
helpText: '',
|
||||
placeholder: '',
|
||||
options: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
children: null,
|
||||
className: '',
|
||||
controlClassName: '',
|
||||
};
|
||||
|
||||
FormGroup.propTypes = {
|
||||
as: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleClick: PropTypes.func,
|
||||
helpText: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.func,
|
||||
trailingElement: PropTypes.element,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
children: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
controlClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormGroup;
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import FormGroup from './FormGroup';
|
||||
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('@edx/paragon/icons');
|
||||
|
||||
const mockHandleChange = jest.fn();
|
||||
const mockHandleFocus = jest.fn();
|
||||
const mockHandleClick = jest.fn();
|
||||
const mockHandleBlur = jest.fn();
|
||||
const defaultProps = {
|
||||
as: 'input',
|
||||
errorMessage: '',
|
||||
borderClass: '',
|
||||
autoComplete: null,
|
||||
readOnly: false,
|
||||
handleBlur: mockHandleBlur,
|
||||
handleChange: mockHandleChange,
|
||||
handleFocus: mockHandleFocus,
|
||||
handleClick: mockHandleClick,
|
||||
helpText: 'helpText text',
|
||||
options: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
children: null,
|
||||
className: '',
|
||||
floatingLabel: 'floatingLabel text',
|
||||
name: 'title',
|
||||
value: '',
|
||||
};
|
||||
|
||||
const renderComponent = (props) => render(<FormGroup {...props} />);
|
||||
|
||||
describe('FormGroup', () => {
|
||||
it('renders component without error', () => {
|
||||
renderComponent(defaultProps);
|
||||
expect(screen.getByText(defaultProps.floatingLabel)).toBeVisible();
|
||||
expect(screen.getByText(defaultProps.helpText)).toBeVisible();
|
||||
expect(screen.queryByTestId('errorMessage')).toBeNull();
|
||||
});
|
||||
it('renders component with error', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
errorMessage: 'error message',
|
||||
};
|
||||
renderComponent(newProps);
|
||||
expect(screen.getByText(defaultProps.floatingLabel)).toBeVisible();
|
||||
expect(screen.getByText(newProps.errorMessage)).toBeVisible();
|
||||
expect(screen.queryByText(defaultProps.helpText)).toBeNull();
|
||||
});
|
||||
it('handles element focus', async () => {
|
||||
renderComponent(defaultProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
expect(mockHandleFocus).toHaveBeenCalled();
|
||||
});
|
||||
it('handles element blur', () => {
|
||||
renderComponent(defaultProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
fireEvent.focusOut(formInput);
|
||||
expect(mockHandleBlur).toHaveBeenCalled();
|
||||
});
|
||||
it('handles element click', () => {
|
||||
renderComponent(defaultProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.click(formInput);
|
||||
expect(mockHandleClick).toHaveBeenCalled();
|
||||
});
|
||||
it('handles element change', () => {
|
||||
renderComponent(defaultProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
userEvent.type(formInput, 'opt1');
|
||||
expect(mockHandleChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
268
src/editors/sharedComponents/TypeaheadDropdown/index.jsx
Normal file
268
src/editors/sharedComponents/TypeaheadDropdown/index.jsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Button,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { Add, ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import FormGroup from './FormGroup';
|
||||
|
||||
class TypeaheadDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFocused: false,
|
||||
displayValue: '',
|
||||
icon: this.expandMoreButton(),
|
||||
dropDownItems: [],
|
||||
};
|
||||
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.props.value !== nextProps.value && nextProps.value !== '') {
|
||||
const opt = this.props.options.find((o) => o === nextProps.value);
|
||||
if (opt && opt !== this.state.displayValue) {
|
||||
this.setState({ displayValue: opt });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
getItems(strToFind = '') {
|
||||
let { options } = this.props;
|
||||
|
||||
if (strToFind.length > 0) {
|
||||
options = options.filter((option) => (option.toLowerCase().includes(strToFind.toLowerCase())));
|
||||
}
|
||||
|
||||
return options.map((opt) => {
|
||||
let value = opt;
|
||||
if (value.length > 30) {
|
||||
value = value.substring(0, 30).concat('...');
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="dropdown-item data-hj-suppress"
|
||||
value={value}
|
||||
key={value}
|
||||
onClick={(e) => { this.handleItemClick(e); }}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
if (this.props.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.handleChange) {
|
||||
this.props.handleChange(value);
|
||||
}
|
||||
|
||||
const opt = this.props.options.find((o) => o === value);
|
||||
if (opt && opt !== this.state.displayValue) {
|
||||
this.setState({ displayValue: opt });
|
||||
}
|
||||
}
|
||||
|
||||
setDisplayValue(value) {
|
||||
const normalized = value.toLowerCase();
|
||||
const opt = this.props.options.find((o) => o.toLowerCase() === normalized);
|
||||
if (opt) {
|
||||
this.setValue(opt);
|
||||
this.setState({ displayValue: opt });
|
||||
} else {
|
||||
this.setValue('');
|
||||
this.setState({ displayValue: value });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const dropDownItems = this.getItems(e.target.value);
|
||||
if (dropDownItems.length > 1) {
|
||||
this.setState({ dropDownItems, icon: this.expandLessButton() });
|
||||
}
|
||||
|
||||
if (this.state.dropDownItems.length > 0) {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
};
|
||||
|
||||
handleOnChange = (e) => {
|
||||
const findstr = e.target.value;
|
||||
|
||||
if (findstr.length) {
|
||||
const filteredItems = this.getItems(findstr);
|
||||
this.setState({ dropDownItems: filteredItems, icon: this.expandLessButton() });
|
||||
} else {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
this.setDisplayValue(e.target.value);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/no-unused-class-component-methods
|
||||
handleClickOutside = () => {
|
||||
if (this.state.dropDownItems.length > 0) {
|
||||
this.setState(() => ({
|
||||
icon: this.expandMoreButton(),
|
||||
dropDownItems: '',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
handleExpandLess() {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
handleExpandMore(e) {
|
||||
const dropDownItems = this.getItems(e.target.value);
|
||||
this.setState({ dropDownItems, icon: this.expandLessButton() });
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
this.setState({ isFocused: true });
|
||||
if (this.props.handleFocus) { this.props.handleFocus(e); }
|
||||
}
|
||||
|
||||
handleOnBlur(e) {
|
||||
this.setState({ isFocused: false });
|
||||
if (this.props.handleBlur) { this.props.handleBlur(e); }
|
||||
}
|
||||
|
||||
handleItemClick(e) {
|
||||
this.setValue(e.target.value);
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
expandMoreButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-more"
|
||||
data-testid="expand-more-button"
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-more"
|
||||
onClick={(e) => { this.handleExpandMore(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
expandLessButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-less"
|
||||
data-testid="expand-less-button"
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-less"
|
||||
onClick={(e) => { this.handleExpandLess(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const noOptionsMessage = (
|
||||
<ActionRow className="p-2 pl-3">
|
||||
<div className="muted">{this.props.noOptionsMessage}</div>
|
||||
<ActionRow.Spacer />
|
||||
{this.props.allowNewOption && (
|
||||
<Button
|
||||
data-testid="add-option-button"
|
||||
iconBefore={Add}
|
||||
onClick={this.props.addNewOption}
|
||||
>
|
||||
{this.props.newOptionButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
const dropDownEmptyList = this.state.dropDownItems && this.state.isFocused ? noOptionsMessage : null;
|
||||
return (
|
||||
<div className="dropdown-group-wrapper">
|
||||
<FormGroup
|
||||
name={this.props.name}
|
||||
type="text"
|
||||
value={this.state.displayValue}
|
||||
readOnly={this.props.readOnly}
|
||||
controlClassName={this.props.controlClassName}
|
||||
errorMessage={this.props.errorMessage}
|
||||
trailingElement={this.state.icon}
|
||||
floatingLabel={this.props.floatingLabel}
|
||||
placeholder={this.props.placeholder}
|
||||
helpText={this.props.helpMessage}
|
||||
handleChange={this.handleOnChange}
|
||||
handleClick={this.handleClick}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleFocus}
|
||||
isFocused={this.state.isFocused}
|
||||
>
|
||||
<div
|
||||
data-testid="dropdown-container"
|
||||
className="dropdown-container mt-2 rounded bg-light-100 box-shadow-centered-1 mr-2"
|
||||
style={{ maxHeight: '300px', overflowY: 'scroll' }}
|
||||
>
|
||||
{ this.state.dropDownItems.length > 0 ? this.state.dropDownItems : dropDownEmptyList }
|
||||
</div>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TypeaheadDropdown.defaultProps = {
|
||||
options: null,
|
||||
floatingLabel: null,
|
||||
handleFocus: null,
|
||||
handleChange: null,
|
||||
handleBlur: null,
|
||||
helpMessage: '',
|
||||
placeholder: '',
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
readOnly: false,
|
||||
controlClassName: '',
|
||||
allowNewOption: false,
|
||||
newOptionButtonLabel: '',
|
||||
addNewOption: null,
|
||||
};
|
||||
|
||||
TypeaheadDropdown.propTypes = {
|
||||
noOptionsMessage: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.string),
|
||||
floatingLabel: PropTypes.string,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleBlur: PropTypes.func,
|
||||
helpMessage: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
controlClassName: PropTypes.string,
|
||||
allowNewOption: PropTypes.bool,
|
||||
newOptionButtonLabel: PropTypes.string,
|
||||
addNewOption: PropTypes.func,
|
||||
};
|
||||
|
||||
export default onClickOutside(TypeaheadDropdown);
|
||||
136
src/editors/sharedComponents/TypeaheadDropdown/index.test.jsx
Normal file
136
src/editors/sharedComponents/TypeaheadDropdown/index.test.jsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import TypeaheadDropdown from '.';
|
||||
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('@edx/paragon/icons');
|
||||
|
||||
const defaultProps = {
|
||||
as: 'input',
|
||||
name: 'OrganizationDropdown',
|
||||
floatingLabel: 'floatingLabel text',
|
||||
options: null,
|
||||
handleFocus: null,
|
||||
handleChange: null,
|
||||
handleBlur: null,
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
errorCode: null,
|
||||
readOnly: false,
|
||||
noOptionsMessage: 'No options',
|
||||
};
|
||||
const renderComponent = (props) => render(<TypeaheadDropdown {...props} />);
|
||||
|
||||
describe('common/OrganizationDropdown.jsx', () => {
|
||||
it('renders component without error', () => {
|
||||
renderComponent(defaultProps);
|
||||
expect(screen.getByText(defaultProps.floatingLabel)).toBeVisible();
|
||||
});
|
||||
it('handles element focus', () => {
|
||||
const mockHandleFocus = jest.fn();
|
||||
const newProps = { ...defaultProps, handleFocus: mockHandleFocus };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
expect(mockHandleFocus).toHaveBeenCalled();
|
||||
});
|
||||
it('handles element blur', () => {
|
||||
const mockHandleBlur = jest.fn();
|
||||
const newProps = { ...defaultProps, handleBlur: mockHandleBlur };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
fireEvent.focusOut(formInput);
|
||||
expect(mockHandleBlur).toHaveBeenCalled();
|
||||
});
|
||||
it('renders component with options', () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.click(formInput);
|
||||
const optionsList = within(screen.getByTestId('dropdown-container')).getAllByRole('button');
|
||||
expect(optionsList.length).toEqual(newProps.options.length);
|
||||
});
|
||||
it('selects option', () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.click(formInput);
|
||||
const optionsList = within(screen.getByTestId('dropdown-container')).getAllByRole('button');
|
||||
fireEvent.click(optionsList.at([0]));
|
||||
expect(formInput.value).toEqual(newProps.options[0]);
|
||||
});
|
||||
it('toggles options list', async () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const optionsList = within(screen.getByTestId('dropdown-container')).queryAllByRole('button');
|
||||
expect(optionsList.length).toEqual(0);
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('expand-more-button'));
|
||||
});
|
||||
expect(within(screen.getByTestId('dropdown-container'))
|
||||
.queryAllByRole('button').length).toEqual(newProps.options.length);
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('expand-less-button'));
|
||||
});
|
||||
expect(within(screen.getByTestId('dropdown-container'))
|
||||
.queryAllByRole('button').length).toEqual(0);
|
||||
});
|
||||
it('shows options list depends on field value', () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
userEvent.type(formInput, 'opt1');
|
||||
expect(within(screen.getByTestId('dropdown-container'))
|
||||
.queryAllByRole('button').length).toEqual(1);
|
||||
});
|
||||
it('closes options list on click outside', async () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.click(formInput);
|
||||
expect(within(screen.getByTestId('dropdown-container'))
|
||||
.queryAllByRole('button').length).toEqual(2);
|
||||
userEvent.click(document.body);
|
||||
expect(within(screen.getByTestId('dropdown-container'))
|
||||
.queryAllByRole('button').length).toEqual(0);
|
||||
});
|
||||
describe('empty options list', () => {
|
||||
it('shows empty options list depends on field value', () => {
|
||||
const newProps = { ...defaultProps, options: ['opt1', 'opt2'] };
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
userEvent.type(formInput, '3');
|
||||
const noOptionsList = within(screen.getByTestId('dropdown-container')).getByText('No options');
|
||||
const addButton = within(screen.getByTestId('dropdown-container')).queryByTestId('add-option-button');
|
||||
expect(noOptionsList).toBeVisible();
|
||||
expect(addButton).toBeNull();
|
||||
});
|
||||
it('shows empty options list with add option button', () => {
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
options: ['opt1', 'opt2'],
|
||||
allowNewOption: true,
|
||||
newOptionButtonLabel: 'Add new option',
|
||||
addNewOption: jest.fn(),
|
||||
};
|
||||
renderComponent(newProps);
|
||||
const formInput = screen.getByTestId('formControl');
|
||||
fireEvent.focus(formInput);
|
||||
userEvent.type(formInput, '3');
|
||||
const noOptionsList = within(screen.getByTestId('dropdown-container')).getByText('No options');
|
||||
expect(noOptionsList).toBeVisible();
|
||||
const addButton = within(screen.getByTestId('dropdown-container')).getByTestId('add-option-button');
|
||||
expect(addButton).toHaveTextContent(newProps.newOptionButtonLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import ErrorAlert from './editors/sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import Footer from './footer';
|
||||
import { TinyMceWidget } from './editors/sharedComponents/TinyMceWidget';
|
||||
import { prepareEditorRef } from './editors/sharedComponents/TinyMceWidget/hooks';
|
||||
import TypeaheadDropdown from './editors/sharedComponents/TypeaheadDropdown';
|
||||
|
||||
export {
|
||||
messages,
|
||||
@@ -18,5 +19,6 @@ export {
|
||||
Footer,
|
||||
TinyMceWidget,
|
||||
prepareEditorRef,
|
||||
TypeaheadDropdown,
|
||||
};
|
||||
export default Placeholder;
|
||||
|
||||
Reference in New Issue
Block a user