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": "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"
}
15 changes: 4 additions & 11 deletions packages/react/src/components/TextField/TextField.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
DelayedRender,
getId,
getNativeProps,
getWindow,
initializeComponentRef,
inputProperties,
isControlled,
Expand Down Expand Up @@ -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;
}
11 changes: 11 additions & 0 deletions packages/react/src/components/TextField/TextField.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions packages/react/src/components/TextField/TextField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<IsolatedTextField type="password" canRevealPassword />);
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(<TextField />);
const input = getByRole('textbox') as HTMLInputElement;
Expand Down
9 changes: 5 additions & 4 deletions packages/react/src/components/TextField/TextField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,15 +285,16 @@ export interface ITextFieldProps extends React.AllHTMLAttributes<HTMLInputElemen

/**
* Whether to show the reveal password button for input type `'password'`. This will be ignored
* if the `type` prop is not set to `'password'`, or if the browser is known to have a built-in
* reveal button for password inputs (Edge, IE).
* if the `type` prop is not set to `'password'`, or in IE 11 (which has a built-in unhideable
* reveal button). In other browsers with a native reveal button (Edge), the native button is
* hidden in favor of this one.
*/
canRevealPassword?: boolean;

/**
* If `canRevealPassword` is true, aria label for the reveal password button (example: "Show
* password"). Note that this will NOT be used in browsers known to have a built-in reveal
* password button for password inputs (Edge, IE).
* password"). Note that this will NOT be used in IE 11, which only shows its built-in reveal
* button for password inputs.
*/
revealPasswordAriaLabel?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,9 @@ exports[`TextField snapshots renders with reveal password button 1`] = `
@media screen and (-ms-high-contrast: active), screen and (forced-colors: active){&::-ms-input-placeholder {
color: GrayText;
}
&::-ms-reveal {
display: none;
}
id="TextField0"
type="password"
value=""
Expand Down Expand Up @@ -1362,6 +1365,9 @@ exports[`TextField snapshots renders with reveal password button 1`] = `
top: 2px;
z-index: 1;
}
@media screen and (-ms-high-contrast: active), screen and (forced-colors: active){& {
color: ButtonText;
}
&:hover {
background-color: #f3f2f1;
color: #106ebe;
Expand Down