Skip to content
This repository has been archived by the owner on Aug 21, 2023. It is now read-only.

Commit

Permalink
Merge pull request #709 from helpscout/feature/editable-fields-validate
Browse files Browse the repository at this point in the history
Allow for richer validation
  • Loading branch information
knicklabs committed Sep 24, 2019
2 parents 422c1ab + 222774d commit 6010e38
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 14 deletions.
86 changes: 78 additions & 8 deletions src/components/EditableField/EditableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ export class EditableField extends React.Component<
)
}

handleInputBlur = ({ name, event }) => {
handleInputBlur = payload => {
const { name, event } = payload
const {
activeField,
fieldValue,
Expand All @@ -246,6 +247,28 @@ export class EditableField extends React.Component<
fieldValue.length === 1
? fieldValue[0]
: find(fieldValue, val => val.id === event.target.id)
const initialField =
initialFieldValue.length === 1
? initialFieldValue[0]
: find(initialFieldValue, val => val.id === event.target.id)

/* istanbul ignore next */
if (equal(initialField, changedField)) {
this.setState({ activeField: EMPTY_VALUE }, () => {
onInputBlur({ name, value: fieldValue, event })
})

return
}

/* istanbul ignore next */
if (this.state.disabledItem === changedField.id) {
this.setState({ activeField: EMPTY_VALUE }, () => {
onInputBlur({ name, value: fieldValue, event })
})

return
}

if (!changedField.value) {
if (!multipleValuesEnabled) {
Expand All @@ -269,11 +292,36 @@ export class EditableField extends React.Component<
if (changedField.value && !changedField.validated) {
this.setState({ disabledItem: changedField.id })

// Get the next values and commit prior to validation so that
// we can use it in validation.
let updatedFieldValue = this.state.fieldValue.map(field => {
// tested
/* istanbul ignore next */
if (field.id === changedField.id) {
return { ...changedField, validated: true }
}
// tested
/* istanbul ignore next */
return field
})

validate({
value: changedField.value,
data: {
cause: 'BLUR',
operation:
/* istanbul ignore next */ updatedFieldValue.length >
initialFieldValue.length
? OPERATION.CREATE
: OPERATION.UPDATE,
item: changedField,
},
name: changedField.id,
value: changedField.value,
values: updatedFieldValue,
}).then(validation => {
const updatedFieldValue = this.state.fieldValue.map(field => {
// Since this is async and the state of other fields may have changed,
// we need to recompute this.
updatedFieldValue = this.state.fieldValue.map(field => {
// tested
/* istanbul ignore next */
if (field.id === changedField.id) {
Expand Down Expand Up @@ -434,13 +482,15 @@ export class EditableField extends React.Component<
const isEnter = event.key === key.ENTER
const isEscape = event.key === key.ESCAPE

const { fieldValue: value } = this.state
this.props.onInputKeyDown({ name, value, event })

if (isEnter) {
return this.handleFieldEnterPress({ event, name })
} else if (isEscape) {
return this.handleFieldEscapePress({ event, name })
}
const { fieldValue: value } = this.state
this.props.onInputKeyDown({ name, value, event })

return new Promise((resolve, reject) => {
reject()
})
Expand Down Expand Up @@ -489,15 +539,35 @@ export class EditableField extends React.Component<
// Case 3: value was changed
const impactedField = find(fieldValue, val => val.id === name)

// Get the next values and commit prior to validation so that
// we can use it in validation.
let updatedFieldValue = this.updateFieldValue({
name,
value: inputValue,
})

// Skip if the field was marked as validated
/* istanbul ignore else */
if (!impactedField.validated) {
this.setState({ disabledItem: name })

validate({ value: inputValue, name }).then(validation => {
let updatedFieldValue

validate({
data: {
cause: 'ENTER',
operation:
/* istanbul ignore next */ updatedFieldValue.length >
initialFieldValue.length
? OPERATION.CREATE
: OPERATION.UPDATE,
item: updatedFieldValue.filter(field => field.id === name)[0],
},
name,
value: inputValue,
values: updatedFieldValue,
}).then(validation => {
if (validation.isValid) {
// Since this is async and the state of other fields may have changed,
// we need to recompute this.
updatedFieldValue = this.updateFieldValue({
name,
value: inputValue,
Expand Down
7 changes: 6 additions & 1 deletion src/components/EditableField/EditableField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ export interface EditableFieldProps {
}) => void
onDelete: (args: { name: string; value: FieldValue[]; event: Event }) => void
onDiscard: (args: { value: FieldValue[] }) => void
validate: (args: { value: string; name: string }) => Promise<Validation>
validate: (args: {
data: CommitData
name: string
value: string
values: FieldValue[]
}) => Promise<Validation>
}

export interface EditableFieldState {
Expand Down
15 changes: 12 additions & 3 deletions src/components/PortalWrapper/PortalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import matchPath from '../../utilities/react-router/matchPath'
import Content from './PortalWrapper.Content'

interface PortalWrapperProps extends PortalProps {
closeOnEscape: boolean
isOpen: boolean
trigger: any
isOpenProps: boolean
Expand Down Expand Up @@ -55,6 +56,7 @@ const PortalWrapper = (options = defaultOptions) => ComposedComponent => {
PortalWrapperState
> {
static defaultProps = {
closeOnEscape: true,
isOpen: false,
}
static contextTypes = {
Expand Down Expand Up @@ -90,8 +92,8 @@ const PortalWrapper = (options = defaultOptions) => ComposedComponent => {
props.timeout !== undefined
? props.timeout
: composedWrapperTimeout !== undefined
? composedWrapperTimeout
: extendedOptions.timeout
? composedWrapperTimeout
: extendedOptions.timeout

this.state = {
isOpen: props.isOpen,
Expand Down Expand Up @@ -336,10 +338,17 @@ const PortalWrapper = (options = defaultOptions) => ComposedComponent => {
)
}

renderEventListener() {
const { closeOnEscape } = this.props
return closeOnEscape ? (
<KeypressListener keyCode={Keys.ESCAPE} handler={this.handleOnEsc} />
) : null
}

render() {
return (
<div className="c-PortalWrapper">
<KeypressListener keyCode={Keys.ESCAPE} handler={this.handleOnEsc} />
{this.renderEventListener()}
{this.renderTrigger()}
{this.renderPortal()}
</div>
Expand Down
19 changes: 19 additions & 0 deletions src/components/PortalWrapper/__tests__/PortalWrapper.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { mount } from 'enzyme'
import KeypressListener from '../../KeypressListener'
import PortalWrapper from '../PortalWrapper'
import Keys from '../../../constants/Keys'
import { classNames } from '../../../utilities/classNames'
Expand Down Expand Up @@ -352,6 +353,24 @@ describe('Esc keypress', () => {
window.removeEventListener('keyup', globalSpy)
}, 20)
})

test('Prevents adding KeypressListener when closeOnEscape is false', () => {
const TestComponent = PortalWrapper(options)(TestButton)
const trigger = <div className="trigger">Trigger</div>
const wrapper = mount(
<TestComponent timeout={0} closeOnEscape={false} trigger={trigger} />
)
expect(wrapper.find(KeypressListener)).toHaveLength(0)
})

test('Adds KeypressListener when closeOnEscape is true', () => {
const TestComponent = PortalWrapper(options)(TestButton)
const trigger = <div className="trigger">Trigger</div>
const wrapper = mount(
<TestComponent timeout={0} closeOnEscape trigger={trigger} />
)
expect(wrapper.find(KeypressListener)).toHaveLength(1)
})
})

describe('displayName', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/pkg.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default {
version: '2.63.0',
version: '2.63.1-9',
}
3 changes: 2 additions & 1 deletion stories/EditableField.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ stories.add('Key Events', () => (
</ContainerUI>
))

function validateFieldValue({ name, value }) {
function validateFieldValue(payload) {
const { name, value } = payload
let isValid = value !== 'off' && value !== 'other' && value !== 'warn'

return new Promise(resolve => {
Expand Down
11 changes: 11 additions & 0 deletions stories/Modal.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@ stories.add('default', () => (
</Modal>
))

stories.add('no close on escape', () => (
<Modal closeOnEscape={false} trigger={<Link>Open dis modal</Link>}>
<Modal.Body>
<Heading>Title</Heading>
{ContentSpec.generate(8).map(({ id, content }) => (
<p key={id}>{content}</p>
))}
</Modal.Body>
</Modal>
))

stories.add('open', () => (
<Modal isOpen trigger={<Link>Clicky</Link>}>
<Modal.Content>
Expand Down

0 comments on commit 6010e38

Please sign in to comment.