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';

+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', () => {