Skip to content

Commit

Permalink
Fixes liferay#1829 - Create @clayui/modal
Browse files Browse the repository at this point in the history
  • Loading branch information
matuzalemsteles committed Apr 11, 2019
1 parent b50ae73 commit aaae92e
Show file tree
Hide file tree
Showing 9 changed files with 923 additions and 11 deletions.
55 changes: 54 additions & 1 deletion migrate-the-clay-components-from-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,59 @@ To get to the behavior of having a ClayLink with image, use the composition with

- Added ability to utilize context for passing spritemap down instead of having to pass the prop everywhere.

## ClayModal

Of more freedom to customize `Footer` but still requiring some rules to make everything work well, always use the `<ClayModal.FooterContainer />` component to align the left and right buttons.

```diff
<ClayModal
- body={<h1>{'Hello World!'}</h1>}
+ onClose={() => setVisible(false)}
visible={visible}
-/>
+>
+ <h1>{'Hello World!'}</h1>
+</ClayModal>
```

### API Changes

- `body` deprecated in favor of utilizing `children` prop
- `footerButtons` removed in favor of utilizing `renderFooter` prop
- `onClose` added
- `visible` is the source of truth to open or close the modal, `show` method has been removed.
- `data` deprecated
- `defaultEventHandler` deprecated
- `elementClasses` renamed to `className`

### Compositions

To render the buttons to the footer use the utility and follow some rules to have no problem, in case you forget the component has issued errors in the console to remind you.

- Do not wrap `FooterContainer` with an HTML tag, use `Fragment` to do so.
- Do not use more than two `FooterContainer`.

```diff
<ClayModal
- body={<h1>{'Hello World!'}</h1>}
+ onClose={() => setVisible(false)}
+ renderFooter={
+ <>
+ <ClayModal.FooterContainer>
+ <ClayButton>{'Cancel'}</ClayButton>
+ </ClayModal.FooterContainer>
+ <ClayModal.FooterContainer>
+ <ClayButton>{'Save'}</ClayButton>
+ </ClayModal.FooterContainer>
+ </>
+ }
visible={visible}
-/>
+>
+ <h1>{'Hello World!'}</h1>
+</ClayModal>
```

## ClayRadioGroup, ClayRadio

Using a radio by itself doesn't make much sense, only when 2+ exist does the functionality of radio actually work, which is why we moved from `radio` to `radio-group`. The functionality is the same, but by being grouped together it should make it easier to use because the `ClayRadioGroup` component will internally handle which radio is checked and requires less re-duplication of `inline` and `name` props.
Expand Down Expand Up @@ -128,7 +181,7 @@ Using a radio by itself doesn't make much sense, only when 2+ exist does the fun
- Removed `size` in favor of `large` since there is only default and large options.
- Removed `label` in favor of utilizing `children` prop
- Added `closeButtonProps` which allows you to add attributes to the nested button.
- This is where you would pass a callback for `onClick`.
- This is where you would pass a callback for `onClick`.

## ClayProgressBar

Expand Down
57 changes: 53 additions & 4 deletions packages/clay-modal/demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,66 @@
*
* SPDX-License-Identifier: BSD-3-Clause
*/

import * as React from 'react';

import * as ReactDOM from 'react-dom';
import ClayModal from '../src/ClayModal';
import Button from '@clayui/button';
import Modal from '../src/ClayModal';
import React, {useState} from 'react';

import 'clay-css/lib/css/atlas.css';

const spritemap = 'node_modules/clay-css/lib/images/icons/icons.svg';

