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

ui: Release notes signup - Email subscription form #44744

Merged
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
44 changes: 36 additions & 8 deletions pkg/ui/src/components/button/button.styl
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,63 @@

.crl-button
border-radius 3px

.crl-button--type-flat
border none
cursor pointer
background-color transparent
color $colors--primary-blue-3

&:hover
color $colors--primary-blue-4

&:active
border solid 2px $colors--primary-blue-1

&:focus
outline none
border solid 1px $colors--primary-blue-3

.crl-button--type-flat
border none
&.crl-button--disabled
color $colors--neutral-4
pointer-events none
cursor default

.crl-button--type-primary
cursor pointer
background-color transparent
background-color $colors--primary-green-3
color $colors--white
border solid 1px $colors--primary-green-3

&:hover
color $colors--white

&:active
border solid 1px $colors--primary-green-4

&:focus
outline none
border solid 1px $colors--primary-green-4

&.crl-button--disabled
border solid 1px #c4e6ba
background #c4e6ba
color $colors--white
pointer-events none
cursor default

.crl-button--size-default
height $line-height--larger
min-width $line-height--larger
padding $spacing-smaller $spacing-small
padding-left $spacing-small
padding-right $spacing-small

.crl-button--size-small
height $line-height--medium
min-width $line-height--medium
padding 0 $spacing-xx-small

.crl-button--disabled
color $colors--neutral-4
pointer-events none
cursor default


.crl-button__container
display flex
Expand Down
6 changes: 5 additions & 1 deletion pkg/ui/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ export interface ButtonProps {
icon?: () => React.ReactNode;
iconPosition?: "left" | "right";
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
className?: string;
}

