diff --git a/CHANGELOG.md b/CHANGELOG.md index 843a85952604..6ec6d8682224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,23 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added `EuiDataGrid`'s default sort order property ([#2987](https://github.com/elastic/eui/pull/2987)) +- Added `showOnFocus` prop to `EuiScreenReaderOnly` to force display on keyboard focus ([#2976](https://github.com/elastic/eui/pull/2976)) +- Added `EuiSkipLink` component ([#2976](https://github.com/elastic/eui/pull/2976)) +- Created `EuiBadgeGroup` component ([#2921](https://github.com/elastic/eui/pull/2921)) +- Added `sections` and `position` props to `EuiHeader` ([#2928](https://github.com/elastic/eui/pull/2928)) +- Added `gutterSize` prop to `EuiListGroup` ([#2980](https://github.com/elastic/eui/pull/2980)) +- Added `color` prop to `EuiListGroupItem` and updated size style ([#2980](https://github.com/elastic/eui/pull/2980)) **Bug Fixes** - Fixed `EuiDataGrid`'s sort popover to behave properly on mobile screens ([#2979](https://github.com/elastic/eui/pull/2979)) - Fixed `EuiButton` and other textual components' disabled contrast ([#2874](https://github.com/elastic/eui/pull/2874)) +- Fixed z-index conflict with cell popovers in `EuiDataGrid` while in full screen mode ([#2959](https://github.com/elastic/eui/pull/2959)) +- Adjusted the header on `EuiDataGrid` to fix to the top within constrained containers and full screen mode ([#2959](https://github.com/elastic/eui/pull/2959)) + +**Breaking changes** + +- Updated `@types/react` and `@types/react-dom` to utilize React.RefCallback type instead of custom implementation ([#2929](https://github.com/elastic/eui/pull/2929)) **Theme: Amsterdam** @@ -26,6 +38,7 @@ - Added `notification` and `notificationColor` props to `EuiHeaderSectionItemButton` ([#2914](https://github.com/elastic/eui/pull/2914)) - Added `folderCheck`, `folderExclamation`, `push`, `quote`, `reporter` and `users` icons ([#2935](https://github.com/elastic/eui/pull/2935)) - Updated `folderClosed` and `folderOpen` to match new additions and sit better on the pixel grid ([#2935](https://github.com/elastic/eui/pull/2935)) +- Converted `EuiSearchBar` to Typescript ([#2909](https://github.com/elastic/eui/pull/2909)) **Bug fixes** diff --git a/package.json b/package.json index 2d7126241137..567aa24f55f3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "sideEffects": false, "scripts": { "start": "cross-env BABEL_MODULES=false webpack-dev-server --port 8030 --inline --hot --config=src-docs/webpack.config.js", - "test-docker": "docker pull $npm_package_docker_image && docker run --rm -i -e GIT_COMMITTER_NAME=test -e GIT_COMMITTER_EMAIL=test --user=$(id -u):$(id -g) -e HOME=/tmp -v $(pwd):/app -w /app $npm_package_docker_image bash -c 'npm config set spin false && /opt/yarn*/bin/yarn && npm run test && npm run build'", + "test-docker": "node ./scripts/test-docker.js", "sync-docs": "node ./scripts/docs-sync.js", "build-docs": "cross-env BABEL_MODULES=false cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 webpack --config=src-docs/webpack.config.js", "build": "yarn extract-i18n-strings && node ./scripts/compile-clean.js && node ./scripts/compile-eui.js && node ./scripts/compile-scss.js $npm_package_name", @@ -24,13 +24,15 @@ "lint-sass-fix": "sass-lint-auto-fix -c ./.sass-lint-fix.yml", "test": "yarn lint && yarn test-unit", "test-unit": "cross-env NODE_ENV=test jest --config ./scripts/jest/config.json", + "test-a11y": "node ./scripts/a11y-testing", "test-staged": "yarn lint && node scripts/test-staged.js", - "start-test-server": "webpack-dev-server --config src-docs/webpack.config.js --port 9999", + "start-test-server": "BABEL_MODULES=false NODE_ENV=puppeteer webpack-dev-server --config src-docs/webpack.config.js --port 9999", "test-visual": "wdio test/wdio.conf.js", "yo-component": "yo ./generator-eui/app/component.js", "test-visual-tests": "node ./scripts/run-visual-tests.js", "update-token-changelog": "node ./scripts/update-token-changelog.js", "start-test-server-and-visual-test": "start-server-and-test start-test-server http-get://localhost:9999 test-visual", + "start-test-server-and-a11y-test": "start-server-and-test start-test-server http-get://localhost:9999 test-a11y", "yo-doc": "yo ./generator-eui/app/documentation.js", "release": "node ./scripts/release.js", "postinstall": "node ./scripts/postinstall.js", @@ -88,8 +90,8 @@ "@types/classnames": "^2.2.6", "@types/highlight.js": "^9.12.3", "@types/jest": "^24.0.6", - "@types/react": "^16.9.11", - "@types/react-dom": "^16.9.4", + "@types/react": "^16.9.23", + "@types/react-dom": "^16.9.5", "@types/react-is": "^16.7.1", "@types/resize-observer-browser": "^0.1.1", "@types/tabbable": "^3.1.0", @@ -97,6 +99,8 @@ "@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/parser": "^1.13.0", "autoprefixer": "^7.1.5", + "axe-core": "^3.3.2", + "axe-puppeteer": "^1.0.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^10.0.1", "babel-jest": "^24.1.0", @@ -112,7 +116,7 @@ "chai-webdriverio": "^0.4.3", "chalk": "^2.4.1", "chokidar": "^1.7.0", - "chromedriver": "2.37.0", + "chromedriver": "^77.0.0", "circular-dependency-plugin": "^5.0.2", "core-js": "^2.5.1", "cross-env": "^5.2.0", @@ -160,6 +164,7 @@ "prettier": "^1.17.0", "prompt": "^1.0.0", "prop-types": "^15.6.0", + "puppeteer": "^2.0.0", "raw-loader": "^0.5.1", "react": "^16.12.0", "react-dom": "^16.12.0", @@ -197,8 +202,8 @@ }, "peerDependencies": { "@elastic/datemath": "^5.0.2", - "@types/react": "^16.9.11", - "@types/react-dom": "^16.9.4", + "@types/react": "^16.9.23", + "@types/react-dom": "^16.9.5", "moment": "^2.13.0", "prop-types": "^15.5.0", "react": "^16.12", diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js new file mode 100644 index 000000000000..2ad8e8f7a1f8 --- /dev/null +++ b/scripts/a11y-testing.js @@ -0,0 +1,94 @@ +const chalk = require('chalk'); +const puppeteer = require('puppeteer'); +const { AxePuppeteer } = require('axe-puppeteer'); + +const docsPages = async (root, page) => { + let links = [ + root, + ...(await page.$$eval('nav a', anchors => anchors.map(a => a.href))), + ]; + + links = links.splice(0, 9); + + return links; +}; + +const printResult = result => + console.log(`[${result.id}]: ${result.description} + Help: ${chalk.blue(result.helpUrl)} + Elements: + - ${result.nodes.map(node => node.target).join('\n - ')}`); + +(async () => { + let totalViolationsCount = 0; + let root = 'http://localhost:9999/'; + let browser; + let page; + + try { + browser = await puppeteer.launch({ args: ['--no-sandbox'] }); + page = await browser.newPage(); + + await page.setBypassCSP(true); + } catch (e) { + console.log(chalk.red('Failed to setup puppeteer')); + console.log(e); + process.exit(1); + } + + try { + await page.goto(root); + } catch (e) { + root = 'http://localhost:8030/'; + try { + await page.goto(root); + } catch (e) { + console.log( + chalk.red( + 'No local server found. Expecting localhost:9999 or localhost:8030 to resolve.' + ) + ); + process.exit(1); + } + } + + const links = await docsPages(root, page); + + for (const link of links) { + await page.goto(link); + + const { violations } = await new AxePuppeteer(page) + .disableRules('color-contrast') + .exclude(['figure[role="figure"']) // excluding figure[role="figure"] the duplicatory role is there for ie11 support + .analyze(); + + if (violations.length > 0) { + totalViolationsCount += violations.length; + + const pageName = link.length > 24 ? link.substr(2) : 'the home page'; + console.log(chalk.red(`Errors on ${pageName}`)); + } + + violations.forEach(result => { + printResult(result); + }); + } + + await page.close(); + await browser.close(); + + if (totalViolationsCount > 0) { + const errorsCount = chalk.red( + `${totalViolationsCount} accessibility errors` + ); + + console.log(`${errorsCount} + +Install axe for Chrome or Firefox to debug: +Chrome: https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd +Firefox: https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/`); + process.exit(1); + } else { + console.log(chalk.green('axe found no accessibility errors!')); + } +})(); diff --git a/scripts/test-docker.js b/scripts/test-docker.js new file mode 100644 index 000000000000..a75abf915762 --- /dev/null +++ b/scripts/test-docker.js @@ -0,0 +1,18 @@ +const { execSync } = require('child_process'); + +execSync('docker pull zenato/puppeteer', { + stdio: 'inherit', +}); +/* eslint-disable-next-line no-multi-str */ +execSync("docker run \ + -i --rm --cap-add=SYS_ADMIN --volume=$(pwd):/app --workdir=/app \ + -e GIT_COMMITTER_NAME=test -e GIT_COMMITTER_EMAIL=test -e HOME=/tmp \ + --user=$(id -u):$(id -g) \ + zenato/puppeteer \ + bash -c 'npm config set spin false \ + && /opt/yarn*/bin/yarn \ + && npm run test \ + && npm run start-test-server-and-a11y-test \ + && npm run build'", { + stdio: 'inherit', +}); diff --git a/src-docs/src/views/accessibility/accessibility_example.js b/src-docs/src/views/accessibility/accessibility_example.js index 140c87e7237b..0986f1020a04 100644 --- a/src-docs/src/views/accessibility/accessibility_example.js +++ b/src-docs/src/views/accessibility/accessibility_example.js @@ -5,26 +5,56 @@ import { renderToHtml } from '../../services'; import { GuideSectionTypes } from '../../components'; import { + EuiCallOut, EuiCode, EuiLink, EuiKeyboardAccessible, - EuiScreenReaderOnly, + EuiSkipLink, } from '../../../../src/components'; import KeyboardAccessible from './keyboard_accessible'; import ScreenReaderOnly from './screen_reader'; +import SkipLink from './skip_link'; const keyboardAccessibleSource = require('!!raw-loader!./keyboard_accessible'); const keyboardAccessibleHtml = renderToHtml(KeyboardAccessible); +const keyboardAccessibleSnippet = ` + +`; const screenReaderOnlyHtml = renderToHtml(ScreenReaderOnly); const screenReaderOnlySource = require('!!raw-loader!./screen_reader'); +const screenReaderOnlySnippet = [ + ` + + +`, + ` + + +`, +]; + +const skipLinkHtml = renderToHtml(SkipLink); +const skipLinkSource = require('!!raw-loader!./skip_link'); +const skipLinkSnippet = [ + ` + Skip to content + +`, + ` + Skip to main content + +`, +]; + +import { ScreenReaderOnlyDocsComponent } from './props'; export const AccessibilityExample = { title: 'Accessibility', sections: [ { - title: 'KeyboardAccessible', + title: 'Keyboard accessible', source: [ { type: GuideSectionTypes.JS, @@ -37,17 +67,18 @@ export const AccessibilityExample = { ], text: (

- You can make interactive elements keyboard-accessible with this - component. This is necessary for non-button elements and{' '} - a tags without + You can make interactive elements keyboard-accessible with the{' '} + EuiKeyboardAccessible component. This is necessary + for non-button elements and a tags without{' '} href attributes.

), props: { EuiKeyboardAccessible }, + snippet: keyboardAccessibleSnippet, demo: , }, { - title: 'ScreenReaderOnly', + title: 'Screen reader only', source: [ { type: GuideSectionTypes.JS, @@ -61,24 +92,58 @@ export const AccessibilityExample = { text: (

- This class can be useful to add accessibility to older designs that - are still in use, but it shouldn’t be a permanent solution. - See{' '} - { - - http://webaim.org/techniques/css/invisiblecontent/ - - }{' '} - for more information. -

-

- Use a screenreader to verify that there is a second paragraph in - this example: + Use the EuiScreenReaderOnly component to visually + hide elements while still allowing them to be read by screen + readers. In certain cases, you may want to use the{' '} + showOnFocus prop to display screen reader-only + content when in focus.

+ +

+ "In most cases, if content (particularly content that + provides functionality or interactivity) is important enough to + provide to screen reader users, it should probably be made + available to all users."{' '} + + Learn more about invisible content + +

+
), - props: { EuiScreenReaderOnly }, + props: { + EuiScreenReaderOnly: ScreenReaderOnlyDocsComponent, + }, + snippet: screenReaderOnlySnippet, demo: , }, + { + title: 'Skip link', + source: [ + { + type: GuideSectionTypes.JS, + code: skipLinkSource, + }, + { + type: GuideSectionTypes.HTML, + code: skipLinkHtml, + }, + ], + text: ( +

+ The EuiSkipLink component allows users to bypass + navigation, or ornamental elements, and quickly reach the main content + of the page. +

+ ), + props: { EuiSkipLink }, + snippet: skipLinkSnippet, + demo: , + }, ], }; diff --git a/src-docs/src/views/accessibility/keyboard_accessible.js b/src-docs/src/views/accessibility/keyboard_accessible.js index 3f68055265e5..3d08c0ac5984 100644 --- a/src-docs/src/views/accessibility/keyboard_accessible.js +++ b/src-docs/src/views/accessibility/keyboard_accessible.js @@ -1,6 +1,7 @@ import React from 'react'; import { EuiKeyboardAccessible } from '../../../../src/components'; +import { EuiText } from '../../../../src/components/text'; // For custom components, we just need to make sure they delegate props to their rendered root // element, e.g. onClick, tabIndex, and role. @@ -10,33 +11,42 @@ const CustomComponent = ({ children, ...rest }) => ( export default () => (
- -
window.alert('Div clicked')}>Click this div
-
+ + +
window.alert('Div clicked')}>Click this div
+
- - window.alert('Anchor tag clicked')}> - Click this anchor tag - - + + window.alert('Anchor tag clicked')}> + Click this anchor tag + + - - window.alert('Custom component clicked')}> - Click this custom component - - + + window.alert('Custom component clicked')}> + Click this custom component + + - -
window.alert('Outer EuiKeyboardAccessible clicked')}> - This EuiKeyboardAccessible contains another EuiKeyboardAccessible  - - window.alert('Inner EuiKeyboardAccessible clicked')}> - Clicking this inner one should call both onClick handlers - - -
-
+ +
window.alert('Outer EuiKeyboardAccessible clicked')}> + This EuiKeyboardAccessible contains another + EuiKeyboardAccessible  + + + window.alert('Inner EuiKeyboardAccessible clicked') + }> + Clicking this inner one should call both onClick handlers + + +
+
+
); diff --git a/src-docs/src/views/accessibility/props.tsx b/src-docs/src/views/accessibility/props.tsx new file mode 100644 index 000000000000..29e44b11cc84 --- /dev/null +++ b/src-docs/src/views/accessibility/props.tsx @@ -0,0 +1,6 @@ +import React, { FunctionComponent } from 'react'; +import { EuiScreenReaderOnlyProps } from '../../../../src/components/accessibility/screen_reader'; + +export const ScreenReaderOnlyDocsComponent: FunctionComponent< + EuiScreenReaderOnlyProps +> = () =>
; diff --git a/src-docs/src/views/accessibility/screen_reader.tsx b/src-docs/src/views/accessibility/screen_reader.tsx index c5d74bc5b316..dc43ab1e0c11 100644 --- a/src-docs/src/views/accessibility/screen_reader.tsx +++ b/src-docs/src/views/accessibility/screen_reader.tsx @@ -1,16 +1,51 @@ import React from 'react'; import { EuiScreenReaderOnly } from '../../../../src/components/accessibility/screen_reader'; +import { EuiCallOut } from '../../../../src/components/call_out'; +import { EuiText } from '../../../../src/components/text'; +import { EuiTitle } from '../../../../src/components/title'; +import { EuiLink } from '../../../../src/components/link'; export default () => (
-

This is the first paragraph. It is visible to all.

- + + +

Visually hide content

+

- This is the second paragraph. It is hidden for sighted users but visible - to screen readers. + + Use a screenreader to verify that there is a second paragraph in this + example: +

-
-

This is the third paragraph. It is visible to all.

+

This is the first paragraph. It is visible to all.

+ +

+ This is the second paragraph. It is hidden for sighted users but + visible to screen readers. +

+
+

This is the third paragraph. It is visible to all.

+ +

Show on focus

+
+

+ + Tab through this section with your keyboard to display a ‘Skip + navigation’ link: + +

+

+ This link is visible to all on focus:{' '} + + Skip navigation + +

+ +
); diff --git a/src-docs/src/views/accessibility/skip_link.js b/src-docs/src/views/accessibility/skip_link.js new file mode 100644 index 000000000000..15cf70e03c19 --- /dev/null +++ b/src-docs/src/views/accessibility/skip_link.js @@ -0,0 +1,61 @@ +import React, { Fragment, useState } from 'react'; + +import { EuiSkipLink } from '../../../../src/components/accessibility/skip_link'; +import { EuiCallOut } from '../../../../src/components/call_out'; +import { EuiText } from '../../../../src/components/text'; +import { EuiSpacer } from '../../../../src/components/spacer'; +import { EuiSwitch } from '../../../../src/components/form/switch'; + +export default () => { + const [isFixed, setFixed] = useState(false); + + return ( + + + {isFixed ? ( +

+ + Tab through this section and a fixed{' '} + Skip to main content link will appear atop this + page. + +

+ ) : ( +

+ + Tab through this section and a Skip to content{' '} + link will appear below. + +

+ )} +
+ + setFixed(e.target.checked)} + /> + + {isFixed ? ( + + + Skip to main content + + + + ) : ( + + Skip to content + + )} +
+ ); +}; diff --git a/src-docs/src/views/badge/badge_example.js b/src-docs/src/views/badge/badge_example.js index 3568874491d3..39d9b972990e 100644 --- a/src-docs/src/views/badge/badge_example.js +++ b/src-docs/src/views/badge/badge_example.js @@ -11,10 +11,12 @@ import { EuiCode, EuiBetaBadge, EuiNotificationBadge, + EuiBadgeGroup, EuiCallOut, } from '../../../../src/components'; import Badge from './badge'; + const badgeSource = require('!!raw-loader!./badge'); const badgeHtml = renderToHtml(Badge); const badgeSnippet = [ @@ -72,6 +74,12 @@ const badgeButtonSnippet = [ import BadgeTruncate from './badge_truncate'; const badgeTruncateSource = require('!!raw-loader!./badge_truncate'); const badgeTruncateHtml = renderToHtml(BadgeTruncate); +const badgeTruncateSnippet = [ + ` + + +`, +]; import BetaBadge from './beta_badge'; const betaBadgeSource = require('!!raw-loader!./beta_badge'); @@ -170,7 +178,7 @@ export const BadgeExample = { demo: , }, { - title: 'Badge truncation', + title: 'Badge groups and truncation', source: [ { type: GuideSectionTypes.JS, @@ -193,9 +201,15 @@ export const BadgeExample = { to the title attribute of the element to provide default browser tooltips with the full badge text.

+

+ To ensure proper wrapping, truncation and spacing of multiple + badges, it is advisable to wrap them in a{' '} + EuiBadgeGroup +

), demo: , + snippet: badgeTruncateSnippet, }, { title: 'Beta badge type', @@ -233,7 +247,7 @@ export const BadgeExample = {

), - props: { EuiBetaBadge }, + props: { EuiBetaBadge, EuiBadgeGroup }, snippet: betaBadgeSnippet, demo: , }, diff --git a/src-docs/src/views/badge/badge_truncate.js b/src-docs/src/views/badge/badge_truncate.js index 9f26a97dd252..2759cd0e6c56 100644 --- a/src-docs/src/views/badge/badge_truncate.js +++ b/src-docs/src/views/badge/badge_truncate.js @@ -1,41 +1,35 @@ import React from 'react'; -import { EuiBadge, EuiPanel, EuiSpacer } from '../../../../src/components'; +import { EuiBadge, EuiPanel, EuiBadgeGroup } from '../../../../src/components'; export default () => ( - Badge with simple text being truncated - - - - Badge with icon being truncated - - - - {}} onClickAriaLabel="Click this badge to..."> - Badge with onClick being truncated - - - - - {}} - iconOnClickAriaLabel="Click this icon to..."> - Badge with iconOnClick being truncated - - - - - {}} - onClickAriaLabel="Click this badge to..." - iconOnClick={() => {}} - iconOnClickAriaLabel="Click this icon to..."> - Badge with both onClicks being truncated - + + Badge with simple text being truncated + + Badge with icon being truncated + + {}} onClickAriaLabel="Click this badge to..."> + Badge with onClick being truncated + + + {}} + iconOnClickAriaLabel="Click this icon to..."> + Badge with iconOnClick being truncated + + + {}} + onClickAriaLabel="Click this badge to..." + iconOnClick={() => {}} + iconOnClickAriaLabel="Click this icon to..."> + Badge with both onClicks being truncated + + ); diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 8a62ea664b90..036890d65109 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -27,7 +27,7 @@ const columns = [ id: 'email', display: ( // This is an example of an icon next to a title that still respects text truncate - +
email
diff --git a/src-docs/src/views/guidelines/button.js b/src-docs/src/views/guidelines/button.js index b4fc2714e849..c1b1396af284 100644 --- a/src-docs/src/views/guidelines/button.js +++ b/src-docs/src/views/guidelines/button.js @@ -49,7 +49,7 @@ export default () => ( -

Filled buttons are for the primary action

+

Filled buttons are for the primary action

This button has the heaviest visual weight to draw users' attention. @@ -67,7 +67,7 @@ export default () => ( -

Standard buttons are for secondary actions

+

Standard buttons are for secondary actions

Such actions include Add and Apply. This button type works well for multiple actions of equal weight. @@ -85,7 +85,7 @@ export default () => ( -

Empty buttons are for complementary, UI-specific actions

+

Empty buttons are for complementary, UI-specific actions

Close, cancel, filter, refresh, and other actions that reconfigure the UI are appropriate for empty buttons. @@ -111,7 +111,7 @@ export default () => ( -

Icon buttons are for saving space

+

Icon buttons are for saving space

The icon must be immediately understood, for example, a trash can for delete. Use these buttons sparingly, and never for the primary diff --git a/src-docs/src/views/guidelines/modals.js b/src-docs/src/views/guidelines/modals.js index d15fe10ab64c..cfefd39f44f2 100644 --- a/src-docs/src/views/guidelines/modals.js +++ b/src-docs/src/views/guidelines/modals.js @@ -70,17 +70,17 @@ export default () => ( -

The header sets the context

+

The header sets the context

Short and sentence-case, the header should indicate what the modal is about.

-

The body is for a single task

+

The body is for a single task

This task should not require a lot of explanation or user interaction.

-

Buttons are right-aligned

+

Buttons are right-aligned

The primary action is a filled button, and the secondary action is a link button. Labels should use strong action verbs. diff --git a/src-docs/src/views/guidelines/sass.js b/src-docs/src/views/guidelines/sass.js index 304f7af2f383..b4c9f4c560e8 100644 --- a/src-docs/src/views/guidelines/sass.js +++ b/src-docs/src/views/guidelines/sass.js @@ -388,7 +388,7 @@ export const SassGuidelines = ({ selectedTheme }) => {

-

Sizing

+

Sizing

@@ -400,7 +400,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Z-index

+

Z-index

@@ -412,7 +412,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Color

+

Color

@@ -432,7 +432,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Theming patterns

+

Theming patterns

@@ -554,7 +554,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Color contrast patterns

+

Color contrast patterns

@@ -601,7 +601,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

More on color contrast

+

More on color contrast

@@ -654,7 +654,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Text sizes

+

Text sizes

@@ -665,7 +665,7 @@ export const SassGuidelines = ({ selectedTheme }) => {
-

Text colors

+

Text colors

@@ -677,7 +677,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Font families

+

Font families

@@ -752,7 +752,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Use mixins for shadows

+

Use mixins for shadows

@@ -773,7 +773,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Adding color to shadows

+

Adding color to shadows

@@ -793,7 +793,7 @@ export const SassGuidelines = ({ selectedTheme }) => {
-

Shadows to create graceful overflows

+

Shadows to create graceful overflows

@@ -806,9 +806,9 @@ export const SassGuidelines = ({ selectedTheme }) => { -
+

Vertical scrolling with euiYScrollWithShadows -

+
@@ -859,9 +859,9 @@ export const SassGuidelines = ({ selectedTheme }) => { -
+

Horizontal scrolling with euiXScrollWithShadows -

+
@@ -927,7 +927,7 @@ export const SassGuidelines = ({ selectedTheme }) => {
-

Breakpoint sizing

+

Breakpoint sizing

@@ -939,7 +939,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Mixin usage

+

Mixin usage

@@ -1004,7 +1004,7 @@ export const SassGuidelines = ({ selectedTheme }) => { -

Speed

+

Speed

@@ -1015,7 +1015,7 @@ export const SassGuidelines = ({ selectedTheme }) => {
-

Timing

+

Timing

diff --git a/src-docs/src/views/guidelines/toasts.js b/src-docs/src/views/guidelines/toasts.js index becacc3b7518..9beaed046531 100644 --- a/src-docs/src/views/guidelines/toasts.js +++ b/src-docs/src/views/guidelines/toasts.js @@ -109,7 +109,7 @@ and space to read it properly. Alternatively just link to a full page. -

Success toasts indicate that everything worked out

+

Success toasts indicate that everything worked out

They are the most-commonly used toasts.

@@ -128,9 +128,9 @@ and space to read it properly. Alternatively just link to a full page. -

+

Warning toasts direct user attention to a potential problem -

+

These toasts work well in monitoring apps when something significant requires action. @@ -152,7 +152,7 @@ and space to read it properly. Alternatively just link to a full page. -

Error toasts report a problem

+

Error toasts report a problem

An error toast might let users know an action didn't complete or that a form has errors. @@ -176,7 +176,7 @@ and space to read it properly. Alternatively just link to a full page. -

Info toasts relay neutral information

+

Info toasts relay neutral information

The default toast, an info toast might notify users about an ongoing action. diff --git a/src-docs/src/views/guidelines/writing.js b/src-docs/src/views/guidelines/writing.js index c04ab55df7c3..b6035036ce39 100644 --- a/src-docs/src/views/guidelines/writing.js +++ b/src-docs/src/views/guidelines/writing.js @@ -319,13 +319,16 @@ export default () => ( - + - + diff --git a/src-docs/src/views/header/header_example.js b/src-docs/src/views/header/header_example.js index 32e8ad7eca89..72b503e9988f 100644 --- a/src-docs/src/views/header/header_example.js +++ b/src-docs/src/views/header/header_example.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router'; import { renderToHtml } from '../../services'; @@ -17,10 +18,20 @@ import { EuiHeaderLink, } from '../../../../src/components'; +import { EuiHeaderSectionsProp } from './props'; + import Header from './header'; const headerSource = require('!!raw-loader!./header'); const headerHtml = renderToHtml(Header); +import HeaderSections from './header_sections'; +const headerSectionsSource = require('!!raw-loader!./header_sections'); +const headerSectionsHtml = renderToHtml(HeaderSections); + +import HeaderPosition from './header_position'; +const headerPositionSource = require('!!raw-loader!./header_position'); +const headerPositionHtml = renderToHtml(HeaderPosition); + import HeaderAlert from './header_alert'; const headerAlertSource = require('!!raw-loader!./header_alert'); const headerAlertHtml = renderToHtml(HeaderAlert); @@ -45,10 +56,27 @@ const headerSnippet = ` `; +const headerSectionsSnippet = ``; + const headerLinksSnippet = ` @@ -78,7 +106,11 @@ export const HeaderExample = { code: headerHtml, }, ], - text:

The header is made up of several individual components.

, + text: ( +

+ The header is made up of many individual components. +

+ ), props: { EuiHeader, EuiHeaderBreadcrumbs, @@ -86,12 +118,80 @@ export const HeaderExample = { EuiHeaderSectionItem, EuiHeaderSectionItemButton, EuiHeaderLogo, + EuiHeaderSectionsProp, }, snippet: headerSnippet, demo:
, }, { - title: 'Links', + title: 'Sections', + source: [ + { + type: GuideSectionTypes.JS, + code: headerSectionsSource, + }, + { + type: GuideSectionTypes.HTML, + code: headerSectionsHtml, + }, + ], + text: ( + <> +

+ Alternatively, you can pass an array objects to the{' '} + sections props that takes a key of{' '} + items (array of children to wrap in an{' '} + EuiHeaderSectionItem) and/or{' '} + breadcrumbs (array of{' '} + breadcrumb objects). Each + item in the array will be wrapped in an{' '} + EuiHeaderSection. +

+

+ Note: Passing sections and{' '} + children will disregard the{' '} + children as it is not easily interpreted at what + location the children should be placed. +

+ + ), + props: { + EuiHeader, + EuiHeaderSectionsProp, + EuiHeaderSection, + EuiHeaderSectionItem, + }, + snippet: headerSectionsSnippet, + demo: , + }, + { + title: 'Fixed header', + source: [ + { + type: GuideSectionTypes.JS, + code: headerPositionSource, + }, + { + type: GuideSectionTypes.HTML, + code: headerPositionHtml, + }, + ], + text: ( + <> +

+ Most consumer need a header that does not scroll way with the page + contents. You can apply this display by changing{' '} + position to fixed. It will + also add the appropriate padding to the window body by applying a + class. +

+ + ), + snippet: '', + demo: , + }, + { + title: 'Header links', source: [ { type: GuideSectionTypes.JS, @@ -117,7 +217,7 @@ export const HeaderExample = { demo: , }, { - title: 'Display header alerts', + title: 'Alerts in the header', source: [ { type: GuideSectionTypes.JS, diff --git a/src-docs/src/views/header/header_position.js b/src-docs/src/views/header/header_position.js new file mode 100644 index 000000000000..9e9d1de9fe96 --- /dev/null +++ b/src-docs/src/views/header/header_position.js @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; + +import { + EuiHeader, + EuiHeaderLogo, + EuiSwitch, +} from '../../../../src/components'; + +export default () => { + const [position, setPosition] = useState('static'); + + const sections = [ + { + items: [ + , + ], + borders: 'none', + }, + { + items: [ +
+ setPosition(e.target.checked ? 'fixed' : 'static')} + /> +
, + ], + borders: 'none', + }, + ]; + + return ; +}; diff --git a/src-docs/src/views/header/header_sections.js b/src-docs/src/views/header/header_sections.js new file mode 100644 index 000000000000..dbe68843a00b --- /dev/null +++ b/src-docs/src/views/header/header_sections.js @@ -0,0 +1,82 @@ +import React from 'react'; + +import { + EuiHeader, + EuiFieldSearch, + EuiHeaderLogo, +} from '../../../../src/components'; + +import HeaderAppMenu from './header_app_menu'; +import HeaderUserMenu from './header_user_menu'; +import HeaderSpacesMenu from './header_spaces_menu'; + +export default () => { + const renderLogo = ( + + ); + + const breadcrumbs = [ + { + text: 'Management', + href: '#', + onClick: e => { + e.preventDefault(); + console.log('You clicked management'); + }, + 'data-test-subj': 'breadcrumbsAnimals', + className: 'customClass', + }, + { + text: 'Truncation test is here for a really long item', + href: '#', + onClick: e => { + e.preventDefault(); + console.log('You clicked truncation test'); + }, + }, + { + text: 'hidden', + href: '#', + onClick: e => { + e.preventDefault(); + console.log('You clicked hidden'); + }, + }, + { + text: 'Users', + href: '#', + onClick: e => { + e.preventDefault(); + console.log('You clicked users'); + }, + }, + { + text: 'Create', + }, + ]; + + const renderSearch = ( + + ); + + const sections = [ + { + items: [renderLogo, ], + borders: 'right', + breadcrumbs: breadcrumbs, + }, + { + items: [renderSearch,
], + borders: 'none', + }, + { + items: [, ], + }, + ]; + + return ; +}; diff --git a/src-docs/src/views/header/props.tsx b/src-docs/src/views/header/props.tsx new file mode 100644 index 000000000000..4259e7d9842b --- /dev/null +++ b/src-docs/src/views/header/props.tsx @@ -0,0 +1,7 @@ +import React, { FunctionComponent } from 'react'; + +import { EuiHeaderSections } from '../../../../src/components/header'; + +export const EuiHeaderSectionsProp: FunctionComponent< + EuiHeaderSections +> = () =>
; diff --git a/src-docs/src/views/home/home_view.js b/src-docs/src/views/home/home_view.js index 76216960e4c0..93eb9ac166e8 100644 --- a/src-docs/src/views/home/home_view.js +++ b/src-docs/src/views/home/home_view.js @@ -20,6 +20,7 @@ import { EuiText, EuiTitle, EuiToolTip, + EuiScreenReaderOnly, } from '../../../../src/components'; const pkg = require('../../../../package.json'); @@ -44,7 +45,10 @@ export const HomeView = () => ( - + + Elastic repo on GitHub + + diff --git a/src-docs/src/views/list_group/list_group_example.js b/src-docs/src/views/list_group/list_group_example.js index 99758b562e66..7e704b16cfc6 100644 --- a/src-docs/src/views/list_group/list_group_example.js +++ b/src-docs/src/views/list_group/list_group_example.js @@ -26,8 +26,12 @@ import ListGroupExtra from './list_group_extra'; const listGroupExtraSource = require('!!raw-loader!./list_group_extra'); const listGroupExtraHtml = renderToHtml(ListGroupExtra); +import ListGroupItemColor from './list_group_item_color'; +const listGroupItemColorSource = require('!!raw-loader!./list_group_item_color'); +const listGroupItemColorHtml = renderToHtml(ListGroupItemColor); + export const ListGroupExample = { - title: 'List Group', + title: 'List group', sections: [ { source: [ @@ -41,15 +45,24 @@ export const ListGroupExample = { }, ], text: ( -

- The ListGroup component is used to present   - ListGroupItems in a neatly formatted list. Use the -  flush and bordered{' '} - properties for full-width and bordered presentations, respectively. -

+ <> +

+ The EuiListGroup component is used to present{' '} + EuiListGroupItems in a neatly formatted list. Use + the flush and bordered{' '} + properties for full-width and bordered presentations, respectively. +

+

+ Adjust the gutterSize prop to increase or + decrease the spacing between items. +

+ ), props: { EuiListGroup, EuiListGroupItem }, demo: , + snippet: ` + +`, }, { title: 'List of links', @@ -64,16 +77,36 @@ export const ListGroupExample = { }, ], text: ( -

- Present ListGroupItems as links by providing an -  href value and change their appearance with - the size, isActive, and - isDisabled properties. As done in this example, the -  ListGroup component can also accept an array - of items via the listItems property. -

+ <> +

+ Display EuiListGroupItems as links by providing an{' '} + href value and change their state with the{' '} + isActive and isDisabled{' '} + properties. +

+

+ As is done in this example, the EuiListGroup{' '} + component can also accept an array of items via the{' '} + listItems property. +

+ ), demo: , + snippet: ``, }, { title: 'Secondary link actions', @@ -91,13 +124,24 @@ export const ListGroupExample = {

The extraAction property adds a secondary icon button to any list item. It accepts several properties of its own, - including color, onClick,   + including color, onClick,{' '} iconType, and alwaysShow, and can be used for actions such as pinning, favoriting, or deleting an item.

), demo: , + snippet: ``, }, { title: 'Text wrapping and tooltips', @@ -120,6 +164,47 @@ export const ListGroupExample = {

), demo: , + snippet: ` + +`, + }, + { + title: 'List item color and size', + source: [ + { + type: GuideSectionTypes.JS, + code: listGroupItemColorSource, + }, + { + type: GuideSectionTypes.HTML, + code: listGroupItemColorHtml, + }, + ], + text: ( + <> +

+ EuiListGroupItems will inherit the color from their + element type whether it is a button,{' '} + anchor, or span. You can + enforce a different color of primary,{' '} + text, or subdued with the{' '} + color prop. +

+

+ They also accept options for text size;{' '} + xs | s | m | l. +

+ + ), + demo: , + snippet: ``, }, ], }; diff --git a/src-docs/src/views/list_group/list_group_item_color.tsx b/src-docs/src/views/list_group/list_group_item_color.tsx new file mode 100644 index 000000000000..883bd5243389 --- /dev/null +++ b/src-docs/src/views/list_group/list_group_item_color.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { + EuiListGroupItem, + EuiListGroup, +} from '../../../../src/components/list_group'; + +export default () => ( + + + + {}} + label="Primary (s)" + color="primary" + size="s" + /> + + + + + +); diff --git a/src-docs/src/views/list_group/list_group_link_actions.js b/src-docs/src/views/list_group/list_group_link_actions.js index 129681e17dbd..e3e31b1df89f 100644 --- a/src-docs/src/views/list_group/list_group_link_actions.js +++ b/src-docs/src/views/list_group/list_group_link_actions.js @@ -1,24 +1,14 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; -import { - EuiListGroup, - EuiListGroupItem, - EuiSpacer, - EuiSwitch, - EuiCode, - EuiFlexGroup, - EuiFlexItem, -} from '../../../../src/components'; +import { EuiListGroup, EuiListGroupItem } from '../../../../src/components'; export default class extends Component { constructor(props) { super(props); this.state = { - flushWidth: false, - showBorder: false, favorite1: undefined, - favorite2: undefined, + favorite2: 'link2', favorite3: undefined, }; } @@ -65,106 +55,71 @@ export default class extends Component { }; render() { - const { - flushWidth, - showBorder, - favorite1, - favorite2, - favorite3, - } = this.state; + const { favorite1, favorite2, favorite3 } = this.state; return ( - - - - - Show as flush - - } - checked={this.state.flushWidth} - onChange={this.toggleFlushWidth} - /> - - - - Show as bordered - - } - checked={this.state.showBorder} - onChange={this.toggleBorder} - /> - - + + window.alert('Button clicked')} + isActive + extraAction={{ + color: 'subdued', + onClick: this.link1Clicked, + iconType: favorite1 === 'link1' ? 'pinFilled' : 'pin', + iconSize: 's', + 'aria-label': 'Favorite link1', + alwaysShow: favorite1 === 'link1', + }} + /> - + window.alert('Button clicked')} + label="EUI button link" + extraAction={{ + color: 'subdued', + onClick: this.link2Clicked, + iconType: favorite2 === 'link2' ? 'pinFilled' : 'pin', + iconSize: 's', + 'aria-label': 'Favorite link2', + alwaysShow: favorite2 === 'link2', + }} + /> - - window.alert('Button clicked')} - isActive - extraAction={{ - color: 'subdued', - onClick: this.link1Clicked, - iconType: favorite1 === 'link1' ? 'pinFilled' : 'pin', - iconSize: 's', - 'aria-label': 'Favorite link1', - alwaysShow: favorite1 === 'link1', - }} - /> + window.alert('Button clicked')} + iconType="broom" + label="EUI button link" + extraAction={{ + color: 'subdued', + onClick: this.link3Clicked, + iconType: favorite3 === 'link3' ? 'pinFilled' : 'pin', + iconSize: 's', + 'aria-label': 'Favorite link3', + alwaysShow: favorite3 === 'link3', + isDisabled: true, + }} + /> - window.alert('Button clicked')} - label="EUI button link" - extraAction={{ - color: 'subdued', - onClick: this.link2Clicked, - iconType: favorite2 === 'link2' ? 'pinFilled' : 'pin', - iconSize: 's', - 'aria-label': 'Favorite link2', - alwaysShow: favorite2 === 'link2', - }} - /> - - window.alert('Button clicked')} - iconType="broom" - label="EUI button link" - extraAction={{ - color: 'subdued', - onClick: this.link3Clicked, - iconType: favorite3 === 'link3' ? 'pinFilled' : 'pin', - iconSize: 's', - 'aria-label': 'Favorite link3', - alwaysShow: favorite3 === 'link3', - isDisabled: true, - }} - /> - - window.alert('Action clicked'), - iconType: 'pin', - iconSize: 's', - 'aria-label': 'Favorite link4', - }} - /> - - + window.alert('Action clicked'), + iconType: 'pin', + iconSize: 's', + 'aria-label': 'Favorite link4', + }} + /> +
); } } diff --git a/src-docs/src/views/list_group/list_group_links.js b/src-docs/src/views/list_group/list_group_links.js index c2660d171371..c0c5e7198acc 100644 --- a/src-docs/src/views/list_group/list_group_links.js +++ b/src-docs/src/views/list_group/list_group_links.js @@ -1,13 +1,6 @@ -import React, { Component, Fragment } from 'react'; +import React from 'react'; -import { - EuiListGroup, - EuiSpacer, - EuiSwitch, - EuiCode, - EuiFlexGroup, - EuiFlexItem, -} from '../../../../src/components'; +import { EuiListGroup } from '../../../../src/components'; const myContent = [ { @@ -44,62 +37,6 @@ const myContent = [ }, ]; -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - flushWidth: false, - showBorder: false, - }; - } - - toggleFlushWidth = () => { - this.setState(prevState => ({ flushWidth: !prevState.flushWidth })); - }; - - toggleBorder = () => { - this.setState(prevState => ({ showBorder: !prevState.showBorder })); - }; - - render() { - const { flushWidth, showBorder } = this.state; - - return ( - - - - - Show as flush - - } - checked={this.state.flushWidth} - onChange={this.toggleFlushWidth} - /> - - - - Show as bordered - - } - checked={this.state.showBorder} - onChange={this.toggleBorder} - /> - - - - - - - - ); - } -} +export default () => { + return ; +}; diff --git a/src-docs/src/views/search_bar/controlled_search_bar.js b/src-docs/src/views/search_bar/controlled_search_bar.js index 5cb461c49dc4..03ffd47ae3a4 100644 --- a/src-docs/src/views/search_bar/controlled_search_bar.js +++ b/src-docs/src/views/search_bar/controlled_search_bar.js @@ -154,6 +154,9 @@ export class ControlledSearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js index 2f9dd3334b3e..28c6ecfac9b4 100644 --- a/src-docs/src/views/search_bar/search_bar.js +++ b/src-docs/src/views/search_bar/search_bar.js @@ -136,6 +136,9 @@ export class SearchBar extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/search_bar/search_bar_example.js b/src-docs/src/views/search_bar/search_bar_example.js index 31b59653e3e8..27f701293d0e 100644 --- a/src-docs/src/views/search_bar/search_bar_example.js +++ b/src-docs/src/views/search_bar/search_bar_example.js @@ -37,9 +37,9 @@ export const SearchBarExample = { text: (

- A EuiSearchBar is a toolbar that enables the user - to create/define a search query. This can be done either by entering - the query syntax in a search box or by clicking any of the + An EuiSearchBar is a toolbar that enables the + user to create/define a search query. This can be done either by + entering the query syntax in a search box or by clicking any of the configured filters. The query language is not meant to be full blown search language for arbitrary data (e.g. as required in the Discover App in Kibana), yet it does provide some useful features: @@ -231,8 +231,8 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have its query controlled by a - parent component by passing the query prop. + An EuiSearchBar can have its query controlled by + a parent component by passing the query prop. Changes to the query will be passed back up through the{' '} onChange callback where the new query must be stored in state and passed back into the search bar. @@ -256,7 +256,7 @@ export const SearchBarExample = { text: (

- A EuiSearchBar can have custom filter dropdowns + An EuiSearchBar can have custom filter dropdowns that control how a user can search.

diff --git a/src-docs/src/views/search_bar/search_bar_filters.js b/src-docs/src/views/search_bar/search_bar_filters.js index 5a5efe6eac18..1b74a9676721 100644 --- a/src-docs/src/views/search_bar/search_bar_filters.js +++ b/src-docs/src/views/search_bar/search_bar_filters.js @@ -122,6 +122,9 @@ export class SearchBarFilters extends Component { const schema = { strict: true, fields: { + type: { + type: 'string', + }, active: { type: 'boolean', }, diff --git a/src-docs/src/views/suggest/_global_filter_group.scss b/src-docs/src/views/suggest/_global_filter_group.scss index 82908a215aa0..3187c9551fbf 100644 --- a/src-docs/src/views/suggest/_global_filter_group.scss +++ b/src-docs/src/views/suggest/_global_filter_group.scss @@ -2,7 +2,7 @@ @import 'saved_queries'; .globalFilterGroup__filterBar { - margin-top: $euiSizeM; + margin-top: $euiSizeXS; } // sass-lint:disable quotes @@ -20,9 +20,9 @@ .globalFilterGroup__filterFlexItem { overflow: hidden; - padding-bottom: 2px; // Allow the shadows of the pills to show + padding: $euiSizeS; } .globalFilterBar__flexItem { max-width: calc(100% - #{$euiSizeXS}); // Width minus margin around each flex itm -} \ No newline at end of file +} diff --git a/src-docs/src/views/suggest/global_filter_bar.js b/src-docs/src/views/suggest/global_filter_bar.js index 5bd19d2e0771..aefa66cb3dd5 100644 --- a/src-docs/src/views/suggest/global_filter_bar.js +++ b/src-docs/src/views/suggest/global_filter_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { EuiFlexGroup, EuiFlexItem } from '../../../../src/components'; +import { EuiBadgeGroup } from '../../../../src/components'; import GlobalFilterAdd from './global_filter_add'; import { GlobalFilterItem } from './global_filter_item'; @@ -12,45 +12,22 @@ export const GlobalFilterBar = ({ filters, className, ...rest }) => { const pinnedFilters = filters .filter(filter => filter.isPinned) .map(filter => { - return ( - - - - ); + return ; }); const unpinnedFilters = filters .filter(filter => !filter.isPinned) .map(filter => { - return ( - - - - ); + return ; }); return ( - + {/* Show pinned filters first and in a specific group */} {pinnedFilters} {unpinnedFilters} - - - - - + + ); }; diff --git a/src-docs/src/views/suggest/global_filter_item.js b/src-docs/src/views/suggest/global_filter_item.js index a575f10bccd9..87eb2ac55ed0 100644 --- a/src-docs/src/views/suggest/global_filter_item.js +++ b/src-docs/src/views/suggest/global_filter_item.js @@ -100,6 +100,7 @@ export class GlobalFilterItem extends Component { title={title} iconOnClick={this.deleteFilter} iconOnClickAriaLabel={'Delete filter'} + color="hollow" iconType="cross" iconSide="right" onClick={this.togglePopover} diff --git a/src-docs/src/views/suggest/saved_queries.js b/src-docs/src/views/suggest/saved_queries.js index 353ec1f925ce..8f7e04b70221 100644 --- a/src-docs/src/views/suggest/saved_queries.js +++ b/src-docs/src/views/suggest/saved_queries.js @@ -62,7 +62,8 @@ export default class extends Component { }, { id: 'filter1', - field: '@tags.keyword', + field: + 'Filter with a very long title to test if the badge will properly get truncated in the separate set of filter badges that are not quite as long but man does it really need to be long', operator: 'IS', value: 'value', isDisabled: true, diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.js b/src-docs/src/views/tables/in_memory/in_memory_selection.js index 44c6721872e4..630f07f03f49 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_selection.js +++ b/src-docs/src/views/tables/in_memory/in_memory_selection.js @@ -97,7 +97,7 @@ export class Table extends Component { renderToolsLeft() { const selection = this.state.control_columns; - if (selection.length === 0) { + if (!selection || selection.length === 0) { return; } diff --git a/src-docs/webpack.config.js b/src-docs/webpack.config.js index 644e79d63d25..4c907dc8ef79 100644 --- a/src-docs/webpack.config.js +++ b/src-docs/webpack.config.js @@ -7,9 +7,10 @@ const { NODE_ENV, CI } = process.env; const isDevelopment = NODE_ENV !== 'production' && CI == null; const isProduction = NODE_ENV === 'production'; +const bypassCache = NODE_ENV === 'puppeteer'; function useCache(loaders) { - if (isDevelopment) { + if (isDevelopment && !bypassCache) { return ['cache-loader'].concat(loaders); } diff --git a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap index e218d61cb91c..39b7d8e177a8 100644 --- a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap +++ b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap @@ -15,3 +15,12 @@ exports[`EuiScreenReaderOnly adds an accessibility class to a child element when This paragraph is not visibile to sighted users but will be read by screenreaders.

`; + +exports[`EuiScreenReaderOnly will show on focus 1`] = ` + + Link + +`; diff --git a/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap new file mode 100644 index 000000000000..685870e4087a --- /dev/null +++ b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiSkipLink is rendered 1`] = ` + + + + + +`; + +exports[`EuiSkipLink position absolute is rendered 1`] = ` + + + + + +`; + +exports[`EuiSkipLink position fixed is rendered 1`] = ` + + + + + +`; + +exports[`EuiSkipLink position static is rendered 1`] = ` + + + + + +`; diff --git a/src/components/accessibility/_index.scss b/src/components/accessibility/_index.scss index 98294afdf558..b8b7ffe9a8f1 100644 --- a/src/components/accessibility/_index.scss +++ b/src/components/accessibility/_index.scss @@ -1 +1,2 @@ @import 'screen_reader'; +@import 'skip_link'; diff --git a/src/components/accessibility/_screen_reader.scss b/src/components/accessibility/_screen_reader.scss index 398beafda858..6cc8a8e64071 100644 --- a/src/components/accessibility/_screen_reader.scss +++ b/src/components/accessibility/_screen_reader.scss @@ -1,3 +1,4 @@ -.euiScreenReaderOnly { +.euiScreenReaderOnly, +.euiScreenReaderOnly--showOnFocus:not(:focus) { @include euiScreenReaderOnly; } diff --git a/src/components/accessibility/_skip_link.scss b/src/components/accessibility/_skip_link.scss new file mode 100644 index 000000000000..38aa7fd814d6 --- /dev/null +++ b/src/components/accessibility/_skip_link.scss @@ -0,0 +1,20 @@ +.euiSkipLink { + transition: none !important; // sass-lint:disable-line no-important + + &:focus { + animation: none !important; // sass-lint:disable-line no-important + } + + // Set positions on focus only as to no override screenReaderOnly position + // When positioned absolutely, consumers still need to tell it WHERE (top,left,etc...) + &.euiSkipLink--absolute:focus { + position: absolute; + } + + &.euiSkipLink--fixed:focus { + position: fixed; + top: $euiSizeXS; + left: $euiSizeXS; + z-index: $euiZHeader + 1; + } +} diff --git a/src/components/accessibility/index.ts b/src/components/accessibility/index.ts index d63ef5aa8008..06e7f202092d 100644 --- a/src/components/accessibility/index.ts +++ b/src/components/accessibility/index.ts @@ -1,2 +1,3 @@ export { EuiKeyboardAccessible } from './keyboard_accessible'; export { EuiScreenReaderOnly } from './screen_reader'; +export { EuiSkipLink } from './skip_link'; diff --git a/src/components/accessibility/screen_reader.test.tsx b/src/components/accessibility/screen_reader.test.tsx index 5b6c06e59afb..db021b454ac2 100644 --- a/src/components/accessibility/screen_reader.test.tsx +++ b/src/components/accessibility/screen_reader.test.tsx @@ -30,4 +30,14 @@ describe('EuiScreenReaderOnly', () => { expect($paragraph).toMatchSnapshot(); }); }); + + test('will show on focus', () => { + const component = render( + + Link + + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/components/accessibility/screen_reader.tsx b/src/components/accessibility/screen_reader.tsx index 437d8d5a5a0e..55485b541e19 100644 --- a/src/components/accessibility/screen_reader.tsx +++ b/src/components/accessibility/screen_reader.tsx @@ -3,12 +3,23 @@ import classNames from 'classnames'; export interface EuiScreenReaderOnlyProps { children: ReactElement; + + /** + * For keyboard navigation, force content to display visually upon focus. + */ + showOnFocus?: boolean; } export const EuiScreenReaderOnly: FunctionComponent< EuiScreenReaderOnlyProps -> = ({ children }) => { - const classes = classNames('euiScreenReaderOnly', children.props.className); +> = ({ children, showOnFocus }) => { + const classes = classNames( + { + euiScreenReaderOnly: !showOnFocus, + 'euiScreenReaderOnly--showOnFocus': showOnFocus, + }, + children.props.className + ); const props = { ...children.props, diff --git a/src/components/accessibility/skip_link.test.tsx b/src/components/accessibility/skip_link.test.tsx new file mode 100644 index 000000000000..e7a0589d50c3 --- /dev/null +++ b/src/components/accessibility/skip_link.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test'; + +import { EuiSkipLink, POSITIONS } from './skip_link'; + +describe('EuiSkipLink', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + describe('position', () => { + POSITIONS.forEach(position => { + test(`${position} is rendered`, () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/components/accessibility/skip_link.tsx b/src/components/accessibility/skip_link.tsx new file mode 100644 index 000000000000..a87e2880b9c7 --- /dev/null +++ b/src/components/accessibility/skip_link.tsx @@ -0,0 +1,68 @@ +import React, { FunctionComponent, Ref } from 'react'; +import classNames from 'classnames'; +import { EuiButton, EuiButtonProps } from '../button/button'; +import { EuiScreenReaderOnly } from '../accessibility/screen_reader'; +import { PropsForAnchor, PropsForButton, ExclusiveUnion } from '../common'; + +type Positions = 'static' | 'fixed' | 'absolute'; +export const POSITIONS = ['static', 'fixed', 'absolute'] as Positions[]; + +export interface EuiSkipLinkProps extends EuiButtonProps { + /** + * If true, the link will be fixed to the top left of the viewport + */ + position?: Positions; + + /** + * Typically an anchor id (e.g. `a11yMainContent`), the value provided + * will be prepended with a hash `#` and used as the link `href` + */ + destinationId: string; + + tabIndex?: number; +} + +type propsForAnchor = PropsForAnchor< + EuiSkipLinkProps, + { + buttonRef?: Ref; + } +>; + +type propsForButton = PropsForButton< + EuiSkipLinkProps, + { + buttonRef?: Ref; + } +>; + +export type Props = ExclusiveUnion; + +export const EuiSkipLink: FunctionComponent = ({ + destinationId, + tabIndex, + position = 'static', + children, + className, + ...rest +}) => { + const classes = classNames( + 'euiSkipLink', + [`euiSkipLink--${position}`], + className + ); + + return ( + + + {children} + + + ); +}; diff --git a/src/components/badge/_index.scss b/src/components/badge/_index.scss index a567f7a42de2..a271275b2dc8 100644 --- a/src/components/badge/_index.scss +++ b/src/components/badge/_index.scss @@ -1,3 +1,4 @@ @import 'badge'; +@import 'badge_group/index'; @import 'beta_badge/index'; @import 'notification_badge/index'; diff --git a/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap new file mode 100644 index 000000000000..57ba7cbeb02e --- /dev/null +++ b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiBadgeGroup gutterSize none is rendered 1`] = ` +
+`; + +exports[`EuiBadgeGroup gutterSize s is rendered 1`] = ` +
+`; + +exports[`EuiBadgeGroup gutterSize xs is rendered 1`] = ` +
+`; + +exports[`EuiBadgeGroup is rendered 1`] = ` +
+ + + + + Content + + + + +
+`; diff --git a/src/components/badge/badge_group/_badge_group.scss b/src/components/badge/badge_group/_badge_group.scss new file mode 100644 index 000000000000..a1a0b0ec5d3c --- /dev/null +++ b/src/components/badge/badge_group/_badge_group.scss @@ -0,0 +1,23 @@ +$euiBadgeGroupGutterTypes: ( + gutterExtraSmall: $euiSizeXS, + gutterSmall: $euiSizeS, +); + +.euiBadgeGroup__item { + display: inline-block; + max-width: 100%; +} + +// Gutter Sizes +@each $gutterName, $gutterSize in $euiBadgeGroupGutterTypes { + $halfGutterSize: $gutterSize * .5; + + .euiBadgeGroup--#{$gutterName} { + margin: -$halfGutterSize; + + & > .euiBadgeGroup__item { + margin: $halfGutterSize; + max-width: calc(100% - #{$gutterSize}); + } + } +} diff --git a/src/components/badge/badge_group/_index.scss b/src/components/badge/badge_group/_index.scss new file mode 100644 index 000000000000..21500c4a8cf7 --- /dev/null +++ b/src/components/badge/badge_group/_index.scss @@ -0,0 +1 @@ +@import './badge_group'; diff --git a/src/components/badge/badge_group/badge_group.test.tsx b/src/components/badge/badge_group/badge_group.test.tsx new file mode 100644 index 000000000000..895f94e08b96 --- /dev/null +++ b/src/components/badge/badge_group/badge_group.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../../test/required_props'; + +import { EuiBadge } from '../badge'; +import { EuiBadgeGroup, GUTTER_SIZES } from './badge_group'; + +describe('EuiBadgeGroup', () => { + test('is rendered', () => { + const component = render( + + Content + + ); + + expect(component).toMatchSnapshot(); + }); + + describe('gutterSize', () => { + GUTTER_SIZES.forEach(size => { + it(`${size} is rendered`, () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/components/badge/badge_group/badge_group.tsx b/src/components/badge/badge_group/badge_group.tsx new file mode 100644 index 000000000000..ce3b97565c08 --- /dev/null +++ b/src/components/badge/badge_group/badge_group.tsx @@ -0,0 +1,48 @@ +import React, { HTMLAttributes, Ref, ReactNode } from 'react'; +import classNames from 'classnames'; +import { CommonProps, keysOf } from '../../common'; + +const gutterSizeToClassNameMap = { + none: null, + xs: 'euiBadgeGroup--gutterExtraSmall', + s: 'euiBadgeGroup--gutterSmall', +}; + +export const GUTTER_SIZES = keysOf(gutterSizeToClassNameMap); +type BadgeGroupGutterSize = keyof typeof gutterSizeToClassNameMap; + +export interface EuiBadgeGroupProps { + /** + * Space between badges + */ + gutterSize?: BadgeGroupGutterSize; + /** + * Should be a list of EuiBadge's but can also be any other element + * Will apply an extra class to add spacing + */ + children?: ReactNode; +} + +export const EuiBadgeGroup = React.forwardRef< + HTMLDivElement, + CommonProps & HTMLAttributes & EuiBadgeGroupProps +>( + ( + { children, className, gutterSize = 'xs', ...rest }, + ref: Ref + ) => { + const classes = classNames( + 'euiBadgeGroup', + gutterSizeToClassNameMap[gutterSize as BadgeGroupGutterSize], + className + ); + + return ( +
+ {React.Children.map(children, (child: ReactNode) => ( + {child} + ))} +
+ ); + } +); diff --git a/src/components/badge/badge_group/index.ts b/src/components/badge/badge_group/index.ts new file mode 100644 index 000000000000..347af71bc336 --- /dev/null +++ b/src/components/badge/badge_group/index.ts @@ -0,0 +1 @@ +export { EuiBadgeGroup, EuiBadgeGroupProps } from './badge_group'; diff --git a/src/components/badge/index.ts b/src/components/badge/index.ts index 6ab2cc152c30..125b867860dd 100644 --- a/src/components/badge/index.ts +++ b/src/components/badge/index.ts @@ -3,3 +3,5 @@ export { EuiBadge, EuiBadgeProps } from './badge'; export { EuiBetaBadge } from './beta_badge'; export { EuiNotificationBadge } from './notification_badge'; + +export { EuiBadgeGroup, EuiBadgeGroupProps } from './badge_group'; diff --git a/src/components/basic_table/in_memory_table.test.tsx b/src/components/basic_table/in_memory_table.test.tsx index 798d977024ac..415d1498e59c 100644 --- a/src/components/basic_table/in_memory_table.test.tsx +++ b/src/components/basic_table/in_memory_table.test.tsx @@ -2,13 +2,10 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { requiredProps } from '../../test'; -import { - EuiInMemoryTable, - EuiInMemoryTableProps, - FilterConfig, -} from './in_memory_table'; +import { EuiInMemoryTable, EuiInMemoryTableProps } from './in_memory_table'; import { ENTER } from '../../services/key_codes'; import { SortDirection } from '../../services'; +import { FilterConfig } from '../search_bar/filters'; interface BasicItem { id: number | string; @@ -652,6 +649,7 @@ describe('EuiInMemoryTable', () => { pagination: true, sorting: true, search: { + onChange: () => {}, defaultQuery: 'name:name1', box: { incremental: true, @@ -702,7 +700,9 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: {}, + search: { + onChange: () => true, + }, className: 'testTable', }; @@ -768,7 +768,10 @@ describe('EuiInMemoryTable', () => { name: 'Name', }, ], - search: { defaultQuery: 'No' }, + search: { + onChange: () => {}, + defaultQuery: 'No', + }, className: 'testTable', message: No items found!, }; @@ -810,6 +813,7 @@ describe('EuiInMemoryTable', () => { }, ], search: { + onChange: () => {}, defaultQuery: 'No', }, className: 'testTable', diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index e4d9b8033440..37ce69bd15dc 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -18,134 +18,16 @@ import { } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; -// @ts-ignore -import { EuiSearchBar } from '../search_bar'; +import { EuiSearchBar, Query } from '../search_bar'; import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; - -// Search bar types. Should be moved when it is typescriptified. -interface SearchBoxConfig { - placeholder?: string; - incremental?: boolean; - schema?: SchemaType; -} - -interface SchemaType { - strict?: boolean; - fields?: object; - flags?: string[]; -} - -interface IsFilterConfigType { - type: 'is'; - field: string; - name: string; - negatedName?: string; - available?: () => boolean; -} - -interface FieldValueOptionType { - field?: string; - value: any; - name?: string; - view?: ReactNode; -} - -interface FieldValueSelectionFilterConfigType { - type: 'field_value_selection'; - field?: string; - autoClose?: boolean; - name: string; - options: - | FieldValueOptionType[] - | ((query: Query) => Promise); - filterWith?: - | ((name: string, query: string, options: object) => boolean) - | 'prefix' - | 'includes'; - cache?: number; - multiSelect?: boolean | 'and' | 'or'; - loadingMessage?: string; - noOptionsMessage?: string; - searchThreshold?: number; - available?: () => boolean; -} - -interface FieldValueToggleFilterConfigType { - type: 'field_value_toggle'; - field: string; - value: string | number | boolean; - name: string; - negatedName?: string; - available?: () => boolean; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterItem { - value: string | number | boolean; - name: string; - negatedName?: string; - operator?: 'eq' | 'exact' | 'gt' | 'gte' | 'lt' | 'lte'; -} - -interface FieldValueToggleGroupFilterConfigType { - type: 'field_value_toggle_group'; - field: string; - items: FieldValueToggleGroupFilterItem[]; - available?: () => boolean; -} - -export type FilterConfig = - | IsFilterConfigType - | FieldValueSelectionFilterConfigType - | FieldValueToggleFilterConfigType - | FieldValueToggleGroupFilterConfigType; - -type SearchBox = Omit & { - schema?: boolean | SchemaType; -}; - -/* Should point at search_bar/query type when it is converted to typescript */ -type Query = any; +import { EuiSearchBarProps } from '../search_bar/search_bar'; +import { SchemaType } from '../search_bar/search_box'; interface onChangeArgument { - query: Query; + query: Query | null; queryText: string; - error: string; -} - -interface EuiSearchBarProps { - /** - The initial query the bar will hold when first mounted - */ - defaultQuery?: Query; - /** - If you wish to use the search bar as a controlled component, continuously pass the query - via this prop - */ - query?: Query; - /** - Configures the search box. Set `placeholder` to change the placeholder text in the box and - `incremental` to support incremental (as you type) search. - */ - box?: SearchBox; - /** - An array of search filters. - */ - filters?: FilterConfig[]; - /** - * Tools which go to the left of the search bar. - */ - toolsLeft?: React.ReactNode; - /** - * Tools which go to the right of the search bar. - */ - toolsRight?: React.ReactNode; - /** - * Date formatter to use when parsing date values - */ - dateFormat?: object; - onChange?: (values: onChangeArgument) => boolean | void; + error: Error | null; } function isEuiSearchBarProps( @@ -208,7 +90,7 @@ interface State { sortName: ReactNode; sortDirection?: Direction; }; - query: Query; + query: Query | null; pageIndex: number; pageSize?: number; pageSizeOptions?: number[]; @@ -219,11 +101,13 @@ interface State { } const getInitialQuery = (search: Search | undefined) => { + let query: Query | string; if (!search) { - return; + query = ''; + } else { + query = (search as EuiSearchBarProps).defaultQuery || ''; } - const query = (search as EuiSearchBarProps).defaultQuery || ''; return isString(query) ? EuiSearchBar.Query.parse(query) : query; }; @@ -382,7 +266,7 @@ export class EuiInMemoryTable extends Component< pageSizeOptions, sortName, sortDirection, - allowNeutralSort: allowNeutralSort === false ? false : true, + allowNeutralSort: allowNeutralSort !== false, hidePerPageOptions, }; } @@ -444,14 +328,21 @@ export class EuiInMemoryTable extends Component< }; onQueryChange = ({ query, queryText, error }: onChangeArgument) => { - if (isEuiSearchBarProps(this.props.search)) { - const search = this.props.search; + const { search } = this.props; + if (isEuiSearchBarProps(search)) { if (search.onChange) { - const shouldQueryInMemory = search.onChange({ - query, - queryText, - error, - }); + const shouldQueryInMemory = + error == null + ? search.onChange({ + query: query!, + queryText, + error: null, + }) + : search.onChange({ + query: null, + queryText, + error, + }); if (!shouldQueryInMemory) { return; } @@ -468,14 +359,17 @@ export class EuiInMemoryTable extends Component< renderSearchBar() { const { search } = this.props; if (search) { - let searchBarProps: EuiSearchBarProps = {}; + let searchBarProps: Omit = {}; if (isEuiSearchBarProps(search)) { const { onChange, ..._searchBarProps } = search; searchBarProps = _searchBarProps; if (searchBarProps.box && searchBarProps.box.schema === true) { - searchBarProps.box.schema = this.resolveSearchSchema(); + searchBarProps.box = { + ...searchBarProps.box, + schema: this.resolveSearchSchema(), + }; } } @@ -483,7 +377,7 @@ export class EuiInMemoryTable extends Component< } } - resolveSearchSchema() { + resolveSearchSchema(): SchemaType { const { columns } = this.props; return columns.reduce<{ strict: boolean; @@ -501,7 +395,7 @@ export class EuiInMemoryTable extends Component< ); } - getItemSorter() { + getItemSorter(): (a: T, b: T) => number { const { sortName, sortDirection } = this.state; const { columns } = this.props; @@ -512,7 +406,7 @@ export class EuiInMemoryTable extends Component< if (sortColumn == null) { // can't return a non-function so return a function that says everything is the same - return () => () => 0; + return () => 0; } const sortable = sortColumn.sortable; diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index cd29bea80b8d..ef3c5f9905a1 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -7,8 +7,9 @@ import React, { Component, FocusEventHandler, - KeyboardEventHandler, HTMLAttributes, + KeyboardEventHandler, + RefCallback, } from 'react'; import classNames from 'classnames'; @@ -34,7 +35,6 @@ import { EuiComboBoxOptionsListProps } from './combo_box_options_list/combo_box_ import { UpdatePositionHandler, OptionHandler, - RefCallback, RefInstance, EuiComboBoxOptionOption, EuiComboBoxOptionsListPosition, diff --git a/src/components/combo_box/combo_box_input/combo_box_input.tsx b/src/components/combo_box/combo_box_input/combo_box_input.tsx index de91822d9e49..b5e605dbe822 100644 --- a/src/components/combo_box/combo_box_input/combo_box_input.tsx +++ b/src/components/combo_box/combo_box_input/combo_box_input.tsx @@ -1,4 +1,9 @@ -import React, { Component, FocusEventHandler, ChangeEventHandler } from 'react'; +import React, { + ChangeEventHandler, + Component, + FocusEventHandler, + RefCallback, +} from 'react'; import classNames from 'classnames'; import AutosizeInput from 'react-input-autosize'; @@ -11,7 +16,6 @@ import { EuiComboBoxOptionOption, EuiComboBoxSingleSelectionShape, OptionHandler, - RefCallback, UpdatePositionHandler, } from '../types'; import { CommonProps } from '../../common'; diff --git a/src/components/combo_box/combo_box_options_list/combo_box_option.tsx b/src/components/combo_box/combo_box_options_list/combo_box_option.tsx index 3ee02230ee80..382dbbab372a 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_option.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_option.tsx @@ -1,13 +1,14 @@ import React, { Component, - ReactNode, - KeyboardEventHandler, HTMLAttributes, + KeyboardEventHandler, + ReactNode, + RefCallback, } from 'react'; import classNames from 'classnames'; import { ENTER, SPACE } from '../../../services/key_codes'; -import { EuiComboBoxOptionOption, OptionHandler, RefCallback } from '../types'; +import { EuiComboBoxOptionOption, OptionHandler } from '../types'; import { CommonProps } from '../../common'; export interface EuiComboBoxOptionProps diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index 5530df3027b3..04967ce18b06 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -1,4 +1,9 @@ -import React, { Component, ReactNode, ComponentProps } from 'react'; +import React, { + Component, + ComponentProps, + ReactNode, + RefCallback, +} from 'react'; import classNames from 'classnames'; import { List, ListProps } from 'react-virtualized'; // eslint-disable-line import/named @@ -20,7 +25,6 @@ import { EuiComboBoxOptionsListPosition, EuiComboBoxSingleSelectionShape, OptionHandler, - RefCallback, RefInstance, UpdatePositionHandler, } from '../types'; diff --git a/src/components/combo_box/types.ts b/src/components/combo_box/types.ts index e0c3f1c7c4c7..445007de1c76 100644 --- a/src/components/combo_box/types.ts +++ b/src/components/combo_box/types.ts @@ -17,11 +17,6 @@ export type UpdatePositionHandler = ( ) => void; export type OptionHandler = (option: EuiComboBoxOptionOption) => void; -// See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/42482/files -export type RefCallback = { - bivarianceHack(instance: T | null): void; -}['bivarianceHack']; - export type RefInstance = T | null; export type EuiComboBoxOptionsListPosition = 'top' | 'bottom'; diff --git a/src/components/common.ts b/src/components/common.ts index a0ed67e1b980..6e61a578fa77 100644 --- a/src/components/common.ts +++ b/src/components/common.ts @@ -15,10 +15,6 @@ export interface CommonProps { export type NoArgCallback = () => T; -export type RefCallback = ( - element: Element -) => void; - // utility types: /** diff --git a/src/components/datagrid/_data_grid.scss b/src/components/datagrid/_data_grid.scss index 314140b5393f..85a95cc83550 100644 --- a/src/components/datagrid/_data_grid.scss +++ b/src/components/datagrid/_data_grid.scss @@ -25,21 +25,20 @@ .euiDataGrid__content { @include euiScrollBar; - @include euiScrollBar; height: 100%; - overflow-y: auto; + overflow: auto; font-feature-settings: 'tnum' 1; // Tabular numbers - overflow-x: auto; scroll-padding: 0; max-width: 100%; width: 100%; + z-index: 2; // Sits above the pagination below it, but below the controls above it } .euiDataGrid__controls { background: $euiPageBackgroundColor; position: relative; - z-index: 2; + z-index: 3; // Needs to sit above the content blow that sits below it border: $euiBorderThin; padding: $euiSizeXS; flex-grow: 0; diff --git a/src/components/datagrid/_data_grid_column_resizer.scss b/src/components/datagrid/_data_grid_column_resizer.scss index f46bce3d6186..09ab4287586b 100644 --- a/src/components/datagrid/_data_grid_column_resizer.scss +++ b/src/components/datagrid/_data_grid_column_resizer.scss @@ -7,7 +7,7 @@ width: $euiSize; cursor: ew-resize; opacity: 0; - z-index: 2; + z-index: 2; // Needs to be a level above the cells themselves in case of overlaps // Center a vertical line within the button above &:after { diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index 98b7cb2000e4..3a8b038e9360 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -34,8 +34,7 @@ margin-top: -1px; box-shadow: 0 0 0 2px $euiFocusRingColor; border-radius: 1px; - // Needed so it sits above potential striping in the next row - z-index: 2; + z-index: 2; // Needed so it sits above potential striping in the next row .euiDataGridRowCell__expandButton { margin-left: $euiDataGridCellPaddingM; diff --git a/src/components/datagrid/_data_grid_header_row.scss b/src/components/datagrid/_data_grid_header_row.scss index 79702d44a643..658ec05c6aed 100644 --- a/src/components/datagrid/_data_grid_header_row.scss +++ b/src/components/datagrid/_data_grid_header_row.scss @@ -1,6 +1,10 @@ .euiDataGridHeader { display: inline-flex; min-width: 100%; // Needed to prevent wraps. Inline flex is tricky + z-index: 3; // Needs to sit above the content and focused cells + background: $euiColorLightestShade; + position: sticky; // In IE11 this does not work, but doesn't cause a break. + top: 0; } @include euiDataGridHeaderCell { @@ -30,8 +34,7 @@ border: 1px solid transparent; box-shadow: 0 0 0 2px $euiFocusRingColor; border-radius: 1px; - // Needed so it sits above the other rows - z-index: 2; + z-index: 2; // Needed so the focus ring sits above the other row below it. } // We only truncate if the cell is not a control column. @@ -47,6 +50,12 @@ // Header alternates // Often these need extra specificity because they need to gracefully clash with the border settings +@include euiDataGridStyles(bordersNone, bordersHorizontal) { + .euiDataGridHeader { + background: $euiColorEmptyShade; + } +} + @include euiDataGridStyles(headerUnderline) { @include euiDataGridHeaderCell { border-top: none; diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 598ff5786fc8..d2a952bd1ca3 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -8,6 +8,7 @@ import React, { Fragment, ReactChild, useMemo, + useRef, Dispatch, SetStateAction, } from 'react'; @@ -623,6 +624,18 @@ export const EuiDataGrid: FunctionComponent = props => { orderedVisibleColumns ); + const contentRef = useRef(null); + + // Because of a weird Chrome bug with position:sticky css items and focus, we force scrolling to the top + // if the item is in the first row. This prevents the cell from ever being under the sticky header. + useEffect(() => { + if (focusedCell !== undefined && focusedCell[1] === 0) { + if (contentRef.current != null) { + contentRef.current.scrollTop = 0; + } + } + }, [focusedCell]); + const classes = classNames( 'euiDataGrid', fontSizesToClassMap[gridStyles.fontSize!], @@ -842,6 +855,7 @@ export const EuiDataGrid: FunctionComponent = props => { /> ) : null}
diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 26f3f6140cf5..a36b4e4a4d23 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -368,7 +368,7 @@ export class EuiDataGridCell extends Component< isOpen={this.state.popoverIsOpen} ownFocus panelClassName="euiDataGridRowCell__popover" - zIndex={2000} + zIndex={8001} display="block" closePopover={() => this.setState({ popoverIsOpen: false })} onTrapDeactivation={this.updateFocus}> diff --git a/src/components/facet/facet_button.tsx b/src/components/facet/facet_button.tsx index 795c2c72d0bd..68a603411b94 100644 --- a/src/components/facet/facet_button.tsx +++ b/src/components/facet/facet_button.tsx @@ -3,10 +3,11 @@ import React, { HTMLAttributes, MouseEventHandler, ReactNode, + RefCallback, } from 'react'; import classNames from 'classnames'; -import { CommonProps, RefCallback } from '../common'; +import { CommonProps } from '../common'; import { EuiNotificationBadge } from '../badge'; diff --git a/src/components/header/__snapshots__/header.test.tsx.snap b/src/components/header/__snapshots__/header.test.tsx.snap index 2ac2a381dee3..3654b8e49a9a 100644 --- a/src/components/header/__snapshots__/header.test.tsx.snap +++ b/src/components/header/__snapshots__/header.test.tsx.snap @@ -3,17 +3,101 @@ exports[`EuiHeader is rendered 1`] = `
`; exports[`EuiHeader renders children 1`] = `
Hello!
`; + +exports[`EuiHeader renders in fixed position 1`] = ` +
+ + Hello! + +
+`; + +exports[`EuiHeader sections render breadcrumbs and props 1`] = ` +
+ +
+`; + +exports[`EuiHeader sections render simple items and borders 1`] = ` +
+
+
+ Item 1 +
+
+ Item 2 +
+
+
+
+ Item A +
+
+ Item B +
+
+
+`; + +exports[`EuiHeader throws a warning if both children and sections were passed 1`] = ` +
+
+
+ Item 1 +
+
+ Item 2 +
+
+
+`; diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss index ca9c05b1145e..389fb60f9495 100644 --- a/src/components/header/_header.scss +++ b/src/components/header/_header.scss @@ -6,6 +6,19 @@ position: relative; z-index: $euiZHeader; // ensure the shadow shows above content display: flex; + justify-content: space-between; background: $euiHeaderBackgroundColor; border-bottom: $euiBorderThin; + + &--fixed { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: $euiZLevel7; + } +} + +.euiBody--headerIsFixed { + padding-top: $euiHeaderChildSize + $euiSizeS; // Extra padding to accound for the shadow } diff --git a/src/components/header/_header_logo.scss b/src/components/header/_header_logo.scss index 1ac01250f810..b8687ef6639d 100644 --- a/src/components/header/_header_logo.scss +++ b/src/components/header/_header_logo.scss @@ -12,6 +12,10 @@ vertical-align: middle; white-space: nowrap; + &:hover { + background: $euiColorLightestShade; + } + &:focus, &:hover { text-decoration: none; diff --git a/src/components/header/header.test.tsx b/src/components/header/header.test.tsx index 5a36ac03da4c..8be9d11b7068 100644 --- a/src/components/header/header.test.tsx +++ b/src/components/header/header.test.tsx @@ -20,4 +20,84 @@ describe('EuiHeader', () => { expect(component).toMatchSnapshot(); }); + + test('renders in fixed position', () => { + const component = render( + + Hello! + + ); + + expect(component).toMatchSnapshot(); + }); + + describe('sections', () => { + test('render simple items and borders', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('render breadcrumbs and props', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); + + describe('throws a warning', () => { + const oldConsoleError = console.warn; + let consoleStub: jest.Mock; + + beforeEach(() => { + // We don't use jest.spyOn() here, because EUI's tests apply a global + // console.error() override that throws an exception. For these + // tests, we just want to know if console.error() was called. + console.warn = consoleStub = jest.fn(); + }); + + afterEach(() => { + console.warn = oldConsoleError; + }); + + test('if both children and sections were passed', () => { + const component = render( + + Child + + ); + + expect(consoleStub).toBeCalled(); + expect(consoleStub.mock.calls[0][0]).toMatch( + 'cannot accept both `children` and `sections`' + ); + expect(component).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index 1c59a263e1d4..2da6d3eeb3c4 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -1,20 +1,124 @@ -import React, { FunctionComponent, HTMLAttributes } from 'react'; +import React, { FunctionComponent, HTMLAttributes, useEffect } from 'react'; import classNames from 'classnames'; - import { CommonProps } from '../common'; -export type EuiHeaderProps = CommonProps & HTMLAttributes; +import { + EuiHeaderSectionItem, + EuiHeaderSectionItemProps, + EuiHeaderSection, +} from './header_section'; +import { EuiHeaderBreadcrumbs } from './header_breadcrumbs'; +import { Breadcrumb, EuiBreadcrumbsProps } from '../breadcrumbs'; + +type EuiHeaderSectionItemType = EuiHeaderSectionItemProps['children']; +type EuiHeaderSectionBorderType = EuiHeaderSectionItemProps['border']; + +export interface EuiHeaderSections { + /** + * An arry of items that will be wrapped in a #EuiHeaderSectionItem + */ + items?: EuiHeaderSectionItemType[]; + /** + * Apply the passed border side to each #EuiHeaderSectionItem + */ + borders?: EuiHeaderSectionBorderType; + /** + * Breadcrumbs in the header cannot be wrapped in a #EuiHeaderSection in order for truncation to work. + * Simply pass the array of Breadcrumb objects + */ + breadcrumbs?: Breadcrumb[]; + /** + * Other props to pass to #EuiHeaderBreadcrumbs + */ + breadcrumbProps?: Omit; +} + +function createHeaderSection( + sections: EuiHeaderSectionItemType[], + border?: EuiHeaderSectionBorderType +) { + return sections.map((section, index) => { + return ( + + {section} + + ); + }); +} + +export type EuiHeaderProps = CommonProps & + HTMLAttributes & { + /** + * An array of objects to wrap in a #EuiHeaderSection. + * Each section is spaced using `space-between`. + * See #EuiHeaderSectionsProp for object details. + * This prop disregards the prop `children` if both are passed. + */ + sections?: EuiHeaderSections[]; + /** + * Helper that positions the header against the window body and + * adds the correct amount of top padding to the window when in `fixed` mode + */ + position?: 'static' | 'fixed'; + }; export const EuiHeader: FunctionComponent = ({ children, className, + sections, + position = 'static', ...rest }) => { - const classes = classNames('euiHeader', className); + const classes = classNames('euiHeader', `euiHeader--${position}`, className); + + useEffect(() => { + if (position === 'fixed') { + document.body.classList.add('euiBody--headerIsFixed'); + } + return () => { + document.body.classList.remove('euiBody--headerIsFixed'); + }; + }, [position]); + + let contents; + if (sections) { + if (children) { + // In case both children and sections are passed, warn in the console that the children will be disregarded + console.warn( + 'EuiHeader cannot accept both `children` and `sections`. It will disregard the `children`.' + ); + } + + contents = sections.map((section, index) => { + const content = []; + if (section.items) { + // Items get wrapped in EuiHeaderSection and each item in a EuiHeaderSectionItem + content.push( + + {createHeaderSection(section.items, section.borders)} + + ); + } + if (section.breadcrumbs) { + content.push( + // Breadcrumbs are separate and cannot be contained in a EuiHeaderSection + // in order for truncation to work + + ); + } + return content; + }); + } else { + contents = children; + } return (
- {children} + {contents}
); }; diff --git a/src/components/header/header_section/_header_section_item.scss b/src/components/header/header_section/_header_section_item.scss index 098b6a98667b..ac58b1774c14 100644 --- a/src/components/header/header_section/_header_section_item.scss +++ b/src/components/header/header_section/_header_section_item.scss @@ -2,10 +2,8 @@ .euiHeaderSectionItem { position: relative; - - &:hover { - background: $euiColorLightestShade; - } + display: flex; + align-items: center; &:after { position: absolute; @@ -23,6 +21,10 @@ text-align: center; font-size: 0; // aligns icons better vertically + &:hover { + background: $euiColorLightestShade; + } + &:focus { background: $euiFocusBackgroundColor; } diff --git a/src/components/header/header_section/header_section_item.tsx b/src/components/header/header_section/header_section_item.tsx index 4bcdff3937da..2989c03c3212 100644 --- a/src/components/header/header_section/header_section_item.tsx +++ b/src/components/header/header_section/header_section_item.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, ReactNode } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; @@ -11,16 +11,14 @@ const borderToClassNameMap: { [border in Border]: string | undefined } = { none: undefined, }; -type Props = CommonProps & { +export type EuiHeaderSectionItemProps = CommonProps & { border?: Border; + children?: ReactNode; }; -export const EuiHeaderSectionItem: FunctionComponent = ({ - border = 'left', - children, - className, - ...rest -}) => { +export const EuiHeaderSectionItem: FunctionComponent< + EuiHeaderSectionItemProps +> = ({ border = 'left', children, className, ...rest }) => { const classes = classNames( 'euiHeaderSectionItem', borderToClassNameMap[border], diff --git a/src/components/header/header_section/index.ts b/src/components/header/header_section/index.ts index 472a50057a32..2d032f7b0886 100644 --- a/src/components/header/header_section/index.ts +++ b/src/components/header/header_section/index.ts @@ -1,5 +1,8 @@ export { EuiHeaderSection } from './header_section'; -export { EuiHeaderSectionItem } from './header_section_item'; +export { + EuiHeaderSectionItem, + EuiHeaderSectionItemProps, +} from './header_section_item'; export { EuiHeaderSectionItemButton } from './header_section_item_button'; diff --git a/src/components/header/index.ts b/src/components/header/index.ts index 731612722e68..029216ab1b49 100644 --- a/src/components/header/index.ts +++ b/src/components/header/index.ts @@ -1,4 +1,4 @@ -export { EuiHeader, EuiHeaderProps } from './header'; +export { EuiHeader, EuiHeaderProps, EuiHeaderSections } from './header'; export { EuiHeaderAlert, EuiHeaderAlertProps } from './header_alert'; diff --git a/src/components/index.js b/src/components/index.js index f4cb7cf66528..78214b35586a 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -4,9 +4,18 @@ export { EuiAspectRatio } from './aspect_ratio'; export { EuiAvatar } from './avatar'; -export { EuiKeyboardAccessible, EuiScreenReaderOnly } from './accessibility'; +export { + EuiKeyboardAccessible, + EuiScreenReaderOnly, + EuiSkipLink, +} from './accessibility'; -export { EuiBadge, EuiBetaBadge, EuiNotificationBadge } from './badge'; +export { + EuiBadge, + EuiBetaBadge, + EuiNotificationBadge, + EuiBadgeGroup, +} from './badge'; export { EuiBottomBar } from './bottom_bar'; diff --git a/src/components/list_group/__snapshots__/list_group.test.tsx.snap b/src/components/list_group/__snapshots__/list_group.test.tsx.snap index fbd22fd489eb..72252c294432 100644 --- a/src/components/list_group/__snapshots__/list_group.test.tsx.snap +++ b/src/components/list_group/__snapshots__/list_group.test.tsx.snap @@ -3,7 +3,148 @@ exports[`EuiListGroup is rendered 1`] = `