Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dialog] Bad repositioning when closing keyboard after editing a TextField on mobile #4045

Closed
skasch opened this issue Apr 20, 2016 · 8 comments
Labels
bug 🐛 Something doesn't work component: dialog This is the name of the generic UI component, not the React module! v0.x

Comments

@skasch
Copy link

skasch commented Apr 20, 2016

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)
@nathanmarks nathanmarks added the bug 🐛 Something doesn't work label Apr 20, 2016
@TheIrvingBarajas
Copy link

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.

@skasch
Copy link
Author

skasch commented Apr 26, 2016

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

@w01fgang
Copy link

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

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

@ethan-deng
Copy link

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>

@darkowic
Copy link
Contributor

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.

@darkowic
Copy link
Contributor

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.

@lucasbento lucasbento added component: dialog This is the name of the generic UI component, not the React module! Cross Browser Support labels Nov 13, 2016
@dapids
Copy link

dapids commented Nov 13, 2016

Kind of related to #3553 and #1748.

@oliviertassinari
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: dialog This is the name of the generic UI component, not the React module! v0.x
Projects
None yet
Development

No branches or pull requests

9 participants