Skip to content

Commit

Permalink
fix(input): prevent layout expansion when password managers aren't there
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhermerodz committed Mar 18, 2024
1 parent 115de01 commit 20f791b
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 60 deletions.
2 changes: 1 addition & 1 deletion apps/website/src/app/(local-pages)/shadcn/pwmb/page.tsx
Expand Up @@ -26,7 +26,7 @@ export default function ShadcnPage() {
// test pwmb
type="password"
autoComplete="password webauthn"
pushPasswordManagerStrategy="experimental-no-flickering"
pushPasswordManagerStrategy="increase-width"
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
//
// value={value}
Expand Down
27 changes: 8 additions & 19 deletions packages/input-otp/src/input.tsx
Expand Up @@ -5,10 +5,12 @@ import * as React from 'react'
import { REGEXP_ONLY_DIGITS } from './regexp'
import { syncTimeouts } from './sync-timeouts'
import { OTPInputProps, RenderProps } from './types'
import { usePasswordManagerBadge } from './use-pwm-badge'
import { usePrevious } from './use-previous'
import { usePasswordManagerBadge } from './use-pwm-badge'

export const OTPInputContext = React.createContext<RenderProps>({} as RenderProps)
export const OTPInputContext = React.createContext<RenderProps>(
{} as RenderProps,
)

export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
(
Expand Down Expand Up @@ -59,7 +61,6 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
/** useRef */
const inputRef = React.useRef<HTMLInputElement>(null)
const containerRef = React.useRef<HTMLDivElement>(null)
const pwmAreaRef = React.useRef<HTMLDivElement>(null)
const initialLoadRef = React.useRef({
value,
onChange,
Expand Down Expand Up @@ -267,8 +268,8 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
}, [maxLength, onComplete, previousValue, value])

const pwmb = usePasswordManagerBadge({
containerRef,
inputRef,
pwmAreaRef: pwmAreaRef,
pushPasswordManagerStrategy,
isFocused,
})
Expand Down Expand Up @@ -341,7 +342,6 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'none',
// clipPath: willPushPWMBadge ? 'inset(-2px)' : undefined,
}),
[props.disabled],
)
Expand All @@ -353,6 +353,9 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
width: pwmb.willPushPWMBadge
? `calc(100% + ${pwmb.PWM_BADGE_SPACE_WIDTH})`
: '100%',
clipPath: pwmb.willPushPWMBadge
? `inset(0 ${pwmb.PWM_BADGE_SPACE_WIDTH} 0 0)`
: undefined,
height: '100%',
display: 'flex',
textAlign,
Expand Down Expand Up @@ -496,20 +499,6 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
style={rootStyle}
className={containerClassName}
>
<div
ref={pwmAreaRef}
style={{
position: 'absolute',
top: 0,
right: `calc(-1 * ${pwmb.PWM_BADGE_SPACE_WIDTH})`,
bottom: 0,
left: '100%',
pointerEvents: 'none',
userSelect: 'none',
background: 'transparent',
}}
/>

{renderedChildren}

<div
Expand Down
5 changes: 1 addition & 4 deletions packages/input-otp/src/types.ts
Expand Up @@ -20,10 +20,7 @@ type OTPInputBaseProps = OverrideProps<
textAlign?: 'left' | 'center' | 'right'

onComplete?: (...args: any[]) => unknown
pushPasswordManagerStrategy?:
| 'increase-width'
| 'none'
| 'experimental-no-flickering'
pushPasswordManagerStrategy?: 'increase-width' | 'none'

containerClassName?: string

Expand Down
80 changes: 44 additions & 36 deletions packages/input-otp/src/use-pwm-badge.tsx
Expand Up @@ -2,7 +2,8 @@ import * as React from 'react'
import { OTPInputProps } from './types'

const PWM_BADGE_MARGIN_RIGHT = 18
const PWM_BADGE_SPACE_WIDTH = '40px'
const PWM_BADGE_SPACE_WIDTH_PX = 40
const PWM_BADGE_SPACE_WIDTH = `${PWM_BADGE_SPACE_WIDTH_PX}px` as const

