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] Unusable on small screens #3295

Closed
liesislukas opened this issue Feb 11, 2016 · 7 comments
Closed

[Dialog] Unusable on small screens #3295

liesislukas opened this issue Feb 11, 2016 · 7 comments
Labels
duplicate This issue or pull request already exists

Comments

@liesislukas
Copy link
Contributor

Dialogs look great at big screen, but while browsing with phone - it's almost unusable. Contents collapses to small box with scrollbar if params set right. That was very very bad UX for my case. Here it's w/o modifications:

image

I've modified dialog.js to keep scrolling available while dialog is on and do not collapse contents, just leave it as it is when screen is too small for all dialog. Now it's scrollable and UX is a lot better. I also changed position from fixed to absolute and when showing dialog scroll to top of the screen to see actual dialog, not gray matter if clicked somewhere scrolled down. So now it looks like this:

image

I can scroll and read all the contents easily. Even on such a small viewport height.

I've cloned material-ui v0.13.4 and updating for my needs, so use this code with this info in your mind.

Hope some of this will go to main lib for better UX at least add option for such behaviour.

dialog.js:

'use strict';

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }

var React = require('react');
var ReactDOM = require('react-dom');
var WindowListenable = require('./mixins/window-listenable');
var CssEvent = require('./utils/css-event');
var KeyCode = require('./utils/key-code');
var Transitions = require('./styles/transitions');
var StylePropable = require('./mixins/style-propable');
var FlatButton = require('./flat-button');
var Overlay = require('./overlay');
var Paper = require('./paper');
var DefaultRawTheme = require('./styles/raw-themes/light-raw-theme');
var ThemeManager = require('./styles/theme-manager');
var warning = require('warning');

var ReactTransitionGroup = require('react-addons-transition-group');

var TransitionItem = React.createClass({
  displayName: 'TransitionItem',

  mixins: [StylePropable],

  contextTypes: {
    muiTheme: React.PropTypes.object
  },

  //for passing default theme context to children
  childContextTypes: {
    muiTheme: React.PropTypes.object
  },

  getChildContext: function getChildContext() {
    return {
      muiTheme: this.state.muiTheme
    };
  },

  getInitialState: function getInitialState() {
    return {
      style: {},
      muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme)
    };
  },

  //to update theme inside state whenever a new theme is passed down
  //from the parent / owner using context
  componentWillReceiveProps: function componentWillReceiveProps(nextProps, nextContext) {
    var newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
    this.setState({ muiTheme: newMuiTheme });
  },

  componentWillEnter: function componentWillEnter(callback) {
    var spacing = this.state.muiTheme.rawTheme.spacing;

    this.setState({
      style: {
        opacity: 1,
        transform: 'translate3d(0, ' + spacing.desktopKeylineIncrement + 'px, 0)'
      }
    });

    setTimeout(callback, 450); // matches transition duration
  },

  componentWillLeave: function componentWillLeave(callback) {
    var _this = this;

    this.setState({
      style: {
        opacity: 0,
        transform: 'translate3d(0, 0, 0)'
      }
    });

    setTimeout(function () {
      if (_this.isMounted()) callback();
    }, 450); // matches transition duration
  },

  render: function render() {
    var _props = this.props;
    var style = _props.style;

    var other = _objectWithoutProperties(_props, ['style']);

    return React.createElement(
      'div',
      _extends({}, other, { style: this.prepareStyles(this.state.style, style) }),
      this.props.children
    );
  }
});

