Material-ui: [Dialog] Bad repositioning when closing keyboard after editing a TextField on mobile

Created on 20 Apr 2016  路  8Comments  路  Source: mui-org/material-ui

Problem Description

Hello!

So, I found the following issue: on mobile phone, when I fill a TextField inside a Dialog, I have a weird behavior appearing:

  • The Dialog opens up with a TextField, everything looks good;
  • When I click on the TextField, the keyboard opens up normally and the Dialog is resized as intended.
  • Then, when I close the keyboard, the Dialog is not re positioned correctly, it is positioned as follows:

screenshot_2016-04-20-11-35-05

The code for the Dialog is the following:

import React from 'react'
import Component from 'react/lib/ReactComponent'
import PureRenderMixin from 'react-addons-pure-render-mixin'
import Moment from 'moment'
import { Map, fromJS } from 'immutable'
import Dialog from 'material-ui/Dialog'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
import FloatingActionButton from 'material-ui/FloatingActionButton'
import ContentAdd from 'material-ui/svg-icons/content/add'
import TextField from 'material-ui/TextField'
import SelectField from 'material-ui/SelectField'
import MenuItem from 'material-ui/MenuItem'
import DatePicker from 'material-ui/DatePicker/DatePicker'
import TimePicker from 'material-ui/TimePicker/TimePicker'
import AutoComplete from 'material-ui/AutoComplete'
import Divider from 'material-ui/Divider'
import * as Colors from 'material-ui/styles/colors'

import { categories, minDate, maxDate } from './Event'

const createStyle = {
    position: 'fixed'
  ,right: 16
  ,bottom: 16
  ,zIndex: 100
}

const dialogStyle = {
  width: '90%'
  ,minWidth: '320px'
  ,maxWidth: '1080px'
}

export default class CreateEvent extends Component {
  constructor(props) {
    super(props)
    this.shouldComponentUpdate = PureRenderMixin
        .shouldComponentUpdate.bind(this)
    this.state = {
      open: false
            ,owner: this.props.owner
            ,name: this.props.name
            ,startDay: this.props.startTime
            ,startTime: this.props.startTime
            ,endDay: this.props.endTime
            ,endTime: this.props.endTime
            ,description: this.props.description
            ,location: this.props.location
            ,category: this.props.category
            ,nameError: null
            ,startDayError: null
            ,startTimeError: null
            ,endDayError: null
            ,endTimeError: null
            ,descriptionError: null
            ,locationError: null
            ,categoryError: null

  }}

  componentWillReceiveProps(newProps) {
      if (newProps.owner !== this.state.owner) {
        this.setState({ owner: newProps.owner });
      }
    }

    handleOpen() {
    this.setState({ open: true })
  }

  handleOk() {
    const fields = fromJS({
        name: 'name' 
        ,startDay: 'starting day'
        ,startTime: 'starting time'
        ,endDay: 'ending day'
        ,endTime: 'ending time'
        ,description: 'description'
        ,location: 'location'
        ,category: 'category'
    })
    const valid = fields.keySeq().reduce((valid, field) => {
        if (!this.state[field] || this.state[field] === '') {
            this.setState({
                [field + 'Error']: 'The event ' + fields.get(field) + 
                    ' cannot be empty'
            })
            return false
        }
        return valid
    }, true)
    if (valid) {
        const event = fromJS({
            name: this.state.name
            ,startTime: Moment(
                Moment(this.state.startDay).format('YYYY-MM-DD ') +
                Moment(this.state.startTime).format('HH:mm:ss')
                ,'YYYY-MM-DD HH:mm:ss'
            )
            ,endTime: Moment(
                Moment(this.state.endDay).format('YYYY-MM-DD ') +
                Moment(this.state.endTime).format('HH:mm:ss')
                ,'YYYY-MM-DD HH:mm:ss'
            )
            ,owner: this.state.owner
            ,description: this.state.description
            ,location: this.state.location
            ,category: this.state.category.get('name')
        })
        if (event.get('endTime') > event.get('startTime')) {
            (this.props.create) ?
                this.props.postEvent(event) :
                this.props.updateEvent(event, this.props.eventId)
            this.setState({open: false})
        } else {
            this.setState({
                endDayError: 'The ending day should be after the starting day'
                ,endTimeError: 'The ending time should be after the starting time'
            })
        }
    }
  }

