diff --git a/out8492 b/out8492
new file mode 160000
index 00000000000..30cace674aa
--- /dev/null
+++ b/out8492
@@ -0,0 +1 @@
+Subproject commit 30cace674aa93af4e30717927f698f88bce80f41
diff --git a/packages/patternfly-react/package.json b/packages/patternfly-react/package.json
index 7d3ac7bc5b0..b559807b2f6 100644
--- a/packages/patternfly-react/package.json
+++ b/packages/patternfly-react/package.json
@@ -34,7 +34,9 @@
"react-bootstrap-typeahead": "^3.1.3",
"react-c3js": "^0.1.20",
"react-click-outside": "^3.0.1",
+ "react-collapse": "^4.0.3",
"react-fontawesome": "^1.6.1",
+ "react-motion": "^0.5.2",
"reactabular-table": "^8.14.0",
"recompose": "^0.26.0"
},
diff --git a/packages/patternfly-react/src/components/LoginPage/LoginPage.js b/packages/patternfly-react/src/components/LoginPage/LoginPage.js
new file mode 100644
index 00000000000..b4699105f3b
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/LoginPage.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Container from './components/LoginPageComponents/LoginPageContainer';
+import Header from './components/LoginPageComponents/LoginPageHeader';
+import Footer from './components/LoginPageComponents/LoginPageFooter';
+import FooterLinks from './components/LoginPageComponents/LoginFooterLinks';
+import LoginCard from './components/LoginCardComponents/LoginCard';
+import WithTranslation from './components/LoginPageComponents/LoginPageWithTranslation';
+import LoginPageAlert from './components/LoginPageComponents/LoginPageAlert';
+import LoginPageLink from './components/LoginPageComponents/LoginPageLink';
+import SocialLoginPage from './SocialLoginPage';
+import SocialLoginPageContainer from './components/LoginPageComponents/SocialLoginPageContainer';
+import BasicLoginPageLayout from './components/LoginPageComponents/BasicLoginPageLayout';
+
+const LoginPagePattern = ({ container, header, footerLinks, card }) => (
+
+
+
+
+
+
+
+
+ {card.title}
+
+
+
+
+
+
+
+
+
+
+);
+
+const LoginPage = props => (
+
+
+
+);
+
+LoginPage.Container = Container;
+LoginPage.Header = Header;
+LoginPage.Footer = Footer;
+LoginPage.Card = LoginCard;
+LoginPage.FooterLinks = FooterLinks;
+LoginPage.WithTranslation = WithTranslation;
+LoginPage.Alert = LoginPageAlert;
+LoginPage.Pattern = LoginPagePattern;
+LoginPage.Social = SocialLoginPage;
+LoginPage.SocialContainer = SocialLoginPageContainer;
+LoginPage.BasicLayout = BasicLoginPageLayout;
+LoginPage.Link = LoginPageLink;
+
+LoginPagePattern.propTypes = {
+ container: PropTypes.shape({ ...LoginPage.Container.propTypes }),
+ header: PropTypes.shape({ ...LoginPage.Header.propTypes }),
+ card: PropTypes.shape({
+ ...LoginCard.LanguagePicker.propTypes,
+ ...LoginCard.Form.propTypes,
+ ...LoginCard.SignUp.propTypes,
+ ...LoginCard.RememberMe.propTypes,
+ ...LoginCard.ForgotPassword.propTypes
+ }),
+ footerLinks: PropTypes.array
+};
+
+LoginPagePattern.defaultProps = {
+ container: { ...LoginPage.Container.defaultProps },
+ header: { ...LoginPage.Header.defaultProps },
+ card: {
+ ...LoginCard.LanguagePicker.defaultProps,
+ ...LoginCard.Form.defaultProps,
+ ...LoginCard.SignUp.defaultProps,
+ ...LoginCard.RememberMe.defaultProps,
+ ...LoginCard.ForgotPassword.defaultProps
+ },
+ footerLinks: [...LoginPage.Footer.defaultProps.links]
+};
+
+LoginPage.propTypes = { ...LoginPagePattern.propTypes };
+LoginPage.defaultProps = { ...LoginPagePattern.defaultProps };
+
+export default LoginPage;
diff --git a/packages/patternfly-react/src/components/LoginPage/LoginPage.stories.js b/packages/patternfly-react/src/components/LoginPage/LoginPage.stories.js
new file mode 100644
index 00000000000..35c615946dd
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/LoginPage.stories.js
@@ -0,0 +1,238 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withKnobs, number } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import { defaultTemplate } from 'storybook/decorators/storyTemplates';
+import {
+ storybookPackageName,
+ STORYBOOK_CATEGORY,
+ DOCUMENTATION_URL
+} from 'storybook/constants/siteConstants';
+import LoginPage from './LoginPage';
+import englishMessages from './mocks/messages.en';
+import frenchMessages from './mocks/messages.fr';
+import images from './assets/img';
+import { name } from '../../../package.json';
+
+const stories = storiesOf(
+ `${storybookPackageName(name)}/${
+ STORYBOOK_CATEGORY.APPLICATION_FRAMEWORK
+ }/Login Page`,
+ module
+);
+
+stories.addDecorator(withKnobs);
+stories.addDecorator(
+ defaultTemplate({
+ title: 'Login Page',
+ documentationLink: `${
+ DOCUMENTATION_URL.PATTERNFLY_ORG_APP_FRAMEWORK
+ }login-page`
+ })
+);
+
+const storyAction = (e, message) => {
+ e.preventDefault();
+ action(message)();
+};
+
+const createProps = () => {
+ const { header, footerLinks, card } = englishMessages;
+ footerLinks.forEach(link => {
+ link.onClick = e => storyAction(e, 'Footer Link was pressed');
+ });
+ return {
+ container: {
+ backgroundUrl: images.background,
+ translations: { en: englishMessages, fr: frenchMessages },
+ className: '',
+ alert: {
+ message: header.alert,
+ onDismiss: e => storyAction(e, 'Notification was dismissed'),
+ show: true
+ }
+ },
+ header: {
+ logoSrc: images.brand,
+ logoTitle: header.logo,
+ caption: header.caption
+ },
+ footerLinks,
+ card: {
+ title: card.header.title,
+ selectedLanguage: card.header.selectedLanguage,
+ availableLanguages: card.header.availableLanguages,
+ signUp: {
+ label: card.signUp.label,
+ link: {
+ children: card.signUp.link.label,
+ href: '#',
+ onClick: e => storyAction(e, 'sign up was clicked')
+ }
+ },
+ form: {
+ validate: true,
+ submitError: card.form.error,
+ showError: true,
+ usernameField: {
+ id: 'card_email',
+ type: 'email',
+ placeholder: card.usernameField.placeholder,
+ errors: card.usernameField.errors,
+ error: card.usernameField.errors.invalid,
+ showError: true
+ },
+ passwordField: {
+ id: 'card_password',
+ type: 'password',
+ placeholder: card.passwordField.placeholder,
+ minLength: 8,
+ errors: card.passwordField.errors,
+ warnings: card.passwordField.warnings,
+ warning: card.passwordField.warnings.capsLock,
+ showWarning: true
+ },
+ additionalFields: null,
+ rememberMe: {
+ label: card.rememberMe,
+ onClick: e => action('remember me checkbox was clicked')()
+ },
+ forgotPassword: {
+ label: card.forgotPassword,
+ href: '#',
+ onClick: e => storyAction(e, 'Forgot password was clicked')
+ },
+ disableSubmit: false,
+ submitText: card.form.submitText,
+ onSubmit: e => storyAction(e, 'Form was submitted')
+ },
+ social: {
+ links: createLogoList()
+ }
+ }
+ };
+};
+
+const createLogoList = () => {
+ const socialLinkClick = e => storyAction(e, 'Social Link was clicked');
+ const {
+ google,
+ facebook,
+ linkedin,
+ github,
+ instagram,
+ stackExchange,
+ twitter,
+ git,
+ openID,
+ dropbox,
+ fedora,
+ skype
+ } = images;
+ return [
+ {
+ src: google,
+ alt: 'Google',
+ text: 'Google',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: facebook,
+ alt: 'Facebook',
+ text: 'Facebook',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: linkedin,
+ alt: 'Linkedin',
+ text: 'Linkedin',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: github,
+ alt: 'Github',
+ text: 'Github',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: instagram,
+ alt: 'Instagram',
+ text: 'Instagram',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: git,
+ alt: 'Git',
+ text: 'Git',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: openID,
+ alt: 'OpenID',
+ text: 'OpenID',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: dropbox,
+ alt: 'Dropbox',
+ text: 'Dropbox',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: fedora,
+ alt: 'Fedora',
+ text: 'Fedora',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: skype,
+ alt: 'Skype',
+ text: 'Skype',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: twitter,
+ alt: 'Twitter',
+ text: 'Twitter',
+ onClick: e => socialLinkClick(e)
+ },
+ {
+ src: stackExchange,
+ alt: 'StackExchange',
+ text: 'StackExchange',
+ onClick: e => socialLinkClick(e)
+ }
+ ];
+};
+
+stories.addWithInfo('Managed Basic Login Page', () => (
+
+));
+
+stories.addWithInfo('Build Your own Basic Login Page', () => {
+ const props = { ...createProps() };
+ props.card.form.validate = false;
+ return LoginPage.Pattern(props);
+});
+
+stories.addWithInfo('Managed Social Login Page', () => {
+ const logoListCopy = createLogoList();
+ const listSize = number('Social List Size', 12);
+ const socialLinks = logoListCopy.splice(0, listSize);
+
+ const props = { ...createProps() };
+ props.card.social.links = socialLinks;
+
+ return ;
+});
+
+stories.addWithInfo('Build Your own Social Login Page', () => {
+ const logoListCopy = createLogoList();
+ const listSize = number('Social List Size', 12);
+ const socialLinks = logoListCopy.splice(0, listSize);
+
+ const props = { ...createProps() };
+ props.card.social.links = socialLinks;
+ props.card.form.validate = false;
+ return LoginPage.Social.Pattern(props);
+});
diff --git a/packages/patternfly-react/src/components/LoginPage/LoginPage.test.js b/packages/patternfly-react/src/components/LoginPage/LoginPage.test.js
new file mode 100644
index 00000000000..c9c04388c26
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/LoginPage.test.js
@@ -0,0 +1,366 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { DropdownButton } from 'react-bootstrap';
+import englishMessages from './mocks/messages.en';
+import frenchMessages from './mocks/messages.fr';
+import { LoginPage, LoginCardWithValidation } from './index';
+
+const { Input, ForgotPassword, SignUp } = LoginPage.Card;
+const { FooterLinks } = LoginPage;
+
+const mockFunction = jest.fn();
+const createProps = () => {
+ const { header, card } = englishMessages;
+ return {
+ container: {
+ translations: { en: englishMessages, fr: frenchMessages },
+ alert: {
+ message: header.alert,
+ show: true
+ }
+ },
+ header: {
+ logoTitle: header.logo,
+ caption: header.caption
+ },
+ footerLinks: [
+ { children: 'Terms of Use', href: '#', onClick: mockFunction },
+ { children: 'Help', href: '#', onClick: mockFunction },
+ { children: 'Privacy Policy', href: '#', onClick: mockFunction }
+ ],
+ card: {
+ title: card.header.title,
+ selectedLanguage: card.header.selectedLanguage,
+ availableLanguages: card.header.availableLanguages,
+ signUp: {
+ label: card.signUp.label,
+ link: {
+ label: card.signUp.link.label,
+ href: '#',
+ onClick: mockFunction
+ }
+ },
+ form: {
+ error: card.form.error,
+ showError: true,
+ usernameField: {
+ id: 'card_email',
+ type: 'email',
+ placeholder: card.usernameField.placeholder,
+ errors: card.usernameField.errors,
+ error: card.usernameField.errors.invalid,
+ showError: true
+ },
+ passwordField: {
+ id: 'card_password',
+ type: 'password',
+ placeholder: card.passwordField.placeholder,
+ errors: card.passwordField.errors,
+ minLength: 8,
+ warnings: card.passwordField.warnings,
+ warning: card.passwordField.warnings.capsLock,
+ showWarning: true
+ },
+ rememberMe: {
+ label: card.rememberMe,
+ onChange: mockFunction
+ },
+ forgotPassword: {
+ label: card.forgotPassword,
+ href: '#',
+ onClick: mockFunction
+ },
+ disableSubmit: false,
+ submitText: card.form.submitText
+ }
+ }
+ };
+};
+
+afterEach(() => mockFunction.mockClear());
+
+test('Component matches snapshot', () => {
+ const component = mount( );
+ expect(component.render()).toMatchSnapshot();
+});
+
+test('Alert closes succesfully', () => {
+ const component = mount( );
+ component
+ .find('div.alert button')
+ .at(0)
+ .simulate('click');
+
+ expect(
+ component
+ .find('div.alert button')
+ .at(0)
+ .exists()
+ ).toEqual(false);
+});
+
+test('Dropdown updates succesfully', () => {
+ const component = mount( );
+ component
+ .find('ul.dropdown-menu li a')
+ .at(1)
+ .simulate('click');
+
+ expect(
+ component
+ .find(DropdownButton)
+ .at(0)
+ .props().title
+ ).toEqual(frenchMessages.card.header.selectedLanguage.text);
+});
+
+test('Toggle Caps lock warning in password field by the events: focus, blur and mouseEnter', () => {
+ const component = mount( );
+ const passwordElement = component.find('input[type="password"]').at(0);
+
+ passwordElement
+ .simulate('focus')
+ .simulate('keypress', { keyCode: 80, shiftKey: false });
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().warning
+ ).toEqual(englishMessages.card.passwordField.warnings.capsLock);
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showWarning
+ ).toEqual(true);
+
+ passwordElement
+ .simulate('blur')
+ .simulate('keypress', { keyCode: 80, shiftKey: false });
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showWarning
+ ).toEqual(false);
+
+ passwordElement
+ .simulate('keypress', { keyCode: 80, shiftKey: false })
+ .simulate('focus');
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showWarning
+ ).toEqual(true);
+});
+
+test('Toggle CapsLock cause warning to show under password field when focused', () => {
+ const component = mount( );
+ component
+ .find('input[type="password"]')
+ .simulate('change', { target: { value: 'test' } });
+ component
+ .find(LoginCardWithValidation)
+ .instance()
+ .toggleCapsLock({ key: 'CapsLock' });
+
+ component.find('input[type="password"]').simulate('focus');
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showWarning
+ ).toBeTruthy();
+});
+
+test('Submit while inputs are empty cause specific errors to be shown and onChange they disappear', () => {
+ const component = mount( );
+ const usernameElement = component.find('input[type="email"]').at(0);
+ const passwordElement = component.find('input[type="password"]').at(0);
+ component
+ .find('form')
+ .at(0)
+ .simulate('submit');
+
+ // check username field
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().error
+ ).toEqual(englishMessages.card.usernameField.errors.empty);
+
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().showError
+ ).toEqual(true);
+
+ usernameElement.simulate('change', { target: { value: 'Ron' } });
+
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().showError
+ ).toEqual(false);
+
+ // check password field
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().error
+ ).toEqual(englishMessages.card.passwordField.errors.empty);
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showError
+ ).toEqual(true);
+
+ passwordElement.simulate('change', { target: { value: 'Q!w2e3' } });
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showError
+ ).toEqual(false);
+});
+
+test('Submit while password is too short raises the correct error', () => {
+ const component = mount( );
+ const passwordElement = component.find('input[type="password"]').at(0);
+
+ passwordElement.simulate('change', { target: { value: 'short' } });
+
+ component
+ .find('form')
+ .at(0)
+ .simulate('submit');
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().showError
+ ).toEqual(true);
+
+ expect(
+ component
+ .find(Input)
+ .at(1)
+ .props().error
+ ).toEqual(englishMessages.card.passwordField.errors.short);
+});
+
+test('Submit while username is invalid cause a specific error to be shown and onChange is disappears', () => {
+ const component = mount( );
+ const usernameElement = component.find('input[type="email"]').at(0);
+ usernameElement.simulate('change', { target: { value: 'agagagagag@' } });
+
+ component
+ .find('form')
+ .at(0)
+ .simulate('submit');
+
+ // check username field
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().error
+ ).toEqual(englishMessages.card.usernameField.errors.invalid);
+
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().showError
+ ).toEqual(true);
+
+ usernameElement.simulate('change', { target: { value: 'ron@redhat.com' } });
+
+ expect(
+ component
+ .find(Input)
+ .at(0)
+ .props().showError
+ ).toEqual(false);
+});
+
+test('Remember-me checkbox change trigger mock function', () => {
+ const component = mount( );
+ component
+ .find('div.login-pf-settings input[type="checkbox"]')
+ .at(0)
+ .simulate('change');
+
+ expect(mockFunction).toHaveBeenCalled();
+});
+
+test('Forgot Password link click trigger mock function', () => {
+ const component = mount( );
+ component
+ .find(ForgotPassword)
+ .find('a')
+ .at(0)
+ .simulate('click');
+
+ expect(mockFunction).toHaveBeenCalled();
+});
+
+test('Sign-up click trigger mock function', () => {
+ const component = mount( );
+ component
+ .find(SignUp)
+ .find('a')
+ .at(0)
+ .simulate('click');
+
+ expect(mockFunction).toHaveBeenCalled();
+});
+
+test('Footer-links click trigger mock function', () => {
+ const component = mount( );
+ component
+ .find(FooterLinks)
+ .find('ul li a')
+ .at(0)
+ .simulate('click');
+
+ expect(mockFunction).toHaveBeenCalled();
+});
+
+test('Translation works', () => {
+ const component = mount( );
+
+ // Click on french
+ component
+ .find('ul.dropdown-menu li a')
+ .at(1)
+ .simulate('click');
+
+ expect(
+ component
+ .find(DropdownButton)
+ .at(0)
+ .props().title
+ ).toEqual(frenchMessages.card.header.selectedLanguage.text);
+
+ expect(
+ component
+ .find(LoginPage.Pattern)
+ .at(0)
+ .props().header.caption
+ ).toEqual(frenchMessages.header.caption);
+});
diff --git a/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.js b/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.js
new file mode 100644
index 00000000000..669b1f632e0
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import LoginCard from './components/LoginCardComponents/LoginCard';
+import LoginPage from './LoginPage';
+
+const SocialLoginPagePattern = ({ container, header, footerLinks, card }) => (
+
+
+
+
+
+
+ {card.title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const SocialLoginPage = props => (
+
+
+
+);
+
+const getLoginPage = props => LoginPage(props);
+
+SocialLoginPage.Pattern = SocialLoginPagePattern;
+
+SocialLoginPagePattern.propTypes = {
+ ...getLoginPage.propTypes
+};
+
+SocialLoginPagePattern.defaultProps = {
+ ...getLoginPage.defaultProps
+};
+
+export default SocialLoginPage;
diff --git a/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.test.js b/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.test.js
new file mode 100644
index 00000000000..782eaf640c1
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/SocialLoginPage.test.js
@@ -0,0 +1,180 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import englishMessages from './mocks/messages.en';
+import frenchMessages from './mocks/messages.fr';
+import { SocialLoginPage } from './index';
+
+const mockFunction = jest.fn();
+const logoList = [
+ {
+ alt: 'Google',
+ text: 'Google',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Facebook',
+ text: 'Facebook',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Linkedin',
+ text: 'Linkedin',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Github',
+ text: 'Github',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Instagram',
+ text: 'Instagram',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Git',
+ text: 'Git',
+ onClick: mockFunction
+ },
+ {
+ alt: 'OpenID',
+ text: 'OpenID',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Dropbox',
+ text: 'Dropbox',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Fedora',
+ text: 'Fedora',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Skype',
+ text: 'Skype',
+ onClick: mockFunction
+ },
+ {
+ alt: 'Twitter',
+ text: 'Twitter',
+ onClick: mockFunction
+ },
+ {
+ alt: 'StackExchange',
+ text: 'StackExchange',
+ onClick: mockFunction
+ }
+];
+const createProps = () => {
+ const { header, footerLinks, card } = englishMessages;
+ return {
+ container: {
+ translations: { en: englishMessages, fr: frenchMessages },
+ className: '',
+ alert: {
+ message: header.alert,
+ onDismiss: mockFunction,
+ show: true
+ }
+ },
+ header: {
+ logoTitle: header.logo,
+ caption: header.caption
+ },
+ footerLinks,
+ card: {
+ title: card.header.title,
+ selectedLanguage: card.header.selectedLanguage,
+ availableLanguages: card.header.availableLanguages,
+ signUp: {
+ label: card.signUp.label,
+ link: {
+ children: card.signUp.link.label,
+ href: '#',
+ onClick: mockFunction
+ }
+ },
+ form: {
+ validate: true,
+ submitError: card.form.error,
+ showError: true,
+ usernameField: {
+ id: 'card_email',
+ type: 'email',
+ placeholder: card.usernameField.placeholder,
+ errors: card.usernameField.errors,
+ error: card.usernameField.errors.invalid,
+ showError: true
+ },
+ passwordField: {
+ id: 'card_password',
+ type: 'password',
+ placeholder: card.passwordField.placeholder,
+ minLength: 8,
+ errors: card.passwordField.errors,
+ warnings: card.passwordField.warnings,
+ warning: card.passwordField.warnings.capsLock,
+ showWarning: true
+ },
+ rememberMe: {
+ label: card.rememberMe,
+ onClick: mockFunction
+ },
+ forgotPassword: {
+ label: card.forgotPassword,
+ href: '#',
+ onClick: mockFunction
+ },
+ disableSubmit: false,
+ submitText: card.form.submitText,
+ onSubmit: mockFunction
+ },
+ social: {
+ links: logoList
+ }
+ }
+ };
+};
+
+afterEach(() => mockFunction.mockClear());
+
+test('Component matches snapshot', () => {
+ const props = { ...createProps() };
+ props.card.social.links = [];
+ const component = mount( );
+ expect(component.render()).toMatchSnapshot();
+});
+
+test('Click on social link triggers mock function', () => {
+ const component = mount( );
+ component
+ .find('.login-pf-social-link')
+ .at(0)
+ .find('a')
+ .simulate('click');
+
+ expect(mockFunction).toHaveBeenCalled();
+});
+
+test('Click on the "More" button will expend the list and the button will change to "Less"', () => {
+ const component = mount( );
+ const toggleBtn = component.find('.login-pf-social-toggle');
+
+ toggleBtn.simulate('click');
+
+ expect(toggleBtn.text()).toEqual(expect.stringMatching('Less'));
+
+ expect(component.find('.ReactCollapse--collapse > ul > li')).toBeTruthy();
+});
+
+test("While the social list has 4 or less links, it won't have the 'double-col' class and the expend button won't exist", () => {
+ const props = { ...createProps() };
+ props.card.social.links = [...logoList].splice(0, 4);
+ const component = mount( );
+ const socialList = component.find('ul.login-pf-social');
+ expect(socialList.hasClass('login-pf-social-double-col')).toBeFalsy();
+ const toggleBtn = component.find('.login-pf-social-toggle');
+ expect(toggleBtn.exists()).toBeFalsy();
+});
diff --git a/packages/patternfly-react/src/components/LoginPage/__snapshots__/LoginPage.test.js.snap b/packages/patternfly-react/src/components/LoginPage/__snapshots__/LoginPage.test.js.snap
new file mode 100644
index 00000000000..ec8831364c8
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/__snapshots__/LoginPage.test.js.snap
@@ -0,0 +1,233 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Component matches snapshot 1`] = `
+
+
+
+
+
+
+
+ Patternfly will be updated to 2.13.5 at 00:00 AM, 23th Sep 2018 (UTC). This Update will last for 8-12 hours, please plan in advance for this outage.
+
+
+
+
+
+
+
+
+
+
+
+
+ Need an account?
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/patternfly-react/src/components/LoginPage/__snapshots__/SocialLoginPage.test.js.snap b/packages/patternfly-react/src/components/LoginPage/__snapshots__/SocialLoginPage.test.js.snap
new file mode 100644
index 00000000000..9fa9447b754
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/__snapshots__/SocialLoginPage.test.js.snap
@@ -0,0 +1,227 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Component matches snapshot 1`] = `
+
+
+
+
+
+
+
+ Patternfly will be updated to 2.13.5 at 00:00 AM, 23th Sep 2018 (UTC). This Update will last for 8-12 hours, please plan in advance for this outage.
+
+
+
+
+
+
+`;
diff --git a/packages/patternfly-react/src/components/LoginPage/assets/img/index.js b/packages/patternfly-react/src/components/LoginPage/assets/img/index.js
new file mode 100644
index 00000000000..5cd64a8fa3c
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/assets/img/index.js
@@ -0,0 +1,31 @@
+import dropbox from 'patternfly/dist/img/dropbox-logo.svg';
+import facebook from 'patternfly/dist/img/facebook-logo.svg';
+import fedora from 'patternfly/dist/img/fedora-logo.png';
+import git from 'patternfly/dist/img/git-logo.svg';
+import github from 'patternfly/dist/img/github-logo.svg';
+import google from 'patternfly/dist/img/google-logo.svg';
+import instagram from 'patternfly/dist/img/instagram-logo.png';
+import linkedin from 'patternfly/dist/img/linkedin-logo.svg';
+import openID from 'patternfly/dist/img/open-id-logo.svg';
+import skype from 'patternfly/dist/img/skype-logo.svg';
+import stackExchange from 'patternfly/dist/img/stack-exchange-logo.svg';
+import twitter from 'patternfly/dist/img/twitter-logo.svg';
+import background from 'patternfly/dist/img/bg-login.jpg';
+import brand from 'patternfly/dist/img/Logo_Horizontal_Reversed.svg';
+
+export default {
+ dropbox,
+ facebook,
+ fedora,
+ git,
+ github,
+ google,
+ instagram,
+ linkedin,
+ openID,
+ skype,
+ stackExchange,
+ twitter,
+ background,
+ brand
+};
diff --git a/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-bg.jpg b/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-bg.jpg
new file mode 100644
index 00000000000..3872ebb894d
Binary files /dev/null and b/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-bg.jpg differ
diff --git a/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-logo.svg b/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-logo.svg
new file mode 100644
index 00000000000..5d7d0dc30f6
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/assets/img/login-screen-logo.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/BasicLoginCardLayout.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/BasicLoginCardLayout.js
new file mode 100644
index 00000000000..900fbca020c
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/BasicLoginCardLayout.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Row, Col } from '../../../../index';
+
+const BasicLoginCardLayout = ({ children, layout, ...props }) => (
+
+
+ {children}
+
+
+);
+
+BasicLoginCardLayout.propTypes = {
+ children: PropTypes.node,
+ layout: PropTypes.object
+};
+
+BasicLoginCardLayout.defaultProps = {
+ children: null,
+ layout: null
+};
+
+export default BasicLoginCardLayout;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCard.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCard.js
new file mode 100644
index 00000000000..eeef31d08d6
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCard.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Card from '../../../Cards/Card';
+import Header from './LoginCardHeader';
+import LanguagePicker from './LoginLanguagePicker';
+import WithValidation from './LoginCardWithValidation';
+import Form from './LoginCardForm';
+import SignUp from './LoginCardSignUp';
+import Input from './LoginCardInput';
+import Settings from './LoginCardSettings';
+import FormError from './LoginFormError';
+import LoginCardForgotPassword from './LoginCardForgotPassword';
+import LoginCardRememberMe from './LoginCardRememberMe';
+import LoginCardSocialLink from './LoginCardSocialLink';
+import LoginCardSocialSection from './LoginCardSocialSection';
+import LoginCardSocialColumns from './LoginCardSocialColumns';
+import SocialLoginCard from './SocialLoginCard';
+import BasicLoginCardLayout from './BasicLoginCardLayout';
+
+const LoginCard = ({ children, ...props }) => (
+ {children}
+);
+
+LoginCard.propTypes = {
+ children: PropTypes.node
+};
+
+LoginCard.defaultProps = {
+ children: null
+};
+
+LoginCard.Header = Header;
+LoginCard.LanguagePicker = LanguagePicker;
+LoginCard.WithValidation = WithValidation;
+LoginCard.Form = Form;
+LoginCard.SignUp = SignUp;
+LoginCard.Input = Input;
+LoginCard.Settings = Settings;
+LoginCard.FormError = FormError;
+LoginCard.ForgotPassword = LoginCardForgotPassword;
+LoginCard.RememberMe = LoginCardRememberMe;
+LoginCard.Social = SocialLoginCard;
+LoginCard.SocialLink = LoginCardSocialLink;
+LoginCard.SocialSection = LoginCardSocialSection;
+LoginCard.SocialColumns = LoginCardSocialColumns;
+LoginCard.BasicLayout = BasicLoginCardLayout;
+
+export default LoginCard;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForgotPassword.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForgotPassword.js
new file mode 100644
index 00000000000..dad6a3af9e4
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForgotPassword.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { noop } from '../../../../common/helpers';
+
+const LoginCardForgotPassword = ({ href, onClick, label, ...props }) => (
+
+ {label}
+
+);
+
+LoginCardForgotPassword.propTypes = {
+ label: PropTypes.string,
+ href: PropTypes.string,
+ className: PropTypes.string,
+ onClick: PropTypes.func
+};
+
+LoginCardForgotPassword.defaultProps = {
+ label: 'Forgot password?',
+ href: '#',
+ className: 'forgot-password',
+ onClick: noop
+};
+
+export default LoginCardForgotPassword;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForm.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForm.js
new file mode 100644
index 00000000000..be1923e547e
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardForm.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginCardInput from './LoginCardInput';
+import LoginCardSettings from './LoginCardSettings';
+import LoginFormError from './LoginFormError';
+import { Button, Form } from '../../../../index';
+import { noop } from '../../../../common/helpers';
+
+const LoginCardForm = ({
+ usernameField,
+ passwordField,
+ additionalFields,
+ submitText,
+ disableSubmit,
+ onSubmit,
+ forgotPassword,
+ rememberMe,
+ submitError,
+ showError
+}) => (
+
+);
+
+LoginCardForm.propTypes = {
+ usernameField: PropTypes.shape({ ...LoginCardInput.propTypes }),
+ passwordField: PropTypes.shape({ ...LoginCardInput.propTypes }),
+ additionalFields: PropTypes.node,
+ submitText: PropTypes.string,
+ disableSubmit: PropTypes.bool,
+ onSubmit: PropTypes.func,
+ forgotPassword: PropTypes.object,
+ rememberMe: PropTypes.object,
+ submitError: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ showError: PropTypes.bool
+};
+
+LoginCardForm.defaultProps = {
+ usernameField: {
+ ...LoginCardInput,
+ id: 'card_email',
+ type: 'email',
+ placeholder: 'Email Address'
+ },
+ passwordField: {
+ ...LoginCardInput,
+ id: 'card_password',
+ type: 'password',
+ placeholder: 'Password',
+ minLength: 8
+ },
+ additionalFields: null,
+ submitText: null,
+ disableSubmit: false,
+ onSubmit: noop,
+ forgotPassword: {
+ label: null,
+ href: '#',
+ onClick: noop
+ },
+ rememberMe: { label: null },
+ submitError: null,
+ showError: false
+};
+
+export default LoginCardForm;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardHeader.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardHeader.js
new file mode 100644
index 00000000000..b1174f7df65
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardHeader.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const LoginCardHeader = ({ children, className, ...props }) => (
+
+);
+
+LoginCardHeader.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node
+};
+
+LoginCardHeader.defaultProps = {
+ className: '',
+ children: null
+};
+
+export default LoginCardHeader;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInput.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInput.js
new file mode 100644
index 00000000000..dc8b63402a2
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInput.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Fade } from 'react-bootstrap';
+import LoginCardInputWarning from './LoginCardInputWarning';
+import { FormControl, FormGroup, HelpBlock } from '../../../../index';
+import { noop } from '../../../../common/helpers';
+
+const LoginCardInput = ({
+ id,
+ type,
+ placeholder,
+ size,
+ error,
+ warning,
+ onChange,
+ onFocus,
+ onBlur,
+ onKeyPress,
+ showError,
+ showWarning,
+ className,
+ autoComplete
+}) => {
+ const helpBlock =
+ (showError && {error} ) ||
+ (showWarning && {warning} );
+
+ return (
+
+
+
+
+ {helpBlock}
+
+
+ );
+};
+
+LoginCardInput.propTypes = {
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ type: PropTypes.string,
+ placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ size: PropTypes.string,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ warning: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ onChange: PropTypes.func,
+ showWarning: PropTypes.bool,
+ onBlur: PropTypes.func,
+ onFocus: PropTypes.func,
+ onKeyPress: PropTypes.func,
+ showError: PropTypes.bool,
+ className: PropTypes.string,
+ autoComplete: PropTypes.string
+};
+
+LoginCardInput.defaultProps = {
+ id: Math.random().toString(),
+ type: 'text',
+ placeholder: 'Enter Text',
+ size: 'lg',
+ error: null,
+ warning: null,
+ onChange: noop,
+ showWarning: false,
+ onBlur: noop,
+ onFocus: noop,
+ onKeyPress: noop,
+ showError: false,
+ className: '',
+ autoComplete: ''
+};
+
+export default LoginCardInput;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInputWarning.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInputWarning.js
new file mode 100644
index 00000000000..8d8491c72fc
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardInputWarning.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Icon } from '../../../../index';
+
+const LoginCardInputWarning = ({ children, className, ...props }) =>
+ children && (
+
+
+ {` ${children}`}
+
+ );
+
+LoginCardInputWarning.propTypes = {
+ children: PropTypes.string
+};
+
+LoginCardInputWarning.defaultProps = {
+ children: null
+};
+
+export default LoginCardInputWarning;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardRememberMe.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardRememberMe.js
new file mode 100644
index 00000000000..96fba82db04
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardRememberMe.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { noop } from '../../../../common/helpers';
+
+const LoginCardRememberMe = ({ onClick, label, className, ...props }) => (
+
+ {label}
+
+);
+
+LoginCardRememberMe.propTypes = {
+ label: PropTypes.string,
+ className: PropTypes.string,
+ onClick: PropTypes.func
+};
+
+LoginCardRememberMe.defaultProps = {
+ label: 'Keep me logged in for 30 days',
+ className: '',
+ onClick: noop
+};
+
+export default LoginCardRememberMe;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSettings.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSettings.js
new file mode 100644
index 00000000000..5f000340fd8
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSettings.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import RememberMe from './LoginCardRememberMe';
+import { FormGroup } from '../../../../index';
+import ForgotPassword from './LoginCardForgotPassword';
+
+const LoginCardSettings = ({
+ rememberMe,
+ forgotPassword,
+ className,
+ ...props
+}) => (
+
+
+
+
+);
+
+LoginCardSettings.propTypes = {
+ className: PropTypes.string,
+ rememberMe: PropTypes.shape({ ...RememberMe.propTypes }),
+ forgotPassword: PropTypes.shape({ ...ForgotPassword.propTypes })
+};
+
+LoginCardSettings.defaultProps = {
+ className: '',
+ rememberMe: PropTypes.shape({ ...RememberMe.defaultProps }),
+ forgotPassword: PropTypes.shape({ ...ForgotPassword.defaultProps })
+};
+
+export default LoginCardSettings;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSignUp.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSignUp.js
new file mode 100644
index 00000000000..0fe87fff8b1
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSignUp.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Link from '../LoginPageComponents/LoginPageLink';
+
+const LoginCardSignUp = ({ label, link, className, ...props }) => (
+
+);
+
+LoginCardSignUp.propTypes = {
+ className: PropTypes.string,
+ label: PropTypes.string,
+ link: PropTypes.shape({ ...Link.propTypes })
+};
+
+LoginCardSignUp.defaultProps = {
+ className: '',
+ label: 'Need an account?',
+ link: {}
+};
+
+export default LoginCardSignUp;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialColumns.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialColumns.js
new file mode 100644
index 00000000000..51166a4d640
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialColumns.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Icon } from '../../../../index';
+import LoginCardSocialLink from './LoginCardSocialLink';
+
+class LoginCardSocialColumns extends React.Component {
+ state = {
+ expend: false,
+ width: window.innerWidth
+ };
+
+ componentDidMount() {
+ window.addEventListener('resize', this.updateWindowWidth);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.updateWindowWidth);
+ }
+
+ updateWindowWidth = () => {
+ this.setState({
+ width: window.innerWidth
+ });
+ };
+
+ getListItems = () => {
+ this.hiddenLinks = [];
+ return (
+ this.props.links &&
+ this.props.links.map((link, index) => {
+ if (index >= this.props.shownButtons) {
+ this.hiddenLinks.push(link);
+ return true;
+ }
+
+ return ;
+ })
+ );
+ };
+
+ getHiddenListItems = () =>
+ this.hiddenLinks &&
+ this.hiddenLinks.map((link, index) => (
+
+ ));
+
+ toggleExpend = () => {
+ this.setState({ expend: !this.state.expend });
+ };
+
+ render() {
+ const { links, shownButtons } = this.props;
+ if (!links) {
+ return null;
+ }
+ const { expend, width } = this.state;
+ const expendButton = width > 768 &&
+ links.length > shownButtons && (
+ this.toggleExpend(e)}
+ >
+ {expend ? 'Less' : 'More'}
+
+
+ );
+
+ const doubleColumn = links.length > 4 ? 'login-pf-social-double-col' : '';
+ const moreItems = expend || width < 768 ? this.getHiddenListItems() : null;
+ return (
+
+
+ {this.getListItems()}
+ {moreItems}
+
+ {expendButton}
+
+ );
+ }
+}
+
+LoginCardSocialColumns.propTypes = {
+ links: PropTypes.array,
+ shownButtons: PropTypes.number
+};
+
+LoginCardSocialColumns.defaultProps = {
+ links: [],
+ shownButtons: 8
+};
+
+export default LoginCardSocialColumns;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialLink.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialLink.js
new file mode 100644
index 00000000000..7ac3ae96bd4
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialLink.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Link from '../LoginPageComponents/LoginPageLink';
+
+const LoginCardSocialLink = ({ link }) =>
+ link && (
+
+
+
+ {link.text}
+
+
+ );
+
+LoginCardSocialLink.propTypes = {
+ link: PropTypes.shape({ ...Link.propTypes })
+};
+
+LoginCardSocialLink.defaultProps = {
+ link: { ...Link.defaultProps, onClick: e => e.preventDefault() }
+};
+
+export default LoginCardSocialLink;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialSection.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialSection.js
new file mode 100644
index 00000000000..12043969233
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardSocialSection.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const LoginCardSocialSection = ({ children, className, ...props }) => (
+
+);
+
+LoginCardSocialSection.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node
+};
+
+LoginCardSocialSection.defaultProps = {
+ className: '',
+ children: null
+};
+
+export default LoginCardSocialSection;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardWithValidation.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardWithValidation.js
new file mode 100644
index 00000000000..4c006f70796
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginCardWithValidation.js
@@ -0,0 +1,253 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginCardInput from './LoginCardInput';
+
+class LoginCardWithValidation extends React.Component {
+ state = {
+ usernameField: {
+ value: null,
+ errorType: null,
+ isFocused: false,
+ showError: false
+ },
+ passwordField: {
+ value: null,
+ errorType: null,
+ warningType: null,
+ isFocused: false,
+ showError: false
+ },
+ isCapsLock: false,
+ form: {
+ showError: false,
+ submitError: this.props.submitError
+ }
+ };
+
+ componentDidMount() {
+ window.addEventListener('keyup', this.toggleCapsLock);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('keyup', this.toggleCapsLock);
+ }
+
+ onInputChange = (e, inputType) => {
+ this.props[inputType].onChange && this.props[inputType].onChange(e);
+ this.setState({
+ [inputType]: {
+ ...this.state[inputType],
+ value: e.target.value,
+ showError: false
+ }
+ });
+ };
+
+ onInputFocus = (e, inputType) => {
+ this.props[inputType].onFocus && this.props[inputType].onFocus(e);
+ this.setState({
+ [inputType]: {
+ ...this.state[inputType],
+ isFocused: true,
+ showError: false
+ }
+ });
+ };
+
+ onInputBlur = (e, inputType) => {
+ this.props[inputType].onBlur && this.props[inputType].onBlur(e);
+ this.setState({
+ [inputType]: {
+ ...this.state[inputType],
+ isFocused: false,
+ showError: false
+ },
+ isCapsLock: false
+ });
+ };
+
+ onKeyPress = (e, inputType) => {
+ this.props[inputType].onMouseEnter && this.props[inputType].onMouseEnter(e);
+ this.handleCapsLock(e);
+ };
+
+ onSubmit = e => {
+ e.preventDefault();
+ if (this.isFormValid()) {
+ const { onSubmit, submitError } = this.props;
+ onSubmit && onSubmit(e);
+ submitError &&
+ this.setState({
+ form: {
+ ...this.state.form,
+ showError: true,
+ error: submitError
+ }
+ });
+ } else {
+ this.handleOnInputErrors();
+ }
+ };
+
+ getModifiedProps = () => {
+ const { usernameField, passwordField } = this.props;
+ const passwordFieldWarningType = this.state.isCapsLock
+ ? 'capsLock'
+ : this.state.passwordField.warningType;
+ return {
+ usernameField: {
+ ...usernameField,
+ onChange: e => this.onInputChange(e, 'usernameField'),
+ onFocus: e => this.onInputFocus(e, 'usernameField'),
+ onBlur: e => this.onInputBlur(e, 'usernameField'),
+ onKeyPress: e => this.onKeyPress(e, 'usernameField'),
+ error: usernameField.errors[this.state.usernameField.errorType],
+ showError: this.state.usernameField.showError
+ },
+ passwordField: {
+ ...passwordField,
+ onChange: e => this.onInputChange(e, 'passwordField'),
+ onFocus: e => this.onInputFocus(e, 'passwordField'),
+ onBlur: e => this.onInputBlur(e, 'passwordField'),
+ onKeyPress: e => this.onKeyPress(e, 'passwordField'),
+ warning: passwordField.warnings[passwordFieldWarningType],
+ showWarning:
+ this.state.passwordField.isFocused && this.state.isCapsLock,
+ error: passwordField.errors[this.state.passwordField.errorType],
+ showError: this.state.passwordField.showError
+ },
+ onSubmit: e => this.onSubmit(e),
+ showError: this.state.form.showError
+ };
+ };
+
+ handleOnInputErrors = () => {
+ const { usernameField, passwordField } = this.state;
+ if (usernameField.value) {
+ !this.isUserNameValid() && this.handleOnInvalidUsername();
+ } else {
+ this.handleOnEmptyInput('usernameField');
+ }
+
+ if (passwordField.value) {
+ this.isPasswordShort() && this.handleOnPasswordTooShort();
+ } else {
+ this.handleOnEmptyInput('passwordField');
+ }
+
+ this.hideSubmitError();
+ };
+
+ isFormValid = () =>
+ !!this.state.usernameField.value &&
+ !!this.state.passwordField.value &&
+ !this.isPasswordShort() &&
+ this.isUserNameValid();
+
+ isPasswordShort = () =>
+ this.state.passwordField.value.length < this.props.passwordField.minLength;
+
+ hideSubmitError = () => {
+ this.setState({
+ form: {
+ ...this.state.form,
+ showError: false
+ }
+ });
+ };
+
+ handleOnPasswordTooShort = () => {
+ this.setState({
+ passwordField: {
+ ...this.state.passwordField,
+ errorType: 'short',
+ showError: true
+ }
+ });
+ };
+
+ handleOnInvalidUsername = error => {
+ this.setState({
+ usernameField: {
+ ...this.state.usernameField,
+ errorType: 'invalid',
+ showError: true
+ }
+ });
+ };
+
+ handleOnEmptyInput = inputType => {
+ this.setState({
+ [inputType]: {
+ ...this.state[inputType],
+ errorType: 'empty',
+ showError: true
+ }
+ });
+ };
+
+ toggleCapsLock = e => {
+ if (!this.state.passwordField.value) {
+ return;
+ }
+ e.key === 'CapsLock' &&
+ this.setState({
+ isCapsLock: !this.state.isCapsLock
+ });
+ };
+
+ handleCapsLock = e => {
+ const keyCode = e.keyCode ? e.keyCode : e.which;
+ const shiftKey = e.shiftKey ? e.shiftKey : keyCode === 16;
+ const isCapsLock =
+ (keyCode >= 65 && keyCode <= 90 && !shiftKey) ||
+ (keyCode >= 97 && keyCode <= 122 && shiftKey);
+ this.setState({
+ isCapsLock
+ });
+ };
+
+ isUserNameValid = () => {
+ const mailAddress = this.state.usernameField.value;
+ const atPos = mailAddress.indexOf('@');
+ const dotPos = mailAddress.lastIndexOf('.');
+ return atPos > 1 && dotPos - atPos > 2 && atPos < dotPos;
+ };
+
+ render() {
+ const { validate, children } = this.props;
+ return validate
+ ? React.cloneElement(children, {
+ ...this.props,
+ ...this.getModifiedProps()
+ })
+ : React.cloneElement(children, { ...this.props });
+ }
+}
+
+LoginCardWithValidation.propTypes = {
+ validate: PropTypes.bool,
+ children: PropTypes.node.isRequired,
+ usernameField: PropTypes.shape({
+ ...LoginCardInput.propTypes,
+ errors: PropTypes.object
+ }),
+ passwordField: PropTypes.shape({
+ ...LoginCardInput.propTypes,
+ errors: PropTypes.object,
+ warnings: PropTypes.object,
+ minLength: PropTypes.number
+ }),
+ onSubmit: PropTypes.func,
+ submitError: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
+};
+
+LoginCardWithValidation.defaultProps = {
+ validate: true,
+ usernameField: { ...LoginCardInput.defaultProps.usernameField },
+ passwordField: { ...LoginCardInput.defaultProps.passwordField },
+ onSubmit: e => e.target.submit(),
+ submitError: null
+};
+
+export default LoginCardWithValidation;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginFormError.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginFormError.js
new file mode 100644
index 00000000000..b15e8c1718b
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginFormError.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Collapse from 'react-collapse';
+
+const LoginFormError = ({ children, show, className, ...props }) => (
+
+
+ {children}
+
+
+);
+
+LoginFormError.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ show: PropTypes.bool
+};
+
+LoginFormError.defaultProps = {
+ className: '',
+ children: null,
+ show: false
+};
+
+export default LoginFormError;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginLanguagePicker.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginLanguagePicker.js
new file mode 100644
index 00000000000..42b838bf99a
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/LoginLanguagePicker.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DropdownButton, MenuItem } from '../../../../index';
+import { noop } from '../../../../common/helpers';
+
+class LoginLanguagePicker extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ title:
+ (this.props.selectedLanguage && this.props.selectedLanguage.text) ||
+ (this.props.availableLanguages && this.props.availableLanguages[0].text)
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { selectedLanguage, availableLanguages } = nextProps;
+ const title =
+ (selectedLanguage && selectedLanguage.text) ||
+ (availableLanguages && availableLanguages[0].text);
+
+ this.setState({ title });
+ }
+
+ handleClick = e => {
+ this.props.onLanguageChange && this.props.onLanguageChange(e);
+ this.setState({ title: e.target.text });
+ };
+
+ render() {
+ const { availableLanguages } = this.props;
+ const menuItems = availableLanguages.map((language, index) => (
+ this.handleClick(e)}
+ >
+ {language.text}
+
+ ));
+
+ return (
+
+
+ {menuItems}
+
+
+ );
+ }
+}
+
+LoginLanguagePicker.propTypes = {
+ availableLanguages: PropTypes.array,
+ selectedLanguage: PropTypes.object,
+ onLanguageChange: PropTypes.func,
+ className: PropTypes.string,
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+};
+
+LoginLanguagePicker.defaultProps = {
+ selectedLanguage: { value: 'en', text: 'English' },
+ availableLanguages: [{ value: 'en', text: 'English' }],
+ onLanguageChange: noop,
+ className: '',
+ id: 'language-picker'
+};
+
+export default LoginLanguagePicker;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/SocialLoginCard.js b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/SocialLoginCard.js
new file mode 100644
index 00000000000..05f0f282ef5
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginCardComponents/SocialLoginCard.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginCard from './LoginCard';
+
+const SocialLoginCard = ({ children, className, ...props }) => (
+
+ {children}
+
+);
+
+SocialLoginCard.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string
+};
+
+SocialLoginCard.defaultProps = {
+ children: null,
+ className: ''
+};
+
+export default SocialLoginCard;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/BasicLoginPageLayout.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/BasicLoginPageLayout.js
new file mode 100644
index 00000000000..b1da9139496
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/BasicLoginPageLayout.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Grid, Row, Col } from '../../../../index';
+
+const BasicLoginPageLayout = ({ children, layout, ...props }) => (
+
+
+
+ {children}
+
+
+
+);
+
+BasicLoginPageLayout.propTypes = {
+ children: PropTypes.node,
+ layout: PropTypes.object
+};
+
+BasicLoginPageLayout.defaultProps = {
+ children: null,
+ layout: null
+};
+
+export default BasicLoginPageLayout;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginFooterLinks.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginFooterLinks.js
new file mode 100644
index 00000000000..3ccbd227b7d
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginFooterLinks.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Link from '../LoginPageComponents/LoginPageLink';
+
+const LoginFooterLinks = ({ links }) => {
+ const listItems = links.map((link, index) => (
+
+ link.onClick(e)}
+ >
+ {link.children}
+
+
+ ));
+
+ return (
+
+ );
+};
+
+LoginFooterLinks.propTypes = {
+ links: PropTypes.array
+};
+
+LoginFooterLinks.defaultProps = {
+ links: [
+ { children: 'Terms of Use', href: '#' },
+ { children: 'Help', href: '#' },
+ { children: 'Privacy Policy', href: '#' }
+ ]
+};
+
+export default LoginFooterLinks;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageAlert.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageAlert.js
new file mode 100644
index 00000000000..2e4ac1f7b00
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageAlert.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert } from '../../../Alert';
+import { noop } from '../../../../common/helpers';
+
+class LoginPageAlert extends React.Component {
+ state = { show: this.props.show };
+
+ closeAlert = e => {
+ this.props.onDismiss(e);
+ if (!this.state.show) {
+ return;
+ }
+ this.setState({ show: false });
+ };
+
+ render() {
+ const { type, message } = this.props;
+ return this.state.show ? (
+ this.closeAlert(e)}>
+ {message}
+
+ ) : null;
+ }
+}
+
+LoginPageAlert.propTypes = {
+ type: PropTypes.string,
+ onDismiss: PropTypes.func,
+ message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ show: PropTypes.bool
+};
+
+LoginPageAlert.defaultProps = {
+ type: 'warning',
+ onDismiss: noop,
+ message: null,
+ show: false
+};
+
+export default LoginPageAlert;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageContainer.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageContainer.js
new file mode 100644
index 00000000000..421257b3fa3
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageContainer.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const LoginPageContainer = ({ backgroundUrl, className, children }) => {
+ const style = {
+ backgroundImage: `url(${backgroundUrl})`
+ };
+
+ return (
+
+ );
+};
+
+LoginPageContainer.propTypes = {
+ children: PropTypes.node,
+ backgroundUrl: PropTypes.string,
+ className: PropTypes.string
+};
+
+LoginPageContainer.defaultProps = {
+ children: null,
+ backgroundUrl: null,
+ className: ''
+};
+
+export default LoginPageContainer;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageFooter.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageFooter.js
new file mode 100644
index 00000000000..37584467e2e
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageFooter.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginFooterLinks from './LoginFooterLinks';
+
+const LoginPageFooter = ({ links }) => (
+
+);
+
+LoginPageFooter.propTypes = {
+ links: PropTypes.array
+};
+
+LoginPageFooter.defaultProps = {
+ links: LoginFooterLinks.defaultProps.links
+};
+
+export default LoginPageFooter;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageHeader.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageHeader.js
new file mode 100644
index 00000000000..510537055bc
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageHeader.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const LoginPageHeader = ({ logoSrc, logoTitle, caption }) => (
+
+
+ {caption}
+
+);
+
+LoginPageHeader.propTypes = {
+ logoSrc: PropTypes.string,
+ logoTitle: PropTypes.string,
+ caption: PropTypes.string
+};
+
+LoginPageHeader.defaultProps = {
+ logoSrc: null,
+ logoTitle: null,
+ caption: null
+};
+
+export default LoginPageHeader;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageLink.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageLink.js
new file mode 100644
index 00000000000..f6cf05020e9
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageLink.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { noop } from '../../../../common/helpers';
+
+const LoginPageLink = ({ onClick, href, children, ...props }) => (
+
+ {children}
+
+);
+
+LoginPageLink.propTypes = {
+ children: PropTypes.node,
+ href: PropTypes.string,
+ onClick: PropTypes.func
+};
+
+LoginPageLink.defaultProps = {
+ children: null,
+ href: '#',
+ onClick: noop
+};
+
+export default LoginPageLink;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageWithTranslation.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageWithTranslation.js
new file mode 100644
index 00000000000..60c8a653fbc
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/LoginPageWithTranslation.js
@@ -0,0 +1,167 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginCard from '../LoginCardComponents/LoginCard';
+import LoginCardHeader from '../LoginCardComponents/LoginCardHeader';
+import LoginPageContainer from './LoginPageContainer';
+
+class LoginPageWithTranslation extends React.Component {
+ constructor(props) {
+ super(props);
+ const { selectedLanguage, availableLanguages } = props.card;
+ this.state = {
+ language:
+ (selectedLanguage && selectedLanguage.value) ||
+ (availableLanguages &&
+ availableLanguages[0] &&
+ availableLanguages[0].value),
+ passwordValue: null,
+ usernameValue: null,
+ translatedProps: {}
+ };
+ }
+
+ onLanguageChange = e => {
+ const newLanguage = e.target.attributes.value.value;
+ if (!newLanguage || this.state.language === newLanguage) {
+ return;
+ }
+ this.switchToLanguage(newLanguage);
+ };
+
+ onPasswordChange = e => {
+ const { card } = this.props;
+ card.form.passwordField.onChange && card.form.passwordField.onChange(e);
+ this.setState({ passwordValue: e.target.value });
+ };
+
+ onUsernameChange = e => {
+ const { card } = this.props;
+ card.form.usernameField.onChange && card.form.usernameField.onChange(e);
+ this.setState({ usernameValue: e.target.value });
+ };
+
+ getDefaultPropsToPass = () => {
+ const { card } = this.props;
+ return {
+ card: {
+ ...card,
+ onLanguageChange: e => this.onLanguageChange(e),
+ form: {
+ ...card.form,
+ usernameField: {
+ ...card.form.usernameField,
+ value: this.state.usernameValue,
+ onChange: e => this.onUsernameChange(e)
+ },
+ passwordField: {
+ ...card.form.passwordField,
+ value: this.state.passwordValue,
+ onChange: e => this.onPasswordChange(e)
+ }
+ }
+ }
+ };
+ };
+
+ switchToLanguage = language => {
+ const { container, card, header } = this.props;
+ const languageFile = container.translations[language];
+ const translatedProps = {
+ ...this.props,
+ container: {
+ ...container,
+ alert: {
+ ...header.alert,
+ message: languageFile.header.alert
+ }
+ },
+ header: {
+ ...header,
+ logoTitle: languageFile.header.logo,
+ caption: languageFile.header.caption
+ },
+ footerLinks: languageFile.footerLinks,
+ card: {
+ ...card,
+ title: languageFile.card.header.title,
+ selectedLanguage: languageFile.card.header.selectedLanguage,
+ availableLanguages: languageFile.card.header.availableLanguages,
+ onLanguageChange: e => this.onLanguageChange(e),
+ signUp: {
+ label: languageFile.card.signUp.label,
+ link: {
+ ...card.signUp.link,
+ children: languageFile.card.signUp.link.label
+ }
+ },
+ form: {
+ ...card.form,
+ submitError: languageFile.card.form.error,
+ usernameField: {
+ ...card.form.usernameField,
+ onChange: e => this.onUsernameChange(e),
+ value: this.state.usernameValue,
+ placeholder: languageFile.card.usernameField.placeholder,
+ errors: languageFile.card.usernameField.errors
+ },
+ passwordField: {
+ ...card.form.passwordField,
+ onChange: e => this.onPasswordChange(e),
+ value: this.state.passwordValue,
+ placeholder: languageFile.card.passwordField.placeholder,
+ errors: languageFile.card.passwordField.errors,
+ warnings: languageFile.card.passwordField.warnings
+ },
+ submitText: languageFile.card.form.submitText,
+ rememberMe: {
+ ...card.rememberMe,
+ label: languageFile.card.rememberMe
+ },
+ forgotPassword: {
+ ...card.forgotPassword,
+ label: languageFile.card.forgotPassword
+ }
+ }
+ }
+ };
+
+ this.setState({ translatedProps, language });
+ };
+
+ render() {
+ const newProps = {
+ ...this.props,
+ ...this.getDefaultPropsToPass(),
+ ...this.state.translatedProps
+ };
+
+ return React.cloneElement(this.props.children, newProps);
+ }
+}
+
+LoginPageWithTranslation.propTypes = {
+ card: PropTypes.shape({
+ ...LoginCard.LanguagePicker.propTypes,
+ ...LoginCard.Form.propTypes,
+ ...LoginCard.SignUp.propTypes,
+ ...LoginCard.RememberMe.propTypes,
+ ...LoginCard.ForgotPassword.propTypes
+ }),
+ header: PropTypes.shape({ ...LoginCardHeader.propTypes }),
+ container: PropTypes.shape({ ...LoginPageContainer.propTypes }),
+ children: PropTypes.node.isRequired
+};
+
+LoginPageWithTranslation.defaultProps = {
+ card: {
+ ...LoginCard.LanguagePicker.defaultProps,
+ ...LoginCard.Form.defaultProps,
+ ...LoginCard.SignUp.defaultProps,
+ ...LoginCard.RememberMe.defaultProps,
+ ...LoginCard.ForgotPassword.defaultProps
+ },
+ header: { ...LoginCardHeader.defaultProps },
+ container: { ...LoginPageContainer.defaultProps }
+};
+
+export default LoginPageWithTranslation;
diff --git a/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/SocialLoginPageContainer.js b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/SocialLoginPageContainer.js
new file mode 100644
index 00000000000..be2aac8f233
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/components/LoginPageComponents/SocialLoginPageContainer.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import LoginPageContainer from './LoginPageContainer';
+
+const SocialLoginPageContainer = ({ children, className, ...props }) => (
+
+ {children}
+
+);
+
+SocialLoginPageContainer.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string
+};
+
+SocialLoginPageContainer.defaultProps = {
+ children: null,
+ className: ''
+};
+
+export default SocialLoginPageContainer;
diff --git a/packages/patternfly-react/src/components/LoginPage/index.js b/packages/patternfly-react/src/components/LoginPage/index.js
new file mode 100644
index 00000000000..f8f79f763ff
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/index.js
@@ -0,0 +1,59 @@
+import LoginPage from './LoginPage';
+import LoginPageContainer from './components/LoginPageComponents/LoginPageContainer';
+import LoginPageHeader from './components/LoginPageComponents/LoginPageHeader';
+import LoginPageFooter from './components/LoginPageComponents/LoginPageFooter';
+import LoginFooterLinks from './components/LoginPageComponents/LoginFooterLinks';
+import LoginCard from './components/LoginCardComponents/LoginCard';
+import LoginPageWithTranslation from './components/LoginPageComponents/LoginPageWithTranslation';
+import LoginPageAlert from './components/LoginPageComponents/LoginPageAlert';
+import LoginPageLink from './components/LoginPageComponents/LoginPageLink';
+import SocialLoginPage from './SocialLoginPage';
+import SocialLoginPageContainer from './components/LoginPageComponents/SocialLoginPageContainer';
+import BasicLoginPageLayout from './components/LoginPageComponents/BasicLoginPageLayout';
+import LoginCardHeader from './components/LoginCardComponents/LoginCardHeader';
+import LoginLanguagePicker from './components/LoginCardComponents/LoginLanguagePicker';
+import LoginCardWithValidation from './components/LoginCardComponents/LoginCardWithValidation';
+import LoginCardForm from './components/LoginCardComponents/LoginCardForm';
+import LoginCardSignUp from './components/LoginCardComponents/LoginCardSignUp';
+import LoginCardInput from './components/LoginCardComponents/LoginCardInput';
+import LoginCardInputWarning from './components/LoginCardComponents/LoginCardInputWarning';
+import LoginCardSettings from './components/LoginCardComponents/LoginCardSettings';
+import LoginFormError from './components/LoginCardComponents/LoginFormError';
+import LoginCardForgotPassword from './components/LoginCardComponents/LoginCardForgotPassword';
+import LoginCardRememberMe from './components/LoginCardComponents/LoginCardRememberMe';
+import LoginCardSocialSection from './components/LoginCardComponents/LoginCardSocialSection';
+import LoginCardSocialLink from './components/LoginCardComponents/LoginCardSocialLink';
+import LoginCardSocialColumns from './components/LoginCardComponents/LoginCardSocialColumns';
+import SocialLoginCard from './components/LoginCardComponents/SocialLoginCard';
+import BasicLoginCardLayout from './components/LoginCardComponents/BasicLoginCardLayout';
+
+export {
+ LoginPage,
+ LoginPageContainer,
+ LoginPageHeader,
+ LoginPageFooter,
+ LoginFooterLinks,
+ LoginPageLink,
+ LoginCard,
+ LoginPageWithTranslation,
+ LoginPageAlert,
+ SocialLoginPage,
+ SocialLoginPageContainer,
+ BasicLoginPageLayout,
+ LoginCardHeader,
+ LoginLanguagePicker,
+ LoginCardWithValidation,
+ LoginCardForm,
+ LoginCardSignUp,
+ LoginCardInput,
+ LoginCardInputWarning,
+ LoginCardSettings,
+ LoginFormError,
+ LoginCardForgotPassword,
+ LoginCardRememberMe,
+ LoginCardSocialLink,
+ LoginCardSocialSection,
+ LoginCardSocialColumns,
+ SocialLoginCard,
+ BasicLoginCardLayout
+};
diff --git a/packages/patternfly-react/src/components/LoginPage/mocks/messages.en.js b/packages/patternfly-react/src/components/LoginPage/mocks/messages.en.js
new file mode 100644
index 00000000000..afeadb84fbe
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/mocks/messages.en.js
@@ -0,0 +1,75 @@
+const pageHeader = {
+ alert:
+ 'Patternfly will be updated to 2.13.5 at 00:00 AM, 23th Sep 2018 (UTC). This Update will last for 8-12 hours, please plan in advance for this outage.',
+ logo: 'Patternfly',
+ caption: `This is placeholder text, only. Use this area to place any information or
+ introductory message about your application that may be relevant for
+ users.`
+};
+
+const footerLinks = [
+ { children: 'Terms of Use', href: '#' },
+ { children: 'Help', href: '#' },
+ { children: 'Privacy Policy', href: '#' }
+];
+
+const cardHeader = {
+ title: 'Log In to Your Account',
+ selectedLanguage: { value: 'en', text: 'English' },
+ availableLanguages: [
+ { value: 'en', text: 'English' },
+ { value: 'fr', text: 'French' }
+ ]
+};
+
+const signUp = {
+ label: 'Need an account?',
+ link: {
+ label: 'Sign up'
+ }
+};
+
+const rememberMe = 'Keep me logged in for 30 days';
+
+const forgotPassword = 'Forgot password?';
+
+const form = {
+ error:
+ 'Your account has been blocked. Contact your administrator to unblock it.',
+ submitText: 'Log In'
+};
+
+const passwordField = {
+ placeholder: 'Password',
+ errors: {
+ empty: 'Please enter your password.',
+ short: 'Password too short, minimum length is 8 characters'
+ },
+ warnings: {
+ capsLock: 'Caps lock is currently on.'
+ }
+};
+
+const usernameField = {
+ placeholder: 'Email address',
+ errors: {
+ empty: 'Please enter your email.',
+ invalid: 'Your email is invalid'
+ }
+};
+
+const card = {
+ header: cardHeader,
+ form,
+ passwordField,
+ usernameField,
+ rememberMe,
+ forgotPassword,
+ signUp
+};
+
+export default {
+ header: pageHeader,
+ footerLinks,
+ card
+};
diff --git a/packages/patternfly-react/src/components/LoginPage/mocks/messages.fr.js b/packages/patternfly-react/src/components/LoginPage/mocks/messages.fr.js
new file mode 100644
index 00000000000..520e1d74c72
--- /dev/null
+++ b/packages/patternfly-react/src/components/LoginPage/mocks/messages.fr.js
@@ -0,0 +1,72 @@
+const pageHeader = {
+ alert: `Patternfly sera mis à jour à 2.13.5 à 00:00, 23 Sep 2018 (UTC). Cette mise à jour durera de 8 à 12 heures, veuillez planifier à l'avance pour cette panne.`,
+ logo: 'Patternfly',
+ caption: `Utilisez cette zone pour placer des informations ou un message d'introduction sur votre application qui peut être pertinent pour utilisateurs.`
+};
+
+const footerLinks = [
+ { children: "Conditions d'utilisation", href: '#' },
+ { children: 'Aidez-moi', href: '#' },
+ { children: 'Politique de confidentialité', href: '#' }
+];
+
+const cardHeader = {
+ title: 'Connectez-vous à votre compte',
+ selectedLanguage: { value: 'fr', text: 'Français' },
+ availableLanguages: [
+ { value: 'en', text: 'Anglais' },
+ { value: 'fr', text: 'Français' }
+ ]
+};
+
+const signUp = {
+ label: "Besoin d'un compte?",
+ link: {
+ label: "S'inscrire"
+ }
+};
+
+const rememberMe = 'Restez connecté pendant 30 jours';
+
+const forgotPassword = 'mot de passe oublié?';
+
+const form = {
+ error:
+ 'Votre compte a été bloqué Contactez votre administrateur pour le débloquer.',
+ submitText: "S'identifier"
+};
+
+const passwordField = {
+ placeholder: 'Mot de passe',
+ errors: {
+ empty: 'Veuillez entrer votre mot de passe.',
+ short: 'Mot de passe trop court, la longueur minimale est de 8 caractères'
+ },
+ warnings: {
+ capsLock: 'Le verrouillage des majuscules est actuellement activé.'
+ }
+};
+
+const usernameField = {
+ placeholder: 'Adresse e-mail',
+ errors: {
+ empty: "S'il vous plaît entrer votre email.",
+ invalid: 'Votre email est invalide'
+ }
+};
+
+const card = {
+ header: cardHeader,
+ form,
+ passwordField,
+ usernameField,
+ rememberMe,
+ forgotPassword,
+ signUp
+};
+
+export default {
+ header: pageHeader,
+ footerLinks,
+ card
+};
diff --git a/packages/patternfly-react/src/index.js b/packages/patternfly-react/src/index.js
index d6aab104382..c78f02935f3 100644
--- a/packages/patternfly-react/src/index.js
+++ b/packages/patternfly-react/src/index.js
@@ -19,6 +19,7 @@ export * from './components/Chart';
export * from './components/Label';
export * from './components/ListGroup';
export * from './components/ListView';
+export * from './components/LoginPage';
export * from './components/Masthead';
export * from './components/MenuItem';
export * from './components/MessageDialog';
diff --git a/yarn.lock b/yarn.lock
index 828d87f7379..2c60ec89430 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11324,6 +11324,10 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -12299,7 +12303,7 @@ radium@^0.19.0:
inline-style-prefixer "^2.0.5"
prop-types "^15.5.8"
-raf@^3.4.0:
+raf@^3.1.0, raf@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
dependencies:
@@ -12437,6 +12441,12 @@ react-click-outside@^3.0.1:
dependencies:
hoist-non-react-statics "^2.1.1"
+react-collapse@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/react-collapse/-/react-collapse-4.0.3.tgz#b96de959ed0092a43534630b599a4753dd76d543"
+ dependencies:
+ prop-types "^15.5.8"
+
react-color@^2.14.0:
version "2.14.1"
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.14.1.tgz#db8ad4f45d81e74896fc2e1c99508927c6d084e0"
@@ -12630,6 +12640,14 @@ react-modal@^3.3.2:
react-lifecycles-compat "^3.0.0"
warning "^3.0.0"
+react-motion@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
+ dependencies:
+ performance-now "^0.2.0"
+ prop-types "^15.5.8"
+ raf "^3.1.0"
+
react-onclickoutside@^6.1.1, react-onclickoutside@^6.5.0:
version "6.7.1"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93"