var Dialog = React.createClass({
  displayName: 'Dialog',

  mixins: [WindowListenable, StylePropable],

  contextTypes: {
    muiTheme: React.PropTypes.object
  },

  //for passing default theme context to children
  childContextTypes: {
    muiTheme: React.PropTypes.object
  },

  getChildContext: function getChildContext() {
    return {
      muiTheme: this.state.muiTheme
    };
  },

  propTypes: {
    actions: React.PropTypes.array,
    autoDetectWindowHeight: React.PropTypes.bool,
    autoScrollBodyContent: React.PropTypes.bool,
    bodyStyle: React.PropTypes.object,
    contentClassName: React.PropTypes.string,
    contentStyle: React.PropTypes.object,
    openImmediately: React.PropTypes.bool,
    repositionOnUpdate: React.PropTypes.bool,
    style: React.PropTypes.object,
    title: React.PropTypes.node,
    defaultOpen: React.PropTypes.bool,
    open: React.PropTypes.bool,
    modal: React.PropTypes.bool,
    onDismiss: React.PropTypes.func,
    onShow: React.PropTypes.func,
    onRequestClose: React.PropTypes.func,
    actionFocus: React.PropTypes.string,
    titleStyle: React.PropTypes.object
  },

  windowListeners: {
    keyup: '_handleWindowKeyUp',
    resize: '_handleResize'
  },

  getDefaultProps: function getDefaultProps() {
    return {
      autoDetectWindowHeight: false,
      autoScrollBodyContent: false,
      actions: [],
      repositionOnUpdate: false,
      defaultOpen: false,
      open: null
    };
  },

  getInitialState: function getInitialState() {
    if (process.env.NODE_ENV !== 'production') {
      this._testDeprecations();
    }

    var open = this.props.open;

    if (open === null) {
      open = this.props.openImmediately || this.props.defaultOpen;
    }

    return {
      open: open,
      muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme)
    };
  },

  //to update theme inside state whenever a new theme is passed down
  //from the parent / owner using context
  componentWillReceiveProps: function componentWillReceiveProps(nextProps, nextContext) {
    var newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
    this.setState({ muiTheme: newMuiTheme });

    if (process.env.NODE_ENV !== 'production') {
      this._testDeprecations();
    }

    if (nextProps.open !== this.props.open) {
      if (nextProps.open && !this.state.open) {
        this._show();
      } else if (!nextProps.open && this.state.open) {
        this._dismiss();
      }
    }
  },

  componentDidMount: function componentDidMount() {
    this._positionDialog();
    if (this.state.open) {
      this.refs.dialogOverlay.preventScrolling();
    }
  },

  componentDidUpdate: function componentDidUpdate() {
    this._positionDialog();
  },

  getStyles: function getStyles() {
    var spacing = this.state.muiTheme.rawTheme.spacing;

    var main = {
      position: 'absolute',
      boxSizing: 'border-box',
      WebkitTapHighlightColor: 'rgba(0,0,0,0)',
      zIndex: 10,
      top: 0,
      left: -10000,
      width: '100%',
      height: '100%',
      transition: Transitions.easeOut('0ms', 'left', '450ms')
    };

    var content = {
      boxSizing: 'border-box',
      WebkitTapHighlightColor: 'rgba(0,0,0,0)',
      transition: Transitions.easeOut(),
      position: 'relative',
      width: '75%',
      maxWidth: spacing.desktopKeylineIncrement * 12,
      margin: '0 auto',
      zIndex: 10
    };

    var body = {
      padding: spacing.desktopGutter,
      overflowY: this.props.autoScrollBodyContent ? 'auto' : 'hidden',
      overflowX: 'hidden'
    };

    var gutter = spacing.desktopGutter + 'px ';
    var title = {
      margin: 0,
      padding: gutter + gutter + '0 ' + gutter,
      color: this.state.muiTheme.rawTheme.palette.textColor,
      fontSize: 24,
      lineHeight: '32px',
      fontWeight: '400'
    };

    if (this.state.open) {
      main = this.mergeStyles(main, {
        left: 0,
        transition: Transitions.easeOut('0ms', 'left', '0ms')
      });
    }

    return {
      main: this.mergeStyles(main, this.props.style),
      content: this.mergeStyles(content, this.props.contentStyle),
      paper: {
        background: this.state.muiTheme.rawTheme.palette.canvasColor
      },
      body: this.mergeStyles(body, this.props.bodyStyle),
      title: this.mergeStyles(title, this.props.titleStyle)
    };
  },

  render: function render() {
    var styles = this.getStyles();
    var actions = this._getActionsContainer(this.props.actions);
    var title = undefined;
    if (this.props.title) {
      // If the title is a string, wrap in an h3 tag.
      // If not, just use it as a node.
      title = Object.prototype.toString.call(this.props.title) === '[object String]' ? React.createElement(
        'h3',
        { style: this.prepareStyles(styles.title) },
        this.props.title
      ) : this.props.title;
    }

    return React.createElement(
      'div',
      { ref: 'container', style: this.prepareStyles(styles.main) },
      React.createElement(
        ReactTransitionGroup,
        { component: 'div', ref: 'dialogWindow' },
        this.state.open && React.createElement(
          TransitionItem,
          {
            className: this.props.contentClassName,
            style: styles.content },
          React.createElement(
            Paper,
            {
              style: styles.paper,
              zDepth: 4 },
            title,
            React.createElement(
              'div',
              { ref: 'dialogContent', style: this.prepareStyles(styles.body) },
              this.props.children
            ),
            actions
          )
        )
      ),
      React.createElement(Overlay, {
        ref: 'dialogOverlay',
        show: this.state.open,
        autoLockScrolling: false,
        onTouchTap: this._handleOverlayTouchTap })
    );
  },

  isOpen: function isOpen() {
    return this.state.open;
  },

  _testDeprecations: function _testDeprecations() {
    process.env.NODE_ENV !== 'production' ? warning(!this.props.hasOwnProperty('openImmediately'), 'openImmediately has been deprecated in favor of defaultOpen') : undefined;

    process.env.NODE_ENV !== 'production' ? warning(!(typeof this.props.onShow === 'function'), 'onShow will be removed in favor of explicitly setting open') : undefined;

    process.env.NODE_ENV !== 'production' ? warning(!(typeof this.props.onDismiss === 'function'), 'onDismiss will be removed in favor of explicitly setting open and can be replaced by onRequestClose') : undefined;

    process.env.NODE_ENV !== 'production' ? warning(!this.props.hasOwnProperty('modal'), 'modal will be removed in favor of explicitly setting open and onRequestClose') : undefined;
  },

  _getAction: function _getAction(actionJSON, key) {
    var _this2 = this;

    var props = {
      key: key,
      secondary: true,
      onClick: actionJSON.onClick,
      onTouchTap: function onTouchTap() {
        if (actionJSON.onTouchTap) {
          actionJSON.onTouchTap.call(undefined);
        }
        if (!(actionJSON.onClick || actionJSON.onTouchTap)) {
          _this2._requestClose(true);
        }
      },
      label: actionJSON.text,
      style: {
        marginRight: 8
      }
    };

    if (actionJSON.ref) {
      props.ref = actionJSON.ref;
      props.keyboardFocused = actionJSON.ref === this.props.actionFocus;
    }
    if (actionJSON.id) {
      props.id = actionJSON.id;
    }

    return React.createElement(FlatButton, props);
  },

  _getActionsContainer: function _getActionsContainer(actions) {
    var actionContainer = undefined;
    var actionObjects = [];
    var actionStyle = {
      boxSizing: 'border-box',
      WebkitTapHighlightColor: 'rgba(0,0,0,0)',
      padding: 8,
      marginBottom: 8,
      width: '100%',
      textAlign: 'right'
    };

    if (actions.length) {
      for (var i = 0; i < actions.length; i++) {
        var currentAction = actions[i];

        //if the current action isn't a react object, create one
        if (!React.isValidElement(currentAction)) {
          currentAction = this._getAction(currentAction, i);
        }

        actionObjects.push(currentAction);
      }

      actionContainer = React.createElement(
        'div',
        { style: this.prepareStyles(actionStyle) },
        actionObjects
      );
    }

    return actionContainer;
  },

  _positionDialog: function _positionDialog() {
    if (this.state.open) {
      var clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
      var container = ReactDOM.findDOMNode(this);
      var dialogWindow = ReactDOM.findDOMNode(this.refs.dialogWindow);
      var dialogContent = ReactDOM.findDOMNode(this.refs.dialogContent);
      var minPaddingTop = 16;

      //Reset the height in case the window was resized.
      dialogWindow.style.height = '';
      dialogContent.style.height = '';

      var dialogWindowHeight = dialogWindow.offsetHeight;
      var paddingTop = (clientHeight - dialogWindowHeight) / 2 - 64;
      if (paddingTop < minPaddingTop) paddingTop = minPaddingTop;

      //Vertically center the dialog window, but make sure it doesn't
      //transition to that position.
      if (this.props.repositionOnUpdate || !container.style.paddingTop) {
        container.style.paddingTop = paddingTop + 'px';
      }

      // Force a height if the dialog is taller than clientHeight
      if (this.props.autoDetectWindowHeight || this.props.autoScrollBodyContent) {
        var styles = this.getStyles();
        var maxDialogContentHeight = clientHeight - 2 * (styles.body.padding + 64);

        if (this.props.title) maxDialogContentHeight -= dialogContent.previousSibling.offsetHeight;
        if (this.props.actions.length) maxDialogContentHeight -= dialogContent.nextSibling.offsetHeight;

        dialogContent.style.maxHeight = maxDialogContentHeight + 'px';
      }
    }
  },

  show: function show() {
    process.env.NODE_ENV !== 'production' ? warning(false, 'show has been deprecated in favor of explicitly setting the open property.') : undefined;

    this._show();
  },

  _onShow: function _onShow() {
    if (this.props.onShow) {
      this.props.onShow();
    }
  },

  _show: function _show() {
    //this.refs.dialogOverlay.preventScrolling();
    window.scroll(0, 0);
    this.setState({
      open: true
    }, this._onShow);
  },

  dismiss: function dismiss() {
    process.env.NODE_ENV !== 'production' ? warning(false, 'dismiss has been deprecated in favor of explicitly setting the open property.') : undefined;

    this._dismiss();
  },

  _onDismiss: function _onDismiss() {
    if (this.props.onDismiss) {
      this.props.onDismiss();
    }
  },

  _dismiss: function _dismiss() {
    var _this3 = this;
    window.scroll(0, 0); // bf if one dialog is open and it opens another

    CssEvent.onTransitionEnd(ReactDOM.findDOMNode(this), function () {
      _this3.refs.dialogOverlay.allowScrolling();
    });

    this.setState({
      open: false
    }, this._onDismiss);
  },

  _requestClose: function _requestClose(buttonClicked) {
    process.env.NODE_ENV !== 'production' ? warning(!this.props.hasOwnProperty('modal'), 'modal will be removed in favor of explicitly setting open and onRequestClose') : undefined;

    if (!buttonClicked && this.props.modal) {
      return;
    }

    // Close the dialog if the open state is not explicitly set.
    if (this.props.open === null) {
      this._dismiss();
    }
    if (this.props.onRequestClose) {
      this.props.onRequestClose(!!buttonClicked);
    }
  },

  _handleOverlayTouchTap: function _handleOverlayTouchTap() {
    this._requestClose(false);
  },

  _handleWindowKeyUp: function _handleWindowKeyUp(event) {
    if (event.keyCode === KeyCode.ESC) {
      this._requestClose(false);
    }
  },

  _handleResize: function _handleResize() {
    if (this.state.open) {
      //this.refs.dialogOverlay.preventScrolling();
      window.scroll(0, 0);
      this._positionDialog();
    }
  }

});

