Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/src/components/Menu/Menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@
display: none;

.font-icon-silverstripe-cms:before {
vertical-align: middle;
display: block;
}
}

Expand Down
138 changes: 67 additions & 71 deletions client/src/components/OptionsetField/OptionField.js
Original file line number Diff line number Diff line change
@@ -1,93 +1,98 @@
import React, { Component } from 'react';
import React from 'react';
import { FormGroup, Input, Label } from 'reactstrap';
import classnames from 'classnames';
import castStringToElement from 'lib/castStringToElement';
import PropTypes from 'prop-types';

class OptionField extends Component {
constructor(props) {
super(props);

this.handleChange = this.handleChange.bind(this);
}

/**
* Fetches the properties for the field
*
* @returns {object} properties
*/
getInputProps() {
const classes = classnames({
[this.props.className]: true,
[this.props.extraClass]: true,
checked: this.props.value,
disabled: this.props.readOnly,
'option-field--disabled': this.props.readOnly || this.props.disabled,
});
const inputProps = {
id: this.props.id,
type: this.props.type,
name: this.props.name,
disabled: this.props.disabled || this.props.readOnly,
readOnly: this.props.readOnly,
className: classes,
onChange: this.handleChange,
checked: !!this.props.value,
value: 1,
};
if (this.props.role) {
inputProps.role = this.props.role;
}
return inputProps;
}
const OptionField = (_props) => {
const defaultProps = {
// React considers "undefined" as an uncontrolled component.
extraClass: '',
className: '',
type: 'radio',
leftTitle: null,
rightTitle: null,
};
const props = {
...defaultProps,
..._props,
};

/**
* React recommends using `onClick`, however react-bootstrap uses `onChange`
*
* @param {Event} event
*/
handleChange(event) {
if (this.props.readOnly || this.props.disabled) {
const handleChange = (event) => {
if (props.readOnly || props.disabled) {
event.preventDefault();
return;
}

let callback = null;
if (typeof this.props.onChange === 'function') {
if (typeof props.onChange === 'function') {
// call onChange for `FormBuilder` and `redux-form` to work
callback = this.props.onChange;
} else if (typeof this.props.onClick === 'function') {
callback = props.onChange;
} else if (typeof props.onClick === 'function') {
// for other React components which needs compatibility with this component
callback = this.props.onClick;
callback = props.onClick;
}

if (callback) {
callback(event, {
id: this.props.id,
id: props.id,
value: event.target.checked ? 1 : 0,
});
}
}
};

/**
* Fetches the properties for the field
*
* @returns {object} properties
*/
const getInputProps = () => {
const classes = classnames({
[props.className]: true,
[props.extraClass]: true,
checked: props.value,
disabled: props.readOnly,
'option-field--disabled': props.readOnly || props.disabled,
});
const inputProps = {
id: props.id,
type: props.type,
name: props.name,
disabled: props.disabled || props.readOnly,
readOnly: props.readOnly,
className: classes,
onChange: handleChange,
checked: !!props.value,
value: 1,
};
if (props.role) {
inputProps.role = props.role;
}
return inputProps;
};

render() {
const leftTitle = this.props.leftTitle !== null
? this.props.leftTitle
: this.props.title;
const leftTitle = props.leftTitle !== null
? props.leftTitle
: props.title;

const labelText = this.props.rightTitle !== null
? `${leftTitle} ${this.props.rightTitle}`
: leftTitle;
const labelText = props.rightTitle !== null
? `${leftTitle} ${props.rightTitle}`
: leftTitle;

return (
<FormGroup check>
<Label check>
<Input {...this.getInputProps()} />
{castStringToElement('span', labelText)}
</Label>
</FormGroup>
);
}
}
return (
<FormGroup check>
<Label check>
<Input {...getInputProps()} />
{castStringToElement('span', labelText)}
</Label>
</FormGroup>
);
};

OptionField.propTypes = {
type: PropTypes.oneOf(['checkbox', 'radio']),
Expand All @@ -109,15 +114,6 @@ OptionField.propTypes = {
disabled: PropTypes.bool,
};

OptionField.defaultProps = {
// React considers "undefined" as an uncontrolled component.
extraClass: '',
className: '',
type: 'radio',
leftTitle: null,
rightTitle: null
};

export { OptionField as Component };

export default OptionField;
182 changes: 182 additions & 0 deletions client/src/components/OptionsetField/tests/OptionField-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* global jest, test, describe, it, expect */

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import OptionFieldDefault, { Component as OptionField } from '../OptionField';

const makeProps = (props = {}) => ({
id: 'my-option',
name: 'MyOption',
type: 'radio',
title: 'My Option',
leftTitle: null,
rightTitle: null,
extraClass: '',
className: '',
value: false,
readOnly: false,
disabled: false,
onChange: jest.fn(),
...props,
});

test('OptionField renders without errors with default props', () => {
const { container } = render(<OptionField {...makeProps()} />);
expect(container.querySelector('input')).not.toBeNull();
});

test('OptionField renders a radio input by default', () => {
const { container } = render(<OptionField {...makeProps()} />);
expect(container.querySelector('input[type="radio"]')).not.toBeNull();
});

test('OptionField renders a checkbox input when type is checkbox', () => {
const { container } = render(<OptionField {...makeProps({ type: 'checkbox' })} />);
expect(container.querySelector('input[type="checkbox"]')).not.toBeNull();
});

test('OptionField renders with the correct id', () => {
const { container } = render(<OptionField {...makeProps({ id: 'my-id' })} />);
expect(container.querySelector('input#my-id')).not.toBeNull();
});

test('OptionField renders with the correct name', () => {
const { container } = render(<OptionField {...makeProps({ name: 'my-name' })} />);
expect(container.querySelector('input[name="my-name"]')).not.toBeNull();
});

test('OptionField renders checked when value is truthy', () => {
const { container } = render(<OptionField {...makeProps({ value: true })} />);
expect(container.querySelector('input').checked).toBe(true);
});

test('OptionField renders unchecked when value is falsy', () => {
const { container } = render(<OptionField {...makeProps({ value: false })} />);
expect(container.querySelector('input').checked).toBe(false);
});

test('OptionField applies extraClass to the input', () => {
const { container } = render(
<OptionField {...makeProps({ extraClass: 'my-extra-class' })} />
);
expect(container.querySelector('input.my-extra-class')).not.toBeNull();
});

test('OptionField applies checked CSS class when value is truthy', () => {
const { container } = render(<OptionField {...makeProps({ value: true })} />);
expect(container.querySelector('input.checked')).not.toBeNull();
});

test('OptionField renders as disabled when disabled prop is true', () => {
const { container } = render(<OptionField {...makeProps({ disabled: true })} />);
expect(container.querySelector('input').disabled).toBe(true);
});

test('OptionField applies option-field--disabled class when disabled', () => {
const { container } = render(<OptionField {...makeProps({ disabled: true })} />);
expect(container.querySelector('input.option-field--disabled')).not.toBeNull();
});

test('OptionField renders as disabled when readOnly prop is true', () => {
const { container } = render(<OptionField {...makeProps({ readOnly: true })} />);
expect(container.querySelector('input').disabled).toBe(true);
});

test('OptionField applies option-field--disabled class when readOnly', () => {
const { container } = render(<OptionField {...makeProps({ readOnly: true })} />);
expect(container.querySelector('input.option-field--disabled')).not.toBeNull();
});

test('OptionField applies disabled CSS class when readOnly', () => {
const { container } = render(<OptionField {...makeProps({ readOnly: true })} />);
expect(container.querySelector('input.disabled')).not.toBeNull();
});

test('OptionField calls onChange when changed', () => {
const onChange = jest.fn();
const { container } = render(<OptionField {...makeProps({ onChange })} />);
const input = container.querySelector('input');
fireEvent.click(input);
expect(onChange).toHaveBeenCalled();
});

test('OptionField calls onChange with id and value when changed', () => {
const onChange = jest.fn();
const { container } = render(<OptionField {...makeProps({ id: 'my-option', onChange })} />);
const input = container.querySelector('input');
fireEvent.click(input);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ id: 'my-option' })
);
});

