mui / material-ui

Materialย UI: Ready-to-use foundational React components, free forever. It includes Material UI, which implements Google's Material Design.
https://mui.com/material-ui/
MIT License
92.04k stars 31.64k forks source link

[TextField] Add outlined and filled variants #12076

Closed enagy27 closed 5 years ago

enagy27 commented 5 years ago

capture d ecran 2018-09-16 a 13 26 17

capture d ecran 2018-09-16 a 13 26 07

Closes #11962

oliviertassinari commented 5 years ago

I've taken out the polyfill in favor

Awesome. Regarding taking the best API tradeoff, I believe implementing the filled variant at the same time would allow to have the big picture. I'm having a closer look at the pull request.

mbrookes commented 5 years ago

I believe implementing the filled variant at the same time would allow to have the big picture.

Yeah, in my implementation, I started with the filled variant, then added the outlined. The filled is mercifully straightforward compared to the Outlined.

ghost commented 5 years ago

Cool. I'm working on an implementation using fieldset now. It's going very well- should have it ready for review by the end of next week! Will include both variants in that PR. Going this direction will make it very simple to apply the variant, and I'm feeling good about the top-level API ๐Ÿ˜ƒ

oliviertassinari commented 5 years ago

Cool. I'm working on an implementation using fieldset now.

We have some examples in the documentation doing such. I haven't looked into the topic. From my perspective, the fieldset is useful to group multiples fields. Here, we have a single input. What makes you think it's more appropriate?

ghost commented 5 years ago

@oliviertassinari in one of your earlier comments you mentioned this component as being a possible alternative to the svg, which seems appropriate given that its default styling is similar to the svg. It also has the benefit of being styled more easily than the svg, where its width and height are 100% of the corresponding input, and its border radius and other properties can be set, whereas they cannot be set without sending them down as a property on the svg.

Understandably, this is not its intended purpose which means it's not exactly semantic, but it seems like a better route than the svg.

Which route do you think is most appropriate?

ghost commented 5 years ago

To be clear, this is not leveraging fieldset from the form control, but instead changing the notched outline to use fieldset internally like this:

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { withStyles } from '../styles';

export const styles = theme => {
  const light = theme.palette.type === 'light';

  return {
    /* Styles applied to the root element. */
    root: {
      position: 'absolute',
      width: '100%',
      height: '100%',
      top: 0,
      left: 0,
      margin: 0,
      padding: 0,
      pointerEvents: 'none',
      borderRadius: theme.shape.borderRadius,
      borderStyle: 'solid',
      borderWidth: '1px',
      borderColor: light ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)',

      // Match the Input Label
      transition: theme.transitions.create(['borderColor', 'border-width'], {
        duration: theme.transitions.duration.shorter,
        easing: theme.transitions.easing.easeOut,
      }),
    },
    /* Styles applied to the legend element. */
    legend: {
      marginLeft: 8,
      padding: 0,

      transition: theme.transitions.create('width', {
        duration: theme.transitions.duration.shorter,
        easing: theme.transitions.easing.easeOut,
      }),
    },
    /* Styles applied to the root element if the form control is focused. */
    focused: {
      borderColor: theme.palette.primary.main,
      borderWidth: '2px',
    },
    /* Styles applied to the root element if `error={true}` for the form control. */
    error: {
      borderColor: theme.palette.error.main,
    },
    /* Styles applied to the root element if `disabled={true}` for the form control. */
    disabled: {
      borderColor: theme.palette.action.disabled,
    },
  };
};

/**
 * An outline for form control elements which opens to fit a label.
 */
function NotchedOutline(props, context) {
  const {
    adornedStart: adornedStartProp,
    disabled: disabledProp,
    error: errorProp,
    children,
    classes,
    className,
    filled: filledProp,
    focused: focusedProp,
    notched: notchedProp,
    notchWidth,
    theme,
    ...other
  } = props;

  const { muiFormControl } = context;
  let adornedStart = adornedStartProp;
  let disabled = disabledProp;
  let error = errorProp;
  let filled = filledProp;
  let focused = focusedProp;

  if (muiFormControl) {
    if (typeof adornedStart === 'undefined') {
      adornedStart = muiFormControl.adornedStart;
    }
    if (typeof disabled === 'undefined') {
      disabled = muiFormControl.disabled;
    }
    if (typeof error === 'undefined') {
      error = muiFormControl.error;
    }
    if (typeof filled === 'undefined') {
      filled = muiFormControl.filled;
    }
    if (typeof focused === 'undefined') {
      focused = muiFormControl.focused;
    }
  }

  const notched = notchedProp || focused || filled || adornedStart;

  return (
    <fieldset
      className={classNames(
        classes.root,
        {
          [classes.focused]: focused,
          [classes.error]: error,
          [classes.disabled]: disabled,
        },
        className,
      )}
      {...other}
    >
      <legend
        align={theme.direction === 'rtl' ? 'right' : 'left'}
        style={{ width: notched ? notchWidth : 0 }}
        className={classes.legend}
      />
    </fieldset>
  );
}

