Skip to content

Commit

Permalink
Merge pull request #297 from kiwicom/new/modal-portal-component
Browse files Browse the repository at this point in the history
NEW: Modal and Portal component
  • Loading branch information
tomashapl committed Sep 14, 2018
2 parents 97d847a + ca7cc01 commit a0c20a4
Show file tree
Hide file tree
Showing 33 changed files with 4,495 additions and 4 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"env": {
"browser": true,
"jest": true
},
"root": true,
Expand Down
4 changes: 3 additions & 1 deletion src/ButtonLink/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ type Size = "small" | "normal" | "large";
export type Props = {|
+children?: React$Node,
+component?: string | React$Node,
+onClick?: (e: SyntheticEvent<HTMLButtonElement>) => void | Promise<any>,
+onClick?: (
e: SyntheticEvent<HTMLButtonElement> | SyntheticKeyboardEvent<HTMLDivElement>,
) => void | Promise<any>,
+disabled?: boolean,
+block?: boolean,
+external?: boolean,
Expand Down
20 changes: 20 additions & 0 deletions src/ClickOutside/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ClickOutside
To implement ClickOutside component into your project you'll need to add the import:
```jsx
import ClickOutside from "@kiwicom/orbit-components/lib/ClickOutside";
```
After adding import into your project you can use it simply like:
```jsx
<ClickOutside onClickOutside={...} >
<div>
Content
</div>
</ClickOutside>
```
## Props
Table below contains all types of the props available in the ClickOutside component.

| Name | Type | Default | Description |
| :-------------- | :------------------------------------------ | :-------------- | :------------------------------- |
| children | `React.Node` | | The content of the ClickOutside to render.
| onClickOutside | `(ev: MouseEvent) => void \| Promise<any>` | | Function for handling onClickOutside event.
75 changes: 75 additions & 0 deletions src/ClickOutside/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// @flow
import * as React from "react";
import { shallow, mount } from "enzyme";

import ClickOutside from "../index";

describe("ClickOutside mount", () => {
// $FlowExpected
document.addEventListener = jest.fn();

const component = mount(<ClickOutside onClickOutside={jest.fn()}>Lorem ipsum</ClickOutside>);
const instance = component.instance();

it("should mount", () => {
expect(document.addEventListener).toBeCalledWith("click", instance.handleClickOutside, true);
});
it("should unmount", () => {
// $FlowExpected
document.removeEventListener = jest.fn();
component.unmount();
expect(document.removeEventListener).toBeCalledWith("click", instance.handleClickOutside, true);
});
});

describe("ClickOutside shallow", () => {
it("handler", () => {
const onClickOutside = jest.fn();
const wrapper = shallow(
<ClickOutside onClickOutside={onClickOutside}>Lorem ipsum</ClickOutside>,
);

const instance = wrapper.instance();

instance.node = document.createElement("div");
const node = document.createElement("div");

const ev = { target: node };
instance.handleClickOutside(ev);

expect(onClickOutside).toBeCalledWith(ev);
});

it("handler - no node", () => {
const onClickOutside = jest.fn();
const wrapper = shallow(
<ClickOutside onClickOutside={onClickOutside}>Lorem ipsum</ClickOutside>,
);

const instance = wrapper.instance();
const node = document.createElement("div");

const ev = { target: node };
instance.handleClickOutside(ev);

expect(onClickOutside).not.toBeCalled();
});

it("handler - click inside", () => {
const onClickOutside = jest.fn();
const wrapper = shallow(
<ClickOutside onClickOutside={onClickOutside}>Lorem ipsum</ClickOutside>,
);

const instance = wrapper.instance();

instance.node = document.createElement("div");
const node = document.createElement("div");
instance.node.appendChild(node);

const ev = { target: node };
instance.handleClickOutside(ev);

expect(onClickOutside).not.toBeCalled();
});
});
51 changes: 51 additions & 0 deletions src/ClickOutside/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @flow
import * as React from "react";
import styled from "styled-components";

import type { Props } from "./index";

const Inner = styled.div`
width: 100%;
height: 100%;
`;

class ClickOutside extends React.PureComponent<Props> {
componentDidMount() {
document.addEventListener("click", this.handleClickOutside, true);
}

componentWillUnmount() {
document.removeEventListener("click", this.handleClickOutside, true);
}

handleClickOutside = (ev: MouseEvent) => {
const { onClickOutside } = this.props;

if (
onClickOutside &&
this.node &&
ev.target instanceof Node &&
!this.node.contains(ev.target)
) {
onClickOutside(ev);
}
};

node: ?HTMLDivElement;

render() {
const { children } = this.props;

return (
<Inner
innerRef={node => {
this.node = node;
}}
>
{children}
</Inner>
);
}
}

export default ClickOutside;
8 changes: 8 additions & 0 deletions src/ClickOutside/index.js.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @flow

export type Props = {|
+onClickOutside?: (ev: MouseEvent) => void | Promise<any>,
+children: React$Node | React$Node[],
|};

declare export default React$ComponentType<Props>;
2 changes: 1 addition & 1 deletion src/Heading/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ELEMENT_OPTIONS, TYPE_OPTIONS } from "./consts";

