diff --git a/change/@fluentui-react-bf8f90c6-793f-49b5-baff-4859f3b7aa14.json b/change/@fluentui-react-bf8f90c6-793f-49b5-baff-4859f3b7aa14.json new file mode 100644 index 0000000000000..d6aef6fd6dee8 --- /dev/null +++ b/change/@fluentui-react-bf8f90c6-793f-49b5-baff-4859f3b7aa14.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(react): show TextField's themable password reveal button in Edge and hide the native ::-ms-reveal, and give it a resting forced-colors color", + "packageName": "@fluentui/react", + "email": "144495202+AKnassa@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/react/src/components/TextField/TextField.base.tsx b/packages/react/src/components/TextField/TextField.base.tsx index aac2245018771..279aa1e945488 100644 --- a/packages/react/src/components/TextField/TextField.base.tsx +++ b/packages/react/src/components/TextField/TextField.base.tsx @@ -7,7 +7,6 @@ import { DelayedRender, getId, getNativeProps, - getWindow, initializeComponentRef, inputProperties, isControlled, @@ -680,16 +679,10 @@ let __browserNeedsRevealButton: boolean | undefined; function _browserNeedsRevealButton() { if (typeof __browserNeedsRevealButton !== 'boolean') { - const win = getWindow(); - - if (win?.navigator) { - // Edge, Chromium Edge - const isEdge = /Edg/.test(win.navigator.userAgent || ''); - - __browserNeedsRevealButton = !(isIE11() || isEdge); - } else { - __browserNeedsRevealButton = true; - } + // Only IE 11 keeps its unhideable native reveal button. Edge's native ::-ms-reveal is + // suppressed via the field styles so the themable, keyboard-accessible custom button can + // be shown instead (otherwise both would render). + __browserNeedsRevealButton = !isIE11(); } return __browserNeedsRevealButton; } diff --git a/packages/react/src/components/TextField/TextField.styles.tsx b/packages/react/src/components/TextField/TextField.styles.tsx index 20058d491eb13..5efc9677ac8f7 100644 --- a/packages/react/src/components/TextField/TextField.styles.tsx +++ b/packages/react/src/components/TextField/TextField.styles.tsx @@ -318,6 +318,14 @@ export function getStyles(props: ITextFieldStyleProps): ITextFieldStyles { }, }, getPlaceholderStyles(placeholderStyles), + hasRevealButton && { + selectors: { + // Hide Edge's built-in reveal button since the custom one is shown instead + '::-ms-reveal': { + display: 'none', + }, + }, + }, multiline && !resizable && [ classNames.unresizable, @@ -429,6 +437,9 @@ export function getStyles(props: ITextFieldStyleProps): ITextFieldStyles { backgroundColor: 'transparent', color: semanticColors.link, selectors: { + [HighContrastSelector]: { + color: 'ButtonText', + }, ':hover': { outline: 0, color: semanticColors.primaryButtonBackgroundHovered, diff --git a/packages/react/src/components/TextField/TextField.test.tsx b/packages/react/src/components/TextField/TextField.test.tsx index c54b5e0184331..502b2579886f6 100644 --- a/packages/react/src/components/TextField/TextField.test.tsx +++ b/packages/react/src/components/TextField/TextField.test.tsx @@ -246,6 +246,33 @@ describe('TextField basic props', () => { userEvent.click(reveal); expect(input.type).toEqual('password'); }); + it('renders reveal password button in Edge (Edg UA)', () => { + const originalUAProp = Object.getOwnPropertyDescriptor(window.navigator, 'userAgent'); + Object.defineProperty(window.navigator, 'userAgent', { + value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + + 'Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0', + configurable: true, + }); + // Reuse the already-loaded React inside the isolated module registry so hooks in the + // freshly-loaded component tree use the same dispatcher as the outer `render` + jest.doMock('react', () => React); + try { + jest.isolateModules(() => { + // Fresh module instance so the cached __browserNeedsRevealButton is recomputed + const { TextField: IsolatedTextField } = require('./TextField'); + const { container } = render(); + expect(container.querySelectorAll('.ms-TextField-reveal')).toHaveLength(1); + }); + } finally { + jest.dontMock('react'); + if (originalUAProp) { + Object.defineProperty(window.navigator, 'userAgent', originalUAProp); + } else { + delete (window.navigator as any).userAgent; + } + } + }); it('should not give an aria-labelledby if no label is provided', () => { const { getByRole } = render(); const input = getByRole('textbox') as HTMLInputElement; diff --git a/packages/react/src/components/TextField/TextField.types.ts b/packages/react/src/components/TextField/TextField.types.ts index e583632178801..c7d719caaf18a 100644 --- a/packages/react/src/components/TextField/TextField.types.ts +++ b/packages/react/src/components/TextField/TextField.types.ts @@ -285,15 +285,16 @@ export interface ITextFieldProps extends React.AllHTMLAttributes