module.exports = Dialog;
@mbrookes
Copy link
Member

Please submit a PR with your changes - it makes it easier for us to read, comment and merge. Guide

@mbrookes mbrookes added the new feature New feature or request label Feb 13, 2016
@mbrookes mbrookes changed the title fix for dialog while using small screens [Dialog] Unusable on small screens Feb 13, 2016
@mbrookes mbrookes removed the new feature New feature or request label Mar 29, 2016
@FranBran
Copy link

Dialogs should never be obscured by other elements or appear partially on screen.

To disclose additional content in a dialog, do so using inline expansion within the content area. Or consider alternative components that are optimized for large amounts of content.

https://www.google.com/design/spec/components/dialogs.html#dialogs-behavior

@matthewoates
Copy link
Contributor

👍 On android phones any dialog with an input field causes the content to literally shrink to 0px tall.

@daniel-sim
Copy link

We've got the dialog working pretty well on mobile without too many hacks and no code changes to the dialog component: http://dsh.re/2eef2

<Dialog modal={true}
  repositionOnUpdate={false}
  autoDetectWindowHeight={false}
  autoScrollBodyContent={false}
  contentStyle={{
    width: '100%',
    maxWidth: '450px',
    maxHeight: '100% !important'
  }}
  bodyStyle={{
     maxHeight: '100% !important'
  }}
  style={{
     paddingTop:'0 !important',
     marginTop:'-65px !important',
     bottom: '0 !important',
     overflow: 'scroll !important',
     height: 'auto !important'
  }}>

@mpontikes mpontikes mentioned this issue Aug 5, 2016
13 tasks
@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>

@ThadeuLuz
Copy link

Hey I made this gist, it might help for this and other issues: https://gist.github.com/ThadeuLuz/0fafd4b44682df34ac807ebbd051a6f8

@oliviertassinari oliviertassinari added the component: dialog This is the name of the generic UI component, not the React module! label Dec 18, 2016
@oliviertassinari oliviertassinari added duplicate This issue or pull request already exists and removed bug 🐛 Something doesn't work component: dialog This is the name of the generic UI component, not the React module! labels Jul 28, 2017
@oliviertassinari
Copy link
Member

Closing as a duplicate of #7438. Having a scrollable dialog will address that limitation.

@oliviertassinari oliviertassinari marked this as a duplicate of #7438 Jul 28, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

8 participants