NotchedOutline.propTypes = {
  /**
   * The content of the outline.
   */
  children: PropTypes.node,
  /**
   * Override or extend the styles applied to the component.
   * See [CSS API](#css-api) below for more details.
   */
  classes: PropTypes.object.isRequired,
  /** @ignore */
  className: PropTypes.string,
  /** If `true`, the outline is notched to accommodate text. */
  notched: PropTypes.bool,
  /** The width of the notch, where a label will be placed. */
  notchWidth: PropTypes.number.isRequired,
  /**
   * Render outline for a select component.
   */
  select: PropTypes.bool,
  /**
   * @ignore
   */
  theme: PropTypes.object,
};

NotchedOutline.contextTypes = {
  muiFormControl: PropTypes.object,
};

NotchedOutline.muiName = 'NotchedOutline';

export default withStyles(styles, { withTheme: true, name: 'MuiNotchedOutline' })(NotchedOutline);

Which would be consumed as a sibling of the InputComponent in Input.js

oliviertassinari commented 5 years ago

@enagy27 My only fear with the fieldset route is around accessibility & semantic. We need to make sure it's OK.

ghost commented 5 years ago

Gotcha. To address accessibility, my intent is to mark the outline as aria-hidden. In this implementation the label will remain the same and will just be positioned appropriately within the opening. I believe this should cover any concerns there, but I'm not well versed in accessibility. Will this be sufficient?

On the semantic note I believe the ease of styling and the existence of a native solution outweighs this drawback for me. Is that a deal breaker for you?

oliviertassinari commented 5 years ago

@enagy-earnup The discussion here start to be too long for my computer to display it correctly. Can we chat on gitter? Or reset it with a new pull request?

I think that aria-hidden will be enough. To try, we also need to make sure the output is HTML valid and that it works with the older browsers. Yeah, It's all good to me if we can make the implementation much simpler. How do you think the animation can work?

Michael-M-Judd commented 5 years ago

Just thought I'd say this is looking really good! I'd love to use this in prod material-ui... Any idea of when this could be merged in?

oliviertassinari commented 5 years ago

@Michael-M-Judd It will most likely be released mid-September.

mbrookes commented 5 years ago

@enagy-earnup A couple of small things:

  1. According to the spec for the filled variant, the focused state should have a darker fill than hover. I had used:
    filled: {
      cursor: 'text',
      borderRadius: '4px 4px 0 0',
      backgroundColor: 'rgba(0,0,0,0.045)',
      '&:hover': {
        backgroundColor: 'rgba(0,0,0,0.07)',
      },
      transition: theme.transitions.create('background-color', {
        duration: theme.transitions.duration.shortest,
      }),
    },
    focusedFilled: {
      backgroundColor: 'rgba(0,0,0,0.11)',
      transition: theme.transitions.create('background-color', {
        duration: theme.transitions.duration.shortest,
      }),
    },
  1. For the multiline text fileds, it should ideally be possible for the user to focus the input by clicking anywhere in it. At the moment it gets the hover color, but only part (the taxtarea) is clickable as indicated by the cursor style. I had the textarea the full size of the textfiled, with appropriate padding, but that isn't the right solution, as the text overflows into the padding for some reason. (That was that point at which I moved on to outlined!). We might need to have a click on the outer component to programmatically focus the textarea?

  2. For some reason we don't have examples of disabled text fields. I had added those to each of the demos, right after the "error" example - would be great if you could do the same.

enagy27 commented 5 years ago

@mbrookes Thanks for that!

The spec threw me off a little on the filled variant because active was light, but focused was dark. We are not distinguishing between the focused and active states, so that seems to make the most sense- I'll change that. This also helps with some styling cases for the select where the input itself does not fill the container and causes odd shading presentation.

Good call, I'll see what I can do about using interactions on the outer div for more of these interactions. I'll also need to move some of the padding back to the outer div for the start and end adornments, so this looks like it'll help in a number of cases.

On the examples- you got it!

enagy27 commented 5 years ago

@mbrookes

The more I look at these states, the more puzzled I get. When I take a sample of these states, the active typing and inactive states seem to have the same shading ๐Ÿค” I've just realized also that I'm missing a lack of underline in the inactive state. I wonder if this is why it looks off.

