Skip to content

Commit

Permalink
Merge pull request #429 from dcos-labs/mp/feat/text-input-with-buttons
Browse files Browse the repository at this point in the history
Add components to support text inputs with buttons inside
  • Loading branch information
mperrotti authored Nov 20, 2019
2 parents fde5e4e + c99a216 commit 477eacf
Show file tree
Hide file tree
Showing 16 changed files with 1,097 additions and 7 deletions.
51 changes: 51 additions & 0 deletions packages/button/components/ResetButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react";
import { css, cx } from "emotion";
import { buttonReset } from "../../shared/styles/styleUtils";

const pointerCursor = css`
cursor: pointer;
`;

const outlineNone = css`
&:focus {
outline: none;
}
`;

// Replicates default browser focus ring styles.
//
// The media query targets Webkit browsers, which can
// more accurately replicate the native focus ring style
const keyboardFocus = css`
&:focus > div {
outline-color: Highlight;
outline-width: thin;
@media (-webkit-min-device-pixel-ratio: 0) {
outline-color: -webkit-focus-ring-color;
outline-style: auto;
outline-width: unset;
}
}
`;

const ResetButton = (props: React.HTMLAttributes<HTMLButtonElement>) => {
const { children, className, ...other } = props;
const classNames = cx(
buttonReset,
className,
outlineNone,
keyboardFocus,
pointerCursor
);

return (
<button className={classNames} {...other}>
<div className={outlineNone} tabIndex={-1}>
{children}
</div>
</button>
);
};

export default ResetButton;
2 changes: 2 additions & 0 deletions packages/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export {
export {
default as DangerDropdownButton
} from "./components/DangerDropdownButton";

export { default as ResetButton } from "./components/ResetButton";
8 changes: 7 additions & 1 deletion packages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ export {
} from "./table";
export { TableView, TableViewHeader, TableViewBody } from "./tableView";
export { TabItem, TabTitle, Tabs } from "./tabs";
export { TextInput, TextInputWithIcon, TextInputWithBadges } from "./textInput";
export {
TextInput,
TextInputWithIcon,
TextInputWithBadges,
TextInputWithButtons
} from "./textInput";
export { TextInputButton } from "./textInputButton";
export { Textarea } from "./textarea";
export { Toaster, Toast } from "./toaster";
export { ToggleContent } from "./toggleContent";
Expand Down
10 changes: 9 additions & 1 deletion packages/shared/styles/formStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
themeTextColorPrimary,
themeBrandPrimary,
themeError,
themeBgDisabled
themeBgDisabled,
iconSizeXs
} from "../../design-tokens/build/js/designTokens";