import type { Props } from "./index";

const StyledHeading = styled(({ element: Component, className, children }) => (
export const StyledHeading = styled(({ element: Component, className, children }) => (
<Component className={className}>{children}</Component>
))`
font-family: ${({ theme }) => theme.orbit.fontFamily};
Expand Down
4 changes: 4 additions & 0 deletions src/Heading/index.js.flow
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// @flow
import { StyledComponentClass } from "styled-components";

import defaultTokens from "../defaultTokens";

type Element = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
Expand All @@ -13,3 +15,5 @@ export type Props = {|
|};

declare export default React$ComponentType<Props>;

declare export var StyledHeading: StyledComponentClass<>;
2 changes: 1 addition & 1 deletion src/Illustration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SIZE_OPTIONS, baseURL } from "./consts";

import type { Props } from "./index";

const StyledImage = styled.img`
export const StyledImage = styled.img`
height: ${({ tokens, size }) => tokens.height[size]};
width: auto;
background-color: ${({ theme }) => theme.orbit.backgroundIllustration};
Expand Down
2 changes: 1 addition & 1 deletion src/List/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type Size = "small" | "normal" | "large";
type Type = "primary" | "secondary";

export type Props = {|
+children: Array<React$Element<typeof ListItem>>,
+children: Array<React$Element<typeof ListItem>> | React$Element<typeof ListItem>,
+size?: Size,
+type?: Type,
+theme?: typeof defaultTokens,
Expand Down
125 changes: 125 additions & 0 deletions src/Modal/Modal.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// @flow
import * as React from "react";
import { storiesOf, setAddon } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import styles from "@sambego/storybook-styles";
import chaptersAddon from "react-storybook-addon-chapters";
import { withKnobs, text, boolean, select, array } from "@storybook/addon-knobs/react";

import Button from "../Button";
import SIZES from "./consts";
import ModalHeader from "./ModalHeader";
import ModalSection from "./ModalSection";
import Illustration from "../Illustration";
import Text from "../Text";
import { NAMES } from "../Illustration/consts";
import ModalFooter from "./ModalFooter";
import ChevronLeft from "../icons/ChevronLeft";

import Modal from "./index";

setAddon(chaptersAddon);

storiesOf("Modal", module)
.addDecorator(withKnobs)
.addDecorator(
styles({
padding: "20px",
}),
)
.addWithChapters("Sizes", () => {
const size = select("Size", Object.values(SIZES), SIZES.NORMAL);
const title = text("Title", "Orbit design system");
const description = text("Title", "I'm lovely description");

const onClose = action("onClose");
const content = text(
"Content",
"You can try all possible configurations of this component. However, check Orbit.Kiwi for more detailed design guidelines.",
);
return {
info:
"You can try all possible configurations of this component. However, check Orbit.Kiwi for more detailed design guidelines.",
chapters: [
{
sections: [
{
sectionFn: () => (
<Modal onClose={onClose} size={size}>
<ModalHeader title={title}>{description}</ModalHeader>
<ModalSection>
<Text>{content}</Text>
</ModalSection>
<ModalSection>
<Text>{content}</Text>
</ModalSection>
<ModalSection>
<Text>{content}</Text>
</ModalSection>
<ModalSection>
<Text>{content}</Text>
</ModalSection>
</Modal>
),
},
],
},
],
};
})
.addWithChapters("Full preview", () => {
const size = select("Size", Object.values(SIZES), SIZES.NORMAL);
const title = text("Title", "Orbit design system");
const description = text("Description", "Lorem ispum dolor sit amet");
const illustration = select(
"Illustration",
[undefined, ...Object.values(NAMES)],
"Accommodation",
);
const closable = boolean("closable", true);
const onClose = action("onClose");
const fixed = boolean("fixedFooter", false);
const suppressed = boolean("suppressed", false);
const content = text(
"Text",
"You can try all possible configurations of this component. However, check Orbit.Kiwi for more detailed design guidelines.",
);
const flex = array("Flex", ["0 0 auto", "1 1 100%"]);
return {
info:
"You can try all possible configurations of this component. However, check Orbit.Kiwi for more detailed design guidelines.",
chapters: [
{
sections: [
{
sectionFn: () => (
<Modal onClose={onClose} size={size} closable={closable} fixedFooter={fixed}>
<ModalHeader
title={title}
illustration={illustration && <Illustration name={illustration} size="small" />}
description={description}
suppressed={suppressed}
/>
<ModalSection suppressed={suppressed}>
<Text>{content}</Text>
</ModalSection>
<ModalSection suppressed={suppressed}>
<Text>{content}</Text>
</ModalSection>
<ModalSection suppressed={suppressed}>
<Text>{content}</Text>
</ModalSection>
<ModalFooter flex={flex}>
<Button type="secondary" iconLeft={<ChevronLeft />}>
Back
</Button>
<Button block>Continue to Payment</Button>
</ModalFooter>
</Modal>
),
},
],
},
],
};
});
Loading

0 comments on commit a0c20a4

Please sign in to comment.