diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c6b4fe414..741e6513cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Added support for nodes as "Action" column headers in `EuiBasicTable`, which was overlooked in the original change in `4.5.0` ([#1312](https://github.com/elastic/eui/pull/1312)) +- Updated `GlobalDatePicker` example to include all Kibana features ([#1219](https://github.com/elastic/eui/pull/1219)) +- Adjusted `EuiDatePickerRange` to allow for deeper customization ([#1219](https://github.com/elastic/eui/pull/1219)) +- Added `contentProps` and `textProps` to `EuiButton` and `EuiButtonEmpty` ([#1219](https://github.com/elastic/eui/pull/1219)) **Bug fixes** diff --git a/src-docs/src/theme_dark.scss b/src-docs/src/theme_dark.scss index f09a1aede5c..97d4ef0207c 100644 --- a/src-docs/src/theme_dark.scss +++ b/src-docs/src/theme_dark.scss @@ -1,3 +1,4 @@ @import '../../src/theme_dark'; @import './components/guide_components'; @import './views/header/global_filter_group'; +@import './views/date_picker/global_date_picker'; diff --git a/src-docs/src/theme_k6_dark.scss b/src-docs/src/theme_k6_dark.scss index 99e708a00f1..a3738416b1d 100644 --- a/src-docs/src/theme_k6_dark.scss +++ b/src-docs/src/theme_k6_dark.scss @@ -1,3 +1,4 @@ @import '../../src/theme_k6_dark'; @import './components/guide_components'; @import './views/header/global_filter_group'; +@import './views/date_picker/global_date_picker'; diff --git a/src-docs/src/theme_k6_light.scss b/src-docs/src/theme_k6_light.scss index 544617f335d..6fe9d55fe4b 100644 --- a/src-docs/src/theme_k6_light.scss +++ b/src-docs/src/theme_k6_light.scss @@ -1,3 +1,4 @@ @import '../../src/theme_k6_light'; @import './components/guide_components'; @import './views/header/global_filter_group'; +@import './views/date_picker/global_date_picker'; diff --git a/src-docs/src/theme_light.scss b/src-docs/src/theme_light.scss index 9a8ac0e7586..bd607c9114f 100644 --- a/src-docs/src/theme_light.scss +++ b/src-docs/src/theme_light.scss @@ -1,4 +1,4 @@ @import '../../src/theme_light'; @import './components/guide_components'; @import './views/header/global_filter_group'; - +@import './views/date_picker/global_date_picker'; diff --git a/src-docs/src/views/date_picker/_global_date_picker.scss b/src-docs/src/views/date_picker/_global_date_picker.scss new file mode 100644 index 00000000000..d522ce46bcb --- /dev/null +++ b/src-docs/src/views/date_picker/_global_date_picker.scss @@ -0,0 +1,97 @@ +//// GLOBAL Date picker + +// sass-lint:disable no-important +.euiGlobalDatePicker__quickSelectButton { + // Override prepend border since button already lives inside another prepend + border-right: none !important; + + .euiGlobalDatePicker__quickSelectButtonText { + // Override specificity from universal and sibiling selectors + margin-right: $euiSizeXS !important; + } +} + +.euiGlobalDatePicker.euiFormControlLayout { + max-width: 480px; + + > .euiFormControlLayout__childrenWrapper { + flex: 1 1 100%; + overflow: hidden; + + > .euiDatePickerRange { + max-width: none; + width: auto; + + // sass-lint:disable nesting-depth + .euiPopover__anchor { + display: block; + } + } + } +} + +.euiGlobalDatePicker__dateButton { + @include euiFormControlText; + display: block; + width: 100%; + padding: 0 $euiSizeS; + line-height: $euiFormControlHeight - 2px; + height: $euiFormControlHeight - 2px; + word-break: break-all; + transition: background $euiAnimSpeedFast ease-in; + + $backgroundColor: tintOrShade($euiColorSuccess, 90%, 70%); + $textColor: makeHighContrastColor($euiColorSuccess, $backgroundColor); + + &-isSelected, + &-needsUpdating, + &:hover, + &:focus { + background-color: $backgroundColor; + } + + &-needsUpdating { + color: $textColor; + } + + &-isInvalid { + $backgroundColor: tintOrShade($euiColorDanger, 90%, 70%); + $textColor: makeHighContrastColor($euiColorDanger, $backgroundColor); + background-color: $backgroundColor; + color: $textColor; + } + + .euiFormControlLayout__prepend { + border: none; + } +} + +.euiGlobalDatePicker__dateButton--start { + text-align: right; +} + +.euiGlobalDatePicker__dateButton--end { + text-align: left; +} + +.euiGlobalDatePicker__updateButton { + // Just wide enough for all 3 states + min-width: $euiButtonMinWidth + ($euiSizeXS * 1.5); +} + +.euiGlobalDatePicker__popoverSection { + @include euiScrollBar; + max-height: $euiSizeM * 11; + overflow: hidden; + overflow-y: auto; +} + +@include euiBreakpoint('xs', 's') { + .euiGlobalDatePicker__updateButton { + min-width: 0; + + .euiGlobalDatePicker__updateButtonText { + display: none; + } + } +} diff --git a/src-docs/src/views/date_picker/date_picker_example.js b/src-docs/src/views/date_picker/date_picker_example.js index e40743e58bf..38dcff6a3f9 100644 --- a/src-docs/src/views/date_picker/date_picker_example.js +++ b/src-docs/src/views/date_picker/date_picker_example.js @@ -279,9 +279,7 @@ export const DatePickerExample = {

This documents a visual pattern for the eventual replacement of Kibana's - global date/time picker. It uses all EUI components without any custom styles. However, it - currently depends strongly on react-datepicker's calendarContainer option - which has it's own problems and limitations (like auto-focus on input stealing focus from inputs inside of popover). + global date/time picker. It uses all EUI components with some custom styles.

diff --git a/src-docs/src/views/date_picker/global_date_picker.js b/src-docs/src/views/date_picker/global_date_picker.js index e45e76e2404..13930214175 100644 --- a/src-docs/src/views/date_picker/global_date_picker.js +++ b/src-docs/src/views/date_picker/global_date_picker.js @@ -2,9 +2,10 @@ import React, { Component, Fragment, } from 'react'; +import PropTypes from 'prop-types'; import moment from 'moment'; -import { CalendarContainer } from 'react-datepicker'; +import classNames from 'classnames'; import { EuiDatePicker, @@ -25,7 +26,9 @@ import { EuiTabbedContent, EuiForm, EuiSwitch, - EuiTextColor, + EuiToolTip, + EuiFieldText, + EuiButtonIcon, } from '../../../../src/components'; const commonDates = [ @@ -57,9 +60,16 @@ class GlobalDatePopover extends Component { id: 'absolute', name: 'Absolute', content: ( - - {props.children} - +
+ + + + +
), }, { id: 'relative', @@ -79,7 +89,7 @@ class GlobalDatePopover extends Component { - + @@ -90,14 +100,11 @@ class GlobalDatePopover extends Component { id: 'now', name: 'Now', content: ( - - {moment().format('MMMM Do YYYY')} - - - - {moment().format('h:mm:ss a')} - - + +

+ Setting the time to "Now" means that on every refresh + this time will be set to the time of the refresh. +

), }]; @@ -117,21 +124,115 @@ class GlobalDatePopover extends Component { tabs={this.tabs} selectedTab={this.state.selectedTab} onTabClick={this.onTabClick} + size="s" expand /> ); } } +// eslint-disable-next-line react/no-multi-comp +class GlobalDateButton extends Component { + static propTypes = { + position: PropTypes.oneOf(['start', 'end']), + isInvalid: PropTypes.bool, + needsUpdating: PropTypes.bool, + buttonOnly: PropTypes.bool, + date: PropTypes.string, + } + + constructor(props) { + super(props); + + this.state = { + isPopoverOpen: false, + }; + } + + togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + } + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + } + + render() { + const { + position, + isInvalid, + needsUpdating, + date, + buttonProps, + buttonOnly, + ...rest + } = this.props; + + const { + isPopoverOpen, + } = this.state; + + const classes = classNames([ + 'euiGlobalDatePicker__dateButton', + `euiGlobalDatePicker__dateButton--${position}`, + { + 'euiGlobalDatePicker__dateButton-isSelected': isPopoverOpen, + 'euiGlobalDatePicker__dateButton-isInvalid': isInvalid, + 'euiGlobalDatePicker__dateButton-needsUpdating': needsUpdating + } + ]); + + let title = date; + if (isInvalid) { + title = `Invalid date: ${title}`; + } else if (needsUpdating) { + title = `Update needed: ${title}`; + } + + const button = ( + + ); + + return buttonOnly ? button : ( + + + + ); + } +} + // eslint-disable-next-line react/no-multi-comp export default class extends Component { constructor(props) { super(props); this.state = { - startDate: moment(), - endDate: moment().add(11, 'd'), + startDate: moment().format('MMM DD YYYY h:mm:ss.SSS'), + endDate: moment().add(11, 'd').format('MMM DD YYYY hh:mm:ss.SSS'), isPopoverOpen: false, + showPrettyFormat: false, + showNeedsUpdate: false, + isUpdating: false, + timerIsOn: false, recentlyUsed: [ ['11/25/2017 00:00 AM', '11/25/2017 11:59 PM'], ['3 hours ago', '4 minutes ago'], @@ -141,21 +242,44 @@ export default class extends Component { }; } - handleChangeStart = (date) => { - this.setState({ - startDate: date - }); + setTootipRef = node => (this.tooltip = node); + + showTooltip = () => this.tooltip.showToolTip(); + hideTooltip = () => this.tooltip.hideToolTip(); + + togglePopover = (e) => { + // HACK TODO: + // this works because react listens to all events at the + // document level, and you need to interact with the native + // event's propagation to short-circuit outside click handler + // see also: https://stackoverflow.com/a/24421834 + e.nativeEvent.stopImmediatePropagation(); + + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); } - handleChangeEnd = (date) => { - this.setState({ - endDate: date - }); + togglePrettyFormat = () => { + this.setState(prevState => ({ + showPrettyFormat: !prevState.showPrettyFormat, + })); } - onButtonClick = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, + toggleNeedsUpdate = () => { + this.setState(prevState => { + + if (!prevState.showNeedsUpdate) { + clearTimeout(this.tooltipTimeout); + this.showTooltip(); + this.tooltipTimeout = setTimeout(() => { + this.hideTooltip(); + }, 10000); + } + + return ({ + showNeedsUpdate: !prevState.showNeedsUpdate, + }); }); } @@ -165,18 +289,31 @@ export default class extends Component { }); } + toggleTimer = () => { + this.setState(prevState => ({ + timerIsOn: !prevState.timerIsOn, + })); + } + + toggleIsUpdating = () => { + this.setState(prevState => ({ + isUpdating: !prevState.isUpdating, + })); + } + + render() { const quickSelectButton = ( - + ); @@ -190,51 +327,96 @@ export default class extends Component { isOpen={this.state.isPopoverOpen} closePopover={this.closePopover.bind(this)} anchorPosition="downLeft" - ownFocus > -
+
{this.renderQuickSelect()} - + {commonlyUsed} - + {recentlyUsed} + + {this.renderTimer()}
); return ( - - this.state.endDate} - aria-label="Start date" - calendarContainer={GlobalDatePopover} - showTimeSelect - /> - } - endDateControl={ - this.state.endDate} - aria-label="End date" - calendarContainer={GlobalDatePopover} - showTimeSelect - /> - } - /> - + +   +   + + + + + + + + } + endDateControl={ + + } + > + {this.state.showPrettyFormat && + + + Show dates + + } + + + + + {this.renderUpdateButton()} + + + + ); + } + + renderUpdateButton = () => { + const color = this.state.showNeedsUpdate ? 'secondary' : 'primary'; + const icon = this.state.showNeedsUpdate ? 'kqlFunction' : 'refresh'; + let text = this.state.showNeedsUpdate ? 'Update' : 'Refresh'; + + if (this.state.isUpdating) { + text = 'Updating'; + } + + return ( + + + {text} + + ); } @@ -256,27 +438,41 @@ export default class extends Component { return ( - Quick select + + + Quick select + + + + + + + + + + + + - + - + - + - Apply + Apply @@ -295,7 +491,7 @@ export default class extends Component { Commonly used - + {links} @@ -320,7 +516,7 @@ export default class extends Component { Recently used date ranges - + {links} @@ -328,4 +524,43 @@ export default class extends Component { ); } + + renderTimer = () => { + const lastOptions = [ + { value: 'minutes', text: 'minutes' }, + { value: 'hours', text: 'hours' }, + ]; + + return ( + + Refresh every + + + + + + + + + + + + + + + + {this.state.timerIsOn ? 'Stop' : 'Start'} + + + + + + ); + } + } diff --git a/src/components/button/button.js b/src/components/button/button.js index 9a4875d7b25..9d5fbf68b56 100644 --- a/src/components/button/button.js +++ b/src/components/button/button.js @@ -53,6 +53,8 @@ export const EuiButton = ({ rel, type, buttonRef, + contentProps, + textProps, ...rest }) => { @@ -105,9 +107,9 @@ export const EuiButton = ({ ref={buttonRef} {...rest} > - + {buttonIcon} - {children} + {children} ); @@ -120,9 +122,9 @@ export const EuiButton = ({ ref={buttonRef} {...rest} > - + {buttonIcon} - {children} + {children} ); @@ -165,6 +167,16 @@ EuiButton.propTypes = { */ type: PropTypes.string, buttonRef: PropTypes.func, + + /** + * Passes props to `euiButton__content` span + */ + contentProps: PropTypes.object, + + /** + * Passes props to `euiButton__text` span + */ + textProps: PropTypes.object, }; EuiButton.defaultProps = { diff --git a/src/components/button/button_empty/button_empty.js b/src/components/button/button_empty/button_empty.js index 799db99bf2a..20c57e60f06 100644 --- a/src/components/button/button_empty/button_empty.js +++ b/src/components/button/button_empty/button_empty.js @@ -60,6 +60,8 @@ export const EuiButtonEmpty = ({ rel, type, buttonRef, + contentProps, + textProps, ...rest }) => { @@ -110,9 +112,9 @@ export const EuiButtonEmpty = ({ ref={buttonRef} {...rest} > - + {buttonIcon} - {children} + {children} ); @@ -125,9 +127,9 @@ export const EuiButtonEmpty = ({ ref={buttonRef} {...rest} > - + {buttonIcon} - {children} + {children} ); @@ -155,6 +157,16 @@ EuiButtonEmpty.propTypes = { type: PropTypes.string, buttonRef: PropTypes.func, + + /** + * Passes props to `euiButton__content` span + */ + contentProps: PropTypes.object, + + /** + * Passes props to `euiButton__text` span + */ + textProps: PropTypes.object, }; EuiButtonEmpty.defaultProps = { diff --git a/src/components/button/index.d.ts b/src/components/button/index.d.ts index fe10d678721..fd3096cee25 100644 --- a/src/components/button/index.d.ts +++ b/src/components/button/index.d.ts @@ -1,7 +1,7 @@ /// /// -import { SFC, ButtonHTMLAttributes, AnchorHTMLAttributes, MouseEventHandler } from 'react'; +import { SFC, ButtonHTMLAttributes, AnchorHTMLAttributes, MouseEventHandler, HTMLAttributes } from 'react'; declare module '@elastic/eui' { type EuiButtonPropsForButtonOrLink = ( @@ -33,6 +33,8 @@ declare module '@elastic/eui' { size?: ButtonSize; isLoading?: boolean; isDisabled?: boolean; + contentProps?: HTMLAttributes; + textProps?: HTMLAttributes; } export const EuiButton: SFC< EuiButtonPropsForButtonOrLink @@ -87,6 +89,8 @@ declare module '@elastic/eui' { flush?: EmptyButtonFlush; isLoading?: boolean; isDisabled?: boolean; + contentProps?: HTMLAttributes; + textProps?: HTMLAttributes; } export const EuiButtonEmpty: SFC< diff --git a/src/components/date_picker/_date_picker_range.scss b/src/components/date_picker/_date_picker_range.scss index 593f2085fe7..a88f2331894 100644 --- a/src/components/date_picker/_date_picker_range.scss +++ b/src/components/date_picker/_date_picker_range.scss @@ -5,7 +5,7 @@ * 1. Account for inner box-shadow style border */ - .euiDatePickerRange { +.euiDatePickerRange { @include euiFormControlSize(auto, $includeAlternates: true); // Match just the regular drop shadow of inputs @include euiFormControlDefaultShadow(); @@ -13,8 +13,9 @@ align-items: center; padding: 1px; /* 1 */ - > span { - flex: 1 1 0%; // All values necessary for IE support + // Allow any child to fill entire range container + > * { + flex-grow: 1; } .euiFieldText.euiDatePicker { @@ -36,14 +37,19 @@ height: $euiFormControlHeight - 2px; } } -} -.euiDatePickerRange__icon { - padding-left: $euiFormControlPadding; - padding-right: $euiFormControlPadding; -} + // Direct descendent selectors to override `> span` -.euiDatePickerRange__delimeter { - padding-left: $euiFormControlPadding/2; - padding-right: $euiFormControlPadding/2; + > .euiDatePickerRange__icon { + flex: 0 0 auto; + padding-left: $euiFormControlPadding; + padding-right: $euiFormControlPadding; + } + + > .euiDatePickerRange__delimeter { + line-height: 1 !important; + flex: 0 0 auto; + padding-left: $euiFormControlPadding / 2; + padding-right: $euiFormControlPadding / 2; + } } diff --git a/src/components/date_picker/date_picker_range.js b/src/components/date_picker/date_picker_range.js index a3177121091..8dc3b5e5fd0 100644 --- a/src/components/date_picker/date_picker_range.js +++ b/src/components/date_picker/date_picker_range.js @@ -1,5 +1,5 @@ import React, { - cloneElement, + cloneElement, Fragment, } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -11,11 +11,13 @@ import { } from '../icon'; export const EuiDatePickerRange = ({ + children, className, startDateControl, endDateControl, iconType, fullWidth, + isCustom, ...rest }) => { @@ -38,25 +40,35 @@ export const EuiDatePickerRange = ({ optionalIcon = null; } - const clonedStartDate = cloneElement(startDateControl, { - showIcon: false, - fullWidth: fullWidth, - }); + let startControl = startDateControl; + let endControl = endDateControl; + + if (!isCustom) { + startControl = cloneElement(startDateControl, { + showIcon: false, + fullWidth: fullWidth, + }); + + endControl = cloneElement(endDateControl, { + showIcon: false, + fullWidth: fullWidth, + }); + } - const clonedEndDate = cloneElement(endDateControl, { - showIcon: false, - fullWidth: fullWidth, - }); return (
- {optionalIcon} - {clonedStartDate} - - {clonedEndDate} + {children ? (children) : ( + + {optionalIcon} + {startControl} + + {endControl} + + )}
); }; @@ -78,6 +90,14 @@ EuiDatePickerRange.propTypes = { PropTypes.oneOf(ICON_TYPES), ]), fullWidth: PropTypes.bool, + /** + * Won't apply any additional props to start and end date components + */ + isCustom: PropTypes.bool, + /** + * Including any children will replace all innerds with the provided children + */ + children: PropTypes.node, }; EuiDatePickerRange.defaultProps = { diff --git a/src/components/form/_mixins.scss b/src/components/form/_mixins.scss index 66c65d74f84..f5ecd320de9 100644 --- a/src/components/form/_mixins.scss +++ b/src/components/form/_mixins.scss @@ -13,6 +13,13 @@ * 3. Must supply both values to background-size or some browsers apply the single value to both directions */ +@mixin euiFormControlText { + font-size: $euiFontSizeS; + font-family: $euiFontFamily; + line-height: 1em; // fixes text alignment in IE + color: $euiTextColor; +} + @mixin euiFormControlSize( $height: $euiFormControlHeight, $includeAlternates: false @@ -134,14 +141,11 @@ @mixin euiFormControlStyle($borderOnly: false, $includeStates: true, $includeSizes: true) { @include euiFormControlSize($includeAlternates: $includeSizes); @include euiFormControlDefaultShadow; + @include euiFormControlText; border: none; - font-size: $euiFontSizeS; - font-family: $euiFontFamily; - padding: $euiFormControlPadding; - line-height: 1em; // fixes text alignment in IE - color: $euiTextColor; border-radius: 0; + padding: $euiFormControlPadding; @if ($includeSizes) { &--compressed {