Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "Shows TextField value as title attribute when disabled/read-only and truncated",
"packageName": "@fluentui/react",
"email": "tomas.blazek@brose.com",
"dependentChangeType": "none"
}
35 changes: 35 additions & 0 deletions packages/react/src/components/TextField/TextField.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class TextFieldBase

public componentDidMount(): void {
this._adjustInputHeight();
this._updateTitleAttribute();

if (this.props.validateOnLoad) {
this._validate(this.value);
Expand Down Expand Up @@ -196,6 +197,8 @@ export class TextFieldBase
this._delayedValidate(value);
}
}

this._updateTitleAttribute();
}

public render(): JSXElement {
Expand Down Expand Up @@ -655,6 +658,38 @@ export class TextFieldBase
}
}
}

private _updateTitleAttribute(): void {
const textElement = this._textElement.current;
const value = this.value;
const providedTitle = this.props.title;

if (!textElement) {
return;
}

// Always honor an explicitly provided title prop.
if (providedTitle !== undefined) {
textElement.title = providedTitle;
return;
}

if (
this.props.multiline ||
(!this.props.disabled && !this.props.readOnly) ||
!value ||
!this._isTextElementOverflowing(textElement)
) {
textElement.removeAttribute('title');
return;
}

textElement.title = value;
}

private _isTextElementOverflowing(textElement: HTMLTextAreaElement | HTMLInputElement): boolean {
return textElement.scrollWidth > textElement.clientWidth;
}
}

/** Get the value from the given state and props (converting from number to string if needed) */
Expand Down
66 changes: 66 additions & 0 deletions packages/react/src/components/TextField/TextField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ const textFieldRef: IRefObject<ITextField> = (ref: ITextField | null) => {

const noOp = () => undefined;

function setOverflow(element: HTMLElement, scrollWidth: number, clientWidth: number) {
Object.defineProperty(element, 'scrollWidth', {
configurable: true,
value: scrollWidth,
});
Object.defineProperty(element, 'clientWidth', {
configurable: true,
value: clientWidth,
});
}

function sharedBeforeEach() {
resetIds();
resetControlledWarnings();
Expand Down Expand Up @@ -267,6 +278,61 @@ describe('TextField basic props', () => {
const input = getByRole('textbox') as HTMLInputElement;
expect(input.readOnly).toEqual(true);
});
it('should apply title with the value when disabled and truncated', () => {
const value = 'some very long text that should overflow';
const { getByRole, rerender } = render(<TextField disabled value={value} onChange={noOp} />);
const input = getByRole('textbox') as HTMLInputElement;

setOverflow(input, 200, 100);
rerender(<TextField disabled value={value} onChange={noOp} />);

expect(input.title).toEqual(value);
});
it('should not apply title when disabled and not truncated', () => {
const value = 'short text';
const { getByRole, rerender } = render(<TextField disabled value={value} onChange={noOp} />);
const input = getByRole('textbox') as HTMLInputElement;

setOverflow(input, 100, 100);
rerender(<TextField disabled value={value} onChange={noOp} />);

expect(input.title).toEqual('');
});
it('should apply title with the value when readOnly and truncated', () => {
const value = 'some very long text that should overflow';
const { getByRole, rerender } = render(<TextField readOnly value={value} onChange={noOp} />);
const input = getByRole('textbox') as HTMLInputElement;

setOverflow(input, 200, 100);
rerender(<TextField readOnly value={value} onChange={noOp} />);

expect(input.title).toEqual(value);
});
it('should not apply title when readOnly and not truncated', () => {
const value = 'short text';
const { getByRole, rerender } = render(<TextField readOnly value={value} onChange={noOp} />);
const input = getByRole('textbox') as HTMLInputElement;

setOverflow(input, 100, 100);
rerender(<TextField readOnly value={value} onChange={noOp} />);

expect(input.title).toEqual('');
});
it('should not auto-apply title for multiline fields', () => {
const value = 'some very long text that should overflow';
const { getByRole } = render(<TextField multiline value={value} onChange={noOp} />);
const input = getByRole('textbox') as HTMLTextAreaElement;

setOverflow(input, 200, 100);

expect(input.title).toEqual('');
});
it('should preserve an explicitly provided title prop', () => {
const { getByRole } = render(<TextField multiline title="custom title" value="value" onChange={noOp} />);
const input = getByRole('textbox') as HTMLTextAreaElement;

expect(input.title).toEqual('custom title');
});
it('can render description text', () => {
const testDescription = 'A custom description';
const { container } = render(<TextField description={testDescription} />);
Expand Down