Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(FDS-342): Input range field #270

Merged
merged 9 commits into from
Sep 21, 2021
1 change: 1 addition & 0 deletions packages/cascara/src/modules/DataModule.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ $module-background: #f0f0f0;
.Month,
.Number,
.Password,
.Range,
.Select,
.Tel,
.Text,
Expand Down
24 changes: 24 additions & 0 deletions packages/cascara/src/modules/DataRange/DataRange.doc.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: DataRange
propTable: DataRange.js
type: 'module'
public: false
---

A data module to display **string** values, it renders a input of type **range**.

### Sandbox

Feel free to play around with the following code:

```jsx
<ModuleSandbox
isEditing={true}
record={{
id: 1,
range: 7,
}}
>
<DataRange attribute='range' isEditable={true} label='My Range' value='7' />
</ModuleSandbox>
```
35 changes: 35 additions & 0 deletions packages/cascara/src/modules/DataRange/DataRange.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import DataRange from './DataRange';
import ModuleSandbox from '../ModuleSandbox';

const DataRangeSandbox = ({ isEditing, ...rest }) => (
<ModuleSandbox isEditing={isEditing}>
<DataRange {...rest} />
</ModuleSandbox>
);

const rangeValue = '7';

const displayProps = {
label: 'Display',
value: rangeValue,
};

const editingProps = {
isEditing: true,
label: 'Editing',
min: '0',
max: '11',
value: rangeValue,
step: '1',
};

// These can be used in tests
export { displayProps, editingProps };

export default {
display: <DataRangeSandbox {...displayProps} />,
displayNoLabel: <DataRangeSandbox {...displayProps} isLabeled={false} />,
editing: <DataRangeSandbox {...editingProps} />,
editingNoLabel: <DataRangeSandbox {...editingProps} isLabeled={false} />,
};
75 changes: 75 additions & 0 deletions packages/cascara/src/modules/DataRange/DataRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useContext } from 'react';
import { Input } from 'reakit/Input';
import pt from 'prop-types';
import { ModuleContext } from '../context';
import styles from '../DataModule.module.scss';

import ModuleErrorBoundary from '../ModuleErrorBoundary';
import getAccessibleLabelSetters from '../helpers';

const propTypes = {
/** A module can have an Attribute, which will be used as form field name */
attribute: pt.string,
/** A Module can be defined to not present an editing state */
isEditable: pt.bool,
/** Presents the input without a label. NOT USER CONFIGURABLE */
isLabeled: pt.bool,
/** A Module needs to have a unique label relative to its context */
label: pt.string,
/** A Module can have a value */
value: pt.string,
};

const DataRange = ({
attribute,
isEditable = true,
isLabeled = true,
label,
value,
...rest
}) => {
const { isEditing, formMethods } = useContext(ModuleContext);
const { setAriaLabel, setHtmlFor } = getAccessibleLabelSetters(
isLabeled,
label
);

const renderEditing = (
<label htmlFor={setHtmlFor}>
{label && isLabeled && <span className={styles.LabelText}>{label}</span>}
<Input
{...rest}
aria-label={setAriaLabel}
className={styles.Range}
defaultValue={value}
id={label}
name={attribute || label}
ref={formMethods?.register}
type='range'
/>
</label>
);

const renderDisplay = (
<span>
{label && isLabeled && <span className={styles.LabelText}>{label}</span>}
<span aria-label={label} className={styles.Range} {...rest}>
{value}
</span>
</span>
);

// Do not render an editable input if the module is not editable
return (
<ModuleErrorBoundary>
<div className={styles.Range}>
{isEditing && isEditable ? renderEditing : renderDisplay}
</div>
</ModuleErrorBoundary>
);
};

DataRange.propTypes = propTypes;

export { propTypes };
export default DataRange;
101 changes: 101 additions & 0 deletions packages/cascara/src/modules/DataRange/DataRange.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { fireEvent, render, screen } from '@testing-library/react';

import cosmosFixtures, {
displayProps,
editingProps,
} from './DataRange.fixture';

const { display, editing, displayNoLabel, editingNoLabel } = cosmosFixtures;

