Commit 0ca68bb1 authored by Nate Wienert's avatar Nate Wienert Committed by GitHub

feat: disconnect button hides after hover out, click away, and improv… (#6968)

* feat: disconnect button hides after hover out, click away, and improved animations and colors
parent 430356da
......@@ -104,7 +104,7 @@ const StatusWrapper = styled.div`
display: inline-block;
width: 70%;
max-width: 70%;
padding-right: 14px;
padding-right: 8px;
display: inline-flex;
`
......@@ -252,9 +252,12 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
)}
</StatusWrapper>
<IconContainer>
{!showDisconnectConfirm && (
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
)}
<IconButton
hideHorizontal={showDisconnectConfirm}
data-testid="wallet-settings"
onClick={openSettings}
Icon={Settings}
/>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
......@@ -266,6 +269,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
onShowConfirm={setShowDisconnectConfirm}
Icon={LogOutCentered}
text="Disconnect"
dismissOnHoverOut
/>
</TraceEvent>
</IconContainer>
......
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import { Icon } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import styled, { css, DefaultTheme } from 'styled-components/macro'
import useResizeObserver from 'use-resize-observer'
import { TRANSITION_DURATIONS } from '../../theme/styles'
import Row from '../Row'
export const IconHoverText = styled.span`
......@@ -17,11 +18,12 @@ export const IconHoverText = styled.span`
left: 10px;
`
const widthTransition = `width ease-in 80ms`
const getWidthTransition = ({ theme }: { theme: DefaultTheme }) =>
`width ${theme.transition.timing.inOut} ${theme.transition.duration.fast}`
const IconStyles = css`
const IconStyles = css<{ hideHorizontal?: boolean }>`
background-color: ${({ theme }) => theme.backgroundInteractive};
transition: ${widthTransition};
transition: ${getWidthTransition};
border-radius: 12px;
display: flex;
padding: 0;
......@@ -29,7 +31,7 @@ const IconStyles = css`
position: relative;
overflow: hidden;
height: 32px;
width: 32px;
width: ${({ hideHorizontal }) => (hideHorizontal ? '0px' : '32px')};
color: ${({ theme }) => theme.textPrimary};
:hover {
background-color: ${({ theme }) => theme.hoverState};
......@@ -37,7 +39,7 @@ const IconStyles = css`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} background-color ${timing.in}, ${widthTransition}`};
}) => `${duration.fast} background-color ${timing.in}, ${getWidthTransition}`};
${IconHoverText} {
opacity: 1;
......@@ -45,7 +47,7 @@ const IconStyles = css`
}
:active {
background-color: ${({ theme }) => theme.backgroundSurface};
transition: background-color 50ms linear, ${widthTransition};
transition: background-color ${({ theme }) => theme.transition.duration.fast} linear, ${getWidthTransition};
}
`
......@@ -67,6 +69,7 @@ const IconWrapper = styled.span`
`
interface BaseProps {
Icon: Icon
hideHorizontal?: boolean
children?: React.ReactNode
}
......@@ -96,6 +99,8 @@ type IconWithTextProps = (IconButtonProps | IconLinkProps) & {
text: string
onConfirm?: () => void
onShowConfirm?: (on: boolean) => void
dismissOnHoverOut?: boolean
dismissOnHoverDurationMs?: number
}
const TextWrapper = styled.div`
......@@ -107,6 +112,8 @@ const TextWrapper = styled.div`
const TextHide = styled.div`
overflow: hidden;
transition: width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast},
max-width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast};
`
/**
......@@ -120,9 +127,12 @@ export const IconWithConfirmTextButton = ({
onConfirm,
onShowConfirm,
onClick,
dismissOnHoverOut,
dismissOnHoverDurationMs = TRANSITION_DURATIONS.slow,
...rest
}: IconWithTextProps) => {
const [showText, setShowTextWithoutCallback] = useState(false)
const [frame, setFrame] = useState<HTMLElement | null>()
const frameObserver = useResizeObserver<HTMLElement>()
const hiddenObserver = useResizeObserver<HTMLElement>()
......@@ -136,41 +146,60 @@ export const IconWithConfirmTextButton = ({
const dimensionsRef = useRef({
frame: 0,
hidden: 0,
innerText: 0,
})
const dimensions = (() => {
// once opened, we avoid updating it to prevent constant resize loop
if (!showText) {
dimensionsRef.current = { frame: frameObserver.width || 0, hidden: hiddenObserver.width || 0 }
dimensionsRef.current = { frame: frameObserver.width || 0, innerText: hiddenObserver.width || 0 }
}
return dimensionsRef.current
})()
// keyboard action to cancel
useEffect(() => {
if (!showText) return
const isClient = typeof window !== 'undefined'
if (!isClient) return
if (!showText) return
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (typeof window === 'undefined') return
if (!showText || !frame) return
const closeAndPrevent = (e: Event) => {
setShowText(false)
e.preventDefault()
e.stopPropagation()
}
const clickHandler = (e: MouseEvent) => {
const { target } = e
const shouldClose = !(target instanceof HTMLElement) || !frame.contains(target)
if (shouldClose) {
closeAndPrevent(e)
}
}
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeAndPrevent(e)
}
}
window.addEventListener('click', clickHandler, { capture: true })
window.addEventListener('keydown', keyHandler, { capture: true })
return () => {
window.removeEventListener('click', clickHandler, { capture: true })
window.removeEventListener('keydown', keyHandler, { capture: true })
}
}, [setShowText, showText])
}, [frame, setShowText, showText])
const xPad = showText ? 12 : 0
const width = showText ? dimensions.frame + dimensions.hidden + xPad : 32
const xPad = showText ? 8 : 0
const width = showText ? dimensions.frame + dimensions.innerText + xPad : 32
const mouseLeaveTimeout = useRef<NodeJS.Timeout>()
return (
<IconBlock
ref={frameObserver.ref}
ref={(node) => {
frameObserver.ref(node)
setFrame(node)
}}
{...rest}
style={{
width,
......@@ -187,6 +216,18 @@ export const IconWithConfirmTextButton = ({
setShowText(!showText)
}
}}
{...(dismissOnHoverOut && {
onMouseLeave() {
mouseLeaveTimeout.current = setTimeout(() => {
setShowText(false)
}, dismissOnHoverDurationMs)
},
onMouseEnter() {
if (mouseLeaveTimeout.current) {
clearTimeout(mouseLeaveTimeout.current)
}
},
})}
>
<Row height="100%" gap="xs">
<IconWrapper>
......@@ -196,8 +237,11 @@ export const IconWithConfirmTextButton = ({
{/* this outer div is so we can cut it off but keep the inner text width full-width so we can measure it */}
<TextHide
style={{
maxWidth: showText ? dimensions.hidden : 0,
minWidth: showText ? dimensions.hidden : 0,
maxWidth: showText ? dimensions.innerText : 0,
width: showText ? dimensions.innerText : 0,
// this negative transform offsets for the shift it does due to being 0 width
transform: showText ? undefined : `translateX(-8px)`,
minWidth: showText ? dimensions.innerText : 0,
}}
>
<TextWrapper ref={hiddenObserver.ref}>{text}</TextWrapper>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment