Migrate daisy tooltips to our own to fix cutoffs (#748)
* Make all tooltips use our component * Stop mobile tooltip crop (daisy -> floating-ui) * Show tooltip on tap for touch devices Except tooltips on buttons * migrate another daisy tooltip to ours * Prevent hidden tooltip from covering click/hover
This commit is contained in:
parent
d2b634c775
commit
df858f916b
|
@ -25,14 +25,15 @@
|
||||||
"main": "functions/src/index.js",
|
"main": "functions/src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/node": "1.10.0",
|
"@amplitude/node": "1.10.0",
|
||||||
|
"@floating-ui/react-dom": "1.0.0",
|
||||||
"@google-cloud/functions-framework": "3.1.2",
|
"@google-cloud/functions-framework": "3.1.2",
|
||||||
"@tiptap/core": "2.0.0-beta.181",
|
"@tiptap/core": "2.0.0-beta.181",
|
||||||
"@tiptap/extension-image": "2.0.0-beta.30",
|
"@tiptap/extension-image": "2.0.0-beta.30",
|
||||||
"@tiptap/extension-link": "2.0.0-beta.43",
|
"@tiptap/extension-link": "2.0.0-beta.43",
|
||||||
"@tiptap/extension-mention": "2.0.0-beta.102",
|
"@tiptap/extension-mention": "2.0.0-beta.102",
|
||||||
"@tiptap/starter-kit": "2.0.0-beta.190",
|
"@tiptap/starter-kit": "2.0.0-beta.190",
|
||||||
"dayjs": "1.11.4",
|
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
|
"dayjs": "1.11.4",
|
||||||
"express": "4.18.1",
|
"express": "4.18.1",
|
||||||
"firebase-admin": "10.0.0",
|
"firebase-admin": "10.0.0",
|
||||||
"firebase-functions": "3.21.2",
|
"firebase-functions": "3.21.2",
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
import { getMappedValue } from 'common/pseudo-numeric'
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
|
import { Tooltip } from '../tooltip'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -333,22 +334,19 @@ export function PseudoNumericResolutionOrExpectation(props: {
|
||||||
{resolution === 'CANCEL' ? (
|
{resolution === 'CANCEL' ? (
|
||||||
<CancelLabel />
|
<CancelLabel />
|
||||||
) : (
|
) : (
|
||||||
<div
|
<Tooltip className={textColor} text={value.toFixed(2)}>
|
||||||
className={clsx('tooltip', textColor)}
|
|
||||||
data-tip={value.toFixed(2)}
|
|
||||||
>
|
|
||||||
{formatLargeNumber(value)}
|
{formatLargeNumber(value)}
|
||||||
</div>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<Tooltip
|
||||||
className={clsx('tooltip text-3xl', textColor)}
|
className={clsx('text-3xl', textColor)}
|
||||||
data-tip={value.toFixed(2)}
|
text={value.toFixed(2)}
|
||||||
>
|
>
|
||||||
{formatLargeNumber(value)}
|
{formatLargeNumber(value)}
|
||||||
</div>
|
</Tooltip>
|
||||||
<div className={clsx('text-base', textColor)}>expected</div>
|
<div className={clsx('text-base', textColor)}>expected</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
import advanced from 'dayjs/plugin/advancedFormat'
|
import advanced from 'dayjs/plugin/advancedFormat'
|
||||||
import { ClientRender } from './client-render'
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
@ -13,23 +12,16 @@ export function DateTimeTooltip(props: {
|
||||||
time: number
|
time: number
|
||||||
text?: string
|
text?: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
noTap?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { time, text } = props
|
const { time, text, noTap } = props
|
||||||
|
|
||||||
const formattedTime = dayjs(time).format('MMM DD, YYYY hh:mm a z')
|
const formattedTime = dayjs(time).format('MMM DD, YYYY hh:mm a z')
|
||||||
const toolTip = text ? `${text} ${formattedTime}` : formattedTime
|
const toolTip = text ? `${text} ${formattedTime}` : formattedTime
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Tooltip text={toolTip} noTap={noTap}>
|
||||||
<ClientRender>
|
{props.children}
|
||||||
<span
|
</Tooltip>
|
||||||
className="tooltip hidden cursor-default sm:inline-block"
|
|
||||||
data-tip={toolTip}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
</ClientRender>
|
|
||||||
<span className="whitespace-nowrap sm:hidden">{props.children}</span>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import { MarketModal } from './editor/market-modal'
|
import { MarketModal } from './editor/market-modal'
|
||||||
import { insertContent } from './editor/utils'
|
import { insertContent } from './editor/utils'
|
||||||
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
const DisplayImage = Image.configure({
|
const DisplayImage = Image.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -146,15 +147,15 @@ export function TextEditor(props: {
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{/* Toolbar, with buttons for images and embeds */}
|
{/* Toolbar, with buttons for images and embeds */}
|
||||||
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
|
||||||
<div className="tooltip flex items-center" data-tip="Add image">
|
<Tooltip className="flex items-center" text="Add image" noTap>
|
||||||
<FileUploadButton
|
<FileUploadButton
|
||||||
onFiles={upload.mutate}
|
onFiles={upload.mutate}
|
||||||
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
|
||||||
>
|
>
|
||||||
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
</FileUploadButton>
|
</FileUploadButton>
|
||||||
</div>
|
</Tooltip>
|
||||||
<div className="tooltip flex items-center" data-tip="Add embed">
|
<Tooltip className="flex items-center" text="Add embed" noTap>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIframeOpen(true)}
|
onClick={() => setIframeOpen(true)}
|
||||||
|
@ -167,8 +168,8 @@ export function TextEditor(props: {
|
||||||
/>
|
/>
|
||||||
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Tooltip>
|
||||||
<div className="tooltip flex items-center" data-tip="Add market">
|
<Tooltip className="flex items-center" text="Add market" noTap>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMarketOpen(true)}
|
onClick={() => setMarketOpen(true)}
|
||||||
|
@ -184,7 +185,7 @@ export function TextEditor(props: {
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Tooltip>
|
||||||
{/* Spacer that also focuses editor on click */}
|
{/* Spacer that also focuses editor on click */}
|
||||||
<div
|
<div
|
||||||
className="grow cursor-text self-stretch"
|
className="grow cursor-text self-stretch"
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={clsx('inline', className)}>
|
<div className={clsx('inline', className)}>
|
||||||
<DateTimeTooltip time={createdTime}>
|
<DateTimeTooltip time={createdTime} noTap>
|
||||||
<Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}>
|
<Link href={`/${prefix}/${slug}#${elementId}`} passHref={true}>
|
||||||
<a
|
<a
|
||||||
onClick={(event) => copyLinkToComment(event)}
|
onClick={(event) => copyLinkToComment(event)}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { InformationCircleIcon } from '@heroicons/react/outline'
|
import { InformationCircleIcon } from '@heroicons/react/outline'
|
||||||
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
export function InfoTooltip(props: { text: string }) {
|
export function InfoTooltip(props: { text: string }) {
|
||||||
const { text } = props
|
const { text } = props
|
||||||
return (
|
return (
|
||||||
<div className="tooltip" data-tip={text}>
|
<Tooltip text={text}>
|
||||||
<InformationCircleIcon className="h-5 w-5 text-gray-500" />
|
<InformationCircleIcon className="h-5 w-5 text-gray-500" />
|
||||||
</div>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { ReactNode } from 'react'
|
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { getProbability } from 'common/calculate'
|
import { getProbability } from 'common/calculate'
|
||||||
import { getValueFromBucket } from 'common/calculate-dpm'
|
import { getValueFromBucket } from 'common/calculate-dpm'
|
||||||
|
@ -11,7 +10,7 @@ import {
|
||||||
resolution,
|
resolution,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
||||||
import { ClientRender } from './client-render'
|
import { Tooltip } from './tooltip'
|
||||||
|
|
||||||
export function OutcomeLabel(props: {
|
export function OutcomeLabel(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -91,13 +90,13 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
const chosen = contract.answers?.find((answer) => answer.id === resolution)
|
const chosen = contract.answers?.find((answer) => answer.id === resolution)
|
||||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||||
return (
|
return (
|
||||||
<FreeResponseAnswerToolTip text={chosen.text}>
|
<Tooltip text={chosen.text}>
|
||||||
<AnswerLabel
|
<AnswerLabel
|
||||||
answer={chosen}
|
answer={chosen}
|
||||||
truncate={truncate}
|
truncate={truncate}
|
||||||
className={answerClassName}
|
className={answerClassName}
|
||||||
/>
|
/>
|
||||||
</FreeResponseAnswerToolTip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,23 +173,3 @@ export function AnswerLabel(props: {
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FreeResponseAnswerToolTip(props: {
|
|
||||||
text: string
|
|
||||||
children?: ReactNode
|
|
||||||
}) {
|
|
||||||
const { text } = props
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ClientRender>
|
|
||||||
<span
|
|
||||||
className="tooltip hidden cursor-default sm:inline-block"
|
|
||||||
data-tip={text}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
</ClientRender>
|
|
||||||
<span className="whitespace-nowrap sm:hidden">{props.children}</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -103,8 +103,10 @@ function DownTip(props: { onClick?: () => void }) {
|
||||||
const { onClick } = props
|
const { onClick } = props
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="tooltip-bottom h-6 w-6"
|
className="h-6 w-6"
|
||||||
|
placement="bottom"
|
||||||
text={onClick && `-${formatMoney(5)}`}
|
text={onClick && `-${formatMoney(5)}`}
|
||||||
|
noTap
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="hover:text-red-600 disabled:text-gray-300"
|
className="hover:text-red-600 disabled:text-gray-300"
|
||||||
|
@ -122,8 +124,10 @@ function UpTip(props: { onClick?: () => void; value: number }) {
|
||||||
const IconKind = value >= 10 ? ChevronDoubleRightIcon : ChevronRightIcon
|
const IconKind = value >= 10 ? ChevronDoubleRightIcon : ChevronRightIcon
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="tooltip-bottom h-6 w-6"
|
className="h-6 w-6"
|
||||||
|
placement="bottom"
|
||||||
text={onClick && `Tip ${formatMoney(5)}`}
|
text={onClick && `Tip ${formatMoney(5)}`}
|
||||||
|
noTap
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="hover:text-primary disabled:text-gray-300"
|
className="hover:text-primary disabled:text-gray-300"
|
||||||
|
|
|
@ -1,14 +1,79 @@
|
||||||
|
import {
|
||||||
|
arrow,
|
||||||
|
autoUpdate,
|
||||||
|
flip,
|
||||||
|
offset,
|
||||||
|
Placement,
|
||||||
|
shift,
|
||||||
|
useFloating,
|
||||||
|
} from '@floating-ui/react-dom'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { ReactNode, useRef } from 'react'
|
||||||
|
|
||||||
|
// See https://floating-ui.com/docs/react-dom
|
||||||
|
|
||||||
|
export function Tooltip(props: {
|
||||||
|
text: string | false | undefined | null
|
||||||
|
children: ReactNode
|
||||||
|
className?: string
|
||||||
|
placement?: Placement
|
||||||
|
noTap?: boolean
|
||||||
|
}) {
|
||||||
|
const { text, children, className, placement = 'top', noTap } = props
|
||||||
|
|
||||||
|
const arrowRef = useRef(null)
|
||||||
|
|
||||||
|
const { x, y, refs, reference, floating, strategy, middlewareData } =
|
||||||
|
useFloating({
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
placement,
|
||||||
|
middleware: [
|
||||||
|
offset(8),
|
||||||
|
flip(),
|
||||||
|
shift({ padding: 4 }),
|
||||||
|
arrow({ element: arrowRef }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { x: arrowX, y: arrowY } = middlewareData.arrow ?? {}
|
||||||
|
|
||||||
|
// which side of tooltip arrow is on. like: if tooltip is top-left, arrow is on bottom of tooltip
|
||||||
|
const arrowSide = {
|
||||||
|
top: 'bottom',
|
||||||
|
right: 'left',
|
||||||
|
bottom: 'top',
|
||||||
|
left: 'right ',
|
||||||
|
}[placement.split('-')[0]] as string
|
||||||
|
|
||||||
export function Tooltip(
|
|
||||||
props: {
|
|
||||||
text: string | false | undefined | null
|
|
||||||
} & JSX.IntrinsicElements['div']
|
|
||||||
) {
|
|
||||||
const { text, children, className } = props
|
|
||||||
return text ? (
|
return text ? (
|
||||||
<div className={clsx(className, 'tooltip z-10')} data-tip={text}>
|
<div className="contents">
|
||||||
{children}
|
<div
|
||||||
|
className={clsx('peer inline-block', className)}
|
||||||
|
ref={reference}
|
||||||
|
tabIndex={noTap ? undefined : 0}
|
||||||
|
onTouchStart={() => (refs.reference.current as HTMLElement).focus()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="tooltip"
|
||||||
|
ref={floating}
|
||||||
|
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
|
||||||
|
className="-z-10 max-w-xs rounded bg-slate-700 px-2 py-1 text-center text-sm text-white opacity-0 transition-opacity peer-hover:z-10 peer-hover:opacity-100 peer-focus:z-10 peer-focus:opacity-100"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
<div
|
||||||
|
ref={arrowRef}
|
||||||
|
className="absolute h-2 w-2 rotate-45 bg-slate-700"
|
||||||
|
style={{
|
||||||
|
top: arrowY != null ? arrowY : '',
|
||||||
|
left: arrowX != null ? arrowX : '',
|
||||||
|
right: '',
|
||||||
|
bottom: '',
|
||||||
|
[arrowSide]: '-4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -2178,6 +2178,25 @@
|
||||||
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.1.tgz#0c74724ba6e9ea6ad25a391eab60a79eaba4c556"
|
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.1.tgz#0c74724ba6e9ea6ad25a391eab60a79eaba4c556"
|
||||||
integrity sha512-9FqhNjKQWpQ3fGnSOCovHOm+yhhiorKEqYLAfd525jWavunDJcx8rOW6i6ozAh+FbwcYMkL7b+3j4UR/30MpoQ==
|
integrity sha512-9FqhNjKQWpQ3fGnSOCovHOm+yhhiorKEqYLAfd525jWavunDJcx8rOW6i6ozAh+FbwcYMkL7b+3j4UR/30MpoQ==
|
||||||
|
|
||||||
|
"@floating-ui/core@^1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.1.tgz#00e64d74e911602c8533957af0cce5af6b2e93c8"
|
||||||
|
integrity sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==
|
||||||
|
|
||||||
|
"@floating-ui/dom@^1.0.0":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.1.tgz#3321d4e799d6ac2503e729131d07ad0e714aabeb"
|
||||||
|
integrity sha512-wBDiLUKWU8QNPNOTAFHiIAkBv1KlHauG2AhqjSeh2H+wR8PX+AArXfz8NkRexH5PgMJMmSOS70YS89AbWYh5dA==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core" "^1.0.1"
|
||||||
|
|
||||||
|
"@floating-ui/react-dom@1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.0.0.tgz#e0975966694433f1f0abffeee5d8e6bb69b7d16e"
|
||||||
|
integrity sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/dom" "^1.0.0"
|
||||||
|
|
||||||
"@google-cloud/firestore@^4.5.0":
|
"@google-cloud/firestore@^4.5.0":
|
||||||
version "4.15.1"
|
version "4.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-4.15.1.tgz#ed764fc76823ce120e68fe8c27ef1edd0650cd93"
|
resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-4.15.1.tgz#ed764fc76823ce120e68fe8c27ef1edd0650cd93"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user