export function Button(props: ButtonProps) {
const { children, type, disabled, size, icon, iconPosition, onClick } = props;
const { children, type, disabled, size, icon, iconPosition, onClick, className } = props;

const rootStyles = cn(
"crl-button",
Expand All @@ -33,6 +34,7 @@ export function Button(props: ButtonProps) {
{
"crl-button--disabled": disabled,
},
className,
);

const renderIcon = () => {
Expand Down Expand Up @@ -62,8 +64,10 @@ export function Button(props: ButtonProps) {
}

Button.defaultProps = {
onClick: () => {},
type: "primary",
disabled: false,
size: "default",
className: "",
iconPosition: "left",
};
2 changes: 2 additions & 0 deletions pkg/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
// licenses/APL.txt.

export * from "./badge";
export * from "./button";
export * from "./icon";
export * from "./globalNavigation";
export * from "./sideNavigation";
export * from "./pageHeader";
export * from "./text";
export * from "./textInput";
export * from "./table";
export * from "./tooltip";
export * from "./select";
124 changes: 124 additions & 0 deletions pkg/ui/src/components/textInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import cn from "classnames";

import { Text, TextTypes } from "src/components";
import "./textInput.styl";

interface TextInputProps {
onChange: (value: string) => void;
value: string;
initialValue?: string;
placeholder?: string;
className?: string;
name?: string;
// validate function returns validation message
// in case validation failed or undefined if successful.
validate?: (value: string) => string | undefined;
}

interface TextInputState {
validationMessage: string;
isValid: boolean;
isDirty: boolean;
isTouched: boolean;
needValidation: boolean;
}

export class TextInput extends React.Component<TextInputProps, TextInputState> {
static defaultProps = {
initialValue: "",
validate: () => true,
};

constructor(props: TextInputProps) {
super(props);

this.state = {
isValid: true,
validationMessage: undefined,
isDirty: false,
isTouched: false,
needValidation: false,
};
}

validateInput = (value: string) => {
const { validate } = this.props;
const validationMessage = validate(value);
this.setState({
isValid: !Boolean(validationMessage),
validationMessage,
});
}

handleOnTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const { needValidation, isValid } = this.state;
if (needValidation && !isValid) {
this.validateInput(value);
}
this.setState({
isDirty: true,
});
this.props.onChange(value);
}

handleOnBlur = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
this.validateInput(value);
this.setState({
isTouched: true,
needValidation: true,
});
}

render() {
const { initialValue, placeholder, className, name, value } = this.props;
const { isDirty, isValid, validationMessage } = this.state;
const textValue = isDirty ? value : initialValue;

const classes = cn(
className,
"crl-text-input",
{
"crl-text-input--invalid": !isValid,
},
);
return (
<div className="crl-text-input__wrapper">
<input
name={name}
type="text"
value={textValue}
placeholder={placeholder}
className={classes}
onChange={this.handleOnTextChange}
onBlur={this.handleOnBlur}
autoComplete="off"
/>
{
!isValid && (
<div className="crl-text-input__validation-container">
<Text
textType={TextTypes.Caption}
className="crl-text-input__error-message"
>
{validationMessage}
</Text>
</div>
)
}
</div>
);
}
}
41 changes: 41 additions & 0 deletions pkg/ui/src/components/textInput/textInput.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

@require '~src/components/core/index.styl'

.crl-text-input
height $line-height--larger
border-radius 3px
border solid 1px $colors--neutral-4
background-color $colors--white
padding 0 $spacing-smaller

&:focus
border solid 1px $colors--primary-blue-3

&:hover
border solid 1px $colors--neutral-5

&--invalid
border: solid 1px $colors--functional-red-3
background-color $colors--functional-red-1
&:focus
border solid 1px $colors--functional-red-3

&__wrapper
display flex
flex-direction column

&__error-message
color $colors--functional-red-3
position absolute

&__validation-container
position relative
14 changes: 14 additions & 0 deletions pkg/ui/src/util/validation/isValidEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

export function isValidEmail(value: string): boolean {
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import { assert } from "chai";
import { mount, ReactWrapper } from "enzyme";
import sinon, { SinonSpy } from "sinon";
import "src/enzymeInit";
import { EmailSubscriptionForm } from "./index";

const sandbox = sinon.createSandbox();

describe("EmailSubscriptionForm", () => {
let wrapper: ReactWrapper;
let onSubmitHandler: SinonSpy;

beforeEach(() => {
sandbox.reset();
onSubmitHandler = sandbox.spy();
wrapper = mount(<EmailSubscriptionForm onSubmit={onSubmitHandler} />);
});

describe("when correct email", () => {
it("provides entered email on submit callback", () => {
const emailAddress = "foo@bar.com";
const inputComponent = wrapper.find("input.crl-text-input").first();
inputComponent.simulate("change", { target: { value: emailAddress } });
const buttonComponent = wrapper.find("button.crl-button").first();
buttonComponent.simulate("click");

onSubmitHandler.calledOnceWith(emailAddress);
});
});

describe("when invalid email", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test coverage on the functionality!

beforeEach(() => {
const emailAddress = "foo";
const inputComponent = wrapper.find("input.crl-text-input").first();
inputComponent.simulate("change", { target: { value: emailAddress } });
inputComponent.simulate("blur");
});

it("doesn't call onSubmit callback", () => {
const buttonComponent = wrapper.find("button.crl-button").first();
buttonComponent.simulate("click");
assert.isTrue(onSubmitHandler.notCalled);
});

it("submit button is disabled", () => {
const buttonComponent = wrapper.find("button.crl-button.crl-button--disabled").first();
assert.isTrue(buttonComponent.exists());
});

it("validation message is shown", () => {
const validationMessageWrapper = wrapper.find(".crl-text-input__error-message").first();
assert.isTrue(validationMessageWrapper.exists());
assert.equal(validationMessageWrapper.text(), "Invalid email address.");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

@require '~src/components/core/index.styl'

.email-subscription-form
display flex
flex-direction row

&__input
margin-right $spacing-smaller
width 258px

&__submit-button
min-width 80px
Loading