Skip to content

Commit fac6fd4

Browse files
[core] feat(InputGroup): add leftElement prop (#4063)
1 parent 45f4131 commit fac6fd4

File tree

7 files changed

+80
-18
lines changed

7 files changed

+80
-18
lines changed

packages/core/src/common/classes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export const HTML_TABLE_STRIPED = `${HTML_TABLE}-striped`;
143143
export const INPUT = `${NS}-input`;
144144
export const INPUT_GHOST = `${INPUT}-ghost`;
145145
export const INPUT_GROUP = `${INPUT}-group`;
146+
export const INPUT_LEFT_CONTAINER = `${INPUT}-left-container`;
146147
export const INPUT_ACTION = `${INPUT}-action`;
147148

148149
export const CONTROL = `${NS}-control`;

packages/core/src/common/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const HOTKEYS_WARN_DECORATOR_NO_METHOD = ns + ` @HotkeysTarget-decorated
3636
export const HOTKEYS_WARN_DECORATOR_NEEDS_REACT_ELEMENT =
3737
ns + ` "@HotkeysTarget-decorated components must return a single JSX.Element or an empty render.`;
3838

39+
export const INPUT_WARN_LEFT_ELEMENT_LEFT_ICON_MUTEX =
40+
ns + ` <InputGroup> leftElement and leftIcon prop are mutually exclusive, with leftElement taking priority.`;
41+
3942
export const NUMERIC_INPUT_MIN_MAX = ns + ` <NumericInput> requires min to be no greater than max if both are defined.`;
4043
export const NUMERIC_INPUT_MINOR_STEP_SIZE_BOUND =
4144
ns + ` <NumericInput> requires minorStepSize to be no greater than stepSize.`;

packages/core/src/common/props.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const INVALID_PROPS = [
121121
"inline",
122122
"large",
123123
"loading",
124+
"leftElement",
124125
"leftIcon",
125126
"minimal",
126127
"onRemove", // ITagProps, ITagInputProps

packages/core/src/components/forms/_input-group.scss

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ $input-button-height-small: $pt-button-height-smaller !default;
5959
}
6060

6161
.#{$ns}-input-action,
62+
> .#{$ns}-input-left-container,
6263
> .#{$ns}-button,
6364
> .#{$ns}-icon {
6465
position: absolute;
@@ -83,10 +84,15 @@ $input-button-height-small: $pt-button-height-smaller !default;
8384
&:empty { padding: 0; }
8485
}
8586

86-
// direct descendant to exclude icons in buttons
87+
// bump icon or left content up so it sits above input
88+
> .#{$ns}-input-left-container,
8789
> .#{$ns}-icon {
88-
// bump icon up so it sits above input
8990
z-index: 1;
91+
}
92+
93+
// direct descendant to exclude icons in buttons
94+
> .#{$ns}-input-left-container > .#{$ns}-icon,
95+
> .#{$ns}-icon {
9096
color: $pt-icon-color;
9197

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

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

160+
> .#{$ns}-input-left-container > .#{$ns}-icon,
153161
> .#{$ns}-icon,
154162
.#{$ns}-input-action > .#{$ns}-spinner {
155163
margin: ($pt-input-height-large - $pt-icon-size-standard) / 2;
@@ -179,6 +187,7 @@ $input-button-height-small: $pt-button-height-smaller !default;
179187
margin: ($pt-input-height-small - $pt-button-height-smaller) / 2;
180188
}
181189

190+
> .#{$ns}-input-left-container > .#{$ns}-icon,
182191
> .#{$ns}-icon,
183192
.#{$ns}-input-action > .#{$ns}-spinner {
184193
margin: ($pt-input-height-small - $pt-icon-size-standard) / 2;

packages/core/src/components/forms/inputGroup.tsx

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import classNames from "classnames";
1818
import * as React from "react";
1919
import { polyfill } from "react-lifecycles-compat";
2020
import { AbstractPureComponent2, Classes } from "../../common";
21+
import * as Errors from "../../common/errors";
2122
import {
2223
DISPLAYNAME_PREFIX,
2324
HTMLInputProps,
@@ -29,8 +30,6 @@ import {
2930
} from "../../common/props";
3031
import { Icon, IconName } from "../icon/icon";
3132

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

5251
/**
53-
* Name of a Blueprint UI icon (or an icon element) to render on the left side of the input group,
54-
* before the user's cursor.
52+
* Element to render on the left side of input. This prop is mutually exclusive
53+
* with `leftIcon`.
54+
*/
55+
leftElement?: JSX.Element;
56+
57+
/**
58+
* Name of a Blueprint UI icon to render on the left side of the input group,
59+
* before the user's cursor. This prop is mutually exclusive with `leftElement`.
60+
* Usage with content is deprecated. Use `leftElement` for elements.
5561
*/
5662
leftIcon?: IconName | MaybeElement;
5763

@@ -81,24 +87,26 @@ export interface IInputGroupProps extends IControlledProps, IIntentProps, IProps
8187
}
8288

