Commit b9718900 authored by N's avatar N Committed by GitHub

Merge pull request #55 from blockscout/tooltip-for-shortened-text

tooltip for shortened texts
parents e1cb1434 32e51a06
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
"react-dom": "18.1.0", "react-dom": "18.1.0",
"react-hook-form": "^7.33.1", "react-hook-form": "^7.33.1",
"react-identicons": "^1.2.5", "react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4" "react-jazzicon": "^1.0.4",
"use-font-face-observer": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "17.0.36", "@types/node": "17.0.36",
......
import { theme } from '@chakra-ui/react'; import { theme } from '@chakra-ui/react';
export const BODY_TYPEFACE = 'Inter';
const typography = { const typography = {
fonts: { fonts: {
heading: `Poppins, ${ theme.fonts.heading }`, heading: `Poppins, ${ theme.fonts.heading }`,
body: `Inter, ${ theme.fonts.body }`, body: `${ BODY_TYPEFACE }, ${ theme.fonts.body }`,
}, },
textStyles: { textStyles: {
h2: { h2: {
......
...@@ -19,7 +19,7 @@ const PrivateTags: React.FC = () => { ...@@ -19,7 +19,7 @@ const PrivateTags: React.FC = () => {
<Page> <Page>
<Box h="100%"> <Box h="100%">
<Heading as="h1" size="lg" marginBottom={ 8 }>Private tags</Heading> <Heading as="h1" size="lg" marginBottom={ 8 }>Private tags</Heading>
<Tabs variant="soft-rounded" colorScheme="blue"> <Tabs variant="soft-rounded" colorScheme="blue" isLazy>
<TabList marginBottom={ 8 }> <TabList marginBottom={ 8 }>
<Tab>Address</Tab> <Tab>Address</Tab>
<Tab>Transaction</Tab> <Tab>Transaction</Tab>
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
Tr, Tr,
Td, Td,
HStack, HStack,
Tooltip,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import AddressIcon from 'ui/shared/AddressIcon'; import AddressIcon from 'ui/shared/AddressIcon';
...@@ -14,6 +13,7 @@ import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; ...@@ -14,6 +13,7 @@ import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress'; import type { TPrivateTagsAddressItem } from 'data/privateTagsAddress';
import EditButton from 'ui/shared/EditButton'; import EditButton from 'ui/shared/EditButton';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: TPrivateTagsAddressItem; item: TPrivateTagsAddressItem;
...@@ -39,11 +39,11 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -39,11 +39,11 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</HStack> </HStack>
</Td> </Td>
<Td> <Td>
<Tooltip label={ item.tag }> <TruncatedTextTooltip label={ item.tag }>
<Tag variant="gray" lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ item.tag } { item.tag }
</Tag> </Tag>
</Tooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
......
...@@ -33,9 +33,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -33,9 +33,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.transaction }> <Tr alignItems="top" key={ item.transaction }>
<Td> <Td>
<HStack spacing={ 4 }> <AddressLinkWithTooltip address={ item.transaction }/>
<AddressLinkWithTooltip address={ item.transaction }/>
</HStack>
</Td> </Td>
<Td> <Td>
<Tooltip label={ item.tag }> <Tooltip label={ item.tag }>
......
import React from 'react'; import React from 'react';
import { HStack, Link, Box, Tooltip } from '@chakra-ui/react'; import { HStack, Link } from '@chakra-ui/react';
import AddressWithDots from './AddressWithDots'; import AddressWithDots from './AddressWithDots';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
const FONT_WEIGHT = '600';
const AddressLinkWithTooltip = ({ address }: {address: string}) => { const AddressLinkWithTooltip = ({ address }: {address: string}) => {
return ( return (
<HStack spacing={ 2 } alignContent="center" overflow="hidden"> <HStack spacing={ 2 } alignContent="center" overflow="hidden">
<Link <Link
href="#" href="#"
overflow="hidden" overflow="hidden"
fontWeight={ 600 } fontWeight={ FONT_WEIGHT }
lineHeight="24px" lineHeight="24px"
> >
<Tooltip label={ address }> <AddressWithDots address={ address } fontWeight={ FONT_WEIGHT }/>
<Box overflow="hidden"><AddressWithDots address={ address }/></Box>
</Tooltip>
</Link> </Link>
<CopyToClipboard text={ address }/> <CopyToClipboard text={ address }/>
</HStack> </HStack>
) )
} }
export default AddressLinkWithTooltip; export default React.memo(AddressLinkWithTooltip);
...@@ -9,13 +9,22 @@ ...@@ -9,13 +9,22 @@
// so i did it with js // so i did it with js
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { Tooltip } from '@chakra-ui/react'
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import type { FontFace } from 'use-font-face-observer';
import useFontFaceObserver from 'use-font-face-observer';
import { BODY_TYPEFACE } from 'theme/foundations/typography';
const TAIL_LENGTH = 4; const TAIL_LENGTH = 4;
const HEAD_MIN_LENGTH = 4; const HEAD_MIN_LENGTH = 4;
const AddressWithDots = ({ address }: {address: string}) => { const AddressWithDots = ({ address, fontWeight }: {address: string; fontWeight: FontFace['weight']}) => {
const addressRef = useRef<HTMLSpanElement>(null); const addressRef = useRef<HTMLSpanElement>(null);
const [ displayedAddress, setAddress ] = React.useState(address);
const isFontFaceLoaded = useFontFaceObserver([
{ family: BODY_TYPEFACE, weight: fontWeight },
]);
const calculateString = useCallback(() => { const calculateString = useCallback(() => {
const addressEl = addressRef.current; const addressEl = addressRef.current;
...@@ -40,29 +49,46 @@ const AddressWithDots = ({ address }: {address: string}) => { ...@@ -40,29 +49,46 @@ const AddressWithDots = ({ address }: {address: string}) => {
const res = address.slice(0, address.length - i - TAIL_LENGTH) + '...' + address.slice(-TAIL_LENGTH); const res = address.slice(0, address.length - i - TAIL_LENGTH) + '...' + address.slice(-TAIL_LENGTH);
shadowEl.textContent = res; shadowEl.textContent = res;
if (getWidth(shadowEl) < parentWidth || i === address.length - TAIL_LENGTH - HEAD_MIN_LENGTH) { if (getWidth(shadowEl) < parentWidth || i === address.length - TAIL_LENGTH - HEAD_MIN_LENGTH) {
addressRef.current.textContent = res; setAddress(res);
break; break;
} }
} }
} else { } else {
addressRef.current.textContent = address; setAddress(address);
} }
parent.removeChild(shadowEl); parent.removeChild(shadowEl);
}, [ address ]); }, [ address ]);
// we want to do recalculation when isFontFaceLoaded flag is changed
// but we don't want to create more resize event listeners
// that's why there are separate useEffect hooks
useEffect(() => { useEffect(() => {
calculateString(); calculateString();
}, [ calculateString, isFontFaceLoaded ])
useEffect(() => {
const resizeHandler = _debounce(calculateString, 50) const resizeHandler = _debounce(calculateString, 50)
window.addEventListener('resize', resizeHandler) window.addEventListener('resize', resizeHandler)
return function cleanup() { return function cleanup() {
window.removeEventListener('resize', resizeHandler) window.removeEventListener('resize', resizeHandler)
}; };
}, [ calculateString ]); }, [ calculateString ]);
return <span ref={ addressRef }>{ address }</span>;
const content = <span ref={ addressRef }>{ displayedAddress }</span>;
const isTruncated = address.length !== displayedAddress.length;
if (isTruncated) {
return (
<Tooltip label={ address }>{ content }</Tooltip>
)
}
return content;
} }
function getWidth(el: HTMLElement) { function getWidth(el: HTMLElement) {
return el.getBoundingClientRect().width; return el.getBoundingClientRect().width;
} }
export default AddressWithDots; export default React.memo(AddressWithDots);
import React from 'react';
import { Tooltip } from '@chakra-ui/react'
import debounce from 'lodash/debounce';
import useFontFaceObserver from 'use-font-face-observer';
import { BODY_TYPEFACE } from 'theme/foundations/typography';
interface Props {
children: React.ReactNode;
label: string;
}
const TruncatedTextTooltip = ({ children, label }: Props) => {
const childRef = React.useRef<HTMLElement>(null);
const [ isTruncated, setTruncated ] = React.useState(false);
const isFontFaceLoaded = useFontFaceObserver([
{ family: BODY_TYPEFACE },
]);
const updatedTruncateState = React.useCallback(() => {
if (childRef.current) {
const scrollWidth = childRef.current.scrollWidth;
const clientWidth = childRef.current.clientWidth;
if (scrollWidth > clientWidth) {
setTruncated(true);
} else {
setTruncated(false);
}
}
}, []);
// FIXME: that should be useLayoutEffect, but it keeps complaining about SSR
// let's keep it as it is until the first issue
React.useEffect(() => {
updatedTruncateState()
}, [ updatedTruncateState, isFontFaceLoaded ]);
// we want to do recalculation when isFontFaceLoaded flag is changed
// but we don't want to create more resize event listeners
// that's why there are separate useEffect hooks
React.useEffect(() => {
const handleResize = debounce(updatedTruncateState, 1000)
window.addEventListener('resize', handleResize)
return function cleanup() {
window.removeEventListener('resize', handleResize)
};
}, [ updatedTruncateState ]);
// as for now it supports only one child
// and it is not cleared how to manage case with two or more children
const child = React.Children.only(children) as React.ReactElement & {
ref?: React.Ref<React.ReactNode>;
}
const modifiedChildren = React.cloneElement(
child,
{ ref: childRef },
);
if (isTruncated) {
return <Tooltip label={ label }>{ modifiedChildren }</Tooltip>;
}
return modifiedChildren;
};
export default React.memo(TruncatedTextTooltip);
...@@ -6,11 +6,11 @@ import { ...@@ -6,11 +6,11 @@ import {
Td, Td,
Switch, Switch,
HStack, HStack,
Tooltip,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import EditButton from 'ui/shared/EditButton'; import EditButton from 'ui/shared/EditButton';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import type { TWatchlistItem } from 'data/watchlist'; import type { TWatchlistItem } from 'data/watchlist';
...@@ -35,11 +35,11 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -35,11 +35,11 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
<Tr alignItems="top" key={ item.address }> <Tr alignItems="top" key={ item.address }>
<Td><WatchListAddressItem item={ item }/></Td> <Td><WatchListAddressItem item={ item }/></Td>
<Td> <Td>
<Tooltip label={ item.tag }> <TruncatedTextTooltip label={ item.tag }>
<Tag variant="gray" lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ item.tag } { item.tag }
</Tag> </Tag>
</Tooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td><Switch colorScheme="blue" size="md" isChecked={ item.notification }/></Td> <Td><Switch colorScheme="blue" size="md" isChecked={ item.notification }/></Td>
<Td> <Td>
......
...@@ -1860,6 +1860,11 @@ focus-lock@^0.11.2: ...@@ -1860,6 +1860,11 @@ focus-lock@^0.11.2:
dependencies: dependencies:
tslib "^2.0.3" tslib "^2.0.3"
fontfaceobserver@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.1.0.tgz#e2705d293e2c585a6531c2a722905657317a2991"
integrity sha512-ReOsO2F66jUa0jmv2nlM/s1MiutJx/srhAe2+TE8dJCMi02ZZOcCTxTCQFr3Yet+uODUtnr4Mewg+tNQ+4V1Ng==
framer-motion@^6: framer-motion@^6:
version "6.3.6" version "6.3.6"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.3.6.tgz#9ca52544a7d0c74668f880eb2cab4a5de6dc71a0" resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.3.6.tgz#9ca52544a7d0c74668f880eb2cab4a5de6dc71a0"
...@@ -2823,6 +2828,14 @@ react-style-singleton@^2.2.1: ...@@ -2823,6 +2828,14 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^2.0.0" tslib "^2.0.0"
react@17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
react@18.1.0: react@18.1.0:
version "18.1.0" version "18.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
...@@ -3322,6 +3335,14 @@ use-callback-ref@^1.3.0: ...@@ -3322,6 +3335,14 @@ use-callback-ref@^1.3.0:
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
use-font-face-observer@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/use-font-face-observer/-/use-font-face-observer-1.2.1.tgz#2b33a389b82b48e2744f439abc1d5d6201fc099d"
integrity sha512-5ieKTMvtUux0l7YoOEz842djfgMH3oVg+tO13E/kyS+gGRLDyfAMmRv0D3fzM7UdFag1kz+3AQIFLkkfEF3TUg==
dependencies:
fontfaceobserver "2.1.0"
react "17.0.2"
use-sidecar@^1.1.2: use-sidecar@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
......
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