  handleCancel() {
    this.setState({open: false})
  }

  handleNameChange(event) {
    this.setState({
        nameError: null
        ,name: event.target.value
    })
  }

  handleLocationChange(text) {
    this.setState({
        locationError: null
        ,location: text
    })
  }

  handleCategoryChange(event, index, value) {
    this.setState({
        categoryError: null
        ,category: value
    })
  }

  handleStartDayChange(event, time) {
    this.setState({
        startDayError: null
        ,startDay: time
    })
  }

  handleStartTimeChange(event, time) {
    this.setState({
        startTimeError: null
        ,startTime: time
    })
  }

  handleEndDayChange(event, time) {
    this.setState({
        endDayError: null
        ,endDay: time
    })
  }

  handleEndTimeChange(event, time) {
    this.setState({
        endTimeError: null
        ,endTime: time
    })
  }

  handleDescriptionChange(event) {
    this.setState({
        descriptionError: null
        ,description: event.target.value
    })
  }

  render() {
    const actions = [
      <FlatButton
        label="Ok"
        primary={true}
        onTouchTap={::this.handleOk}
      />
      ,<FlatButton
        label="Cancel"
        secondary={true}
        onTouchTap={::this.handleCancel}
      />
    ]

    return (
      <div>
                {(this.props.create) ? (
                    <FloatingActionButton 
                        style={createStyle} 
                        backgroundColor={Colors.deepOrange700}
                        onTouchTap={::this.handleOpen}
                    >
                <ContentAdd />
                </FloatingActionButton>
            ) : (
                <RaisedButton 
                    label="Edit" 
                    fullWidth={true}
                    primary={true}
                        onTouchTap={::this.handleOpen}
                />
            )}
        <Dialog
          title={
            (
                (this.props.create) ? 
                "Create a new" : 
                "Edit " + ((this.props.isOwner) ?  "your" : "this")
            ) + 
            " awesome event!"
          }
          titleStyle={(this.state.category) ?
            {backgroundColor: this.state.category.get('bgColor') || 'white'} :
            null}
          actions={actions}
          modal={false}
          open={this.state.open}
          onRequestClose={::this.handleCancel}
          contentStyle={dialogStyle}
          autoScrollBodyContent={true}
        >
            <TextField
                hintText='Event title'
                value={this.state.name}
                errorText={this.state.nameError}
                fullWidth={true}
                onChange={::this.handleNameChange}
                disabled={!this.props.isOwner && !this.props.create}
            />
            <div className='container-fluid'>
                <div className='col-sm-6 col-xs-12'>
                    <AutoComplete 
                        hintText="Location"
                        errorText={this.state.locationError}
                        dataSource={[]}
                        onUpdateInput={::this.handleLocationChange}
                        searchText={this.state.location}
                        fullWidth={true}
                    />
                </div>
                <div className='col-sm-6 col-xs-12'>
                    <SelectField
                        floatingLabelText="Category"
                        errorText={this.state.categoryError}
                        onChange={::this.handleCategoryChange}
                        value={this.state.category}
                        disabled={!this.props.isOwner && !this.props.create}
                        fullWidth={true}
                        labelStyle={(this.state.category) ?
                                    {color: this.state.category.get('color') || 'white'} :
                                    null}
                    >
                        {categories.map((category, index) => (
                            <MenuItem
                                key={index}
                                style={{color: category.get('color')}}
                                value={category}
                                primaryText={category.get('name')}
                            />
                        ))}
                    </SelectField>
                </div>
            </div>
            <div className='col-sm-7 col-xs-12'>
                <DatePicker 
                  minDate={minDate}
                  maxDate={maxDate}
                  defaultDate={minDate}
                  disableYearSelection={true}
                    hintText="Start day"
                    errorText={this.state.startDayError} 
                    fullWidth={true} 
                    onChange={::this.handleStartDayChange}
                    value={this.state.startDay}
                />
            </div>
            <div className='col-sm-5 col-xs-offset-2 col-xs-10'>
                <TimePicker 
                    format='24hr'
                    hintText="Start time"
                    errorText={this.state.startTimeError}
                    fullWidth={true}
                    onChange={::this.handleStartTimeChange}
                    value={this.state.startTime}
                />
            </div>
            <div className='col-sm-7 col-xs-12'>
                <DatePicker
                  minDate={minDate}
                  maxDate={maxDate}
                  defaultDate={maxDate}
                  disableYearSelection={true}
                  hintText="End day"
                    errorText={this.state.endDayError}
                  fullWidth={true}
                    onChange={::this.handleEndDayChange}
                    value={this.state.endDay}
                />
            </div>
            <div className='col-sm-5 col-xs-offset-2 col-xs-10'>
                <TimePicker 
                    format='24hr'
                    hintText="End time"
                    errorText={this.state.endTimeError}
                    fullWidth={true}
                    onChange={::this.handleEndTimeChange}
                    value={this.state.endTime}
                />
            </div>
            <TextField
                hintText='Description'
                errorText={this.state.descriptionError}
                fullWidth={true}
                multiLine={true}
                        onChange={::this.handleDescriptionChange}
                    value={this.state.description}
                    disabled={!this.props.isOwner && !this.props.create}
            />
        </Dialog>
      </div>
    )
  }
}