const PASSWORD_MANAGERS_SELECTORS = [
'[data-lastpass-icon-root]', // LastPass
Expand All @@ -12,13 +13,13 @@ const PASSWORD_MANAGERS_SELECTORS = [
].join(',')

export function usePasswordManagerBadge({
containerRef,
inputRef,
pwmAreaRef,
pushPasswordManagerStrategy,
isFocused,
}: {
containerRef: React.RefObject<HTMLDivElement>
inputRef: React.RefObject<HTMLInputElement>
pwmAreaRef: React.RefObject<HTMLDivElement>
pushPasswordManagerStrategy: OTPInputProps['pushPasswordManagerStrategy']
isFocused: boolean
}) {
Expand All @@ -43,29 +44,29 @@ export function usePasswordManagerBadge({
return false
}

const noFlickeringCase =
pushPasswordManagerStrategy === 'experimental-no-flickering' &&
(!done || (done && hasPWMBadgeSpace && hasPWMBadge))

const increaseWidthCase =
pushPasswordManagerStrategy === 'increase-width' &&
hasPWMBadge &&
hasPWMBadgeSpace

return increaseWidthCase || noFlickeringCase
}, [done, hasPWMBadge, hasPWMBadgeSpace, pushPasswordManagerStrategy])
return increaseWidthCase
}, [hasPWMBadge, hasPWMBadgeSpace, pushPasswordManagerStrategy])

const trackPWMBadge = React.useCallback(() => {
const container = containerRef.current
const input = inputRef.current
const pwmArea = pwmAreaRef.current
if (!input || !pwmArea || done || pushPasswordManagerStrategy === 'none') {
if (
!container ||
!input ||
done ||
pushPasswordManagerStrategy === 'none'
) {
return
}

const elementToCompare =
pushPasswordManagerStrategy === 'increase-width' ? input : pwmArea
const elementToCompare = container

// Get the top right-center point of the input.
// Get the top right-center point of the container.
// That is usually where most password managers place their badge.
const rightCornerX =
elementToCompare.getBoundingClientRect().left +
Expand All @@ -75,20 +76,22 @@ export function usePasswordManagerBadge({
elementToCompare.offsetHeight / 2
const x = rightCornerX - PWM_BADGE_MARGIN_RIGHT
const y = centereredY
const maybeBadgeEl = document.elementFromPoint(x, y)

// Do an extra search to check for famous password managers
const pmws = document.querySelectorAll(PASSWORD_MANAGERS_SELECTORS)

const maybeHasBadge =
pmws.length > 0 ||
// If the found element is not the input itself,
// then we assume it's a password manager badge.
// We are not sure. Most times it'll be.
maybeBadgeEl !== input

if (!maybeHasBadge) {
return
// If no password manager is automatically detect,
// we'll try to dispatch document.elementFromPoint
// to identify badges
if (pmws.length === 0) {
const maybeBadgeEl = document.elementFromPoint(x, y)

// If the found element is the input itself,
// then we assume it's not a password manager badge.
// We are not sure. Most times that means there isn't a badge.
if (maybeBadgeEl === container) {
return
}
}

setHasPWMBadge(true)
Expand All @@ -106,24 +109,29 @@ export function usePasswordManagerBadge({

pwmMetadata.current.refocused = true
}
}, [done, inputRef, pushPasswordManagerStrategy, pwmAreaRef])
}, [containerRef, inputRef, done, pushPasswordManagerStrategy])

React.useEffect(() => {
const container = containerRef.current
if (!container || pushPasswordManagerStrategy === 'none') {
return
}

// Check if the PWM area is 100% visible
const observer = new IntersectionObserver(
entries => {
const entry = entries[0]
setHasPWMBadgeSpace(entry.intersectionRatio > 0.99)
},
{ threshold: 1, root: null, rootMargin: '0px' },
)
function checkHasSpace() {
const viewportWidth = window.innerWidth
const distanceToRightEdge =
viewportWidth - container.getBoundingClientRect().right
setHasPWMBadgeSpace(distanceToRightEdge >= PWM_BADGE_SPACE_WIDTH_PX)
}

pwmAreaRef.current && observer.observe(pwmAreaRef.current)
checkHasSpace()
const interval = setInterval(checkHasSpace, 1000)

return () => {
observer.disconnect()
clearInterval(interval)
}
}, [pwmAreaRef])
}, [containerRef, pushPasswordManagerStrategy])

React.useEffect(() => {
const _isFocused = isFocused || document.activeElement === inputRef.current
Expand All @@ -145,5 +153,5 @@ export function usePasswordManagerBadge({
}
}, [inputRef, isFocused, pushPasswordManagerStrategy, trackPWMBadge])

return { willPushPWMBadge, PWM_BADGE_SPACE_WIDTH }
return { hasPWMBadge, willPushPWMBadge, PWM_BADGE_SPACE_WIDTH }
}

0 comments on commit 20f791b

Please sign in to comment.