8389
export interface IInputGroupState {
84-
rightElementWidth: number;
90+
leftElementWidth?: number;
91+
rightElementWidth?: number;
8592
}
8693

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

91-
public state: IInputGroupState = {
92-
rightElementWidth: DEFAULT_RIGHT_ELEMENT_WIDTH,
93-
};
98+
public state: IInputGroupState = {};
9499

100+
private leftElement: HTMLElement;
95101
private rightElement: HTMLElement;
102+
96103
private refHandlers = {
104+
leftElement: (ref: HTMLSpanElement) => (this.leftElement = ref),
97105
rightElement: (ref: HTMLSpanElement) => (this.rightElement = ref),
98106
};
99107

100108
public render() {
101-
const { className, disabled, fill, intent, large, small, leftIcon, round } = this.props;
109+
const { className, disabled, fill, intent, large, small, round } = this.props;
102110
const classes = classNames(
103111
Classes.INPUT_GROUP,
104112
Classes.intentClass(intent),
@@ -111,11 +119,16 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
111119
},
112120
className,
113121
);
114-
const style: React.CSSProperties = { ...this.props.style, paddingRight: this.state.rightElementWidth };
122+
123+
const style: React.CSSProperties = {
124+
...this.props.style,
125+
paddingLeft: this.state.leftElementWidth,
126+
paddingRight: this.state.rightElementWidth,
127+
};
115128

116129
return (
117130
<div className={classes}>
118-
<Icon icon={leftIcon} />
131+
{this.maybeRenderLeftElement()}
119132
<input
120133
type="text"
121134
{...removeNonHTMLProps(this.props)}
@@ -133,11 +146,34 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
133146
}
134147

135148
public componentDidUpdate(prevProps: IInputGroupProps & HTMLInputProps) {
136-
if (prevProps.rightElement !== this.props.rightElement) {
149+
const { leftElement, rightElement } = this.props;
150+
if (prevProps.leftElement !== leftElement || prevProps.rightElement !== rightElement) {
137151
this.updateInputWidth();
138152
}
139153
}
140154

155+
protected validateProps(props: IInputGroupProps) {
156+
if (props.leftElement != null && props.leftIcon != null) {
157+
console.warn(Errors.INPUT_WARN_LEFT_ELEMENT_LEFT_ICON_MUTEX);
158+
}
159+
}
160+
161+
private maybeRenderLeftElement() {
162+
const { leftElement, leftIcon } = this.props;
163+
164+
if (leftElement != null) {
165+
return (
166+
<span className={Classes.INPUT_LEFT_CONTAINER} ref={this.refHandlers.leftElement}>
167+
{leftElement}
168+
</span>
169+
);
170+
} else if (leftIcon != null) {
171+
return <Icon icon={leftIcon} />;
172+
}
173+
174+
return undefined;
175+
}
176+
141177
private maybeRenderRightElement() {
142178
const { rightElement } = this.props;
143179
if (rightElement == null) {
@@ -151,14 +187,26 @@ export class InputGroup extends AbstractPureComponent2<IInputGroupProps & HTMLIn
151187
}
152188

153189
private updateInputWidth() {
190+
const { leftElementWidth, rightElementWidth } = this.state;
191+
192+
if (this.leftElement != null) {
193+
const { clientWidth } = this.leftElement;
194+
// small threshold to prevent infinite loops
195+
if (leftElementWidth === undefined || Math.abs(clientWidth - leftElementWidth) > 2) {
196+
this.setState({ leftElementWidth: clientWidth });
197+
}
198+
} else {
199+
this.setState({ leftElementWidth: undefined });
200+
}
201+
154202
if (this.rightElement != null) {
155203
const { clientWidth } = this.rightElement;
156204
// small threshold to prevent infinite loops
157-
if (Math.abs(clientWidth - this.state.rightElementWidth) > 2) {
205+
if (rightElementWidth === undefined || Math.abs(clientWidth - rightElementWidth) > 2) {
158206
this.setState({ rightElementWidth: clientWidth });
159207
}
160208
} else {
161-
this.setState({ rightElementWidth: DEFAULT_RIGHT_ELEMENT_WIDTH });
209+
this.setState({ rightElementWidth: undefined });
162210
}
163211
}
164212
}

packages/core/test/controls/inputGroupTests.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe("<InputGroup>", () => {
3838
it(`renders right element inside .${Classes.INPUT_ACTION} after input`, () => {
3939
const action = mount(<InputGroup rightElement={<address />} />)
4040
.children()
41-
.childAt(2);
41+
.childAt(1);
4242
assert.isTrue(action.hasClass(Classes.INPUT_ACTION));
4343
assert.lengthOf(action.find("address"), 1);
4444
});

packages/docs-app/src/examples/core-examples/inputGroupExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class InputGroupExample extends React.PureComponent<IExampleProps, IInput
117117
<InputGroup
118118
disabled={disabled}
119119
large={large}
120-
leftIcon="tag"
120+
leftElement={<Icon icon="tag" />}
121121
onChange={this.handleTagChange}
122122
placeholder="Find tags"
123123
rightElement={resultsTag}

0 commit comments

Comments
 (0)