Versions

  • Material-UI: 0.15.0-beta.1
  • React: 15.0.1
  • Browser: Chrome on Android (OnePlus Two)
bug 馃悰 Dialog v0.x

Most helpful comment

Try to add repositionOnUpdate={false} to Dialog component.

<Dialog 
   repositionOnUpdate={false} 
   open={this.props.open}
   autoScrollBodyContent={true}
>
...
</Dialog>

All 8 comments

Hey @skasch, are you building a mobile app only or is this a web app thats mobile responsive? Reason I'm asking is I'm having trouble controlling my dialog component with media queries.

Hey @IrvingAxelB, I am building a responsive web app!

Try to add repositionOnUpdate={false} to Dialog component.

<Dialog 
   repositionOnUpdate={false} 
   open={this.props.open}
   autoScrollBodyContent={true}
>
...
</Dialog>

There is still issue with using Dialog and TextField on Chrome on iPad when the onscreen keyboards opens.

First, the Safari on Ipad works much better. When tap on a TextField of a dialog, the keyboards shows up and the dialog doesn't resize or re-position with the following code. This is much better user experience. When the keyboard hides, there no re-positioning or resizing either.

With Chrome on iPad (not repro on Windows), when keyboard shows up after using start editing a TextField, the dialog will be resized. When the keyboard hides, the dialog will be repositioned to lower part of the screen.

Even worse when I use more complex control on the dialog such as RichText editing control, the dialog will be resized and repositioned out the screen.

<Dialog
          title={dialogTitle}
          actions={actions}
          modal={false}
          open={this.props.showDialog}
          onRequestClose={this.handleClose}
          bodyStyle={{overflow: 'auto', position: 'relative'}}
          repositionOnUpdate={{false}}
          autoScrollBodyContent={{true}}
          autoDetectWindowHeight={{false}}
        >
          <div>{self.state.error}</div>
          {controls}
        </Dialog>

We've had huge problems with this and generally responsiveness of Dialog. Especially when opening keyboard on mobile devices. We are writing application that is responsive and should works on every device.
Second problem is that the gap between top screen and the top of dialog. It stays same for small devices.
screenshot_2016-11-12_20-14-23
For example in angular implementation it is smaller when the window height is smaller.
screenshot_2016-11-12_20-17-30

As a workaround we did some css tricks and we maximize it for small devices and it works well for mobile devices.
screenshot_2016-11-12_20-23-14

I expected similar behavior from mui's Dialog. It should occupy almost 100% of visible place on small devices to works well.

btw I tested how it works on phone and when screen height is too small my device displays only input field
screenshot 12 11 2016 19-39-01

It happens for example in some dialogs (with wifi settings) or in facebook application when adding new status. But not works in gmail app. So it have to be configured in Text Field component.

This issue was fixed along the way on the v1-beta branch.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ryanflorence picture ryanflorence  路  3Comments

finaiized picture finaiized  路  3Comments

ericraffin picture ericraffin  路  3Comments

zabojad picture zabojad  路  3Comments

revskill10 picture revskill10  路  3Comments