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