describe('DataRange', () => {
// without ModuleSandbox will render the property information into a span

describe('display', () => {
// We need a place to store the view for snapshot testing. This is not required when we are using `screen` directly from RTL.
let view;

beforeEach(() => {
// Set the render container to our `view` so it is in scope for the snapshot test
view = render(display).container;
});

test('snapshot', () => {
expect(view).toMatchSnapshot();
});

test('renders a <span> by default', () => {
const input = screen.getByLabelText(displayProps.label);
// Make sure the actual DOM element is not render an input
expect(input.tagName).toMatch('SPAN');
// Make sure the dom element that has our aria-label is the input
expect(input.classList.contains('Range')).toBe(true);
});
});

describe('editing', () => {
// We need a place to store the view for snapshot testing. This is not required when we are using `screen` directly from RTL.
let view;

beforeEach(() => {
// Set the render container to our `view` so it is in scope for the snapshot test
view = render(editing).container;
});

test('snapshot', () => {
expect(view).toMatchSnapshot();
});

test('renders a <input email> by default', () => {
const input = screen.getByLabelText(editingProps.label);
// Check that we also use the correct type
expect(input).toHaveAttribute('type', 'range');
});

test('change value', () => {
const newRange = '5';
const input = screen.getByLabelText(editingProps.label);
fireEvent.change(input, { target: { value: newRange } });
expect(input).toHaveValue(newRange);
});
});

describe('accessibility', () => {
test('editing', () => {
render(editing);

const input = screen.getByLabelText(editingProps.label);
// The label tag is the parent wrapper
const label = input.closest('label');

// Test is written this way to make sure we know that both values need to be the same.
const linkedLabelValue = editingProps.label;

// Verify label for attribute has linked value
expect(label).toHaveAttribute(
'for',
expect.stringContaining(linkedLabelValue)
);
// Verify input id attribute has linked value
expect(input).toHaveAttribute(
'id',
expect.stringContaining(linkedLabelValue)
);
// Check that the input does NOT have an aria-label defined because there is a label tag
expect(input).not.toHaveAttribute('aria-label');
});

test('display no label', () => {
// Make sure that the input is still accessible with label text even when we are not showing a label tag in tables
render(displayNoLabel);
const input = screen.getByLabelText(displayProps.label);
expect(input).toBeDefined();
});

test('editing no label', () => {
// Make sure that the input is still accessible with label text even when we are not showing a label tag in tables
render(editingNoLabel);
const input = screen.getByLabelText(editingProps.label);
expect(input).toBeDefined();
});
});
});
51 changes: 51 additions & 0 deletions packages/cascara/src/modules/DataRange/DataRange.test.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DataRange display snapshot 1`] = `
<div>
<div
class="Range"
>
<span>
<span
class="LabelText"
>
Display
</span>
<span
aria-label="Display"
class="Range"
>
7
</span>
</span>
</div>
</div>
`;

exports[`DataRange editing snapshot 1`] = `
<div>
<div
class="Range"
>
<label
for="Editing"
>
<span
class="LabelText"
>
Editing
</span>
<input
class="Range"
id="Editing"
max="11"
min="0"
name="Editing"
step="1"
type="range"
value="7"
/>
</label>
</div>
</div>
`;
1 change: 1 addition & 0 deletions packages/cascara/src/modules/DataRange/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DataRange';
2 changes: 2 additions & 0 deletions packages/cascara/src/modules/ModuleKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import DataMonth from './DataMonth';
import DataNumber from './DataNumber';
import DataPassword from './DataPassword';
import DataRadio from './DataRadio';
import DataRange from './DataRange';
import DataSelect from './DataSelect';
import DataTel from './DataTel';
import DataText from './DataText';
Expand All @@ -39,6 +40,7 @@ const dataModules = {
number: DataNumber,
passord: DataPassword,
radio: DataRadio,
range: DataRange,
select: DataSelect,
tel: DataTel,
text: DataText,
Expand Down
1 change: 1 addition & 0 deletions packages/cascara/src/modules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { default as DataMonth } from './DataMonth';
export { default as DataNumber } from './DataNumber';
export { default as DataPassword } from './DataPassword';
export { default as DataRadio } from './DataRadio';
export { default as DataRange } from './DataRange';
export { default as DataSelect } from './DataSelect';
export { default as DataTel } from './DataTel';
export { default as DataText } from './DataText';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ exports[`Table snapshot tests dataDisplay with non-existent module 1`] = `
class="Error"
data-testid="module-error"
>
unknownModule is not a valid value for module. Try using one of [checkbox, date, datetime, email, file, image, json, month, number, passord, radio, select, tel, text, textarea, time, url]
unknownModule is not a valid value for module. Try using one of [checkbox, date, datetime, email, file, image, json, month, number, passord, radio, range, select, tel, text, textarea, time, url]
</div>
</td>
<td
Expand Down