const App: React.FunctionComponent = () => {
const [visible, setVisible] = useState(false);
const [visible2, setVisible2] = useState(false);

return (
<div>
<ClayModal />
<Modal
onClose={() => setVisible(false)}
renderFooter={
<>
<Modal.FooterContainer>
<Button
displayType="secondary"
onClick={() => setVisible(false)}
>
{'Cancelar'}
</Button>
</Modal.FooterContainer>
<Modal.FooterContainer>
<Button onClick={() => setVisible(false)}>
{'Save'}
</Button>
</Modal.FooterContainer>
</>
}
size="lg"
spritemap={spritemap}
status="success"
title="Title"
visible={visible}
>
<h1>{'Hello World!'}</h1>
</Modal>

<Modal
onClose={() => setVisible2(false)}
size="full-screen"
spritemap={spritemap}
title="Clay"
url="https://clayui.com"
visible={visible2}
/>

<Button displayType="primary" onClick={() => setVisible(true)}>
{'Open modal'}
</Button>

<Button displayType="primary" onClick={() => setVisible2(true)}>
{'Open modal with iframe'}
</Button>
</div>
);
};
Expand Down
5 changes: 4 additions & 1 deletion packages/clay-modal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
},
"keywords": ["clay", "react"],
"dependencies": {
"classnames": "^2.2.6"
"@clayui/button": "^3.0.0",
"@clayui/icon": "^3.0.0",
"classnames": "^2.2.6",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": "^16.8.1",
Expand Down
193 changes: 189 additions & 4 deletions packages/clay-modal/src/ClayModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,203 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import * as React from 'react';
import Button from '@clayui/button';
import classNames from 'classnames';
import Icon from '@clayui/icon';
import React, {FunctionComponent, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {Footer, FooterContainer, FooterContainerProps} from './Footer';
import {useModal} from './Hook';

interface Props extends React.HTMLAttributes<HTMLDivElement> {}
type Size = 'full-screen' | 'lg' | 'sm';

const ClayModal: React.FunctionComponent<Props> = ({
type Status = 'danger' | 'info' | 'success' | 'warning';

interface Props extends React.HTMLAttributes<HTMLDivElement> {
/**
* Callback called to close the modal.
*/
onClose: () => void;

/**
* Renders the footer with the FooterContainer component utilities.
*/
renderFooter?: React.ReactElement;

/**
* The size of element modal.
*/
size?: Size;

/**
* The path to the SVG spritemap file containing the icons.
*/
spritemap?: string;

/**
* Status messages.
*/
status?: Status;

/**
* Title of the modal.
*/
title?: string;

/**
* Url to place an iframe in the body of the modal.
*/
url?: string;

/**
* Flag to indicate if the modal is open or closed.
*/
visible: boolean;
}

const ICON_MAP = {
danger: 'exclamation-full',
info: 'info-circle',
success: 'check-circle',
warning: 'question-circle-full',
};

const Portal = ({children}: {children: React.ReactNode}) => {
const portalRef = useRef(document.createElement('div'));
const elToMountTo = document && document.body;

useEffect(() => {
elToMountTo.appendChild(portalRef.current);
return () => {
elToMountTo.removeChild(portalRef.current);
};
}, [elToMountTo]);

return createPortal(children, portalRef.current);
};

const delay = (fn: Function) => {
const callbackId = setTimeout(() => {
fn();
clearTimeout(callbackId);
}, 100);
};

const ClayModal: FunctionComponent<Props> & {
FooterContainer: FunctionComponent<FooterContainerProps>;
} = ({
children,
className,
onClose = () => {},
renderFooter,
size,
spritemap,
status,
title,
url,
visible = false,
...otherProps
}) => {
/**
* It is set to keep track of modal visibility and ensure that classes
* are removed when necessary.
*/
const [visibleModal, setVisibleModal] = useState<boolean>(visible);

const [visibleClassShow, setVisibleClassShow] = useState<boolean>(false);

const modalDialogElementRef = useRef<HTMLDivElement | null>(null);

/**
* A hook that takes care of controlling click, keyup and keydown events
* respectively close the modal after a click on the overlay, close the
* modal by pressing the ESC key and control the focus within the Modal.
*/
useModal(modalDialogElementRef, visible, onClose);

/**
* Synchronize the visible of the props with the internal value to update
* the states with delay, this ensures that the animation occurs when
* opening and closing the modal.
*/
useEffect(() => {
if (visible) {
document.body.classList.add('modal-open');
setVisibleModal(visible);
delay(() => setVisibleClassShow(visible));
} else {
document.body.classList.remove('modal-open');
setVisibleClassShow(visible);
delay(() => setVisibleModal(visible));
}
}, [visible]);

return (
<div {...otherProps} className={classNames(className)}>{'ClayModal'}</div>
<>
{visibleModal && (
<Portal>
<div
className={classNames('modal-backdrop fade', {
show: visibleClassShow,
})}
/>
</Portal>
)}
<div
{...otherProps}
className={classNames('fade modal', className, {
'd-block': visibleModal,
show: visibleClassShow,
})}
>
<div
className={classNames('modal-dialog', {
[`modal-${size}`]: size,
[`modal-${status}`]: status,
})}
ref={modalDialogElementRef}
tabIndex={-1}
>
<div className="modal-content">
{title && (
<div className="modal-header">
{status && (
<div className="modal-title-indicator">
<Icon
spritemap={spritemap}
symbol={ICON_MAP[status]}
/>
</div>
)}
<div className="modal-title">{title}</div>
<Button
aria-label="close"
className="close"
displayType="unstyled"
onClick={onClose}
>
<Icon
spritemap={spritemap}
symbol="times"
/>
</Button>
</div>
)}
<div
className={classNames('modal-body', {
'modal-body-iframe': url,
})}
>
{url ? <iframe src={url} title={url} /> : children}
</div>
{renderFooter && <Footer>{renderFooter}</Footer>}
</div>
</div>
</div>
</>
);
};

ClayModal.FooterContainer = FooterContainer;

export default ClayModal;
Loading

0 comments on commit aaae92e

Please sign in to comment.