Skip to content

Commit

Permalink
Feature/rover UI/ever 13326/kite component (#324)
Browse files Browse the repository at this point in the history
* feat: Adding basic kite component

* refactored interface and fixing css styles

* refactored to add kite header and fixed styling

* used shared size variable for margin and removed un-used childrenWrapper class

* fix: left aligned the title text between the icon and close button

* added knobs to the overview

* added knobs for Kite.Icon props

* refactored to make title node more flexible, add context for dismiss and onclose properties

* fixed kite content style

* fixed styling and refactored component to properly align the kite icons and contents.

* updated snapshot test

* chore: EVER-13326: Kite README and storybook knob additional, renamed Kite.Content to Kite.Body, to match convention from Modal

Co-authored-by: Boima Konuwa <boima.konuwa@cision.com>
Co-authored-by: Chris Garcia <pixelbandito@gmail.com>
  • Loading branch information
3 people authored Aug 9, 2021
1 parent 8c0b4d1 commit cf49de4
Show file tree
Hide file tree
Showing 11 changed files with 576 additions and 0 deletions.
22 changes: 22 additions & 0 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
InputTime,
Typography,
Modal,
Kite,
// IMPORT_INJECTOR
} from '@cision/rover-ui';

Expand All @@ -42,6 +43,7 @@ const App = () => {
const [inputCheckboxValue, setInputCheckboxValue] = useState(false);
const [inputTimeValue, setInputTimeValue] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isKiteVisible, setIsKiteVisible] = useState(false);

const toggleTooltip = function () {
setTooltipOpen((prev) => !prev);
Expand Down Expand Up @@ -453,6 +455,26 @@ const App = () => {
</Modal>
</Section>

<Section title="Kite">
<div>
<Button
modifiers={['primary']}
onClick={() => setIsKiteVisible(true)}
>
Show Kite
</Button>
</div>
<Kite
icon={<Kite.Icon fill="green" height="20" name="check" width="20" />}
canBeDismissed
visible={isKiteVisible}
onClose={() => setIsKiteVisible(false)}
ttl={3000}
>
<Kite.Header>Success Kite!</Kite.Header>
</Kite>
</Section>

{/** USAGE_INJECTOR */}
</div>
);
Expand Down
92 changes: 92 additions & 0 deletions src/components/Kite/Kite.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.Kite {
z-index: var(--rvr-zindex-kite);
visibility: hidden;
box-sizing: border-box;
position: fixed;
top: var(--rvr-space-md);
right: var(--rvr-space-md);
transition: transform .6s ease-in-out;
animation: slide-in-right .7s;
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
background-color: var(--rvr-white);
width: 342px;
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);
border-radius: 4px;
padding: 16px;
font-size: var(--rvr-font-size-md);
color: var(--rvr-color-font-dark);
}

.Kite > * {
margin-left: 16px;
}

.Kite > :first-child {
margin-left: 0;
}

.visible {
visibility: visible;
}

.icon {
flex: 0 0 auto;
align-self: flex-start;
}

.content {
flex: 1 1 auto;
min-width: 0;
}

.dismissButton {
flex: 0 0 auto;
margin-right: -16px;
margin-top: -12px;
margin-bottom: -12px;
align-self: flex-start;
}

.header {
font-weight: var(--rvr-font-weight-bold);
}

.header + .body {
margin-top: 8px;
}

.title {
flex-grow: 2;
}

.enterDone {
opacity: 1;
transform: translateX(0);
}

.exit {
opacity: 0;
transform: translateX(-200px);
}

.top-right {
top: 12px;
right: 12px;
transition: all .6s ease-in-out;
transform: translateX(100%);
}




@keyframes slide-in-right {
from {
transform: translateX(100%);
}

to {
transform: translateX(0);
}
}
77 changes: 77 additions & 0 deletions src/components/Kite/Kite.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import userEvent from '@testing-library/user-event';
import Kite from './Kite';

const defaultProps = {
visible: true,
onClose: jest.fn(),
ttl: 3000,
canBeDismissed: true,
};

