Skip to content

Commit 8dadbec

Browse files
committed
fix(emcn): keep focus on the input so the password Hide toggle actually masks
1 parent 9e45939 commit 8dadbec

2 files changed

Lines changed: 41 additions & 12 deletions

File tree

packages/emcn/src/components/chip-modal/chip-modal.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,26 @@ describe("ChipModalField inputType='password'", () => {
288288
expect(passwordInput().className).not.toContain(MASK_CLASS)
289289
expect(document.querySelector('[aria-label="Hide password"]')).not.toBeNull()
290290
})
291+
292+
it('hides a focused password instead of re-revealing it', () => {
293+
mountPasswordField('hunter2-secret')
294+
act(() => passwordInput().focus())
295+
296+
const toggle = document.querySelector<HTMLButtonElement>('[aria-label="Hide password"]')
297+
if (!toggle) throw new Error('Hide toggle did not render')
298+
299+
// jsdom does not move focus on mousedown, so model what a browser does: the
300+
// press blurs the input unless the handler prevents the default. Without the
301+
// control's preventDefault that blur re-masks first, and the click then
302+
// toggles back to revealed — leaving the password on screen.
303+
act(() => {
304+
const press = new MouseEvent('mousedown', { bubbles: true, cancelable: true })
305+
toggle.dispatchEvent(press)
306+
if (!press.defaultPrevented) passwordInput().blur()
307+
toggle.click()
308+
})
309+
310+
expect(passwordInput().className).toContain(MASK_CLASS)
311+
expect(document.querySelector('[aria-label="Show password"]')).not.toBeNull()
312+
})
291313
})

packages/emcn/src/components/chip-modal/chip-modal.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,20 @@ function handleSingleLineEnter(
800800
* is what stops a password manager autofilling the operator's own credentials
801801
* into a field that sets some other account's password.
802802
*/
803+
interface ChipModalPasswordControlProps {
804+
id: string
805+
value: string
806+
onChange: (value: string) => void
807+
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
808+
placeholder?: string
809+
maxLength?: number
810+
autoComplete?: string
811+
disabled?: boolean
812+
mono?: boolean
813+
/** ARIA the owning {@link ChipModalField} derives from its own state. */
814+
aria: ChipModalFieldAria
815+
}
816+
803817
function ChipModalPasswordControl({
804818
id,
805819
value,
@@ -811,18 +825,7 @@ function ChipModalPasswordControl({
811825
disabled,
812826
mono,
813827
aria,
814-
}: {
815-
id: string
816-
value: string
817-
onChange: (value: string) => void
818-
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
819-
placeholder?: string
820-
maxLength?: number
821-
autoComplete?: string
822-
disabled?: boolean
823-
mono?: boolean
824-
aria: ChipModalFieldAria
825-
}) {
828+
}: ChipModalPasswordControlProps) {
826829
const [revealed, setRevealed] = React.useState(false)
827830

828831
return (
@@ -853,6 +856,10 @@ function ChipModalPasswordControl({
853856
type='button'
854857
variant='ghost'
855858
disabled={disabled}
859+
// Keep focus on the input: letting the button take it would fire the
860+
// blur re-mask first, so the click would toggle back from `false`
861+
// and "Hide" would leave a focused password on screen.
862+
onMouseDown={(event) => event.preventDefault()}
856863
onClick={() => setRevealed((current) => !current)}
857864
className='size-6 flex-shrink-0 p-0 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
858865
aria-label={revealed ? 'Hide password' : 'Show password'}

0 commit comments

Comments
 (0)