diff --git a/change/@fluentui-react-4beba3a6-0d78-4d2a-a593-a4b1d998a4d9.json b/change/@fluentui-react-4beba3a6-0d78-4d2a-a593-a4b1d998a4d9.json new file mode 100644 index 00000000000000..cdae7151914cde --- /dev/null +++ b/change/@fluentui-react-4beba3a6-0d78-4d2a-a593-a4b1d998a4d9.json @@ -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" +} diff --git a/packages/react/src/components/TextField/TextField.base.tsx b/packages/react/src/components/TextField/TextField.base.tsx index aac22450187717..e90392ff99603b 100644 --- a/packages/react/src/components/TextField/TextField.base.tsx +++ b/packages/react/src/components/TextField/TextField.base.tsx @@ -133,6 +133,7 @@ export class TextFieldBase public componentDidMount(): void { this._adjustInputHeight(); + this._updateTitleAttribute(); if (this.props.validateOnLoad) { this._validate(this.value); @@ -196,6 +197,8 @@ export class TextFieldBase this._delayedValidate(value); } } + + this._updateTitleAttribute(); } public render(): JSXElement { @@ -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) */ diff --git a/packages/react/src/components/TextField/TextField.test.tsx b/packages/react/src/components/TextField/TextField.test.tsx index c54b5e01843311..e68075ca2104b8 100644 --- a/packages/react/src/components/TextField/TextField.test.tsx +++ b/packages/react/src/components/TextField/TextField.test.tsx @@ -25,6 +25,17 @@ const textFieldRef: IRefObject = (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(); @@ -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(); + const input = getByRole('textbox') as HTMLInputElement; + + setOverflow(input, 200, 100); + rerender(); + + expect(input.title).toEqual(value); + }); + it('should not apply title when disabled and not truncated', () => { + const value = 'short text'; + const { getByRole, rerender } = render(); + const input = getByRole('textbox') as HTMLInputElement; + + setOverflow(input, 100, 100); + rerender(); + + 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(); + const input = getByRole('textbox') as HTMLInputElement; + + setOverflow(input, 200, 100); + rerender(); + + expect(input.title).toEqual(value); + }); + it('should not apply title when readOnly and not truncated', () => { + const value = 'short text'; + const { getByRole, rerender } = render(); + const input = getByRole('textbox') as HTMLInputElement; + + setOverflow(input, 100, 100); + rerender(); + + 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(); + 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(); + 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();