test('OptionField calls onClick if onChange is not provided', () => {
const onClick = jest.fn();
const { container } = render(
<OptionField {...makeProps({ onChange: undefined, onClick })} />
);
const input = container.querySelector('input');
fireEvent.click(input);
expect(onClick).toHaveBeenCalled();
});

test('OptionField does not call onChange when readOnly', () => {
const onChange = jest.fn();
const { container } = render(
<OptionField {...makeProps({ readOnly: true, onChange })} />
);
const input = container.querySelector('input');
fireEvent.click(input);
expect(onChange).not.toHaveBeenCalled();
});

test('OptionField does not call onChange when disabled', () => {
const onChange = jest.fn();
const { container } = render(
<OptionField {...makeProps({ disabled: true, onChange })} />
);
const input = container.querySelector('input');
fireEvent.click(input);
expect(onChange).not.toHaveBeenCalled();
});

test('OptionField renders title as label text when leftTitle is null', () => {
const { container } = render(
<OptionField {...makeProps({ title: 'My Title', leftTitle: null, rightTitle: null })} />
);
expect(container.querySelector('label span').textContent).toBe('My Title');
});

test('OptionField renders leftTitle as label text when provided', () => {
const { container } = render(
<OptionField {...makeProps({ leftTitle: 'Left Title', rightTitle: null })} />
);
expect(container.querySelector('label span').textContent).toBe('Left Title');
});

test('OptionField renders combined leftTitle and rightTitle', () => {
const { container } = render(
<OptionField {...makeProps({ leftTitle: 'Left', rightTitle: 'Right' })} />
);
expect(container.querySelector('label span').textContent).toBe('Left Right');
});

test('OptionField renders with role attribute when provided', () => {
const { container } = render(
<OptionField {...makeProps({ role: 'option' })} />
);
expect(container.querySelector('input[role="option"]')).not.toBeNull();
});

test('OptionField does not render role attribute when not provided', () => {
const { container } = render(<OptionField {...makeProps()} />);
expect(container.querySelector('input[role]')).toBeNull();
});

test('OptionField default export renders correctly', () => {
const { container } = render(
<OptionFieldDefault {...makeProps({ title: 'Test' })} />
);
expect(container.querySelector('input')).not.toBeNull();
});
Loading