Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] feat(InputGroup): add leftElement prop #4063

Merged
merged 5 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const HTML_TABLE_STRIPED = `${HTML_TABLE}-striped`;
export const INPUT = `${NS}-input`;
export const INPUT_GHOST = `${INPUT}-ghost`;
export const INPUT_GROUP = `${INPUT}-group`;
export const INPUT_LEFT_CONTAINER = `${INPUT}-left-container`;
export const INPUT_ACTION = `${INPUT}-action`;

export const CONTROL = `${NS}-control`;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const HOTKEYS_WARN_DECORATOR_NO_METHOD = ns + ` @HotkeysTarget-decorated
export const HOTKEYS_WARN_DECORATOR_NEEDS_REACT_ELEMENT =
ns + ` "@HotkeysTarget-decorated components must return a single JSX.Element or an empty render.`;

export const INPUT_WARN_LEFT_ELEMENT_LEFT_ICON_MUTEX =
ns + ` <InputGroup> leftElement and leftIcon prop are mutually exclusive, with leftElement taking priority.`;

export const NUMERIC_INPUT_MIN_MAX = ns + ` <NumericInput> requires min to be no greater than max if both are defined.`;
export const NUMERIC_INPUT_MINOR_STEP_SIZE_BOUND =
ns + ` <NumericInput> requires minorStepSize to be no greater than stepSize.`;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/common/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const INVALID_PROPS = [
"inline",
"large",
"loading",
"leftElement",
"leftIcon",
"minimal",
"onRemove", // ITagProps, ITagInputProps
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/components/forms/_input-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ $input-button-height-small: $pt-button-height-smaller !default;
}

.#{$ns}-input-action,
> .#{$ns}-input-left-container,
> .#{$ns}-button,
> .#{$ns}-icon {
position: absolute;
Expand All @@ -83,10 +84,15 @@ $input-button-height-small: $pt-button-height-smaller !default;
&:empty { padding: 0; }
}

// direct descendant to exclude icons in buttons
// bump icon or left content up so it sits above input
> .#{$ns}-input-left-container,
> .#{$ns}-icon {
// bump icon up so it sits above input
z-index: 1;
}

// direct descendant to exclude icons in buttons
> .#{$ns}-input-left-container > .#{$ns}-icon,
> .#{$ns}-icon {
color: $pt-icon-color;

&:empty {
Expand All @@ -96,6 +102,7 @@ $input-button-height-small: $pt-button-height-smaller !default;

// adjusting the margin of spinners in input groups
// we have to avoid targetting buttons that contain a spinner
> .#{$ns}-input-left-container > .#{$ns}-icon,
> .#{$ns}-icon,
.#{$ns}-input-action > .#{$ns}-spinner {
margin: ($pt-input-height - $pt-icon-size-standard) / 2;
Expand Down Expand Up @@ -150,6 +157,7 @@ $input-button-height-small: $pt-button-height-smaller !default;
margin: ($pt-input-height-large - $input-button-height-large) / 2;
}

> .#{$ns}-input-left-container > .#{$ns}-icon,
> .#{$ns}-icon,
.#{$ns}-input-action > .#{$ns}-spinner {
margin: ($pt-input-height-large - $pt-icon-size-standard) / 2;
Expand Down Expand Up @@ -179,6 +187,7 @@ $input-button-height-small: $pt-button-height-smaller !default;
margin: ($pt-input-height-small - $pt-button-height-smaller) / 2;
}

