@@ -157,6 +143,24 @@ exports[`EuiDualRange props custom ticks should render 1`] = `
100kb
+
-
-
@@ -362,6 +352,74 @@ exports[`EuiDualRange props levels should render 1`] = `
style="width:80%"
/>
+
+
+
+
+`;
+
+exports[`EuiDualRange props only input should render 1`] = `
+
`;
@@ -373,23 +431,24 @@ exports[`EuiDualRange props range should render 1`] = `
`;
@@ -402,23 +461,6 @@ exports[`EuiDualRange props ticks should render 1`] = `
class="euiRangeTrack"
style="margin-right:0.6em"
>
-
-
+
+
`;
diff --git a/src/components/form/range/__snapshots__/range.test.js.snap b/src/components/form/range/__snapshots__/range.test.js.snap
index 32d5d964d35..6d2a528f324 100644
--- a/src/components/form/range/__snapshots__/range.test.js.snap
+++ b/src/components/form/range/__snapshots__/range.test.js.snap
@@ -9,6 +9,7 @@ exports[`EuiRange allows value prop to accept a number 1`] = `
>
`;
@@ -142,6 +146,7 @@ exports[`EuiRange props disabled should render 1`] = `
`;
+exports[`EuiRange props input only should render 1`] = `
+
+`;
+
exports[`EuiRange props input should render 1`] = `
`;
@@ -278,14 +318,6 @@ exports[`EuiRange props range should render 1`] = `
`;
@@ -306,13 +347,6 @@ exports[`EuiRange props ticks should render 1`] = `
class="euiRangeTrack"
style="margin-right:0.6em"
>
-
+
`;
@@ -385,6 +427,7 @@ exports[`EuiRange props value should render 1`] = `
>
.euiFormControlLayout { /* 1 */
width: auto;
}
diff --git a/src/components/form/range/_variables.scss b/src/components/form/range/_variables.scss
index ce6a5cfd8e6..623e58acce8 100644
--- a/src/components/form/range/_variables.scss
+++ b/src/components/form/range/_variables.scss
@@ -12,3 +12,5 @@ $euiRangeTrackBorderColor: $euiRangeTrackColor !default;
$euiRangeTrackRadius: $euiBorderRadius !default;
$euiRangeDisabledOpacity: .25;
+
+$euiRangeHighlightHeight: $euiSizeXS;
diff --git a/src/components/form/range/dual_range.js b/src/components/form/range/dual_range.js
index 3d3a63f4dc6..8e0b3131bbb 100644
--- a/src/components/form/range/dual_range.js
+++ b/src/components/form/range/dual_range.js
@@ -4,6 +4,9 @@ import classNames from 'classnames';
import { keyCodes } from '../../../services';
import { isWithinRange } from '../../../services/number';
+import { EuiInputPopover } from '../../popover';
+import { EuiFormControlLayoutDelimited } from '../form_control_layout';
+import makeId from '../form_row/make_id';
import { EuiRangeHighlight } from './range_highlight';
import { EuiRangeInput } from './range_input';
@@ -15,15 +18,21 @@ import { EuiRangeWrapper } from './range_wrapper';
export class EuiDualRange extends Component {
state = {
+ id: this.props.id || makeId(),
hasFocus: false,
rangeSliderRefAvailable: false,
+ isPopoverOpen: false,
+ rangeWidth: null,
};
+ maxNode = null;
+ minNode = null;
rangeSliderRef = null;
handleRangeSliderRefUpdate = ref => {
this.rangeSliderRef = ref;
this.setState({
rangeSliderRefAvailable: !!ref,
+ rangeWidth: !!ref ? ref.clientWidth : null,
});
};
@@ -201,7 +210,7 @@ export class EuiDualRange extends Component {
this._handleOnChange(this.lowerValue, upper, e);
};
- calculateThumbPositionStyle = value => {
+ calculateThumbPositionStyle = (value, width) => {
// Calculate the left position based on value
const decimal =
(value - this.props.min) / (this.props.max - this.props.min);
@@ -210,7 +219,11 @@ export class EuiDualRange extends Component {
valuePosition = valuePosition >= 0 ? valuePosition : 0;
const EUI_THUMB_SIZE = 16;
- const thumbToTrackRatio = EUI_THUMB_SIZE / this.rangeSliderRef.clientWidth;
+ const trackWidth =
+ this.props.showInput === 'only' && !!width
+ ? width
+ : this.rangeSliderRef.clientWidth;
+ const thumbToTrackRatio = EUI_THUMB_SIZE / trackWidth;
const trackPositionScale = (1 - thumbToTrackRatio) * 100;
return { left: `${valuePosition * trackPositionScale}%` };
};
@@ -221,13 +234,56 @@ export class EuiDualRange extends Component {
});
};
+ onInputFocus = () => {
+ this.setState({
+ isPopoverOpen: true,
+ });
+ };
+
+ onInputBlur = e => {
+ // Firefox returns `relatedTarget` as `null` for security reasons, but provides a proprietary `explicitOriginalTarget`
+ const relatedTarget = e.relatedTarget || e.explicitOriginalTarget;
+ if (!relatedTarget || relatedTarget.id !== this.state.id) {
+ this.closePopover();
+ }
+ };
+
+ closePopover = () => {
+ this.setState({
+ isPopoverOpen: false,
+ });
+ };
+
+ onResize = width => {
+ this.setState({
+ rangeWidth: width,
+ });
+ };
+
+ inputRef = (node, ref) => {
+ if (!this.props.showInput !== 'inputWithPopover') return;
+
+ // IE11 doesn't support the `relatedTarget` event property for blur events
+ // but does add it for focusout. React doesn't support `onFocusOut` so here we are.
+ if (this[ref] != null) {
+ this[ref].removeEventListener('focusout', this.onInputBlur);
+ }
+
+ this[ref] = node;
+
+ if (this[ref]) {
+ this[ref].addEventListener('focusout', this.onInputBlur);
+ }
+ };
+
render() {
const {
className,
compressed,
disabled,
fullWidth,
- id,
+ readOnly,
+ id: propsId,
max,
min,
name,
@@ -245,34 +301,78 @@ export class EuiDualRange extends Component {
...rest
} = this.props;
- const classes = classNames('euiDualRange', className);
+ const { id } = this.state;
+
const digitTolerance = Math.max(String(min).length, String(max).length);
+ const showInputOnly = showInput === 'inputWithPopover';
+ const canShowDropdown = showInputOnly && !readOnly && !disabled;
- return (
-
- {showInput && (
-
- )}
+ const minInput = !!showInput ? (
+ this.inputRef(node, 'minNode')}
+ />
+ ) : (
+ undefined
+ );
+
+ const maxInput = !!showInput ? (
+ this.inputRef(node, 'maxNode')}
+ />
+ ) : (
+ undefined
+ );
+
+ const classes = classNames('euiDualRange', className);
+ const theRange = (
+
+ {!showInputOnly && minInput}
{showLabels && (
{min}
)}
+ {showRange && this.isValid && (
+
+ )}
+
this.toggleHasFocus(true)}
onBlur={() => this.toggleHasFocus(false)}
- style={this.calculateThumbPositionStyle(this.lowerValue || min)}
+ style={this.calculateThumbPositionStyle(
+ this.lowerValue || min,
+ this.state.rangeWidth
+ )}
aria-describedby={this.props['aria-describedby']}
aria-label={this.props['aria-label']}
/>
@@ -324,48 +440,50 @@ export class EuiDualRange extends Component {
value={this.upperValue}
disabled={disabled}
showTicks={showTicks}
- showInput={showInput}
+ showInput={!!showInput}
onKeyDown={this.handleUpperKeyDown}
onFocus={() => this.toggleHasFocus(true)}
onBlur={() => this.toggleHasFocus(false)}
- style={this.calculateThumbPositionStyle(this.upperValue || max)}
+ style={this.calculateThumbPositionStyle(
+ this.upperValue || max,
+ this.state.rangeWidth
+ )}
aria-describedby={this.props['aria-describedby']}
aria-label={this.props['aria-label']}
/>
)}
-
- {showRange && this.isValid && (
-
- )}
{showLabels && {max} }
- {showInput && (
-
+ );
+
+ const thePopover = showInputOnly ? (
+
- )}
-
+ }
+ fullWidth={fullWidth}
+ isOpen={this.state.isPopoverOpen}
+ closePopover={this.closePopover}
+ disableFocusTrap={true}
+ onPanelResize={this.onResize}>
+ {theRange}
+
+ ) : (
+ undefined
);
+
+ return thePopover || theRange;
}
}
@@ -384,14 +502,16 @@ EuiDualRange.propTypes = {
fullWidth: PropTypes.bool,
compressed: PropTypes.bool,
disabled: PropTypes.bool,
+ readOnly: PropTypes.bool,
/**
* Shows static min/max labels on the sides of the range slider
*/
showLabels: PropTypes.bool,
/**
- * Displays a input controls for direct manipulation
+ * Pass `true` to displays an extra input control for direct manipulation.
+ * Pass `'inputWithPopover'` to only show the input but show the range in a dropdown.
*/
- showInput: PropTypes.bool,
+ showInput: PropTypes.oneOf([true, false, 'inputWithPopover']),
/**
* Shows clickable tick marks and labels at the given interval (`step`/`tickInterval`)
*/
diff --git a/src/components/form/range/dual_range.test.js b/src/components/form/range/dual_range.test.js
index cc010c8b06d..03c9a822023 100644
--- a/src/components/form/range/dual_range.test.js
+++ b/src/components/form/range/dual_range.test.js
@@ -4,6 +4,8 @@ import { requiredProps } from '../../../test/required_props';
import { EuiDualRange } from './dual_range';
+jest.mock('../form_row/make_id', () => () => 'generated-id');
+
describe('EuiDualRange', () => {
test('is rendered', () => {
const component = render(
@@ -86,6 +88,23 @@ describe('EuiDualRange', () => {
expect(component).toMatchSnapshot();
});
+ test('only input should render', () => {
+ const component = render(
+ {}}
+ showInput="inputWithPopover"
+ {...requiredProps}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
test('levels should render', () => {
const component = render(
{
const isValid = isWithinRange(
this.props.min,
@@ -26,13 +38,50 @@ export class EuiRange extends Component {
return isWithinRange(this.props.min, this.props.max, this.props.value);
}
+ onInputFocus = () => {
+ this.setState({
+ isPopoverOpen: true,
+ });
+ };
+
+ onInputBlur = e => {
+ // Firefox returns `relatedTarget` as `null` for security reasons, but provides a proprietary `explicitOriginalTarget`
+ const relatedTarget = e.relatedTarget || e.explicitOriginalTarget;
+ if (!relatedTarget || relatedTarget.id !== this.state.id) {
+ this.closePopover();
+ }
+ };
+
+ closePopover = () => {
+ this.setState({
+ isPopoverOpen: false,
+ });
+ };
+
+ inputRef = node => {
+ if (!this.props.showInput !== 'inputWithPopover') return;
+
+ // IE11 and Safar don't support the `relatedTarget` event property for blur events
+ // but do add it for focusout. React doesn't support `onFocusOut` so here we are.
+ if (this.inputNode != null) {
+ this.inputNode.removeEventListener('focusout', this.onInputBlur);
+ }
+
+ this.inputNode = node;
+
+ if (this.inputNode) {
+ this.inputNode.addEventListener('focusout', this.onInputBlur);
+ }
+ };
+
render() {
const {
className,
compressed,
disabled,
fullWidth,
- id,
+ readOnly,
+ id: propsId,
max,
min,
name,
@@ -54,11 +103,41 @@ export class EuiRange extends Component {
...rest
} = this.props;
- const classes = classNames('euiRange', className);
+ const { id } = this.state;
+
const digitTolerance = Math.max(String(min).length, String(max).length);
+ const showInputOnly = showInput === 'inputWithPopover';
+ const canShowDropdown = showInputOnly && !readOnly && !disabled;
+
+ const theInput = !!showInput ? (
+
+ ) : (
+ undefined
+ );
- return (
-
+ const classes = classNames('euiRange', className);
+
+ const theRange = (
+
{showLabels && (
{min}
@@ -66,6 +145,7 @@ export class EuiRange extends Component {
)}
+ {showRange && this.isValid && (
+
+ )}
+
{showValue && !!String(value).length && (
)}
-
- {showRange && this.isValid && (
-
- )}
{showLabels && (
{max}
)}
- {showInput && (
-
- )}
+ {!showInputOnly && theInput}
);
+
+ const thePopover = showInputOnly ? (
+
+ {theRange}
+
+ ) : (
+ undefined
+ );
+
+ return thePopover ? thePopover : theRange;
}
}
@@ -152,9 +238,10 @@ EuiRange.propTypes = {
*/
showLabels: PropTypes.bool,
/**
- * Displays an extra input control for direct manipulation
+ * Pass `true` to displays an extra input control for direct manipulation.
+ * Pass `'inputWithPopover'` to only show the input but show the range in a dropdown.
*/
- showInput: PropTypes.bool,
+ showInput: PropTypes.oneOf([true, false, 'inputWithPopover']),
/**
* Shows clickable tick marks and labels at the given interval (`step`/`tickInterval`)
*/
diff --git a/src/components/form/range/range.test.js b/src/components/form/range/range.test.js
index 652a6c104be..a9c113e1b16 100644
--- a/src/components/form/range/range.test.js
+++ b/src/components/form/range/range.test.js
@@ -4,6 +4,8 @@ import { requiredProps } from '../../../test/required_props';
import { EuiRange } from './range';
+jest.mock('../form_row/make_id', () => () => 'generated-id');
+
describe('EuiRange', () => {
test('is rendered', () => {
const component = render(
@@ -99,6 +101,23 @@ describe('EuiRange', () => {
expect(component).toMatchSnapshot();
});
+ test('input only should render', () => {
+ const component = render(
+ {}}
+ showInput="inputWithPopover"
+ {...requiredProps}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
test('levels should render', () => {
const component = render(
= ({
upperValue,
max,
min,
+ compressed,
}) => {
// Calculate the width the range based on value
// const rangeWidth = (value - min) / (max - min);
@@ -29,6 +31,7 @@ export const EuiRangeHighlight: FunctionComponent = ({
const classes = classNames('euiRangeHighlight', {
'euiRangeHighlight--hasTicks': showTicks,
+ 'euiRangeHighlight--compressed': compressed,
});
const progressClasses = classNames('euiRangeHighlight__progress', {
diff --git a/src/components/form/range/range_input.js b/src/components/form/range/range_input.js
index 732a28b5541..d7e240f9411 100644
--- a/src/components/form/range/range_input.js
+++ b/src/components/form/range/range_input.js
@@ -14,12 +14,16 @@ export const EuiRangeInput = ({
name,
side,
digitTolerance,
+ fullWidth,
+ autoSize,
...rest
}) => {
// Chrome will properly size the input based on the max value, but FF & IE do not.
// Calculate the width of the input based on highest number of characters.
// Add 2 to accomodate for input stepper
- const widthStyle = { width: `${digitTolerance / 1.25 + 2}em` };
+ const widthStyle = autoSize
+ ? { width: `${digitTolerance / 1.25 + 2}em` }
+ : undefined;
return (
);
@@ -48,7 +53,11 @@ EuiRangeInput.propTypes = {
name: PropTypes.string,
digitTolerance: PropTypes.number.isRequired,
side: PropTypes.oneOf(['min', 'max']),
+ fullWidth: PropTypes.bool,
+ autoSize: PropTypes.bool,
+ inputRef: PropTypes.func,
};
EuiRangeInput.defaultProps = {
side: 'max',
+ autoSize: true,
};
diff --git a/src/components/form/range/range_levels.tsx b/src/components/form/range/range_levels.tsx
index b59c63cf852..c607b100aa4 100644
--- a/src/components/form/range/range_levels.tsx
+++ b/src/components/form/range/range_levels.tsx
@@ -21,6 +21,7 @@ export interface EuiRangeLevelsProps {
max: number;
min: number;
showTicks?: boolean;
+ compressed?: boolean;
}
export const EuiRangeLevels: FunctionComponent = ({
@@ -28,6 +29,7 @@ export const EuiRangeLevels: FunctionComponent = ({
max,
min,
showTicks,
+ compressed,
}) => {
const validateLevelIsInRange = (level: EuiRangeLevel) => {
if (level.min < min) {
@@ -44,6 +46,7 @@ export const EuiRangeLevels: FunctionComponent = ({
const classes = classNames('euiRangeLevels', {
'euiRangeLevels--hasTicks': showTicks,
+ 'euiRangeLevels--compressed': compressed,
});
return (
diff --git a/src/components/form/range/range_slider.tsx b/src/components/form/range/range_slider.tsx
index a9120f0fc74..4c16e277000 100644
--- a/src/components/form/range/range_slider.tsx
+++ b/src/components/form/range/range_slider.tsx
@@ -16,6 +16,7 @@ export type EuiRangeSliderProps = InputHTMLAttributes &
min: number;
max: number;
step?: number;
+ compressed?: boolean;
hasFocus?: boolean;
showRange?: boolean;
showTicks?: boolean;
@@ -43,6 +44,7 @@ export const EuiRangeSlider: FunctionComponent<
showTicks,
showRange,
hasFocus,
+ compressed,
...rest
},
ref: Ref
@@ -53,6 +55,7 @@ export const EuiRangeSlider: FunctionComponent<
'euiRangeSlider--hasTicks': showTicks,
'euiRangeSlider--hasFocus': hasFocus,
'euiRangeSlider--hasRange': showRange,
+ 'euiRangeSlider--compressed': compressed,
},
className
);
diff --git a/src/components/form/range/range_ticks.tsx b/src/components/form/range/range_ticks.tsx
index 545707c5d54..d8d63ffc322 100644
--- a/src/components/form/range/range_ticks.tsx
+++ b/src/components/form/range/range_ticks.tsx
@@ -24,6 +24,7 @@ export type EuiRangeTicksProps = Omit<
value?: number | string | Array;
min: number;
max: number;
+ compressed?: boolean;
interval?: number;
disabled?: boolean;
onChange?: MouseEventHandler;
@@ -38,6 +39,7 @@ export const EuiRangeTicks: FunctionComponent = ({
max,
min,
interval = 1,
+ compressed,
}) => {
// Calculate the width of each tick mark
const percentageWidth = (interval / (max - min + interval)) * 100;
@@ -48,8 +50,12 @@ export const EuiRangeTicks: FunctionComponent = ({
? undefined
: { margin: `0 ${percentageWidth / -2}%`, left: 0, right: 0 };
+ const classes = classNames('euiRangeTicks', {
+ 'euiRangeTicks--compressed': compressed,
+ });
+
return (
-
+
{tickSequence.map(tickValue => {
const tickStyle: { left?: string; width?: string } = {};
let customTick;
diff --git a/src/components/form/range/range_tooltip.tsx b/src/components/form/range/range_tooltip.tsx
index 2795deacf3e..73b40f710a9 100644
--- a/src/components/form/range/range_tooltip.tsx
+++ b/src/components/form/range/range_tooltip.tsx
@@ -9,6 +9,7 @@ export interface EuiRangeTooltipProps {
min: number;
name?: string;
showTicks?: boolean;
+ compressed?: boolean;
}
export const EuiRangeTooltip: FunctionComponent
= ({
@@ -19,7 +20,12 @@ export const EuiRangeTooltip: FunctionComponent = ({
min,
name,
showTicks,
+ compressed,
}) => {
+ const classes = classNames('euiRangeTooltip', {
+ 'euiRangeTooltip--compressed': compressed,
+ });
+
// Calculate the left position based on value
let val = 0;
if (typeof value === 'number') {
@@ -52,7 +58,7 @@ export const EuiRangeTooltip: FunctionComponent = ({
);
return (
-
+
;
+ compressed?: boolean;
disabled?: boolean;
showTicks?: boolean;
tickInterval?: number;
@@ -117,6 +118,7 @@ export class EuiRangeTrack extends Component {
levels,
onChange,
value,
+ compressed,
} = this.props;
// TODO: Move these to only re-calculate if no-value props have changed
@@ -147,9 +149,9 @@ export class EuiRangeTrack extends Component {
return (
- {children}
{levels && !!levels.length && (
{
{tickSequence && (
{
interval={tickInterval || step}
/>
)}
+ {children}
);
}
diff --git a/src/components/form/range/range_wrapper.tsx b/src/components/form/range/range_wrapper.tsx
index 78cb6783f21..8179bab0239 100644
--- a/src/components/form/range/range_wrapper.tsx
+++ b/src/components/form/range/range_wrapper.tsx
@@ -4,17 +4,20 @@ import classNames from 'classnames';
export interface EuiRangeWrapperProps {
className?: string;
fullWidth?: boolean;
+ compressed?: boolean;
}
export const EuiRangeWrapper: FunctionComponent = ({
children,
className,
fullWidth,
+ compressed,
}) => {
const classes = classNames(
'euiRangeWrapper',
{
'euiRangeWrapper--fullWidth': fullWidth,
+ 'euiRangeWrapper--compressed': compressed,
},
className
);
diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx
index 6659708b0aa..a9f4f2df543 100644
--- a/src/components/popover/input_popover.tsx
+++ b/src/components/popover/input_popover.tsx
@@ -15,9 +15,11 @@ import { cascadingMenuKeyCodes } from '../../services';
interface EuiInputPopoverProps
extends Omit {
+ disableFocusTrap?: boolean;
fullWidth?: boolean;
input: EuiPopoverProps['button'];
inputRef?: EuiPopoverProps['buttonRef'];
+ onPanelResize?: (width?: number) => void;
}
type Props = CommonProps &
@@ -27,8 +29,10 @@ type Props = CommonProps &
export const EuiInputPopover: FunctionComponent = ({
children,
className,
+ disableFocusTrap = false,
input,
fullWidth = false,
+ onPanelResize,
...props
}) => {
const [inputEl, setInputEl] = useState();
@@ -42,6 +46,9 @@ export const EuiInputPopover: FunctionComponent = ({
if (panelEl && (!!inputElWidth || !!width)) {
const newWidth = !!width ? width : inputElWidth;
panelEl.style.width = `${newWidth}px`;
+ if (onPanelResize) {
+ onPanelResize(newWidth);
+ }
}
};
const onResize = () => {
@@ -68,8 +75,9 @@ export const EuiInputPopover: FunctionComponent = ({
);
});
if (
- tabbableItems.length &&
- tabbableItems[tabbableItems.length - 1] === document.activeElement
+ disableFocusTrap ||
+ (tabbableItems.length &&
+ tabbableItems[tabbableItems.length - 1] === document.activeElement)
) {
props.closePopover();
}
@@ -96,7 +104,7 @@ export const EuiInputPopover: FunctionComponent = ({
panelRef={panelRef}
className={classes}
{...props}>
-
+
{children}