image

mbrookes commented 5 years ago

The more I look at these states, the more puzzled I get.

ISWYM! I hadn't even noticed the "activated" state - to me it's the same as focused.

Looking at MCW, thay seem to have followed the same three-shade approach as ours: https://material-components.github.io/material-components-web-catalog/#/component/text-field

Regarding the underline, I had noticed that before - there are contradictory examples throughout that page, and while the states don't include it, the spec does.

Again, looking at MCW, they include it. That's also the simplest option.

enagy27 commented 5 years ago

@mbrookes Cool, I've added back the underline and will darken on focus. Absolutely makes the most sense. Also added cursor: text and forced focused for the root onClick.

The last challenge I'm having is the placement of the adornments. Ideally the input should fill the entire container, which helps to account for input-specific styling, such as the yellow autocomplete coloring. Unfortunately this means that the input adornments are not positioned properly. Part of the challenge here I guess is that in the spec the label also moves to accommodate the adornments.

In order to position the adornments properly, my options seem a bit limited:

  1. Following MCW's lead, leave a set space at the beginning or the end for the adornments. This requires the label to move to the right to allow for vertically centered content, and creates inconsistency with the original underlined input styling.
  2. Without FormControl context, I could add the variant or margin prop to the InputAdornment. This seems like an unwieldy or unintuitive solution.
  3. Accept the need for styling this manually

These all seem like not great options. Wondering if you've got thoughts how we might tackle this?

mbrookes commented 5 years ago

It looks like the style change that's needed in InputAdornment root style is to disable display: flex, and apply margin-top; but only when the TextField variant is outlined.

Given the choice between conditionally applying a class to the adornments in Input, or checking context in InputAdornment, if feels like the latter would be the least messy.

Thanks for adding the disabled examples. Could you tweak the ordering for the default variant?

enagy27 commented 5 years ago

Sounds great! There's one minor snag that I'm seeing. Input is setting the form control context to null, which means that it is not accessible from this level. This is also a challenge for the outline, but given that it is only used in this context and rendered directly within Input this was ok, as we could still pass focused as a prop.

Seeing a few different solutions, and I'm not sure which is most in-line with future work.

  1. Clone the startAdornment and the endAdornment and inject the variant prop
  2. Accept adornments as render props as well or nodes and conditionally inject the variant prop if using the render prop API
  3. Prevent setting muiFormControl context to null in Input

In what way do you mean for tweaking the order of the default variant? Are you referring to showing the adornments sections immediately after their corresponding sections?

oliviertassinari commented 5 years ago

Input is setting the form control context to null, which means that it is not accessible from this level.

@enagy27 The Input is setting the context to null for its children to avoid infinite rendering cycles.

I'm not sure which is most in-line with future work

For now, I would say add a variant property to the InputAdornment component. It's consistent with asking for a position property.

mbrookes commented 5 years ago

Thanks for adding the disabled examples. Could you tweak the ordering for the default variant?

In what way do you mean for tweaking the order of the default variant? Are you referring to showing the adornments sections immediately after their corresponding sections?

I was referring to the disabled examples. The order is different for the default variant โ€“ it comes before the error example. Not a big deal, but might be nice to be consistent.

enagy27 commented 5 years ago

Oh, my mistake. Fixed!

enagy27 commented 5 years ago

I've added the variant prop to the InputAdornment. Questioning if doing this via cloneElement will constitute a breaking change and therefore that this should be a prop that's manually applied ๐Ÿค”

oliviertassinari commented 5 years ago

Questioning if doing this via cloneElement will constitute a breaking change and therefore that this should be a prop that's manually applied ๐Ÿค”

@enagy27 I don't think that it really matters now. I think that we can focus on merging the pull request, sort that out after. cloneElement isn't a breaking change. The issue is more about cloneElement being an anti-pattern, it's unpredictable from a user point of view. I know we are already using it at a lot of different places, the fewer, the better.

enagy27 commented 5 years ago

Absolutely. Let me know if there's anything further you need from me ๐Ÿ˜ƒ

oliviertassinari commented 5 years ago

I'm expecting people to open issues regarding the new variants once we release them. It's a major undertaking. Let's hope for the best ๐Ÿคž๐Ÿ€.

mbrookes commented 5 years ago

@enagy27 Thanks for the solid contribution, and for your perseverance given all the back and forth to arrive at the best approach!

@oliviertassinari

I'm expecting people to open issues

The only small issue I can see right now is the height of the select text fields, as mentioned in chat. But as you say, let's see! Nice job on the InputBase.