import {
Expand Down Expand Up @@ -71,6 +72,13 @@ export const toggleInputApperances = {
`
};

export const inputIconWrapper = css`
svg {
max-width: ${iconSizeXs};
height: auto;
}
`;

export const getIconAppearanceStyle = appearance => {
switch (appearance) {
case "standard":
Expand Down
12 changes: 12 additions & 0 deletions packages/textInput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ If the `tooltipContent` prop is set, an icon tooltip with the given text will be

## TextInputWithBadges
`TextInputWithBadges` extends the `TextInputWithIcon` component. More info coming soon...

## TextInputWithButtons
`TextInputWithButtons` extends the `TextInputWithIcon` component, does not take an `iconEnd` prop.

A `TextInputWithButtons` component is used when there is an action a user can take that is related to the text input. For example, clicking a button to clear the input's value.

Please do not pass more than 2 buttons into a `TextInputWithButtons`. Instead, consider putting additional buttons outside of the text input.

### Buttons
The `TextInputButton` component is provided to keep text input buttons consistent. To save space, the buttons are always icons. These icon buttons can be filled with any color, and they do not get the same fill color as icons inside the text input that are not buttons.

Because these buttons contain no text, please pass an `aria-label` to the button. For example, a button that clears the text input might get `aria-label="Clear input"`.
6 changes: 4 additions & 2 deletions packages/textInput/components/TextInputWithBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
inputContainer
} from "../../shared/styles/formStyles";
import { Badge } from "../../badge";
import { flex, textTruncate } from "../../shared/styles/styleUtils";
import { flex, textTruncate, flexItem } from "../../shared/styles/styleUtils";
import {
badgeInput,
badgeInputContainer,
Expand Down Expand Up @@ -97,6 +97,7 @@ export class TextInputWithBadges extends TextInputWithIcon<
getInputAppearanceStyle(inputAppearance)
)}
>
{this.getIconStartContent()}
{this.props.badges &&
this.props.badges.map(badge => (
<span
Expand Down Expand Up @@ -129,10 +130,11 @@ export class TextInputWithBadges extends TextInputWithIcon<
</span>
))}
{this.getInputElement(
[badgeInput, badgeInputContents],
[badgeInput, badgeInputContents, flexItem("grow")],
isValid,
describedByIds
)}
{this.getIconEndContent()}
</div>
{getHintContent}
{getValidationErrors}
Expand Down
87 changes: 87 additions & 0 deletions packages/textInput/components/TextInputWithButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from "react";
import { cx } from "emotion";
import TextInputWithIcon, { TextInputWithIconProps } from "./TextInputWithIcon";
import FormFieldWrapper from "../../shared/components/FormFieldWrapper";
import { flex, padding, flexItem } from "../../shared/styles/styleUtils";
import {
inputContainer,
getInputAppearanceStyle
} from "../../shared/styles/formStyles";
import { TextInputButtonProps } from "../../textInputButton/components/TextInputButton";

export interface TextInputWithButtonsProps
extends Omit<TextInputWithIconProps, "iconEnd"> {
/**
* An array of TextInputButton components to render at the end of the input
*/
buttons: Array<React.ReactElement<TextInputButtonProps>>;
}

class TextInputWithButtons extends TextInputWithIcon<
TextInputWithButtonsProps,
{}
> {
protected getInputElementProps() {
let baseProps = super.getInputElementProps();
const { buttons, ...inputProps } = baseProps as TextInputWithButtonsProps;

return inputProps;
}

protected getInputContent() {
const inputAppearance = this.getInputAppearance();

return (
<FormFieldWrapper
id={this.props.id}
errors={this.props.errors}
hintContent={this.props.hintContent}
>
{({ getValidationErrors, getHintContent, isValid, describedByIds }) => (
<React.Fragment>
<div
className={cx(
flex(),
padding("horiz", "s"),
inputContainer,
getInputAppearanceStyle(inputAppearance)
)}
>
{this.getIconStartContent()}
{this.getInputElement(
[flexItem("grow"), padding("all", "none")],
isValid,
describedByIds
)}
{this.getButtons()}
</div>
{getHintContent}
{getValidationErrors}
</React.Fragment>
)}
</FormFieldWrapper>
);
}

private getButtons() {
if (!this.props.buttons.filter(Boolean)) {
return;
}

return this.props.buttons.map((button, i) => (
// TODO: consider making a component for this wrapper span
<span
className={cx(
flex({ align: "center", justify: "center" }),
flexItem("shrink"),
{ [padding("left", "s")]: i !== 0 }
)}
key={(React.isValidElement(button) && button.key) || i}
>
{button}
</span>
));
}
}

export default TextInputWithButtons;
9 changes: 6 additions & 3 deletions packages/textInput/components/TextInputWithIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { TextInput, TextInputProps } from "./TextInput";
import {
getInputAppearanceStyle,
inputContainer,
getIconAppearanceStyle
getIconAppearanceStyle,
inputIconWrapper
} from "../../shared/styles/formStyles";
import { flex, flexItem, flush, padding } from "../../shared/styles/styleUtils";
import FormFieldWrapper from "../../shared/components/FormFieldWrapper";
Expand Down Expand Up @@ -82,7 +83,8 @@ export class TextInputWithIcon<
padding("right", "xs"),
flex({ align: "center", justify: "center" }),
flexItem("shrink"),
getIconAppearanceStyle(this.getInputAppearance())
getIconAppearanceStyle(this.getInputAppearance()),
inputIconWrapper
)}
>
{this.props.iconStart}
Expand All @@ -100,7 +102,8 @@ export class TextInputWithIcon<
flex({ align: "center", justify: "center" }),
flexItem("shrink"),
flush("left"),
getIconAppearanceStyle(this.getInputAppearance())
getIconAppearanceStyle(this.getInputAppearance()),
inputIconWrapper
)}
>
{this.props.iconEnd}
Expand Down
3 changes: 3 additions & 0 deletions packages/textInput/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export { default as TextInputWithIcon } from "./components/TextInputWithIcon";
export {
default as TextInputWithBadges
} from "./components/TextInputWithBadges";
export {
default as TextInputWithButtons
} from "./components/TextInputWithButtons";
88 changes: 88 additions & 0 deletions packages/textInput/stories/TextInputWithButtons.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as React from "react";
import { storiesOf } from "@storybook/react";
import { withReadme } from "storybook-readme";
import { TextInputWithButtons } from "../index";
import { inputStoryWrapper } from "../../../decorators/inputStoryWrapper";
import { SystemIcons } from "../../icons/dist/system-icons-enum";
import { TextInputButton } from "../../textInputButton";
import { Icon } from "../../icon";

const readme = require("../README.md");

const btnClickFn = () => {
alert("button one clicked");
};

storiesOf("Forms/TextInputWithButtons", module)
.addDecorator(withReadme([readme]))
.addDecorator(inputStoryWrapper)
.addParameters({
info: {
propTables: [TextInputWithButtons, TextInputButton]
}
})
.add("one button", () => (
<TextInputWithButtons
id="oneBtn"
inputLabel="One button"
buttons={[
<TextInputButton
key={0}
shape={SystemIcons.Close}
onClick={btnClickFn}
aria-label="Clear input"
/>
]}
/>
))
.add("two buttons", () => (
<TextInputWithButtons
id="twoBtn"
inputLabel="Two buttons"
buttons={[
<TextInputButton
key={0}
shape={SystemIcons.Close}
onClick={btnClickFn}
aria-label="Clear input"
/>,
<TextInputButton
key={1}
shape={SystemIcons.Funnel}
onClick={btnClickFn}
aria-label="Activate filter"
/>
]}
/>
))
.add("with an icon", () => (
<TextInputWithButtons
id="withIcon"
inputLabel="With icon"
iconStart={<Icon shape={SystemIcons.Search} />}
buttons={[
<TextInputButton
key={0}
shape={SystemIcons.Close}
onClick={btnClickFn}
aria-label="Clear input"
/>
]}
/>
))
.add("with a custom colored icon", () => (
<TextInputWithButtons
id="withIcon.colored"
inputLabel="With colored icon"
iconStart={<Icon shape={SystemIcons.Search} />}
buttons={[
<TextInputButton
key={0}
color="red"
shape={SystemIcons.Close}
onClick={btnClickFn}
aria-label="Clear input"
/>
]}
/>
));
Loading

0 comments on commit 477eacf

Please sign in to comment.