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

[OutlinedInput] Fix offscreen label strikethrough #17680

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/pages/api/outlined-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
| <span class="prop-name">inputComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">'input'</span> | The component used for the native input. Either a string to use a DOM element or a component. |
| <span class="prop-name">inputProps</span> | <span class="prop-type">object</span> | | [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element. |
| <span class="prop-name">inputRef</span> | <span class="prop-type">ref</span> | | Pass a ref to the `input` element. |
| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The width of the label. |
| <span class="prop-name">label</span> | <span class="prop-type">node</span> | | The label of the input. It is only used for layout. The actual labelling is handled by `InputLabel`. If specified `labelWidth` is ignored. |
| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The width of the label. Is ignored if `label` is provided. Prefer `label` if the input label appears with a strike through. |
| <span class="prop-name">margin</span> | <span class="prop-type">'dense'<br>&#124;&nbsp;'none'</span> | | If `dense`, will adjust vertical spacing. This is normally obtained via context from FormControl. |
| <span class="prop-name">multiline</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, a textarea element will be rendered. |
| <span class="prop-name">name</span> | <span class="prop-type">string</span> | | Name attribute of the `input` element. |
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/api/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
| <span class="prop-name">IconComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">ArrowDropDownIcon</span> | The icon that displays the arrow. |
| <span class="prop-name">input</span> | <span class="prop-type">element</span> | | An `Input` element; does not have to be a material-ui specific `Input`. |
| <span class="prop-name">inputProps</span> | <span class="prop-type">object</span> | | [Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) applied to the `input` element. When `native` is `true`, the attributes are applied on the `select` element. |
| <span class="prop-name">label</span> | <span class="prop-type">node</span> | | See [OutlinedLabel#label](/api/outlined-input/#props) |
| <span class="prop-name">labelId</span> | <span class="prop-type">string</span> | | The idea of an element that acts as an additional label. The Select will be labelled by the additional label and the selected value. |
| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | The label width to be used on OutlinedInput. This prop is required when the `variant` prop is `outlined`. |
| <span class="prop-name">labelWidth</span> | <span class="prop-type">number</span> | <span class="prop-default">0</span> | See OutlinedLabel#label |
| <span class="prop-name">MenuProps</span> | <span class="prop-type">object</span> | | Props applied to the [`Menu`](/api/menu/) element. |
| <span class="prop-name">multiple</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, `value` must be an array and the menu will support multiple selections. |
| <span class="prop-name">native</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the component will be using a native `select` element. |
Expand Down
17 changes: 2 additions & 15 deletions docs/src/pages/components/text-fields/ComposedTextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,9 @@ const useStyles = makeStyles(theme => ({
}));

export default function ComposedTextField() {
const [labelWidth, setLabelWidth] = React.useState(0);
const [name, setName] = React.useState('Composed TextField');
const labelRef = React.useRef(null);
const classes = useStyles();

React.useEffect(() => {
setLabelWidth(labelRef.current.offsetWidth);
}, []);

const handleChange = event => {
setName(event.target.value);
};
Expand Down Expand Up @@ -61,15 +55,8 @@ export default function ComposedTextField() {
<FormHelperText id="component-error-text">Error</FormHelperText>
</FormControl>
<FormControl variant="outlined">
<InputLabel ref={labelRef} htmlFor="component-outlined">
Name
</InputLabel>
<OutlinedInput
id="component-outlined"
value={name}
onChange={handleChange}
labelWidth={labelWidth}
/>
<InputLabel htmlFor="component-outlined">Name</InputLabel>
<OutlinedInput id="component-outlined" value={name} onChange={handleChange} label="Name" />
</FormControl>
<FormControl variant="filled">
<InputLabel htmlFor="component-filled">Name</InputLabel>
Expand Down
17 changes: 2 additions & 15 deletions docs/src/pages/components/text-fields/ComposedTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@ const useStyles = makeStyles((theme: Theme) =>
);

export default function ComposedTextField() {
const [labelWidth, setLabelWidth] = React.useState(0);
const [name, setName] = React.useState('Composed TextField');
const labelRef = React.useRef<HTMLLabelElement>(null);
const classes = useStyles();

React.useEffect(() => {
setLabelWidth(labelRef.current!.offsetWidth);
}, []);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
Expand Down Expand Up @@ -63,15 +57,8 @@ export default function ComposedTextField() {
<FormHelperText id="component-error-text">Error</FormHelperText>
</FormControl>
<FormControl variant="outlined">
<InputLabel ref={labelRef} htmlFor="component-outlined">
Name
</InputLabel>
<OutlinedInput
id="component-outlined"
value={name}
onChange={handleChange}
labelWidth={labelWidth}
/>
<InputLabel htmlFor="component-outlined">Name</InputLabel>
<OutlinedInput id="component-outlined" value={name} onChange={handleChange} label="Name" />
</FormControl>
<FormControl variant="filled">
<InputLabel htmlFor="component-filled">Name</InputLabel>
Expand Down
59 changes: 56 additions & 3 deletions packages/material-ui/src/OutlinedInput/NotchedOutline.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const styles = theme => {
left: 0,
margin: 0,
padding: 0,
paddingLeft: 8,
pointerEvents: 'none',
borderRadius: 'inherit',
borderStyle: 'solid',
Expand All @@ -28,16 +29,40 @@ export const styles = theme => {
easing: theme.transitions.easing.easeOut,
}),
},
/* Styles applied to the legend element. */
/* Styles applied to the legend element when `labelWidth` is provided. */
legend: {
textAlign: 'left',
padding: 0,
lineHeight: '11px',
lineHeight: '11px', // sync with `height` in `legend` styles
transition: theme.transitions.create('width', {
duration: theme.transitions.duration.shorter,
easing: theme.transitions.easing.easeOut,
}),
},
/* Styles applied to the legend element. */
legendLabelled: {
textAlign: 'left',
padding: 0,
height: 11, // sync with `lineHeight` in `legend` styles
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the box size computation if max-width < actual width. Seems like line-height is ignored if max-width < width

fontSize: '0.75rem',
visibility: 'hidden',
maxWidth: 0.01,
transition: theme.transitions.create('max-width', {
duration: theme.transitions.duration.shorter,
easing: theme.transitions.easing.easeOut,
}),
'& span': {
paddingLeft: 4,
paddingRight: 6,
},
},
legendNotched: {
maxWidth: 1000,
transition: theme.transitions.create('max-width', {
duration: theme.transitions.duration.shorter,
easing: theme.transitions.easing.easeOut,
}),
},
};
};

Expand All @@ -49,14 +74,38 @@ const NotchedOutline = React.forwardRef(function NotchedOutline(props, ref) {
children,
classes,
className,
label,
labelWidth: labelWidthProp,
notched,
style,
...other
} = props;
const theme = useTheme();
const align = theme.direction === 'rtl' ? 'right' : 'left';
const labelWidth = labelWidthProp > 0 ? labelWidthProp * 0.75 + 8 : 0;

if (label !== undefined) {
return (
<fieldset
aria-hidden
className={clsx(classes.root, className)}
ref={ref}
style={style}
{...other}
>
<legend
className={clsx(classes.legendLabelled, {
[classes.legendNotched]: notched,
})}
>
{/* Use the nominal use case of the legend, avoid rendering artefacts. */}
{/* eslint-disable-next-line react/no-danger */}
{label ? <span>{label}</span> : <span dangerouslySetInnerHTML={{ __html: '&#8203;' }} />}
</legend>
</fieldset>
);
}

const labelWidth = labelWidthProp > 0 ? labelWidthProp * 0.75 + 8 : 0.01;

return (
<fieldset
Expand Down Expand Up @@ -100,6 +149,10 @@ NotchedOutline.propTypes = {
* @ignore
*/
className: PropTypes.string,
/**
* The label.
*/
label: PropTypes.node,
/**
* The width of the label.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/material-ui/src/OutlinedInput/OutlinedInput.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { StandardProps } from '..';
import { InputBaseProps } from '../InputBase';

export interface OutlinedInputProps extends StandardProps<InputBaseProps, OutlinedInputClassKey> {
label?: React.ReactNode;
notched?: boolean;
labelWidth: number;
labelWidth?: number;
}

export type OutlinedInputClassKey =
Expand Down
10 changes: 9 additions & 1 deletion packages/material-ui/src/OutlinedInput/OutlinedInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const OutlinedInput = React.forwardRef(function OutlinedInput(props, ref) {
classes,
fullWidth = false,
inputComponent = 'input',
label,
labelWidth = 0,
multiline = false,
notched,
Expand All @@ -115,6 +116,7 @@ const OutlinedInput = React.forwardRef(function OutlinedInput(props, ref) {
renderSuffix={state => (
<NotchedOutline
className={classes.notchedOutline}
label={label}
labelWidth={labelWidth}
notched={
typeof notched !== 'undefined'
Expand Down Expand Up @@ -201,7 +203,13 @@ OutlinedInput.propTypes = {
*/
inputRef: refType,
/**
* The width of the label.
* The label of the input. It is only used for layout. The actual labelling
* is handled by `InputLabel`. If specified `labelWidth` is ignored.
*/
label: PropTypes.node,
/**
* The width of the label. Is ignored if `label` is provided. Prefer `label`
* if the input label appears with a strike through.
*/
labelWidth: PropTypes.number,
/**
Expand Down
1 change: 1 addition & 0 deletions packages/material-ui/src/Select/Select.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface SelectProps
displayEmpty?: boolean;
IconComponent?: React.ElementType;
input?: React.ReactNode;
label?: React.ReactNode;
labelId?: string;
labelWidth?: number;
MenuProps?: Partial<MenuProps>;
Expand Down
10 changes: 7 additions & 3 deletions packages/material-ui/src/Select/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const Select = React.forwardRef(function Select(props, ref) {
id,
input,
inputProps,
label,
labelId,
labelWidth = 0,
MenuProps,
Expand Down Expand Up @@ -53,7 +54,7 @@ const Select = React.forwardRef(function Select(props, ref) {
input ||
{
standard: <Input />,
outlined: <OutlinedInput labelWidth={labelWidth} />,
outlined: <OutlinedInput label={label} labelWidth={labelWidth} />,
filled: <FilledInput />,
}[variant];

Expand Down Expand Up @@ -141,14 +142,17 @@ Select.propTypes = {
* When `native` is `true`, the attributes are applied on the `select` element.
*/
inputProps: PropTypes.object,
/**
* See [OutlinedLabel#label](/api/outlined-input/#props)
*/
label: PropTypes.node,
/**
* The idea of an element that acts as an additional label. The Select will
* be labelled by the additional label and the selected value.
*/
labelId: PropTypes.string,
/**
* The label width to be used on OutlinedInput.
* This prop is required when the `variant` prop is `outlined`.
* See OutlinedLabel#label
*/
labelWidth: PropTypes.number,
/**
Expand Down
22 changes: 9 additions & 13 deletions packages/material-ui/src/TextField/TextField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { refType } from '@material-ui/utils';
Expand Down Expand Up @@ -93,16 +92,6 @@ const TextField = React.forwardRef(function TextField(props, ref) {
...other
} = props;

const [labelWidth, setLabelWidth] = React.useState(0);
const labelRef = React.useRef(null);
React.useEffect(() => {
if (variant === 'outlined') {
// #StrictMode ready
const labelNode = ReactDOM.findDOMNode(labelRef.current);
setLabelWidth(labelNode != null ? labelNode.offsetWidth : 0);
}
}, [variant, required, label]);

if (process.env.NODE_ENV !== 'production') {
if (select && !children) {
console.error(
Expand All @@ -118,7 +107,14 @@ const TextField = React.forwardRef(function TextField(props, ref) {
InputMore.notched = InputLabelProps.shrink;
}

InputMore.labelWidth = labelWidth;
InputMore.label = label ? (
<React.Fragment>
{label}
{required && '\u00a0*'}
</React.Fragment>
) : (
label
);
}
if (select) {
// unset defaults from textbox inputs
Expand Down Expand Up @@ -170,7 +166,7 @@ const TextField = React.forwardRef(function TextField(props, ref) {
{...other}
>
{label && (
<InputLabel htmlFor={id} ref={labelRef} id={inputLabelId} {...InputLabelProps}>
<InputLabel htmlFor={id} id={inputLabelId} {...InputLabelProps}>
{label}
</InputLabel>
)}
Expand Down
14 changes: 12 additions & 2 deletions packages/material-ui/src/TextField/TextField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,19 @@ describe('<TextField />', () => {

describe('with an outline', () => {
it('should set outline props', () => {
const wrapper = mount(<TextField variant="outlined" />);
const { container, getAllByTestId } = render(
<TextField
InputProps={{ classes: { notchedOutline: 'notch' } }}
label={<div data-testid="label">label</div>}
required
variant="outlined"
/>,
);

expect(wrapper.find(OutlinedInput).props()).to.have.property('labelWidth', 0);
const [, fakeLabel] = getAllByTestId('label');
const notch = container.querySelector('.notch legend');
expect(notch).to.contain(fakeLabel);
expect(notch).to.have.text('label\u00a0*');
});

it('should set shrink prop on outline from label', () => {
Expand Down