const renderKite = (props = defaultProps) =>
render(
<Kite
{...props}
icon={<Kite.Icon fill="red" height="20" name="warning" width="20" />}
>
<Kite.Header>Kite Title</Kite.Header>
<Kite.Body>
<p>Kite Content Goes here!</p>
</Kite.Body>
</Kite>
);
describe('Kite', () => {
it('renders correctly', () => {
const { baseElement } = renderKite();
expect(baseElement).toMatchSnapshot();
});

it("does not render when 'visible' prop is false", () => {
render(<Kite visible={false} data-testid="Kite-Test" />);
expect(screen.queryByTestId('Modal-Test')).not.toBeInTheDocument();
});

describe('Dismiss Button', () => {
test.each`
canBeDismissed | visible
${true} | ${true}
${false} | ${false}
`(
'when canBeDismissed = $canBeDismissed, the close button visible should be: $visible',
({ canBeDismissed }) => {
render(
<Kite {...defaultProps} canBeDismissed={canBeDismissed}>
<Kite.Header>Title</Kite.Header>
</Kite>
);

const dismissButton = screen.queryByTestId('kite-dismiss-button');
if (canBeDismissed) {
expect(dismissButton).toBeInTheDocument();
} else {
expect(dismissButton).not.toBeInTheDocument();
}
}
);
});

describe('onClose callback', () => {
it('calls onClose callback when dismiss button is clicked', async () => {
render(
<Kite {...defaultProps} onClose={defaultProps.onClose}>
<Kite.Header>Title</Kite.Header>
</Kite>
);
const dismissButton = screen.getByTestId('kite-dismiss-button');
expect(dismissButton).toBeInTheDocument();

userEvent.click(dismissButton);

expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});
});
});
110 changes: 110 additions & 0 deletions src/components/Kite/Kite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useEffect, CSSProperties } from 'react';
import classNames from 'classnames';

import ReactDOM from 'react-dom';
import { CSSTransition } from 'react-transition-group';
import styles from './Kite.module.css';
import Button from '../Button';
import Icon from '../Icon';

interface KiteProps {
children?: React.ReactNode;
canBeDismissed?: boolean;
className?: string;
visible?: boolean;
icon?: React.ReactNode;
onClose?: () => void;
style?: CSSProperties;
ttl?: number;
}

type KiteIconProps = Parameters<typeof Icon>[0];
type KiteChildProps = React.HTMLAttributes<HTMLDivElement>;

type KiteType = React.FC<KiteProps> & {
Body: React.FC<KiteChildProps>;
Header: React.FC<KiteChildProps>;
Icon: React.FC<KiteIconProps>;
};

const Kite: KiteType = ({
canBeDismissed = true,
children = undefined,
className: passedClassName = '',
visible = false,
icon = undefined,
onClose = () => {},
ttl = undefined,
...passedProps
}) => {
useEffect(() => {
const interval = setInterval(() => {
if (onClose && ttl) {
onClose();
}
}, ttl);
return () => {
clearInterval(interval);
};
}, [onClose, ttl]);

return ReactDOM.createPortal(
<CSSTransition
in={visible}
unmountOnExit
timeout={{ enter: 0, exit: 300 }}
classNames={{
enterDone: styles.enterDone,
exit: styles.exit,
}}
>
<div
className={classNames(
styles.Kite,
{ [styles.visible]: visible },
passedClassName
)}
{...passedProps}
>
{icon && <div className={styles.icon}>{icon}</div>}
<div className={styles.content}>{children}</div>
{canBeDismissed && (
<div className={styles.dismissButton}>
<Button
level="text"
onClick={onClose}
data-testid="kite-dismiss-button"
>
<Button.Addon>
<Icon
height="20"
name="close"
style={{ display: 'block' }}
width="20"
/>
</Button.Addon>
</Button>
</div>
)}
</div>
</CSSTransition>,
document.body
);
};

const KiteIcon: React.FC<KiteIconProps> = ({ className, ...props }) => {
return <Icon {...props} className={classNames(styles.icon, className)} />;
};

const KiteHeader: React.FC<KiteChildProps> = ({ className, ...props }) => (
<div {...props} className={classNames(styles.header, className)} />
);
const KiteBody: React.FC<KiteChildProps> = ({ className, ...props }) => {
return <div {...props} className={classNames(styles.body, className)} />;
};

Kite.Icon = KiteIcon;
Kite.Header = KiteHeader;
Kite.Body = KiteBody;

export default Kite;
27 changes: 27 additions & 0 deletions src/components/Kite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# \<Kite\>

**A configurable Kite component that automatically triggers onClose when the time to live interval elapses**

This component can be used as a toast that slides in from the top left to display information.

Below is an example structure of the `<Kite>`

```js
<Kite
canBeDismissed
icon={<Kite.Icon />}
onClose={() => setVisible(false)}
ttl={3000}
visible={visible}
>
<Kite.Header>Bold title</Kite.Header>
<Kite.Body>Optional plain text / React node content</Kite.Body>
</Kite>
```

This will render a Kite with an icon, heading, and body content.

- The parent component controls visibility via the `onClose` callback and `visible` prop.
- If a `ttl` is provided, the component will call `onClose` that many ms after `visible` changes to `true`.
- `icon` and `children` can be anything React can render. Kites expert simple `Kite.Icon`, `Kite.Header`, and `Kite.Body` helpers. `Kite.Icon` uses the same interface as the base `Icon` component, and the heading and body provide some default spacing and font-weight variation.
- If `canBeDismissed` is false, the kite will not include a standard close button.
Loading

0 comments on commit cf49de4

Please sign in to comment.