diff --git a/.eslintplugin.js b/.eslintplugin.js new file mode 100644 index 00000000000..92202cd32a0 --- /dev/null +++ b/.eslintplugin.js @@ -0,0 +1,3 @@ +exports.rules = { + i18n: require('./scripts/eslint-plugin-i18n/i18n'), +}; diff --git a/.eslintrc.json b/.eslintrc.json index 1e9a09dc712..8ad8598db09 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,10 +14,12 @@ "@elastic/eslint-config-kibana" ], "plugins": [ - "prettier" + "prettier", + "local" ], "rules": { - "prefer-template": "error" + "prefer-template": "error", + "local/i18n": "error" }, "env": { "jest": true diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f3fc1de87..b29fbf95064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `6.8.0`. +- Converted a number of components to support text localization ([#1504](https://github.com/elastic/eui/pull/1504)) ## [`6.8.0`](https://github.com/elastic/eui/tree/v6.8.0) diff --git a/package.json b/package.json index 74ef66d77cd..fadcf82cd17 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "eslint-plugin-import": "^2.8.0", "eslint-plugin-jest": "^21.6.2", "eslint-plugin-jsx-a11y": "^6.0.2", + "eslint-plugin-local": "^1.0.0", "eslint-plugin-mocha": "^4.11.0", "eslint-plugin-prefer-object-spread": "^1.2.1", "eslint-plugin-prettier": "^2.6.0", diff --git a/scripts/eslint-plugin-i18n/i18n.js b/scripts/eslint-plugin-i18n/i18n.js new file mode 100644 index 00000000000..c7fdd820c01 --- /dev/null +++ b/scripts/eslint-plugin-i18n/i18n.js @@ -0,0 +1,325 @@ +// Enforce EuiI18n token names & variable names in render prop + +const path = require('path'); + +function attributesArrayToLookup(attributesArray) { + return attributesArray.reduce( + (lookup, attribute) => { + lookup[attribute.name.name] = attribute.value; + return lookup; + }, + {} + ); +} + +function getDefinedValues(valuesNode) { + if (valuesNode == null) return new Set(); + return valuesNode.expression.properties.reduce( + (valueNames, property) => { + valueNames.add(property.key.name); + return valueNames; + }, + new Set() + ); +} + +function formatSet(set) { + return Array.from(set).sort().join(', '); +} + +function getExpectedValueNames(defaultString) { + const matches = defaultString.match(/{([a-zA-Z0-9_-]+)}/g); + const expectedNames = new Set(); + + if (matches) { + matches.forEach(match => { + expectedNames.add(match.substring(1, match.length - 1)); + }); + } + + return expectedNames; +} + +function areSetsEqual(set1, set2) { + if (set1.size !== set2.size) return false; + const entries = Array.from(set1); + for (let i = 0; i < entries.length; i++) { + if (set2.has(entries[i]) === false) return false; + } + return true; +} + +function getRenderPropFromChildren(children) { + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child.type === 'JSXExpressionContainer' && child.expression.type === 'ArrowFunctionExpression') { + return child.expression; + } + } + } else { + if (children.type === 'JSXExpressionContainer' && children.expression.type === 'ArrowFunctionExpression') { + return children.expression; + } + } +} + +function getExpectedParamNameFromToken(tokenValue) { + const tokenParts = tokenValue.split(/\./g); + return tokenParts[tokenParts.length - 1]; +} + +module.exports = { + meta: { + type: 'problem', + + docs: { + description: 'Enforce EuiI18n token names & variable names in render prop', + }, + + messages: { + invalidToken: 'token value "{{ tokenValue }}" must be of format {{ tokenNamespace }}.tokenName', + mismatchedValues: 'expected values "{{ expected }}" but provided {{ provided }}', + mismatchedTokensAndDefaults: 'given {{ tokenLength }} tokens but {{ defaultsLength }} defaults', + tokenNamesNotUsedInRenderProp: 'tokens {{ tokenNames }} is not used by render prop params {{ paramNames }}', + invalidTokenType: 'token expects a string value, {{ type }} passed instead', + invalidTokensType: 'tokens expects an array of strings, {{ type }} passed instead', + invalidDefaultType: 'default expects a string or arrow function, {{ type }} passed instead', + invalidDefaultsType: 'defaults expects an array of strings or arrow functions, {{ type }} passed instead', + }, + }, + create: function (context) { + const filename = context.getFilename(); + const basename = path.basename(filename, path.extname(filename)); + const expectedTokenNamespace = `eui${basename.replace(/(^|_)([a-z])/g, (match, leading, char) => char.toUpperCase())}`; + + return { + JSXOpeningElement(node) { + // only process elements + if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'EuiI18n') return; + + const jsxElement = node.parent; + const hasRenderProp = jsxElement.children.length > 0; + + const attributes = attributesArrayToLookup(node.attributes); + + // validate attribute types + if (attributes.hasOwnProperty('token')) { + // `token` must be a Literal + if (attributes.token.type !== 'Literal') { + context.report({ + node, + loc: attributes.token.loc, + messageId: 'invalidTokenType', + data: { type: attributes.token.type } + }); + return; + } + } + + if (attributes.hasOwnProperty('default')) { + // default must be either a Literal of an ArrowFunctionExpression + const isLiteral = attributes.default.type === 'Literal'; + const isArrowExpression = + attributes.default.type === 'JSXExpressionContainer' && + attributes.default.expression.type === 'ArrowFunctionExpression'; + if (!isLiteral && !isArrowExpression) { + context.report({ + node, + loc: attributes.default.loc, + messageId: 'invalidDefaultType', + data: { type: attributes.default.expression.type } + }); + return; + } + } + + if (attributes.hasOwnProperty('tokens')) { + // tokens must be an array of Literals + if (attributes.tokens.type !== 'JSXExpressionContainer') { + context.report({ + node, + loc: attributes.tokens.loc, + messageId: 'invalidTokensType', + data: { type: attributes.tokens.type } + }); + return; + } + + if (attributes.tokens.expression.type !== 'ArrayExpression') { + context.report({ + node, + loc: attributes.tokens.loc, + messageId: 'invalidTokensType', + data: { type: attributes.tokens.expression.type } + }); + return; + } + + for (let i = 0; i < attributes.tokens.expression.elements.length; i++) { + const tokenNode = attributes.tokens.expression.elements[i]; + if (tokenNode.type !== 'Literal' || typeof tokenNode.value !== 'string') { + context.report({ + node, + loc: tokenNode.loc, + messageId: 'invalidTokensType', + data: { type: tokenNode.type } + }); + return; + } + } + } + + if (attributes.hasOwnProperty('defaults')) { + // defaults must be an array of either Literals or ArrowFunctionExpressions + if (attributes.defaults.type !== 'JSXExpressionContainer') { + context.report({ + node, + loc: attributes.defaults.loc, + messageId: 'invalidDefaultsType', + data: { type: attributes.defaults.type } + }); + return; + } + + if (attributes.defaults.expression.type !== 'ArrayExpression') { + context.report({ + node, + loc: attributes.defaults.loc, + messageId: 'invalidDefaultsType', + data: { type: attributes.defaults.expression.type } + }); + return; + } + + for (let i = 0; i < attributes.defaults.expression.elements.length; i++) { + const defaultNode = attributes.defaults.expression.elements[i]; + if (defaultNode.type !== 'Literal' || typeof defaultNode.value !== 'string') { + console.log('::', defaultNode.value, typeof defaultNode.value); + context.report({ + node, + loc: defaultNode.loc, + messageId: 'invalidDefaultsType', + data: { type: defaultNode.type } + }); + return; + } + } + } + + const hasMultipleTokens = attributes.hasOwnProperty('tokens'); + + if (!hasMultipleTokens) { + // validate token format + const tokenParts = attributes.token.value.split('.'); + if (tokenParts.length <= 1 || tokenParts[0] !== expectedTokenNamespace) { + context.report({ + node, + loc: attributes.token.loc, + messageId: 'invalidToken', + data: { tokenValue: attributes.token.value, tokenNamespace: expectedTokenNamespace } + }); + } + + // validate default string interpolation matches values + const valueNames = getDefinedValues(attributes.values); + + if (attributes.default.type === 'Literal') { + // default is a string literal + const expectedNames = getExpectedValueNames(attributes.default.value); + if (areSetsEqual(expectedNames, valueNames) === false) { + context.report({ + node, + loc: attributes.values.loc, + messageId: 'mismatchedValues', + data: { expected: formatSet(expectedNames), provided: formatSet(valueNames) } + }); + } + } else { + // default is a function + // validate the destructured param defined by default function match the values + const defaultFn = attributes.default.expression; + const objProperties = defaultFn.params ? defaultFn.params[0].properties : []; + const expectedNames = new Set(objProperties.map(property => property.key.name)); + if (areSetsEqual(valueNames, expectedNames) === false) { + context.report({ + node, + loc: attributes.values.loc, + messageId: 'mismatchedValues', + data: { expected: formatSet(expectedNames), provided: formatSet(valueNames) } + }); + } + } + } else { + // has multiple tokens + // validate their names + const tokens = attributes.tokens.expression.elements; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const tokenParts = token.value.split('.'); + if (tokenParts.length <= 1 || tokenParts[0] !== expectedTokenNamespace) { + context.report({ + node, + loc: token.loc, + messageId: 'invalidToken', + data: { tokenValue: token.value, tokenNamespace: expectedTokenNamespace } + }); + } + } + + // validate the number of tokens equals the number of defaults + const defaults = attributes.defaults.expression.elements; + if (tokens.length !== defaults.length) { + context.report({ + node, + loc: node.loc, + messageId: 'mismatchedTokensAndDefaults', + data: { tokenLength: tokens.length, defaultsLength: defaults.length } + }); + } + } + + if (hasRenderProp) { + // validate the render prop + const renderProp = getRenderPropFromChildren(jsxElement.children); + + if (hasMultipleTokens) { + // multiple tokens, verify each token matches an array-destructured param + const params = renderProp.params[0].elements; + const tokens = attributes.tokens.expression.elements; + + const paramsSet = new Set(params.map(element => element.name)); + const tokensSet = new Set(tokens.map(element => getExpectedParamNameFromToken(element.value))); + + if (areSetsEqual(paramsSet, tokensSet) === false) { + context.report({ + node, + loc: node.loc, + messageId: 'tokenNamesNotUsedInRenderProp', + data: { tokenNames: formatSet(tokensSet), paramNames: formatSet(paramsSet) } + }); + } + + } else { + // single token, single param should be a matching identifier + const param = renderProp.params[0]; + const tokenName = getExpectedParamNameFromToken(attributes.token.value); + const paramName = param.name; + + if (tokenName !== paramName) { + context.report({ + node, + loc: node.loc, + messageId: 'tokenNamesNotUsedInRenderProp', + data: { tokenNames: tokenName, paramNames: paramName } + }); + } + } + } + + // debugger; + } + // callback functions + }; + } +}; diff --git a/scripts/eslint-plugin-i18n/i18n.test.js b/scripts/eslint-plugin-i18n/i18n.test.js new file mode 100644 index 00000000000..6b3045d71d9 --- /dev/null +++ b/scripts/eslint-plugin-i18n/i18n.test.js @@ -0,0 +1,173 @@ +const rule = require('./i18n'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: 'babel-eslint' +}); + +const valid = [ + // nothing to validate against + '', + + // values agree with default string + ``, + + // valid tokens + ``, + + // token name is used by render prop + ` + {tokenName => 'asdf'} + `, + ` + {(tokenName) => 'asdf'} + `, + + // token names are used by render prop + ` + {([token1, token2]) => 'asdf'} + `, + + // default callback params match values + ` name}/>`, +]; +const invalid = [ + // token doesn't match file name + { + code: '', + errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooeyBar.tokenName', tokenNamespace: 'euiFooBar' } }] + }, + + // token doesn't have at least two parts + { + code: '', + errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooBar', tokenNamespace: 'euiFooBar' } }] + }, + { + code: '', + errors: [{ messageId: 'invalidToken', data: { tokenValue: 'tokenName', tokenNamespace: 'euiFooBar' } }] + }, + + // invalid tokens + { + code: ``, + errors: [{ messageId: 'invalidToken', data: { tokenValue: 'token2', tokenNamespace: 'euiFooBar' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooeyBar.token1', tokenNamespace: 'euiFooBar' } }] + }, + { + code: ``, + errors: [{ messageId: 'mismatchedTokensAndDefaults', data: { tokenLength: 1, defaultsLength: 2 } }] + }, + + // values not in agreement with default string + { + code: ``, + errors: [{ + messageId: 'mismatchedValues', + data: { + expected: 'value, value2', + provided: 'value2, valuee' + } + }] + }, + { + code: ``, + errors: [{ + messageId: 'mismatchedValues', + data: { + expected: 'value2, valuee', + provided: 'value, value2' + } + }] + }, + + // token name isn't used by render prop + { + code: ` + {tokenGame => 'asdf'} + `, + errors: [{ + messageId: 'tokenNamesNotUsedInRenderProp', + data: { + tokenNames: 'tokenName', + paramNames: 'tokenGame', + } + }], + }, + + // token names aren't used by render prop + { + code: ` + {([tokener1, token2]) => 'asdf'} + `, + errors: [{ + messageId: 'tokenNamesNotUsedInRenderProp', + data: { + tokenNames: 'token1, token2', + paramNames: 'token2, tokener1' + } + }], + }, + + // default callback params don't match values + { + code: ` name}/>`, + errors: [{ + messageId: 'mismatchedValues', + data: { + expected: 'name', + provided: 'nare' + } + }] + }, + + // invalid attribute types + { + code: '', + errors: [{ messageId: 'invalidTokenType', data: { type: 'JSXExpressionContainer' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidTokensType', data: { type: 'Literal' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidTokensType', data: { type: 'Literal' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidTokensType', data: { type: 'Literal' } }] + }, + { + code: '', + errors: [{ messageId: 'invalidDefaultType', data: { type: 'Literal' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }] + }, + { + code: ``, + errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }] + }, +]; + +function withFilename(ruleset) { + return ruleset.map(code => { + const definition = typeof code === 'string' ? { code } : code; + definition.filename = 'foo_bar.js'; + return definition; + }); +} + +ruleTester.run('i18n', rule, { + valid: withFilename(valid), + invalid: withFilename(invalid), +}); diff --git a/src-docs/src/views/context/context.js b/src-docs/src/views/context/context.js index 8bdb0b7506d..28851cb7a80 100644 --- a/src-docs/src/views/context/context.js +++ b/src-docs/src/views/context/context.js @@ -14,13 +14,13 @@ import { const mappings = { fr: { - english: 'Anglais', - french: 'Française', - greeting: 'Salutations!', - guestNo: 'Vous êtes invité #', - question: 'Quel est votre nom?', - placeholder: 'Jean Dupont', - action: 'Soumettre', + 'euiContext.english': 'Anglais', + 'euiContext.french': 'Française', + 'euiContext.greeting': 'Salutations!', + 'euiContext.guestNo': 'Vous êtes invité #', + 'euiContext.question': 'Quel est votre nom?', + 'euiContext.placeholder': 'Jean Dupont', + 'euiContext.action': 'Soumettre', }, }; @@ -44,35 +44,35 @@ export default class extends Component { this.setLanguage('en')}> - + this.setLanguage('fr')}> - + - + -

+

- + {([question, action]) => ( - + {placeholder => ( { return (

diff --git a/src-docs/src/views/i18n/i18n_multi.js b/src-docs/src/views/i18n/i18n_multi.js index 35a93f4bb49..1fa46b92746 100644 --- a/src-docs/src/views/i18n/i18n_multi.js +++ b/src-docs/src/views/i18n/i18n_multi.js @@ -13,7 +13,7 @@ export default () => { Both title and description for the card are looked up in one call to EuiI18n

{([title, description]) => ( diff --git a/src-docs/src/views/i18n/i18n_renderprop.js b/src-docs/src/views/i18n/i18n_renderprop.js index 68e518a25da..656967a1e84 100644 --- a/src-docs/src/views/i18n/i18n_renderprop.js +++ b/src-docs/src/views/i18n/i18n_renderprop.js @@ -13,7 +13,7 @@ export default () => { This text field's placeholder reads from i18n.renderpropexample

- + {placeholderName => }
diff --git a/src/components/basic_table/__snapshots__/basic_table.test.js.snap b/src/components/basic_table/__snapshots__/basic_table.test.js.snap index b6ab68d5639..23424377961 100644 --- a/src/components/basic_table/__snapshots__/basic_table.test.js.snap +++ b/src/components/basic_table/__snapshots__/basic_table.test.js.snap @@ -15,9 +15,15 @@ exports[`EuiBasicTable cellProps renders cells with custom props from a callback aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -109,9 +115,15 @@ exports[`EuiBasicTable cellProps renders rows with custom props from an object 1 aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -205,9 +217,15 @@ exports[`EuiBasicTable empty is rendered 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 0 - items. + @@ -252,9 +270,15 @@ exports[`EuiBasicTable empty renders a node as a custom message 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 0 - items. + @@ -307,9 +331,15 @@ exports[`EuiBasicTable empty renders a string as a custom message 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 0 - items. + @@ -354,9 +384,15 @@ exports[`EuiBasicTable footers do not render without a column footer definition aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -517,9 +553,15 @@ exports[`EuiBasicTable footers render with pagination, selection, sorting, and f aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -528,16 +570,9 @@ exports[`EuiBasicTable footers render with pagination, selection, sorting, and f scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -877,9 +897,15 @@ exports[`EuiBasicTable rowProps renders rows with custom props from a callback 1 aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -977,9 +1003,15 @@ exports[`EuiBasicTable rowProps renders rows with custom props from an object 1` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -1077,9 +1109,15 @@ exports[`EuiBasicTable with pagination - 2nd page 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 2 - items. + @@ -1157,9 +1195,15 @@ exports[`EuiBasicTable with pagination 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -1253,9 +1297,15 @@ exports[`EuiBasicTable with pagination and error 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -1306,9 +1356,15 @@ exports[`EuiBasicTable with pagination and selection 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -1317,16 +1373,9 @@ exports[`EuiBasicTable with pagination and selection 1`] = ` scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -1578,9 +1612,15 @@ exports[`EuiBasicTable with pagination, selection and sorting 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + @@ -1589,16 +1629,9 @@ exports[`EuiBasicTable with pagination, selection and sorting 1`] = ` scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -1767,16 +1785,9 @@ exports[`EuiBasicTable with pagination, selection, sorting and a single record a scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -2039,16 +2035,9 @@ exports[`EuiBasicTable with pagination, selection, sorting and column dataType 1 scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -2217,16 +2191,9 @@ exports[`EuiBasicTable with pagination, selection, sorting and column renderer 1 scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -2395,16 +2347,9 @@ exports[`EuiBasicTable with pagination, selection, sorting and multiple record a scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -2685,16 +2615,9 @@ exports[`EuiBasicTable with pagination, selection, sorting, column renderer and scope="col" width="24px" > - - - - - Below is a table of - 3 - items. + @@ -2937,9 +2845,15 @@ exports[`EuiBasicTable with sorting 1`] = ` aria-relevant="text" role="status" > - Below is a table of - 3 - items. + diff --git a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap index 9232729b322..3090096d259 100644 --- a/src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap +++ b/src/components/basic_table/__snapshots__/collapsed_item_actions.test.js.snap @@ -1,53 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CollapsedItemActions render 1`] = ` - +
+ - - - } - closePopover={[Function]} - hasArrow={true} - id="id-actions" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - popoverRef={[Function]} -> - + + + +
+ `; diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap index 6a959585cc2..4f04ca19f34 100644 --- a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap +++ b/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap @@ -114,9 +114,17 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` className="euiScreenReaderOnly" role="status" > - Below is a table of - 2 - items. + + Below is a table of 2 items. + @@ -405,42 +413,35 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` className="euiPagination" role="group" > - - - - + + + + +
+ - - - - - + + + + - - - - - + + + + - - + + + + + + diff --git a/src/components/basic_table/basic_table.js b/src/components/basic_table/basic_table.js index 9f854e6e435..d6ddad381c6 100644 --- a/src/components/basic_table/basic_table.js +++ b/src/components/basic_table/basic_table.js @@ -33,6 +33,7 @@ import { EuiTableHeaderMobile } from '../table/mobile/table_header_mobile'; import { EuiTableSortMobile } from '../table/mobile/table_sort_mobile'; import { withRequiredProp } from '../../utils/prop_types/with_required_prop'; import { EuiScreenReaderOnly, EuiKeyboardAccessible } from '../accessibility'; +import { EuiI18n } from '../i18n'; const dataTypesProfiles = { auto: { @@ -417,7 +418,13 @@ export class EuiBasicTable extends Component { return ( - Below is a table of {items.length} items. + + + ); } @@ -449,15 +456,19 @@ export class EuiBasicTable extends Component { headers.push( - + + {selectAllRows => ( + + )} + ); } @@ -723,16 +734,20 @@ export class EuiBasicTable extends Component { }; return ( - + + {selectThisRow => ( + + )} + ); } diff --git a/src/components/basic_table/collapsed_item_actions.js b/src/components/basic_table/collapsed_item_actions.js index 5d368d469cf..dca9bd53bf1 100644 --- a/src/components/basic_table/collapsed_item_actions.js +++ b/src/components/basic_table/collapsed_item_actions.js @@ -3,6 +3,7 @@ import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu'; import { EuiPopover } from '../popover'; import { EuiButtonIcon } from '../button'; import { EuiToolTip } from '../tool_tip'; +import { EuiI18n } from '../i18n'; export class CollapsedItemActions extends Component { @@ -88,21 +89,29 @@ export class CollapsedItemActions extends Component { }, []); const popoverButton = ( - + + {allActions => ( + + )} + ); const withTooltip = !allDisabled && ( - - {popoverButton} - + + {allActions => ( + + {popoverButton} + + )} + ); return ( diff --git a/src/components/basic_table/collapsed_item_actions.test.js b/src/components/basic_table/collapsed_item_actions.test.js index 26ed847a12c..54852b5ab9b 100644 --- a/src/components/basic_table/collapsed_item_actions.test.js +++ b/src/components/basic_table/collapsed_item_actions.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { render } from 'enzyme'; import { CollapsedItemActions } from './collapsed_item_actions'; describe('CollapsedItemActions', () => { @@ -27,7 +27,7 @@ describe('CollapsedItemActions', () => { onFocus: () => {} }; - const component = shallow( + const component = render( ); diff --git a/src/components/form/super_select/__snapshots__/super_select.test.js.snap b/src/components/form/super_select/__snapshots__/super_select.test.js.snap index 97ad52e0b57..a90e97d037d 100644 --- a/src/components/form/super_select/__snapshots__/super_select.test.js.snap +++ b/src/components/form/super_select/__snapshots__/super_select.test.js.snap @@ -144,8 +144,7 @@ exports[`EuiSuperSelect props custom display is propagated to dropdown 1`] = ` class="euiScreenReaderOnly" role="alert" > - You are in a form selector of 2 items and must select a single option. - Use the up and down keys to navigate or escape to close. + You are in a form selector of 2 items and must select a single option. Use the up and down keys to navigate or escape to close.

- You are in a form selector of 2 items and must select a single option. - Use the up and down keys to navigate or escape to close. + You are in a form selector of 2 items and must select a single option. Use the up and down keys to navigate or escape to close.

- You are in a form selector of 2 items and must select a single option. - Use the up and down keys to navigate or escape to close. + You are in a form selector of 2 items and must select a single option. Use the up and down keys to navigate or escape to close.

@@ -950,8 +946,7 @@ exports[`EuiSuperSelect props options are rendered when select is open 1`] = ` class="euiScreenReaderOnly" role="alert" > - You are in a form selector of 2 items and must select a single option. - Use the up and down keys to navigate or escape to close. + You are in a form selector of 2 items and must select a single option. Use the up and down keys to navigate or escape to close.

diff --git a/src/components/header/header_alert/header_alert.js b/src/components/header/header_alert/header_alert.js index 44658c2894c..4d9b5e92f4f 100644 --- a/src/components/header/header_alert/header_alert.js +++ b/src/components/header/header_alert/header_alert.js @@ -11,6 +11,10 @@ import { EuiFlexItem, } from '../../flex'; +import { + EuiI18n, +} from '../../i18n'; + export const EuiHeaderAlert = ({ action, className, @@ -22,33 +26,37 @@ export const EuiHeaderAlert = ({ const classes = classNames('euiHeaderAlert', className); return ( -
- - -
{title}
- -
{text}
- - - -
{action}
-
- - -
- {date} -
-
-
-
+ + {dismiss => ( +
+ + +
{title}
+ +
{text}
+ + + +
{action}
+
+ + +
+ {date} +
+
+
+
+ )} +
); }; diff --git a/src/components/header/header_links/header_links.js b/src/components/header/header_links/header_links.js index 8171f23e1bb..57e9e1deb38 100644 --- a/src/components/header/header_links/header_links.js +++ b/src/components/header/header_links/header_links.js @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { EuiIcon } from '../../icon'; import { EuiPopover } from '../../popover'; +import { EuiI18n } from '../../i18n'; import { EuiHeaderSectionItemButton, EuiHeaderSectionItem } from '../header_section'; export class EuiHeaderLinks extends Component { @@ -42,40 +43,48 @@ export class EuiHeaderLinks extends Component { const button = ( - - - + + {openNavigationMenu => ( + + + + )} + ); return ( - + )} +
); } } diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index 2d59252791b..db7c9fe898b 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -9,6 +9,8 @@ import { keyCodes } from '../../services'; import { EuiButtonIcon } from '../button'; +import { EuiI18n } from '../i18n'; + export class EuiModal extends Component { onKeyDown = event => { if (event.keyCode === keyCodes.ESCAPE) { @@ -59,13 +61,17 @@ export class EuiModal extends Component { style={newStyle || style} {...rest} > - + + {closeModal => ( + + )} +
{children}
diff --git a/src/components/pagination/pagination.js b/src/components/pagination/pagination.js index 45b2d1d35d8..aa692856147 100644 --- a/src/components/pagination/pagination.js +++ b/src/components/pagination/pagination.js @@ -4,6 +4,7 @@ import classNames from 'classnames'; import { EuiPaginationButton } from './pagination_button'; import { EuiButtonIcon } from '../button'; +import { EuiI18n } from '../i18n'; const MAX_VISIBLE_PAGES = 5; const NUMBER_SURROUNDING_PAGES = Math.floor(MAX_VISIBLE_PAGES * 0.5); @@ -24,43 +25,63 @@ export const EuiPagination = ({ for (let i = firstPageInRange, index = 0; i < lastPageInRange; i++, index++) { pages.push( - - {i + 1} - + {pageOfTotal => ( + + {i + 1} + + )} + ); } const previousButton = ( - + + {previousPage => ( + + )} + ); const firstPageButtons = []; if (firstPageInRange > 0) { firstPageButtons.push( - - 1 - + {pageOfTotal => ( + + 1 + + )} + ); if (firstPageInRange > 1) { @@ -94,26 +115,38 @@ export const EuiPagination = ({ } lastPageButtons.push( - - {pageCount} - + {jumpToLastPage => ( + + {pageCount} + + )} + ); } const nextButton = ( - + + {nextPage => ( + + )} + ); if (pages.length > 1) { diff --git a/src/components/steps/__snapshots__/step.test.js.snap b/src/components/steps/__snapshots__/step.test.js.snap index caf632c20aa..ba9b4124a7a 100644 --- a/src/components/steps/__snapshots__/step.test.js.snap +++ b/src/components/steps/__snapshots__/step.test.js.snap @@ -9,7 +9,7 @@ exports[`EuiStep is rendered 1`] = ` - Step + Step 
- Step + Step 
- Step + Step 
- Incomplete Step + Incomplete Step 
- Step + Step 
- Step + Step 
- Incomplete Step + Incomplete Step 
- Step + Step 
- Step + Step 
- Incomplete Step + Incomplete Step 
{ const classes = classNames('euiStep', className); - let screenReaderPrefix; + let screenReaderStep; if (status === 'incomplete') { - screenReaderPrefix = 'Incomplete'; + screenReaderStep = ; + } else { + screenReaderStep = ; } return ( @@ -37,7 +43,7 @@ export const EuiStep = ({ {...rest} > - {screenReaderPrefix} Step + {screenReaderStep}  diff --git a/src/components/steps/step_horizontal.js b/src/components/steps/step_horizontal.js index b4b9e122a96..827b0f1dae5 100644 --- a/src/components/steps/step_horizontal.js +++ b/src/components/steps/step_horizontal.js @@ -7,6 +7,10 @@ import { EuiKeyboardAccessible, } from '../accessibility'; +import { + EuiI18n, +} from '../i18n'; + import { STATUS, EuiStepNumber, @@ -30,14 +34,10 @@ export const EuiStepHorizontal = ({ 'euiStepHorizontal-isDisabled': disabled, }); - let titleAppendix = ''; - if (disabled) { status = 'disabled'; - titleAppendix = ' is disabled'; } else if (isComplete) { status = 'complete'; - titleAppendix = ' is complete'; } else if (isSelected) { status = status; } else if (!isComplete && !status) { @@ -52,29 +52,44 @@ export const EuiStepHorizontal = ({ onClick(e); }; - const buttonTitle = `Step ${step}: ${title}${titleAppendix}`; - return ( - -
-
Step
+ { + let titleAppendix = ''; + if (disabled) { + titleAppendix = ' is disabled'; + } else if (isComplete) { + titleAppendix = ' is complete'; + } + + return `Step ${step}: ${title}${titleAppendix}`; + }} + values={{ step, title, disabled, isComplete }} + > + {buttonTitle => ( + +
+
- + -
- {title} -
-
-
+
+ {title} +
+
+
+ )} +
); }; diff --git a/src/components/steps/step_number.js b/src/components/steps/step_number.js index 5658f10a7fc..eeeca02d9c1 100644 --- a/src/components/steps/step_number.js +++ b/src/components/steps/step_number.js @@ -6,6 +6,10 @@ import { EuiIcon, } from '../icon'; +import { + EuiI18n, +} from '../i18n'; + const statusToClassNameMap = { complete: 'euiStepNumber--complete', incomplete: 'euiStepNumber--incomplete', @@ -34,11 +38,23 @@ export const EuiStepNumber = ({ let numberOrIcon; if (status === 'complete') { - numberOrIcon = ; + numberOrIcon = ( + + {isComplete => } + + ); } else if (status === 'warning') { - numberOrIcon = ; + numberOrIcon = ( + + {hasWarnings => } + + ); } else if (status === 'danger') { - numberOrIcon = ; + numberOrIcon = ( + + {hasErrors => } + + ); } else if (!isHollow) { numberOrIcon = number; } diff --git a/src/components/table/mobile/table_sort_mobile.js b/src/components/table/mobile/table_sort_mobile.js index cc6a601a5fa..40ed8648813 100644 --- a/src/components/table/mobile/table_sort_mobile.js +++ b/src/components/table/mobile/table_sort_mobile.js @@ -5,6 +5,7 @@ import classNames from 'classnames'; import { EuiButtonEmpty } from '../../button/button_empty'; import { EuiPopover } from '../../popover'; import { EuiContextMenuPanel } from '../../context_menu'; +import { EuiI18n } from '../../i18n'; import { EuiTableSortMobileItem } from './table_sort_mobile_item'; export class EuiTableSortMobile extends Component { @@ -55,7 +56,7 @@ export class EuiTableSortMobile extends Component { flush="right" size="xs" > - Sorting + ); diff --git a/src/components/toast/__snapshots__/toast.test.js.snap b/src/components/toast/__snapshots__/toast.test.js.snap index 07fbc28fc2e..c7ba123b2a5 100644 --- a/src/components/toast/__snapshots__/toast.test.js.snap +++ b/src/components/toast/__snapshots__/toast.test.js.snap @@ -1,143 +1,272 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiToast Props color danger is rendered 1`] = ` -
- -

- A new notification appears -

-
- + +

+ + A new notification appears + +

+
+ +
+ +
+
-
+ `; exports[`EuiToast Props color primary is rendered 1`] = ` -
- -

- A new notification appears -

-
- + +

+ + A new notification appears + +

+
+ +
+ +
+
-
+ `; exports[`EuiToast Props color success is rendered 1`] = ` -
- -

- A new notification appears -

-
- + +

+ + A new notification appears + +

+
+ +
+ +
+
-
+ `; exports[`EuiToast Props color warning is rendered 1`] = ` -
- -

- A new notification appears -

-
- + +

+ + A new notification appears + +

+
+ +
+ +
+
-
+ `; exports[`EuiToast Props iconType is rendered 1`] = ` -
- -

- A new notification appears -

-
-
-
+ `; exports[`EuiToast Props title is rendered 1`] = ` -
- -

- A new notification appears -

-
- +

+ + A new notification appears + +

+ + - toast title -
+
+ + toast title + +
+
-
+ `; exports[`EuiToast is rendered 1`] = ` diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index 8d9b169a341..192cafa1f1e 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { EuiScreenReaderOnly } from '../accessibility'; +import { EuiI18n } from '../i18n'; import { ICON_TYPES, @@ -45,19 +46,23 @@ export const EuiToast = ({ title, color, iconType, onClose, children, className, if (onClose) { closeButton = ( - + + {dismissToast => ( + + )} + ); } @@ -78,20 +83,24 @@ export const EuiToast = ({ title, color, iconType, onClose, children, className, {...rest} > -

A new notification appears

+

-
- {headerIcon} - - - {title} - -
+ + {notification => ( +
+ {headerIcon} + + + {title} + +
+ )} +
{closeButton} {optionalBody} diff --git a/src/components/toast/toast.test.js b/src/components/toast/toast.test.js index ca9c6fa4fae..43ec623346f 100644 --- a/src/components/toast/toast.test.js +++ b/src/components/toast/toast.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render, shallow } from 'enzyme'; +import { render, mount } from 'enzyme'; import sinon from 'sinon'; import { findTestSubject, @@ -27,7 +27,7 @@ describe('EuiToast', () => { describe('title', () => { test('is rendered', () => { const component = ; - expect(shallow(component)).toMatchSnapshot(); + expect(mount(component)).toMatchSnapshot(); }); }); @@ -35,7 +35,7 @@ describe('EuiToast', () => { COLORS.forEach(color => { test(`${color} is rendered`, () => { const component = ; - expect(shallow(component)).toMatchSnapshot(); + expect(mount(component)).toMatchSnapshot(); }); }); }); @@ -43,7 +43,7 @@ describe('EuiToast', () => { describe('iconType', () => { test('is rendered', () => { const component = ; - expect(shallow(component)).toMatchSnapshot(); + expect(mount(component)).toMatchSnapshot(); }); }); @@ -51,7 +51,7 @@ describe('EuiToast', () => { test('is called when the close button is clicked', () => { const onCloseHandler = sinon.stub(); - const component = shallow( + const component = mount( ); const closeButton = findTestSubject(component, 'toastCloseButton'); diff --git a/yarn.lock b/yarn.lock index 93d55fcceec..e168ff3e35c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4571,6 +4571,11 @@ eslint-plugin-jsx-a11y@^6.0.2: emoji-regex "^6.1.0" jsx-ast-utils "^2.0.0" +eslint-plugin-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-local/-/eslint-plugin-local-1.0.0.tgz#f0c07011c95fec42bfb4d909debb6ea035f3b2a4" + integrity sha1-8MBwEclf7EK/tNkJ3rtuoDXzsqQ= + eslint-plugin-mocha@^4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-4.11.0.tgz#91193a2f55e20a5e35974054a0089d30198ee578"