diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e75f603 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "parser": "typescript", + "semi": true, + "printWidth": 96, + "useTabs": true, + "tabWidth": 4, + "singleQuote": true, + "trailingComma": "es5", + "jsxBracketSameLine": false +} \ No newline at end of file diff --git a/package.json b/package.json index 045b616..08a8497 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react": "^16.x.x", "react-create-ref": "^1.0.1", "react-dom": "^16.x.x", + "react-onclickoutside": "^6.9.0", "react-scripts": "3.4.0", "react-slick": "^0.25.2", "reset-css": "^5.0.1", @@ -50,6 +51,7 @@ "@types/node": "^12.0.0", "@types/react": "^16.x.x", "@types/react-dom": "^16.x.x", + "@types/react-onclickoutside": "^6.7.3", "@types/react-slick": "^0.23.4", "@types/sinon": "^7.5.1", "chai": "^4.2.0", @@ -86,7 +88,7 @@ "compilerOptions": { "target": "ES5", "module": "CommonJS", - "inlineSourceMap": true, + "inlineSourceMap": true, "removeComments": false, "experimentalDecorators": true } diff --git a/src/surfaces/abs-container/abs-container.module.scss b/src/surfaces/abs-container/abs-container.module.scss new file mode 100644 index 0000000..4b74c4f --- /dev/null +++ b/src/surfaces/abs-container/abs-container.module.scss @@ -0,0 +1,12 @@ +.container { + position: relative; +} + +.containee { + position: absolute; + + display: none; + [data-open='true'] & { + display: initial; + } +} diff --git a/src/surfaces/abs-container/containee/containee.tsx b/src/surfaces/abs-container/containee/containee.tsx new file mode 100644 index 0000000..09d56c5 --- /dev/null +++ b/src/surfaces/abs-container/containee/containee.tsx @@ -0,0 +1,42 @@ +import React, { Component, RefObject } from 'react'; +import classNames from 'classnames'; +//@ts-ignore +import createRef from 'react-create-ref'; + +import styles from '../abs-container.module.scss'; +import positionStyles from './positions.module.scss'; + +export type Position = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'cover' + | 'none' + | 'top-start' + | 'right-start' + | 'bottom-start' + | 'left-start' + | 'top-end' + | 'right-end' + | 'bottom-end' + | 'left-end'; + +export type ContaineeProps = { position?: Position } & React.HTMLAttributes; + +export class Containee extends Component { + private ref: RefObject = createRef(); + + render() { + const { className, position = 'bottom', ...rest } = this.props; + const positionClass = positionStyles[position]; + + return ( +
+ ); + } +} diff --git a/src/surfaces/abs-container/containee/index.ts b/src/surfaces/abs-container/containee/index.ts new file mode 100644 index 0000000..c2ccdc3 --- /dev/null +++ b/src/surfaces/abs-container/containee/index.ts @@ -0,0 +1 @@ +export * from './containee'; diff --git a/src/surfaces/abs-container/containee/positions.module.scss b/src/surfaces/abs-container/containee/positions.module.scss new file mode 100644 index 0000000..00b0c22 --- /dev/null +++ b/src/surfaces/abs-container/containee/positions.module.scss @@ -0,0 +1,62 @@ +.top { + bottom: 100%; + + &-start { + bottom: 100%; + left: 0%; + } + + &-end { + bottom: 100%; + right: 0%; + } +} + +.right { + left: 100%; + + &-start { + left: 100%; + top: 0%; + } + + &-end { + left: 100%; + bottom: 0%; + } +} + +.bottom { + top: 100%; + + &-start { + top: 100%; + left: 0%; + } + + &-end { + top: 100%; + right: 0%; + } +} + +.left { + right: 100%; + + &-start { + right: 100%; + top: 0%; + } + + &-end { + right: 100%; + bottom: 0%; + } +} + +.cover { + top: 0%; + left: 0%; +} + +// .none {} diff --git a/src/surfaces/abs-container/container/container.tsx b/src/surfaces/abs-container/container/container.tsx new file mode 100644 index 0000000..59a1fd0 --- /dev/null +++ b/src/surfaces/abs-container/container/container.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from '../abs-container.module.scss'; + +export type ContainerProps = { open?: boolean } & React.HTMLAttributes; + +export function Container(props: ContainerProps) { + const { className, open, ...rest } = props; + + return
; +} diff --git a/src/surfaces/abs-container/container/index.ts b/src/surfaces/abs-container/container/index.ts new file mode 100644 index 0000000..85ee15b --- /dev/null +++ b/src/surfaces/abs-container/container/index.ts @@ -0,0 +1 @@ +export * from './container'; diff --git a/src/surfaces/abs-container/index.ts b/src/surfaces/abs-container/index.ts new file mode 100644 index 0000000..4a7900f --- /dev/null +++ b/src/surfaces/abs-container/index.ts @@ -0,0 +1,7 @@ +import styles from './abs-container.module.scss'; + +export * from './containee'; +export * from './container'; + +export const containerClass = styles.container; +export const containeeClass = styles.containee; \ No newline at end of file diff --git a/src/surfaces/click-outside/click-outside.tsx b/src/surfaces/click-outside/click-outside.tsx new file mode 100644 index 0000000..e5f53ab --- /dev/null +++ b/src/surfaces/click-outside/click-outside.tsx @@ -0,0 +1,35 @@ +import React, { Component, ReactNode } from 'react'; +import onClickOutside, { + OnClickOutProps, + InjectedOnClickOutProps, +} from 'react-onclickoutside'; + +type Props = { + children: ReactNode; + disable?: boolean; + onClick: (e: React.MouseEvent) => void; +} & InjectedOnClickOutProps; + +class MyComponent extends Component implements OnClickOutProps { + componentDidUpdate(prevProps: Props) { + const nextProps = this.props; + + if (prevProps.disable !== nextProps.disable) { + if (nextProps.disable) { + this.props.disableOnClickOutside(); + } else { + this.props.enableOnClickOutside(); + } + } + } + + handleClickOutside = (evt: React.MouseEvent) => { + this.props.onClick(evt); + }; + + render() { + return this.props.children; + } +} + +export const ClickOutside = onClickOutside(MyComponent); diff --git a/src/surfaces/click-outside/index.ts b/src/surfaces/click-outside/index.ts new file mode 100644 index 0000000..378eaaa --- /dev/null +++ b/src/surfaces/click-outside/index.ts @@ -0,0 +1 @@ +export * from './click-outside'; \ No newline at end of file diff --git a/src/surfaces/drawer/default-placeholder.tsx b/src/surfaces/drawer/default-placeholder.tsx new file mode 100644 index 0000000..48cecd8 --- /dev/null +++ b/src/surfaces/drawer/default-placeholder.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './drawer.module.scss'; + +export type DrawerPlaceholderProps = React.HTMLAttributes; + +export function DefaultPlaceholder(props: DrawerPlaceholderProps) { + return ( +
+ ); +} diff --git a/src/surfaces/drawer/drawer.module.scss b/src/surfaces/drawer/drawer.module.scss new file mode 100644 index 0000000..772455d --- /dev/null +++ b/src/surfaces/drawer/drawer.module.scss @@ -0,0 +1,4 @@ +.placeholder { + cursor: pointer; + user-select: none; +} diff --git a/src/surfaces/drawer/drawer.tsx b/src/surfaces/drawer/drawer.tsx new file mode 100644 index 0000000..40e5b85 --- /dev/null +++ b/src/surfaces/drawer/drawer.tsx @@ -0,0 +1,165 @@ +import React, { Component, ReactNode, ComponentType } from 'react'; + +import { Container } from '../abs-container'; +import { ClickOutside } from '../click-outside'; + +import { DefaultPlaceholder, DrawerPlaceholderProps } from './default-placeholder'; + +export type DrawerProps = { + open?: boolean; + PlaceholderComponent?: ComponentType; + placeholder: ReactNode; + + clickToggles?: boolean; + clickPlaceholderToggles?: boolean; + clickOutside?: boolean; + hoverToOpen?: boolean; + + onChange?: (evt: React.MouseEvent | undefined, open: boolean) => void; + onContainerToggle?: (e: React.MouseEvent) => void; + onPlaceholderToggle?: (e: React.MouseEvent) => void; + onContaineeToggle?: (e: React.MouseEvent) => void; + onClickOutside?: (e: React.MouseEvent) => void; +} & React.HTMLAttributes; + +type DrawerState = { + isOpen: boolean; +}; + +export class Drawer extends Component { + state: DrawerState = { + isOpen: this.props.open || false, + }; + + static defaultProps = { + PlaceholderComponent: DefaultPlaceholder, + clickPlaceholderToggles: true, + clickOutside: true, + clickToggles: true, + }; + + componentWillReceiveProps(nextProps: DrawerProps) { + const prevProps = this.props; + + if (prevProps.open !== nextProps.open && nextProps.open !== undefined) { + this.setState({ isOpen: nextProps.open }); + } + } + + private get isControlled() { + return this.props.open !== undefined; + } + + toggle = (evt: React.MouseEvent) => { + const { isOpen } = this.state; + const nextOpen = !isOpen; + + if (!this.isControlled) { + this.setState({ isOpen: nextOpen }); + } + + this.props.onChange && this.props.onChange(evt, nextOpen); + }; + + close = (evt: React.MouseEvent) => { + const { isOpen } = this.state; + const nextOpen = false; + + if (!this.isControlled && isOpen) { + this.setState({ isOpen: nextOpen }); + } + + this.props.onChange && this.props.onChange(evt, nextOpen); + }; + + open = (evt: React.MouseEvent) => { + const { isOpen } = this.state; + const nextOpen = true; + + if (!this.isControlled && !isOpen) { + this.setState({ isOpen: true }); + } + + this.props.onChange && this.props.onChange(evt, nextOpen); + }; + + private handePlaceholderClick = (e: React.MouseEvent) => { + //TODO - consider stopping event propagation + this.props.clickPlaceholderToggles && this.toggle(e); + + this.props.onPlaceholderToggle && this.props.onPlaceholderToggle(e); + }; + + private handleContainerClick = (e: React.MouseEvent) => { + this.props.clickToggles && this.toggle(e); + + this.props.onClick && this.props.onClick(e); + }; + + private handleClickOutside = (e: React.MouseEvent) => { + this.props.clickOutside && this.close(e); + + this.props.onClickOutside && this.props.onClickOutside(e); + }; + + private handleLeaveContainer = (e: React.MouseEvent) => { + //TODO - add grace period + this.props.hoverToOpen && this.close(e); + + this.props.onMouseLeave && this.props.onMouseLeave(e); + }; + + private handleEnterContainer = (e: React.MouseEvent) => { + this.props.hoverToOpen && this.open(e); + + this.props.onMouseEnter && this.props.onMouseEnter(e); + }; + + render() { + const { + placeholder, + children, + className, + PlaceholderComponent = DefaultPlaceholder, + clickOutside, + + //replaced by handlers: + onMouseEnter, + onMouseLeave, + + //virtualProps (should not be included in 'rest') + open, + hoverToOpen, + + onChange, + onPlaceholderToggle, + onContaineeToggle, + onClickOutside, + + ...rest + } = this.props; + const { isOpen } = this.state; + + return ( + + + + {placeholder} + + {children} + + + ); + } +} diff --git a/src/surfaces/drawer/index.ts b/src/surfaces/drawer/index.ts new file mode 100644 index 0000000..a150839 --- /dev/null +++ b/src/surfaces/drawer/index.ts @@ -0,0 +1 @@ +export * from './drawer';