diff --git a/docs/Fields.md b/docs/Fields.md index 2b3f0b1f647..7439b98fd8a 100644 --- a/docs/Fields.md +++ b/docs/Fields.md @@ -149,6 +149,18 @@ import { BooleanField } from 'react-admin'; ![BooleanField](./img/boolean-field.png) +The `BooleanField` also includes an hidden text for accessibility (or to query in end to end tests). By default, it includes the translated label and the translated value, for example `Published: false`. + +If you need to override it, you can use the `valueLabelTrue` and `valueLabelFalse` props which both accept a string. Those strings may be translation keys: + +```jsx +// Simple texts + + +// Translation keys + +``` + ## `` Displays a value inside a ["Chip"](http://www.material-ui.com/#/components/chip), which is Material UI's term for a label. diff --git a/packages/ra-core/src/util/FieldTitle.js b/packages/ra-core/src/util/FieldTitle.js index d43afbf4e40..53224192ab7 100644 --- a/packages/ra-core/src/util/FieldTitle.js +++ b/packages/ra-core/src/util/FieldTitle.js @@ -5,6 +5,7 @@ import pure from 'recompose/pure'; import compose from 'recompose/compose'; import translate from '../i18n/translate'; +import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs'; export const FieldTitle = ({ resource, @@ -14,16 +15,7 @@ export const FieldTitle = ({ translate, }) => ( - {typeof label !== 'undefined' - ? translate(label, { _: label }) - : typeof source !== 'undefined' - ? translate(`resources.${resource}.fields.${source}`, { - _: inflection.transform(source, [ - 'underscore', - 'humanize', - ]), - }) - : ''} + {translate(...getFieldLabelTranslationArgs({ label, resource, source }))} {isRequired && ' *'} ); diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.js b/packages/ra-core/src/util/getFieldLabelTranslationArgs.js new file mode 100644 index 00000000000..ec774f19f1b --- /dev/null +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.js @@ -0,0 +1,29 @@ +import inflection from 'inflection'; + +/** + * Returns an array of arguments to use with the translate function for the label of a field. + * The label will be the one specified by the label prop or one computed from the resource and source props. + * + * Usage: + * + * {translate(...getFieldLabelTranslationArgs(label, resource, source))} + * + */ +export default options => { + if (!options) { + return ['']; + } + + const { label, resource, source } = options; + + return typeof label !== 'undefined' + ? [label, { _: label }] + : typeof source !== 'undefined' + ? [`resources.${resource}.fields.${source}`, { + _: inflection.transform(source, [ + 'underscore', + 'humanize', + ]) + }] + : ['']; +}; diff --git a/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts new file mode 100644 index 00000000000..8236ef39939 --- /dev/null +++ b/packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts @@ -0,0 +1,58 @@ +import assert from 'assert'; +import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs'; + +describe('getFieldLabelTranslationArgs', () => { + it('should return empty span by default', () => + assert.deepEqual(getFieldLabelTranslationArgs(), [''])); + + it('should return the label when given', () => + assert.deepEqual( + getFieldLabelTranslationArgs({ + label: 'foo', + resource: 'posts', + source: 'title', + }), + ['foo', { _: 'foo' }] + )); + + it('should return the humanized source when given', () => { + assert.deepEqual( + getFieldLabelTranslationArgs({ + resource: 'posts', + source: 'title', + }), + [`resources.posts.fields.title`, { _: 'Title' }] + ); + + assert.deepEqual( + getFieldLabelTranslationArgs({ + resource: 'posts', + source: 'title_with_underscore', + }), + [ + `resources.posts.fields.title_with_underscore`, + { _: 'Title with underscore' }, + ] + ); + + assert.deepEqual( + getFieldLabelTranslationArgs({ + resource: 'posts', + source: 'titleWithCamelCase', + }), + [ + `resources.posts.fields.titleWithCamelCase`, + { _: 'Title with camel case' }, + ] + ); + }); + + it('should return the source and resource as translate key', () => + assert.deepEqual( + getFieldLabelTranslationArgs({ + resource: 'posts', + source: 'title', + }), + [`resources.posts.fields.title`, { _: 'Title' }] + )); +}); diff --git a/packages/ra-core/src/util/index.js b/packages/ra-core/src/util/index.js index cb053fbb11d..a2e90de5cdf 100644 --- a/packages/ra-core/src/util/index.js +++ b/packages/ra-core/src/util/index.js @@ -1,6 +1,7 @@ import downloadCSV from './downloadCSV'; import FieldTitle from './FieldTitle'; import getFetchedAt from './getFetchedAt'; +import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs'; import HttpError from './HttpError'; import linkToRecord from './linkToRecord'; import removeEmpty from './removeEmpty'; @@ -15,6 +16,7 @@ export { downloadCSV, FieldTitle, getFetchedAt, + getFieldLabelTranslationArgs, HttpError, linkToRecord, removeEmpty, diff --git a/packages/ra-ui-materialui/src/field/BooleanField.js b/packages/ra-ui-materialui/src/field/BooleanField.js index cb15616f0e9..c4a170212f5 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.js +++ b/packages/ra-ui-materialui/src/field/BooleanField.js @@ -5,11 +5,54 @@ import pure from 'recompose/pure'; import FalseIcon from '@material-ui/icons/Clear'; import TrueIcon from '@material-ui/icons/Done'; import Typography from '@material-ui/core/Typography'; +import { createStyles, withStyles } from '@material-ui/core/styles'; +import compose from 'recompose/compose'; +import { translate } from 'ra-core'; import sanitizeRestProps from './sanitizeRestProps'; -export const BooleanField = ({ className, source, record = {}, ...rest }) => { - if (get(record, source) === false) { +const styles = createStyles({ + label: { + // Move the text out of the flow of the container. + position: 'absolute', + + // Reduce its height and width to just one pixel. + height: 1, + width: 1, + + // Hide any overflowing elements or text. + overflow: 'hidden', + + // Clip the box to zero pixels. + clip: 'rect(0, 0, 0, 0)', + + // Text won't wrap to a second line. + whiteSpace: 'nowrap', + } +}); + +export const BooleanField = ({ + className, + classes, + source, + record = {}, + translate, + valueLabelTrue, + valueLabelFalse, + ...rest +}) => { + const value = get(record, source); + let ariaLabel = value + ? valueLabelTrue + : valueLabelFalse; + + if (!ariaLabel) { + ariaLabel = value === false + ? translate('ra.boolean.false') + : translate('ra.boolean.true'); + } + + if (value === false) { return ( { className={className} {...sanitizeRestProps(rest)} > + {ariaLabel} ); } - if (get(record, source) === true) { + if (value === true) { return ( { className={className} {...sanitizeRestProps(rest)} > + {ariaLabel} ); @@ -55,9 +100,20 @@ BooleanField.propTypes = { record: PropTypes.object, sortBy: PropTypes.string, source: PropTypes.string.isRequired, + valueLabelTrue: PropTypes.string, + valueLabelFalse: PropTypes.string, +}; + +BooleanField.defaultProps = { + classes: {}, + translate: x => x, }; -const PureBooleanField = pure(BooleanField); +const PureBooleanField = compose( + pure, + withStyles(styles), + translate, +)(BooleanField); PureBooleanField.defaultProps = { addLabel: true, diff --git a/packages/ra-ui-materialui/src/field/BooleanField.spec.js b/packages/ra-ui-materialui/src/field/BooleanField.spec.js index 324106881a2..9ceecaa1da5 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.spec.js +++ b/packages/ra-ui-materialui/src/field/BooleanField.spec.js @@ -4,21 +4,41 @@ import { shallow } from 'enzyme'; import { BooleanField } from './BooleanField'; describe('', () => { - it('should display tick if value is true', () => { + it('should display tick and truthy text if value is true', () => { const wrapper = shallow( - + ); assert.ok(wrapper.first().is('WithStyles(Typography)')); assert.equal(wrapper.first().find('pure(Done)').length, 1); + assert.equal(wrapper.first().find('span').text(), 'ra.boolean.true'); }); - it('should display cross if value is false', () => { + it('should display tick and custom truthy text if value is true', () => { const wrapper = shallow( - + + ); + assert.ok(wrapper.first().is('WithStyles(Typography)')); + assert.equal(wrapper.first().find('pure(Done)').length, 1); + assert.equal(wrapper.first().find('span').text(), 'Has been published'); + }); + + it('should display cross and falsy text if value is false', () => { + const wrapper = shallow( + ); assert.ok(wrapper.first().is('WithStyles(Typography)')); assert.equal(wrapper.first().find('pure(Clear)').length, 1); + assert.equal(wrapper.first().find('span').text(), 'ra.boolean.false'); + }); + + it('should display tick and custom falsy text if value is true', () => { + const wrapper = shallow( + + ); + assert.ok(wrapper.first().is('WithStyles(Typography)')); + assert.equal(wrapper.first().find('pure(Clear)').length, 1); + assert.equal(wrapper.first().find('span').text(), 'Has not been published yet'); }); it('should not display anything if value is null', () => {