> .#{$ns}-input-left-container > .#{$ns}-icon,
> .#{$ns}-icon,
.#{$ns}-input-action > .#{$ns}-spinner {
margin: ($pt-input-height-small - $pt-icon-size-standard) / 2;
Expand Down
76 changes: 62 additions & 14 deletions packages/core/src/components/forms/inputGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import classNames from "classnames";
import * as React from "react";
import { polyfill } from "react-lifecycles-compat";
import { AbstractPureComponent2, Classes } from "../../common";
import * as Errors from "../../common/errors";
import {
DISPLAYNAME_PREFIX,
HTMLInputProps,
Expand All @@ -29,8 +30,6 @@ import {
} from "../../common/props";
import { Icon, IconName } from "../icon/icon";

const DEFAULT_RIGHT_ELEMENT_WIDTH = 10;

// NOTE: This interface does not extend HTMLInputProps due to incompatiblity with `IControlledProps`.
// Instead, we union the props in the component definition, which does work and properly disallows `string[]` values.
export interface IInputGroupProps extends IControlledProps, IIntentProps, IProps {
Expand All @@ -50,8 +49,15 @@ export interface IInputGroupProps extends IControlledProps, IIntentProps, IProps
inputRef?: (ref: HTMLInputElement | null) => any;

/**
* Name of a Blueprint UI icon (or an icon element) to render on the left side of the input group,
* before the user's cursor.
* Element to render on the left side of input. This prop is mutually exclusive
* with `leftIcon`.
*/
leftElement?: JSX.Element;

/**
* Name of a Blueprint UI icon to render on the left side of the input group,
* before the user's cursor. This prop is mutually exclusive with `leftElement`.
* Usage with content is deprecated. Use `leftElement` for elements.
*/
leftIcon?: IconName | MaybeElement;

Expand Down Expand Up @@ -81,24 +87,26 @@ export interface IInputGroupProps extends IControlledProps, IIntentProps, IProps
}

export interface IInputGroupState {
rightElementWidth: number;
leftElementWidth?: number;
rightElementWidth?: number;
}

@polyfill
export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLInputProps, IInputGroupState> {
public static displayName = `${DISPLAYNAME_PREFIX}.InputGroup`;

public state: IInputGroupState = {
rightElementWidth: DEFAULT_RIGHT_ELEMENT_WIDTH,
};
public state: IInputGroupState = {};

private leftElement: HTMLElement;
private rightElement: HTMLElement;

private refHandlers = {
leftElement: (ref: HTMLSpanElement) => (this.leftElement = ref),
rightElement: (ref: HTMLSpanElement) => (this.rightElement = ref),
};

public render() {
const { className, disabled, fill, intent, large, small, leftIcon, round } = this.props;
const { className, disabled, fill, intent, large, small, round } = this.props;
const classes = classNames(
Classes.INPUT_GROUP,
Classes.intentClass(intent),
Expand All @@ -111,11 +119,16 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
},
className,
);
const style: React.CSSProperties = { ...this.props.style, paddingRight: this.state.rightElementWidth };

const style: React.CSSProperties = {
...this.props.style,
paddingLeft: this.state.leftElementWidth,
paddingRight: this.state.rightElementWidth,
};

return (
<div className={classes}>
<Icon icon={leftIcon} />
{this.maybeRenderLeftElement()}
<input
type="text"
{...removeNonHTMLProps(this.props)}
Expand All @@ -133,11 +146,34 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
}

public componentDidUpdate(prevProps: IInputGroupProps & HTMLInputProps) {
if (prevProps.rightElement !== this.props.rightElement) {
const { leftElement, rightElement } = this.props;
if (prevProps.leftElement !== leftElement || prevProps.rightElement !== rightElement) {
this.updateInputWidth();
}
}

protected validateProps(props: IInputGroupProps) {
if (props.leftElement != null && props.leftIcon != null) {
console.warn(Errors.INPUT_WARN_LEFT_ELEMENT_LEFT_ICON_MUTEX);
}
}

private maybeRenderLeftElement() {
const { leftElement, leftIcon } = this.props;

if (leftElement != null) {
return (
<span className={Classes.INPUT_LEFT_CONTAINER} ref={this.refHandlers.leftElement}>
{leftElement}
</span>
);
} else if (leftIcon != null) {
return <Icon icon={leftIcon} />;
}
justinbhopper marked this conversation as resolved.
Show resolved Hide resolved

return undefined;
}

private maybeRenderRightElement() {
const { rightElement } = this.props;
if (rightElement == null) {
Expand All @@ -151,14 +187,26 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
}

private updateInputWidth() {
const { leftElementWidth, rightElementWidth } = this.state;

if (this.leftElement != null) {
const { clientWidth } = this.leftElement;
// small threshold to prevent infinite loops
if (leftElementWidth === undefined || Math.abs(clientWidth - leftElementWidth) > 2) {
this.setState({ leftElementWidth: clientWidth });
}
} else {
this.setState({ leftElementWidth: undefined });
}

if (this.rightElement != null) {
const { clientWidth } = this.rightElement;
// small threshold to prevent infinite loops
if (Math.abs(clientWidth - this.state.rightElementWidth) > 2) {
if (rightElementWidth === undefined || Math.abs(clientWidth - rightElementWidth) > 2) {
this.setState({ rightElementWidth: clientWidth });
}
} else {
this.setState({ rightElementWidth: DEFAULT_RIGHT_ELEMENT_WIDTH });
this.setState({ rightElementWidth: undefined });
}
}
}
2 changes: 1 addition & 1 deletion packages/core/test/controls/inputGroupTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("<InputGroup>", () => {
it(`renders right element inside .${Classes.INPUT_ACTION} after input`, () => {
const action = mount(<InputGroup rightElement={<address />} />)
.children()
.childAt(2);
.childAt(1);
assert.isTrue(action.hasClass(Classes.INPUT_ACTION));
assert.lengthOf(action.find("address"), 1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class InputGroupExample extends React.PureComponent<IExampleProps, IInput
<InputGroup
disabled={disabled}
large={large}
leftIcon="tag"
leftElement={<Icon icon="tag" />}
onChange={this.handleTagChange}
placeholder="Find tags"
rightElement={resultsTag}
Expand Down