Refactor tooltips

This commit is contained in:
IRBorisov 2023-12-21 00:12:24 +03:00
parent 8e84d570e3
commit 4d4d0a611d
24 changed files with 130 additions and 48 deletions

View File

@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface ButtonProps interface ButtonProps
@ -12,8 +14,8 @@ extends CProps.Control, CProps.Colors, CProps.Button {
} }
function Button({ function Button({
text, icon, loading, text, icon, title,
dense, disabled, noBorder, noOutline, loading, dense, disabled, noBorder, noOutline,
colors = 'clr-btn-default', colors = 'clr-btn-default',
className, className,
...restProps ...restProps
@ -36,6 +38,8 @@ function Button({
className, className,
colors colors
)} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...restProps} {...restProps}
> >
{icon ? icon : null} {icon ? icon : null}

View File

@ -1,6 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { globalIDs } from '@/utils/constants';
import { CheckboxCheckedIcon } from '../Icons'; import { CheckboxCheckedIcon } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import Label from './Label'; import Label from './Label';
@ -15,7 +17,7 @@ extends Omit<CProps.Button, 'value' | 'onClick'> {
} }
function Checkbox({ function Checkbox({
id, disabled, label, id, disabled, label, title,
className, value, setValue, ...restProps className, value, setValue, ...restProps
}: CheckboxProps) { }: CheckboxProps) {
const cursor = useMemo( const cursor = useMemo(
@ -48,6 +50,8 @@ function Checkbox({
)} )}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...restProps} {...restProps}
> >
<div className={clsx( <div className={clsx(

View File

@ -2,12 +2,17 @@ import clsx from 'clsx';
import type { TabProps } from 'react-tabs'; import type { TabProps } from 'react-tabs';
import { Tab } from 'react-tabs'; import { Tab } from 'react-tabs';
import { globalIDs } from '@/utils/constants';
interface ConceptTabProps interface ConceptTabProps
extends Omit<TabProps, 'children'> { extends Omit<TabProps, 'children'> {
label?: string label?: string
} }
function ConceptTab({ label, className, ...otherProps }: ConceptTabProps) { function ConceptTab({
label, title, className,
...otherProps
}: ConceptTabProps) {
return ( return (
<Tab <Tab
className={clsx( className={clsx(
@ -18,6 +23,8 @@ function ConceptTab({ label, className, ...otherProps }: ConceptTabProps) {
'select-none hover:cursor-pointer', 'select-none hover:cursor-pointer',
className className
)} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...otherProps} {...otherProps}
> >
{label} {label}

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { ReactNode } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ITooltip, Tooltip } from 'react-tooltip'; import { ITooltip, Tooltip } from 'react-tooltip';
@ -9,9 +10,11 @@ import { useConceptTheme } from '@/context/ThemeContext';
interface ConceptTooltipProps interface ConceptTooltipProps
extends Omit<ITooltip, 'variant'> { extends Omit<ITooltip, 'variant'> {
layer?: string layer?: string
text?: string
} }
function ConceptTooltip({ function ConceptTooltip({
text, children,
layer='z-tooltip', layer='z-tooltip',
place='bottom', place='bottom',
className, className,
@ -24,20 +27,25 @@ function ConceptTooltip({
return null; return null;
} }
return createPortal( return createPortal(
<Tooltip (<Tooltip
delayShow={500} delayShow={1000}
delayHide={100}
opacity={0.97} opacity={0.97}
className={clsx( className={clsx(
'overflow-auto', 'overflow-hidden',
'border shadow-md', 'border shadow-md',
layer, layer,
className className
)} )}
classNameArrow={layer}
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}} style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}}
variant={(darkMode ? 'dark' : 'light')} variant={(darkMode ? 'dark' : 'light')}
place={place} place={place}
{...restProps} {...restProps}
/>, document.body); >
{text}
{children as ReactNode}
</Tooltip>), document.body);
} }
export default ConceptTooltip; export default ConceptTooltip;

View File

@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface DropdownButtonProps interface DropdownButtonProps
@ -11,7 +13,9 @@ extends CProps.Button {
} }
function DropdownButton({ function DropdownButton({
text, icon, className, onClick, text, icon,
className, title,
onClick,
children, children,
...restProps ...restProps
}: DropdownButtonProps) { }: DropdownButtonProps) {
@ -29,6 +33,8 @@ function DropdownButton({
}, },
className className
)} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...restProps} {...restProps}
> >
{children ? children : null} {children ? children : null}

View File

@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface MiniButtonProps interface MiniButtonProps
@ -10,7 +12,7 @@ extends CProps.Button {
function MiniButton({ function MiniButton({
icon, noHover, tabIndex, icon, noHover, tabIndex,
className, title, className,
...restProps ...restProps
}: MiniButtonProps) { }: MiniButtonProps) {
return ( return (
@ -27,6 +29,8 @@ function MiniButton({
}, },
className className
)} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...restProps} {...restProps}
> >
{icon} {icon}

View File

@ -1,5 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface SelectorButtonProps interface SelectorButtonProps
@ -12,7 +14,7 @@ extends CProps.Button {
} }
function SelectorButton({ function SelectorButton({
text, icon, text, icon, title,
colors = 'clr-btn-default', colors = 'clr-btn-default',
className, className,
transparent, transparent,
@ -20,6 +22,8 @@ function SelectorButton({
}: SelectorButtonProps) { }: SelectorButtonProps) {
return ( return (
<button type='button' <button type='button'
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
className={clsx( className={clsx(
'px-1 flex flex-start items-center gap-1', 'px-1 flex flex-start items-center gap-1',
'text-sm small-caps select-none', 'text-sm small-caps select-none',

View File

@ -16,7 +16,8 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
} }
function TextInput({ function TextInput({
id, label, dense, noBorder, noOutline, allowEnter, disabled, id, label,
dense, noBorder, noOutline, allowEnter, disabled,
className, className,
colors = 'clr-input', colors = 'clr-input',
onKeyDown, onKeyDown,

View File

@ -2,6 +2,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { globalIDs } from '@/utils/constants';
import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons'; import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
import { CheckboxProps } from './Checkbox'; import { CheckboxProps } from './Checkbox';
import Label from './Label'; import Label from './Label';
@ -13,7 +15,7 @@ extends Omit<CheckboxProps, 'value' | 'setValue'> {
} }
function Tristate({ function Tristate({
id, disabled, label, id, disabled, label, title,
className, className,
value, setValue, value, setValue,
...restProps ...restProps
@ -53,6 +55,8 @@ function Tristate({
)} )}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
{...restProps} {...restProps}
> >
<div className={clsx( <div className={clsx(

View File

@ -28,7 +28,7 @@ function HelpTermGraph() {
<Divider margins='mt-2' /> <Divider margins='mt-2' />
<InfoCstClass title='Классы конституент' /> <InfoCstClass header='Классы конституент' />
</div> </div>
</div>); </div>);
} }

View File

@ -42,19 +42,19 @@ function Navigation () {
<div className='flex'> <div className='flex'>
<NavigationButton <NavigationButton
text='Новая схема' text='Новая схема'
description='Создать новую схему' title='Создать новую схему'
icon={<FaSquarePlus size='1.5rem' />} icon={<FaSquarePlus size='1.5rem' />}
onClick={navigateCreateNew} onClick={navigateCreateNew}
/> />
<NavigationButton <NavigationButton
text='Библиотека' text='Библиотека'
description='Библиотека концептуальных схем' title='Список схем'
icon={<IoLibrary size='1.5rem' />} icon={<IoLibrary size='1.5rem' />}
onClick={navigateLibrary} onClick={navigateLibrary}
/> />
<NavigationButton <NavigationButton
text='Справка' text='Справка'
description='Справочные материалы и обучение' title='Справочные материалы'
icon={<EducationIcon />} icon={<EducationIcon />}
onClick={navigateHelp} onClick={navigateHelp}
/> />

View File

@ -1,17 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
interface NavigationButtonProps { interface NavigationButtonProps {
id?: string
text?: string text?: string
icon: React.ReactNode icon: React.ReactNode
description?: string title?: string
onClick?: () => void onClick?: () => void
} }
function NavigationButton({ id, icon, description, onClick, text }: NavigationButtonProps) { function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
return ( return (
<button id={id} type='button' tabIndex={-1} <button type='button' tabIndex={-1}
title={description} data-tooltip-id={title ? (globalIDs.tooltip) : undefined}
data-tooltip-content={title}
onClick={onClick} onClick={onClick}
className={clsx( className={clsx(
'mr-1 h-full', 'mr-1 h-full',

View File

@ -18,13 +18,12 @@ function UserMenu() {
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
{!user ? {!user ?
<NavigationButton <NavigationButton
description='Перейти на страницу логина' title='Перейти на страницу логина'
icon={<InDoorIcon />} icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
onClick={navigateLogin} onClick={navigateLogin}
/> : null} /> : null}
{user ? {user ?
<NavigationButton <NavigationButton
description={`Пользователь ${user?.username}`}
icon={<FaCircleUser size='1.5rem' />} icon={<FaCircleUser size='1.5rem' />}
onClick={menu.toggle} onClick={menu.toggle}
/> : null} /> : null}

View File

@ -7,15 +7,15 @@ import { prefixes } from '@/utils/constants';
import { describeCstClass, labelCstClass } from '@/utils/labels'; import { describeCstClass, labelCstClass } from '@/utils/labels';
interface InfoCstClassProps { interface InfoCstClassProps {
title?: string header?: string
} }
function InfoCstClass({ title }: InfoCstClassProps) { function InfoCstClass({ header }: InfoCstClassProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1 mb-2'> <div className='flex flex-col gap-1 mb-2'>
{title ? <h1>{title}</h1> : null} {header ? <h1>{header}</h1> : null}
{Object.values(CstClass).map( {Object.values(CstClass).map(
(cclass, index) => { (cclass, index) => {
return ( return (

View File

@ -1,9 +1,12 @@
'use client'; 'use client';
import clsx from 'clsx';
import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useContext, useLayoutEffect, useMemo, useState } from 'react';
import ConceptTooltip from '@/components/Common/ConceptTooltip';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { darkT, IColorTheme, lightT } from '@/utils/color'; import { darkT, IColorTheme, lightT } from '@/utils/color';
import { globalIDs } from '@/utils/constants';
interface IThemeContext { interface IThemeContext {
viewportHeight: string viewportHeight: string
@ -85,6 +88,17 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
setNoFooter, setShowScroll, setNoFooter, setShowScroll,
viewportHeight, mainHeight viewportHeight, mainHeight
}}> }}>
<>
<ConceptTooltip float
id={`${globalIDs.tooltip}`}
layer='z-topmost'
place='right-start'
className={clsx(
'mt-3 translate-y-1/2',
'max-w-[20rem]'
)}
/>
{children} {children}
</>
</ThemeContext.Provider>); </ThemeContext.Provider>);
} }

View File

@ -83,6 +83,9 @@
.z-modal-tooltip { .z-modal-tooltip {
z-index: 90; z-index: 90;
} }
.z-topmost {
z-index: 99;
}
:root { :root {

View File

@ -1,4 +1,7 @@
import clsx from 'clsx';
import { TokenID } from '@/models/rslang'; import { TokenID } from '@/models/rslang';
import { globalIDs } from '@/utils/constants';
interface RSLocalButtonProps { interface RSLocalButtonProps {
text: string text: string
@ -11,8 +14,14 @@ function RSLocalButton({ text, title, disabled, onInsert }: RSLocalButtonProps)
return ( return (
<button type='button' tabIndex={-1} <button type='button' tabIndex={-1}
disabled={disabled} disabled={disabled}
title={title} data-tooltip-id={title ? globalIDs.tooltip: undefined}
className='w-[2rem] h-6 cursor-pointer disabled:cursor-default rounded-none clr-hover clr-btn-clear' data-tooltip-content={title}
className={clsx(
'w-[2rem] h-6',
'cursor-pointer disabled:cursor-default',
'rounded-none',
'clr-hover clr-btn-clear'
)}
onClick={() => onInsert(TokenID.ID_LOCAL, text)} onClick={() => onInsert(TokenID.ID_LOCAL, text)}
> >
{text} {text}

View File

@ -1,4 +1,7 @@
import clsx from 'clsx';
import { TokenID } from '@/models/rslang'; import { TokenID } from '@/models/rslang';
import { globalIDs } from '@/utils/constants';
import { describeToken, labelToken } from '@/utils/labels'; import { describeToken, labelToken } from '@/utils/labels';
interface RSTokenButtonProps { interface RSTokenButtonProps {
@ -9,13 +12,23 @@ interface RSTokenButtonProps {
function RSTokenButton({ token, disabled, onInsert }: RSTokenButtonProps) { function RSTokenButton({ token, disabled, onInsert }: RSTokenButtonProps) {
const label = labelToken(token); const label = labelToken(token);
const width = label.length > 3 ? 'w-[4.5rem]' : 'w-[2.25rem]';
return ( return (
<button type='button' tabIndex={-1} <button type='button' tabIndex={-1}
disabled={disabled} disabled={disabled}
onClick={() => onInsert(token)} onClick={() => onInsert(token)}
title={describeToken(token)} className={clsx(
className={`px-1 cursor-pointer disabled:cursor-default h-6 ${width} outline-none clr-hover clr-btn-clear`} 'h-6',
'px-1',
'outline-none',
'clr-hover clr-btn-clear',
'cursor-pointer disabled:cursor-default',
{
'w-[4.5rem]': label.length > 3,
'w-[2.25rem]': label.length <= 3
}
)}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-content={describeToken(token)}
> >
{label ? <span className='whitespace-nowrap'>{label}</span> : null} {label ? <span className='whitespace-nowrap'>{label}</span> : null}
</button>); </button>);

View File

@ -10,6 +10,7 @@ import { type IConstituenta } from '@/models/rsform';
import { inferStatus } from '@/models/rsformAPI'; import { inferStatus } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang'; import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { colorbgCstStatus } from '@/utils/color'; import { colorbgCstStatus } from '@/utils/color';
import { globalIDs } from '@/utils/constants';
import { labelExpressionStatus } from '@/utils/labels'; import { labelExpressionStatus } from '@/utils/labels';
import StatusIcon from './StatusIcon'; import StatusIcon from './StatusIcon';
@ -37,7 +38,6 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
return ( return (
<div <div
title='Проверить определение [Ctrl + Q]'
className={clsx( className={clsx(
'w-[10rem] h-[1.75rem]', 'w-[10rem] h-[1.75rem]',
'px-2 flex items-center justify-center gap-2', 'px-2 flex items-center justify-center gap-2',
@ -47,6 +47,8 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
'duration-500 transition-colors' 'duration-500 transition-colors'
)} )}
style={{backgroundColor: processing ? colors.bgDefault : colorbgCstStatus(status, colors)}} style={{backgroundColor: processing ? colors.bgDefault : colorbgCstStatus(status, colors)}}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-content='Проверить определение [Ctrl + Q]'
onClick={onAnalyze} onClick={onAnalyze}
> >
{processing ? {processing ?

View File

@ -52,7 +52,7 @@ function RSFormToolbar({
onClick={onDownload} onClick={onDownload}
/> />
<MiniButton <MiniButton
title={'отслеживание: ' + (isSubscribed ? '[включено]' : '[выключено]')} title={'Отслеживание ' + (isSubscribed ? 'включено' : 'выключено')}
disabled={anonymous || processing} disabled={anonymous || processing}
icon={isSubscribed icon={isSubscribed
? <FiBell size='1.25rem' className='clr-text-primary' /> ? <FiBell size='1.25rem' className='clr-text-primary' />
@ -62,7 +62,7 @@ function RSFormToolbar({
onClick={onToggleSubscribe} onClick={onToggleSubscribe}
/> />
<MiniButton <MiniButton
title={claimable ? 'Стать владельцем' : 'Невозможно стать владельцем' } title='Стать владельцем'
icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'}/>} icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'}/>}
disabled={!claimable || anonymous || processing} disabled={!claimable || anonymous || processing}
onClick={onClaim} onClick={onClaim}

View File

@ -419,10 +419,7 @@ function RSTabs() {
/> />
<ConceptTab <ConceptTab
label='Содержание' label='Содержание'
title={[ title={`Конституент: ${schema.stats?.count_all ?? 0} | Ошибок: ${schema.stats?.count_errors ?? 0}`}
`Всего конституент: ${schema.stats?.count_all ?? 0}`,
`Количество ошибок: ${schema.stats?.count_errors ?? 0}`
].join('\n')}
/> />
<ConceptTab label='Редактор' /> <ConceptTab label='Редактор' />
<ConceptTab label='Граф термов' /> <ConceptTab label='Граф термов' />

View File

@ -172,7 +172,7 @@ function RSTabsMenu({
<div ref={accessMenu.ref}> <div ref={accessMenu.ref}>
<Button dense noBorder tabIndex={-1} <Button dense noBorder tabIndex={-1}
title={`режим ${labelAccessMode(mode)}`} title={`Режим ${labelAccessMode(mode)}`}
className='h-full pr-2' className='h-full pr-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
icon={ icon={

View File

@ -189,9 +189,9 @@ export function domTooltipEntityReference(ref: IEntityReference, cst: IConstitue
'select-none cursor-auto' 'select-none cursor-auto'
); );
const title = document.createElement('p'); const header = document.createElement('p');
title.innerHTML = '<b>Ссылка на конституенту</b>'; header.innerHTML = '<b>Ссылка на конституенту</b>';
dom.appendChild(title); dom.appendChild(header);
const term = document.createElement('p'); const term = document.createElement('p');
term.innerHTML = `<b>${ref.entity}:</b> ${describeConstituentaTerm(cst)}`; term.innerHTML = `<b>${ref.entity}:</b> ${describeConstituentaTerm(cst)}`;
@ -237,9 +237,9 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
'select-none cursor-auto' 'select-none cursor-auto'
); );
const title = document.createElement('p'); const header = document.createElement('p');
title.innerHTML = '<b>Связывание слов</b>'; header.innerHTML = '<b>Связывание слов</b>';
dom.appendChild(title); dom.appendChild(header);
const offset = document.createElement('p'); const offset = document.createElement('p');
offset.innerHTML = `<b>Смещение:</b> ${ref.offset}`; offset.innerHTML = `<b>Смещение:</b> ${ref.offset}`;

View File

@ -83,6 +83,7 @@ export const urls = {
* Global unique IDs. * Global unique IDs.
*/ */
export const globalIDs = { export const globalIDs = {
tooltip: 'global-tooltip',
password_tooltip: 'password-tooltip', password_tooltip: 'password-tooltip',
main_scroll: 'main-scroll', main_scroll: 'main-scroll',
library_item_editor: 'library-item-editor', library_item_editor: 'library-item-editor',