Skip to content

Commit 93a311e

Browse files
authored
Unbreak HKModal (#38)
* Make HKModal a controlled component again Also update stories to properly consume the button events. * Correction to first line of state flow * Update storyshots snaps * Get that lint off ya shoulder * Adjust transition easing of flyout animation * Address feedback * Update storyshots * Document HKModal in README * Add an HKModal example with a type-to-confirm field * Use HKButton for "Show the Modal" * Break HKModal stories into initially open vs closed
1 parent fdb82bb commit 93a311e

File tree

4 files changed

+505
-63
lines changed

4 files changed

+505
-63
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,60 @@ If you want to use `HKFlagIcon`, you will need to tell Webpack to copy the flag
3636
See [react-hk-components.herokuapp.com](https://react-hk-components.herokuapp.com)
3737
for a complete list of components that are available.
3838

39+
#### HKModal
40+
41+
The HKModal component displays modal dialogs of two kinds: normal, which appear in the middle of the browser viewport, and flyout, which slides in from the right. It takes the following props:
42+
43+
* **`isFlyout`**: `boolean?`. Defaults to false.
44+
* **`show`**: `boolean`. Set it to `true` in order to trigger display of the modal, or `false` to trigger hiding.
45+
* **`type`**: `string?`. Set to `destructive` if you want the title of the modal to be rendered in red.
46+
* **`onDismiss`**: `(value?: string) => any`. A handler that is invoked with the close value when the modal is dismissed. Closing the modal by clicking outside the modal or by clicking on the X at the top-right of the modal will result in the handler being invoked with a value of `cancel`.
47+
* **`header`**: JSX element. What is rendered in the header of the modal.
48+
* **`buttons`**: `IButtonDefinition[]?`: an optional array of button definitions. These will be rendered left-to-right as buttons in the footer of the modal.
49+
50+
The contents of the modal are the children passed in the body of the react element, e.g. `<HKModal> stuff to render in body of modal </HKModal>`
51+
52+
Buttons are defined as follows:
53+
54+
* **`text`**: `string`. The text on the button
55+
* **`type`**: `Button.Type`. Primary, secondary, danger, etc.
56+
* **`disabled`**: `boolean`. Set to `true` if you want the button disabled.
57+
* **`value`**: `string`. The value associated with the button. This is what will be remitted to the `onDismiss` handler when the user closes the modal by clicking on this button.
58+
59+
So here's a working example:
60+
61+
```tsx
62+
public class MyModalWrapper extends React.Component {
63+
handleDismiss = (value?: string): any => {
64+
switch(value) {
65+
case 'ok':
66+
// handle the OK case
67+
break
68+
case cancel:
69+
default:
70+
// handle the cancel case, which is also the default
71+
}
72+
}
73+
public render() {
74+
return (<HKModal
75+
isFlyout={false}
76+
show={true}
77+
onDismiss={this.handleDismiss}
78+
header={<div>My Modal</div>}
79+
buttons={[
80+
{text: 'cancel', value: 'cancel', type: 'tertiary'},
81+
{text: 'OK', value: 'ok', type: 'primary'},
82+
]}
83+
>
84+
<div>Look at my shiny modal content!</div>
85+
<p>Such shiny. Much wow.</p>
86+
</HKModal>)
87+
}
88+
}
3989

90+
91+
92+
```
4093
## Development
4194

4295
### Installation

src/HKModal.tsx

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,99 @@ import * as classnames from 'classnames'
33
import * as React from 'react'
44
import { Transition } from 'react-transition-group'
55
import SRMModal from 'simple-react-modal'
6+
import {
7+
default as HKButton,
8+
Type as ButtonType,
9+
} from './HKButton'
610

711
export enum Type {
812
Actionable = 'actionable',
913
Destructive = 'destructive',
1014
Presentation = 'presentation',
1115
}
1216

17+
interface IButtonDefinition {
18+
classNames?: string,
19+
disabled: boolean,
20+
text: string,
21+
type: ButtonType,
22+
value: string,
23+
}
24+
1325
interface IModalProps {
1426
children: React.ReactNode,
1527
header: React.ReactNode,
16-
footer: React.ReactNode,
28+
buttons?: IButtonDefinition[],
1729
isFlyout?: boolean,
18-
onDismiss: (...args: any[]) => any,
30+
onDismiss: (value?: string) => any,
1931
show: boolean,
2032
type?: Type,
2133
}
2234

2335
interface IModalState {
24-
isClosing: boolean
36+
isShowing: boolean,
37+
isClosing: boolean,
2538
}
2639

40+
/*
41+
Dramatis personae of the event flow in this component
42+
* Show: props.show. Used by the controlling component to indicate a desire to show/hide.
43+
* iS: state.isShowing. Initially false.
44+
* iC: state.isClosing. Initially false.
45+
* in: What should be passed as a prop to <Transition in={ aBoolean } ... />
46+
47+
Show iS iC in
48+
1. F F F - never displayed
49+
2. T F F - controlling component wants us to display! gDSFP will derive the state on the following line.
50+
3. T T F T Shows the modal. In becomes true so transition in happens.
51+
4. *** props.onDismiss() is called, resulting in the controlling component setting show to F ***
52+
5. F T F T controlling component wants us to hide! gDSFP will derive the following state
53+
6. F T T F Transition to closed. In becomes false, triggers transition state "exiting"
54+
7. *** Transition out continues until handleExited() fires, setting isShowing to false
55+
8. F F T F Modal is hidden. Transition state is 'exited'
56+
9. T F T F Controlling component wants us to display! gDSFP will take us to step 3.
57+
*/
2758
export default class HKModal extends React.Component<IModalProps, IModalState> {
2859
public static defaultProps: Partial<IModalProps> = {
2960
isFlyout: false,
3061
}
3162

3263
public static getDerivedStateFromProps (props, state) {
33-
if (!props.show && state.isClosing) {
34-
return { isClosing: false }
64+
if (props.show) {
65+
// reset state, we're showing the thing and not closing at all
66+
return { isShowing: true, isClosing: false }
3567
} else {
36-
return state
68+
// We're somewhere in the process of closing the modal
69+
// So set isClosing to true.
70+
// isShowing will be set to false by handleExited(),
71+
// which is the handler fired at the end of the transition out.
72+
return { isClosing: true }
3773
}
3874
}
3975

4076
public state = {
4177
isClosing: false,
78+
isShowing: false,
79+
}
80+
81+
public handleClose = (e: React.MouseEvent<HTMLElement>) => {
82+
this.props.onDismiss('cancel')
4283
}
4384

44-
public handleClose = () => {
45-
this.setState({
46-
isClosing: true,
47-
})
85+
public handleButtonClick = (e: React.MouseEvent<HTMLInputElement>) => {
86+
this.props.onDismiss(e.currentTarget.value)
4887
}
4988

50-
public handleExited = (node) => {
51-
node.addEventListener('transitionend', this.props.onDismiss, false)
89+
public handleExited = (node: Element) => {
90+
node.addEventListener('transitionend', () => {
91+
this.setState({ isShowing: false })
92+
}, false)
5293
}
5394

5495
public render () {
5596
const duration = 250
56-
const { show, children, onDismiss, header, footer, isFlyout, type } = this.props
97+
const { children, onDismiss, header, buttons, isFlyout, type } = this.props
98+
const { isShowing, isClosing } = this.state
5799

58100
const fadeTransition = {
59101
transition: `background ${duration}ms ease-in-out, opacity ${duration}ms ease-in-out`,
@@ -68,7 +110,7 @@ export default class HKModal extends React.Component<IModalProps, IModalState> {
68110

69111
const innerTransition = isFlyout ? {
70112
transform: 'translateX(100%)',
71-
transition: `transform ${duration}ms ease-in-out`,
113+
transition: `transform ${duration}ms cubic-bezier(0,1,0.5,1)`,
72114
} : {}
73115

74116
const innerStyles = isFlyout ? {
@@ -106,10 +148,13 @@ export default class HKModal extends React.Component<IModalProps, IModalState> {
106148
'flex-auto': isFlyout,
107149
}
108150

109-
const transitionIn = this.state.isClosing ? false : show
151+
const footer = (buttons || []).map((b) => (
152+
<HKButton key={b.value} value={b.value} type={b.type} disabled={b.disabled} onClick={this.handleButtonClick} className={classnames('ml1', b.classNames)}>{b.text}</HKButton>
153+
))
110154

155+
// in={!isClosing} is derived from the state table at the top of this component
111156
return (
112-
<Transition in={transitionIn} timeout={0} onExited={this.handleExited}>
157+
<Transition in={!isClosing} timeout={0} onExited={this.handleExited}>
113158
{(state) => (
114159
<SRMModal
115160
containerStyle={{
@@ -129,7 +174,7 @@ export default class HKModal extends React.Component<IModalProps, IModalState> {
129174
}}
130175
className={classnames('flex flex-column', modalParentClass)}
131176
closeOnOuterClick={true}
132-
show={show}
177+
show={isShowing}
133178
onClose={this.handleClose}
134179
>
135180
<div className='bg-near-white dark-gray bb b--light-silver f4 flex items-center justify-center br--top br2'>
@@ -139,7 +184,7 @@ export default class HKModal extends React.Component<IModalProps, IModalState> {
139184

140185
<div className={classnames(modalChildrenClass)}>{children}</div>
141186

142-
{footer && <div className='bt b--light-silver w-100 pa3 tr'>{footer}</div>}
187+
{buttons && <div className='bt b--light-silver w-100 pa3 tr'>{footer}</div>}
143188
</SRMModal>
144189
)}
145190
</Transition>

stories/HKModal.stories.tsx

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import * as React from 'react'
22

3+
import { action } from '@storybook/addon-actions'
34
import { storiesOf } from '@storybook/react'
45

5-
import { default as HKButton } from '../src/HKButton'
6+
import {
7+
default as HKButton,
8+
Type as ButtonType,
9+
} from '../src/HKButton'
610
import { default as HKModal, Type } from '../src/HKModal'
711

812
interface IModalWrapperProps {
913
initialShowModal?: boolean,
10-
isFlyout?: boolean
14+
isFlyout?: boolean,
15+
hasConfirm?: boolean,
1116
type?: Type,
1217
}
1318

1419
interface IModalWrapperState {
20+
confirmValue?: string,
1521
showModal: boolean,
16-
isFlyout?: boolean
22+
isFlyout?: boolean,
1723
}
1824

1925
class ModalWrapper extends React.Component<
@@ -23,47 +29,83 @@ class ModalWrapper extends React.Component<
2329
constructor (props) {
2430
super(props)
2531
this.state = {
32+
confirmValue: '',
2633
isFlyout: props.isFlyout,
2734
showModal: props.initialShowModal,
2835
}
2936
}
3037

3138
public render () {
39+
let confirmInput
40+
let okDisabled = false
41+
if (this.props.hasConfirm) {
42+
confirmInput = (
43+
<div>
44+
<p>Type 'confirm' to enable the OK button</p>
45+
<input
46+
id='verifyForceRotateCredential'
47+
className='hk-input w-100'
48+
autoCorrect='off'
49+
spellCheck={false}
50+
autoFocus={true}
51+
onChange={this.handleConfirmChange}
52+
value={this.state.confirmValue}
53+
/>
54+
</div>
55+
)
56+
okDisabled = this.state.confirmValue !== 'confirm'
57+
}
3258
return (
3359
<div>
34-
<button onClick={this.showModal}>show it</button>
60+
<HKButton type={ButtonType.Primary} onClick={this.showModal}>Show the Modal</HKButton>
3561
<HKModal
3662
isFlyout={this.state.isFlyout}
3763
type={this.props.type && this.props.type}
3864
show={this.state.showModal}
3965
onDismiss={this.handleModalDismiss}
4066
header={<div>header text</div>}
41-
footer={<HKButton>Submit</HKButton>}
67+
buttons={[
68+
{
69+
disabled: false,
70+
text: 'Cancel',
71+
type: ButtonType.Tertiary,
72+
value: 'cancel',
73+
},
74+
{
75+
disabled: okDisabled,
76+
text: 'OK',
77+
type: this.props.type === Type.Destructive ? ButtonType.Danger : ButtonType.Primary,
78+
value: 'ok',
79+
},
80+
]}
4281
>
43-
<div className='pa6'>with some important details here below</div>
82+
<div className='pa6'>
83+
<p>with some important details here below</p>
84+
{confirmInput}
85+
</div>
4486
</HKModal>
4587
</div>
4688
)
4789
}
4890

49-
private handleModalDismiss = () => {
50-
this.setState({ showModal: false })
91+
private handleModalDismiss = (value?: string) => {
92+
this.setState({ showModal: false, confirmValue: '' })
93+
action('Modal Dismiss')(value)
94+
}
95+
96+
private handleConfirmChange = (e) => {
97+
this.setState({ confirmValue: e.target.value })
5198
}
5299

53100
private showModal = () => {
54101
this.setState({ showModal: true })
55102
}
56103
}
57104

58-
storiesOf('HKModal', module)
59-
.add('default', () => <ModalWrapper />)
60-
.add('default initially open', () => <ModalWrapper initialShowModal={true} />)
61-
.add('flyout', () => (
62-
<ModalWrapper isFlyout={true} initialShowModal={false} />
63-
))
64-
.add('flyout initially open', () => (
65-
<ModalWrapper isFlyout={true} initialShowModal={true} />
66-
))
67-
.add('destructive', () => (
68-
<ModalWrapper type={Type.Destructive} />
69-
))
105+
[true, false].forEach((open) => {
106+
storiesOf(`HKModal/${open ? 'initially open' : 'initially closed'}`, module)
107+
.add('default', () => <ModalWrapper initialShowModal={open}/>)
108+
.add('flyout', () => <ModalWrapper isFlyout={true} initialShowModal={open}/>)
109+
.add('destructive', () => <ModalWrapper type={Type.Destructive} initialShowModal={open}/>)
110+
.add('with confirmation', () => <ModalWrapper type={Type.Destructive} hasConfirm={true} initialShowModal={open}/>)
111+
})

0 commit comments

Comments
 (0)