0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): improve tip components usage (#2608)

This commit is contained in:
Xiao Yijun 2022-12-09 11:40:39 +08:00 committed by GitHub
parent 63cbd9a832
commit 6ae4919762
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 257 additions and 279 deletions

View file

@ -1,9 +1,9 @@
import classNames from 'classnames';
import { nanoid } from 'nanoid';
import type { ReactNode } from 'react';
import { useRef, useState } from 'react';
import { useState } from 'react';
import Tooltip from '../Tooltip';
import { Tooltip } from '../Tip';
import Icon from './Icon';
import * as styles from './index.module.scss';
@ -21,8 +21,6 @@ type Props = {
const Checkbox = ({ value, onChange, label, disabled, className, disabledTooltip }: Props) => {
const [id, setId] = useState(nanoid());
const tipRef = useRef<HTMLDivElement>(null);
return (
<div className={classNames(styles.checkbox, className)}>
<input
@ -35,10 +33,11 @@ const Checkbox = ({ value, onChange, label, disabled, className, disabledTooltip
}}
/>
{disabled && disabledTooltip && (
<>
<div ref={tipRef} className={styles.disabledMask} />
<Tooltip anchorRef={tipRef} content={disabledTooltip} />
</>
<Tooltip
horizontalAlign="start"
anchorClassName={styles.disabledMask}
content={disabledTooltip}
/>
)}
<Icon className={styles.icon} />
{label && <label htmlFor={id}>{label}</label>}

View file

@ -31,8 +31,11 @@
text-overflow: ellipsis;
}
.copyIconButton {
.copyToolTipAnchor {
margin-left: _.unit(3);
}
.copyIconButton {
height: 20px;
width: 20px;

View file

@ -10,7 +10,7 @@ import Eye from '@/assets/images/eye.svg';
import { onKeyDownHandler } from '@/utilities/a11y';
import IconButton from '../IconButton';
import Tooltip from '../Tooltip';
import { Tooltip } from '../Tip';
import * as styles from './index.module.scss';
type Props = {
@ -78,20 +78,20 @@ const CopyToClipboard = ({
</IconButton>
</div>
)}
<IconButton
ref={copyIconReference}
className={styles.copyIconButton}
iconClassName={styles.copyIcon}
onClick={copy}
>
<Copy />
</IconButton>
<Tooltip
anchorRef={copyIconReference}
content={t(copyState)}
horizontalAlign="center"
className={classNames(copyState === 'copied' && styles.successfulTooltip)}
/>
anchorClassName={styles.copyToolTipAnchor}
content={t(copyState)}
>
<IconButton
ref={copyIconReference}
className={styles.copyIconButton}
iconClassName={styles.copyIcon}
onClick={copy}
>
<Copy />
</IconButton>
</Tooltip>
</div>
</div>
);

View file

@ -17,7 +17,7 @@
}
.toggleTipButton {
margin-left: _.unit(1);
margin-left: _.unit(0.5);
}
.required {

View file

@ -3,9 +3,12 @@ import classNames from 'classnames';
import type { ReactElement, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import Tip from '@/assets/images/tip.svg';
import type DangerousRaw from '../DangerousRaw';
import IconButton from '../IconButton';
import Spacer from '../Spacer';
import ToggleTipButton from '../ToggleTipButton';
import { ToggleTip } from '../Tip';
import * as styles from './index.module.scss';
export type Props = {
@ -25,7 +28,11 @@ const FormField = ({ title, children, isRequired, className, tip, headlineClassN
<div className={classNames(styles.headline, headlineClassName)}>
<div className={styles.title}>{typeof title === 'string' ? t(title) : title}</div>
{tip && (
<ToggleTipButton className={styles.toggleTipButton} render={() => <div>{t(tip)}</div>} />
<ToggleTip anchorClassName={styles.toggleTipButton} content={<div>{t(tip)}</div>}>
<IconButton size="small">
<Tip />
</IconButton>
</ToggleTip>
)}
<Spacer />
{isRequired && <div className={styles.required}>{t('general.required')}</div>}

View file

@ -1,18 +1,16 @@
import classNames from 'classnames';
import type { ForwardedRef, HTMLProps, ReactNode } from 'react';
import type { ForwardedRef, HTMLProps } from 'react';
import { forwardRef, useRef } from 'react';
import Tooltip from '../Tooltip';
import * as styles from './index.module.scss';
export type Props = Omit<HTMLProps<HTMLButtonElement>, 'size' | 'type'> & {
size?: 'small' | 'medium' | 'large';
tooltip?: ReactNode;
iconClassName?: string;
};
const IconButton = (
{ size = 'medium', children, className, tooltip, iconClassName, ...rest }: Props,
{ size = 'medium', children, className, iconClassName, ...rest }: Props,
reference: ForwardedRef<HTMLButtonElement>
) => {
const tipRef = useRef<HTMLDivElement>(null);
@ -27,9 +25,6 @@ const IconButton = (
<div ref={tipRef} className={classNames(styles.icon, iconClassName)}>
{children}
</div>
{tooltip && (
<Tooltip anchorRef={tipRef} content={tooltip} position="top" horizontalAlign="center" />
)}
</button>
);
};

View file

@ -0,0 +1,104 @@
import type { ReactNode } from 'react';
import { useCallback, useState, useRef } from 'react';
import ReactModal from 'react-modal';
import type { HorizontalAlignment } from '@/hooks/use-position';
import usePosition from '@/hooks/use-position';
import { onKeyDownHandler } from '@/utilities/a11y';
import type { TipBubblePosition } from '../TipBubble';
import TipBubble from '../TipBubble';
import {
getVerticalAlignment,
getHorizontalAlignment,
getVerticalOffset,
getHorizontalOffset,
} from '../TipBubble/utils';
import * as styles from './index.module.scss';
type Props = {
children: ReactNode;
className?: string;
anchorClassName?: string;
position?: TipBubblePosition;
horizontalAlign?: HorizontalAlignment;
content?: ((closeTip: () => void) => ReactNode) | ReactNode;
};
const ToggleTip = ({
children,
className,
anchorClassName,
position = 'top',
horizontalAlign = 'center',
content,
}: Props) => {
const overlayRef = useRef<HTMLDivElement>(null);
const anchorRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const onClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
const {
position: layoutPosition,
positionState,
mutate,
} = usePosition({
verticalAlign: getVerticalAlignment(position),
horizontalAlign: getHorizontalAlignment(position, horizontalAlign),
offset: {
vertical: getVerticalOffset(position),
horizontal: getHorizontalOffset(position, horizontalAlign),
},
anchorRef,
overlayRef,
});
return (
<>
<div
ref={anchorRef}
role="tab"
tabIndex={0}
className={anchorClassName}
onClick={() => {
setIsOpen(true);
}}
onKeyDown={onKeyDownHandler(() => {
setIsOpen(true);
})}
>
{children}
</div>
<ReactModal
shouldCloseOnOverlayClick
shouldCloseOnEsc
isOpen={isOpen}
style={{
content: {
...(!layoutPosition && { opacity: 0 }),
...layoutPosition,
},
}}
className={styles.content}
overlayClassName={styles.overlay}
onRequestClose={onClose}
onAfterOpen={mutate}
>
<TipBubble
ref={overlayRef}
position={position}
className={className}
horizontalAlignment={positionState.horizontalAlign}
>
{typeof content === 'function' ? content(onClose) : content}
</TipBubble>
</ReactModal>
</>
);
};
export default ToggleTip;

View file

@ -1,4 +1,4 @@
import type { ReactNode, RefObject } from 'react';
import type { ReactNode } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
@ -16,23 +16,26 @@ import {
import * as styles from './index.module.scss';
type Props = {
content: ReactNode | Record<string, unknown>;
anchorRef: RefObject<Element>;
className?: string;
isKeepOpen?: boolean;
position?: TipBubblePosition;
horizontalAlign?: HorizontalAlignment;
anchorClassName?: string;
children?: ReactNode;
content?: ReactNode;
};
const Tooltip = ({
content,
anchorRef,
className,
isKeepOpen = false,
position = 'top',
horizontalAlign = 'start',
horizontalAlign = 'center',
anchorClassName,
children,
content,
}: Props) => {
const [tooltipDom, setTooltipDom] = useState<HTMLDivElement>();
const anchorRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const {
@ -119,25 +122,30 @@ const Tooltip = ({
useLayoutEffect(() => {
mutate();
}, [content, mutate]);
}, [mutate, content]);
if (!tooltipDom) {
return null;
}
return createPortal(
<div className={styles.tooltip}>
<TipBubble
ref={tooltipRef}
className={className}
style={{ ...(!layoutPosition && { opacity: 0 }), ...layoutPosition }}
position={position}
horizontalAlignment={positionState.horizontalAlign}
>
<div className={styles.content}>{content}</div>
</TipBubble>
</div>,
tooltipDom
return (
<>
<div ref={anchorRef} className={anchorClassName}>
{children}
</div>
{tooltipDom &&
content &&
createPortal(
<div className={styles.tooltip}>
<TipBubble
ref={tooltipRef}
className={className}
style={{ ...(!layoutPosition && { opacity: 0 }), ...layoutPosition }}
position={position}
horizontalAlignment={positionState.horizontalAlign}
>
<div className={styles.content}>{content}</div>
</TipBubble>
</div>,
tooltipDom
)}
</>
);
};

View file

@ -0,0 +1,2 @@
export { default as Tooltip } from './Tooltip';
export { default as ToggleTip } from './ToggleTip';

View file

@ -1,79 +0,0 @@
import type { HTMLProps, ReactNode, RefObject } from 'react';
import { useRef } from 'react';
import ReactModal from 'react-modal';
import type { HorizontalAlignment } from '@/hooks/use-position';
import usePosition from '@/hooks/use-position';
import type { TipBubblePosition } from '../TipBubble';
import TipBubble from '../TipBubble';
import {
getVerticalAlignment,
getHorizontalAlignment,
getVerticalOffset,
getHorizontalOffset,
} from '../TipBubble/utils';
import * as styles from './index.module.scss';
type Props = HTMLProps<HTMLDivElement> & {
children: ReactNode;
isOpen: boolean;
onClose: () => void;
anchorRef: RefObject<HTMLElement>;
position?: TipBubblePosition;
horizontalAlign?: HorizontalAlignment;
};
const ToggleTip = ({
children,
isOpen,
onClose,
anchorRef,
position = 'top',
horizontalAlign = 'start',
}: Props) => {
const overlayRef = useRef<HTMLDivElement>(null);
const {
position: layoutPosition,
positionState,
mutate,
} = usePosition({
verticalAlign: getVerticalAlignment(position),
horizontalAlign: getHorizontalAlignment(position, horizontalAlign),
offset: {
vertical: getVerticalOffset(position),
horizontal: getHorizontalOffset(position, horizontalAlign),
},
anchorRef,
overlayRef,
});
return (
<ReactModal
shouldCloseOnOverlayClick
shouldCloseOnEsc
isOpen={isOpen}
style={{
content: {
...(!layoutPosition && { opacity: 0 }),
...layoutPosition,
},
}}
className={styles.content}
overlayClassName={styles.overlay}
onRequestClose={onClose}
onAfterOpen={mutate}
>
<TipBubble
ref={overlayRef}
position={position}
horizontalAlignment={positionState.horizontalAlign}
>
{children}
</TipBubble>
</ReactModal>
);
};
export default ToggleTip;

View file

@ -1,19 +0,0 @@
@use '@/scss/underscore' as _;
.toggleTipButton {
border-radius: 4px;
padding: _.unit(1);
.icon {
> svg {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
}
&:hover {
background: var(--color-hover);
}
}

View file

@ -1,59 +0,0 @@
import classNames from 'classnames';
import type { ReactElement } from 'react';
import { useRef, useState } from 'react';
import Tip from '@/assets/images/tip.svg';
import type { HorizontalAlignment } from '@/hooks/use-position';
import { onKeyDownHandler } from '@/utilities/a11y';
import type { TipBubblePosition } from '../TipBubble';
import ToggleTip from '../ToggleTip';
import * as styles from './index.module.scss';
type Props = {
render: (closeTipHandler: () => void) => ReactElement;
className?: string;
tipPosition?: TipBubblePosition;
tipHorizontalAlignment?: HorizontalAlignment;
};
const ToggleTipButton = ({ render, className, tipPosition, tipHorizontalAlignment }: Props) => {
const anchorRef = useRef<HTMLDivElement>(null);
const [isTipOpen, setIsTipOpen] = useState(false);
const closeTipHandler = () => {
setIsTipOpen(false);
};
return (
<div className={classNames(styles.toggleTipButton, className)}>
<div
ref={anchorRef}
role="tab"
tabIndex={0}
className={styles.icon}
onClick={() => {
setIsTipOpen(true);
}}
onKeyDown={onKeyDownHandler(() => {
setIsTipOpen(true);
})}
>
<Tip />
</div>
<ToggleTip
isOpen={isTipOpen}
anchorRef={anchorRef}
position={tipPosition}
horizontalAlign={tipHorizontalAlignment}
onClose={() => {
setIsTipOpen(false);
}}
>
{render(closeTipHandler)}
</ToggleTip>
</div>
);
};
export default ToggleTipButton;

View file

@ -17,9 +17,11 @@
color: var(--color-text-secondary);
}
.githubIcon {
.githubToolTipAnchor {
margin-right: _.unit(4);
}
.githubIcon {
div {
display: flex;
}

View file

@ -1,4 +1,3 @@
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import Close from '@/assets/images/close.svg';
@ -8,7 +7,7 @@ import CardTitle from '@/components/CardTitle';
import DangerousRaw from '@/components/DangerousRaw';
import IconButton from '@/components/IconButton';
import Spacer from '@/components/Spacer';
import Tooltip from '@/components/Tooltip';
import Tooltip from '@/components/Tip/Tooltip';
import { SupportedSdk } from '@/types/applications';
import * as styles from './index.module.scss';
@ -47,7 +46,6 @@ const getSampleProjectUrl = (sdk: SupportedSdk) => {
const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const tipRef = useRef<HTMLDivElement>(null);
const onClickGetSample = () => {
const sampleUrl = getSampleProjectUrl(selectedSdk);
@ -64,12 +62,15 @@ const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props
subtitle="applications.guide.header_description"
/>
<Spacer />
<IconButton className={styles.githubIcon} size="large" onClick={onClickGetSample}>
<div ref={tipRef}>
<Tooltip
position="bottom"
anchorClassName={styles.githubToolTipAnchor}
content={t('applications.guide.get_sample_file')}
>
<IconButton className={styles.githubIcon} size="large" onClick={onClickGetSample}>
<GetSample />
</div>
<Tooltip anchorRef={tipRef} content={t('applications.guide.get_sample_file')} />
</IconButton>
</IconButton>
</Tooltip>
<IconButton size="large" onClick={onClose}>
<Close className={styles.closeIcon} />
</IconButton>

View file

@ -1,7 +1,8 @@
import { phoneRegEx, emailRegEx } from '@logto/core-kit';
import { ConnectorType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -9,7 +10,7 @@ import { useTranslation } from 'react-i18next';
import Button from '@/components/Button';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import Tooltip from '@/components/Tooltip';
import { Tooltip } from '@/components/Tip';
import useApi from '@/hooks/use-api';
import { safeParseJson } from '@/utilities/json';
@ -27,7 +28,6 @@ type FormData = {
};
const SenderTester = ({ connectorId, connectorType, config, className }: Props) => {
const buttonPosReference = useRef(null);
const [showTooltip, setShowTooltip] = useState(false);
const {
handleSubmit,
@ -100,23 +100,19 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
})}
/>
</FormField>
<div ref={buttonPosReference} className={styles.send}>
<Tooltip
isKeepOpen
anchorClassName={styles.send}
className={styles.successfulTooltip}
content={conditional(showTooltip && t('connector_details.test_message_sent'))}
>
<Button
isLoading={isSubmitting}
title="connector_details.send"
type="outline"
onClick={onSubmit}
/>
</div>
{showTooltip && (
<Tooltip
isKeepOpen
horizontalAlign="center"
className={styles.successfulTooltip}
anchorRef={buttonPosReference}
content={t('connector_details.test_message_sent')}
/>
)}
</Tooltip>
</div>
<div className={classNames(inputError?.message ? styles.error : styles.description)}>
{inputError?.message ?? t('connector_details.test_sender_description')}

View file

@ -5,7 +5,7 @@
align-items: center;
.tipButton {
margin-left: _.unit(1);
margin-left: _.unit(0.5);
}
}

View file

@ -1,7 +1,9 @@
import { Trans, useTranslation } from 'react-i18next';
import Tip from '@/assets/images/tip.svg';
import IconButton from '@/components/IconButton';
import TextLink from '@/components/TextLink';
import ToggleTipButton from '@/components/ToggleTipButton';
import { ToggleTip } from '@/components/Tip';
import * as styles from './index.module.scss';
@ -11,10 +13,9 @@ const ConnectorStatusField = () => {
return (
<div className={styles.field}>
{t('connectors.connector_status')}
<ToggleTipButton
tipHorizontalAlignment="center"
className={styles.tipButton}
render={(closeTipHandler) => (
<ToggleTip
anchorClassName={styles.tipButton}
content={(closeTipHandler) => (
<>
<div className={styles.title}>{t('connectors.connector_status')}</div>
<div className={styles.content}>
@ -37,7 +38,11 @@ const ConnectorStatusField = () => {
</div>
</>
)}
/>
>
<IconButton size="small">
<Tip />
</IconButton>
</ToggleTip>
</div>
);
};

View file

@ -35,7 +35,7 @@
align-items: center;
.toggleTipButton {
margin-left: _.unit(1);
margin-left: _.unit(0.5);
}
}

View file

@ -5,8 +5,10 @@ import { useTranslation } from 'react-i18next';
import ArrowDown from '@/assets/images/arrow-down.svg';
import ArrowUp from '@/assets/images/arrow-up.svg';
import Tip from '@/assets/images/tip.svg';
import Card from '@/components/Card';
import ToggleTipButton from '@/components/ToggleTipButton';
import IconButton from '@/components/IconButton';
import { ToggleTip } from '@/components/Tip';
import { formatNumberWithComma } from '@/utilities/number';
import * as styles from './Block.module.scss';
@ -29,7 +31,11 @@ const Block = ({ variant = 'default', count, delta, title, tip }: Props) => {
<div className={styles.title}>
{t(title)}
{tip && (
<ToggleTipButton className={styles.toggleTipButton} render={() => <div>{t(tip)}</div>} />
<ToggleTip anchorClassName={styles.toggleTipButton} content={<div>{t(tip)}</div>}>
<IconButton size="small">
<Tip />
</IconButton>
</ToggleTip>
)}
</div>
<div className={styles.content}>

View file

@ -16,6 +16,7 @@ import Delete from '@/assets/images/delete.svg';
import Button from '@/components/Button';
import ConfirmModal from '@/components/ConfirmModal';
import IconButton from '@/components/IconButton';
import { Tooltip } from '@/components/Tip';
import useApi, { RequestError } from '@/hooks/use-api';
import useUiLanguages from '@/hooks/use-ui-languages';
import {
@ -162,14 +163,15 @@ const LanguageDetails = () => {
)}
</div>
{!isBuiltIn && (
<IconButton
tooltip={t('sign_in_exp.others.manage_language.deletion_tip')}
onClick={() => {
setIsDeletionAlertOpen(true);
}}
>
<Delete />
</IconButton>
<Tooltip content={t('sign_in_exp.others.manage_language.deletion_tip')}>
<IconButton
onClick={() => {
setIsDeletionAlertOpen(true);
}}
>
<Delete />
</IconButton>
</Tooltip>
)}
</div>
<form
@ -189,20 +191,23 @@ const LanguageDetails = () => {
<th>
<span className={style.customValuesColumn}>
{t('sign_in_exp.others.manage_language.custom_values')}
<IconButton
size="small"
className={style.clearButton}
tooltip={t('sign_in_exp.others.manage_language.clear_all_tip')}
onClick={() => {
for (const [key, value] of Object.entries(
flattenTranslation(emptyUiTranslation)
)) {
setValue(key, value, { shouldDirty: true });
}
}}
<Tooltip
anchorClassName={style.clearButton}
content={t('sign_in_exp.others.manage_language.clear_all_tip')}
>
<Clear className={style.clearIcon} />
</IconButton>
<IconButton
size="small"
onClick={() => {
for (const [key, value] of Object.entries(
flattenTranslation(emptyUiTranslation)
)) {
setValue(key, value, { shouldDirty: true });
}
}}
>
<Clear className={style.clearIcon} />
</IconButton>
</Tooltip>
</span>
</th>
</tr>

View file

@ -10,6 +10,7 @@ import Minus from '@/assets/images/minus.svg';
import SwitchArrowIcon from '@/assets/images/switch-arrow.svg';
import Checkbox from '@/components/Checkbox';
import IconButton from '@/components/IconButton';
import { Tooltip } from '@/components/Tip';
import type { SignInMethod } from '@/pages/SignInExperience/types';
import ConnectorSetupWarning from '../ConnectorSetupWarning';
@ -73,13 +74,14 @@ const SignInMethodItem = ({
/>
{identifier !== SignInIdentifier.Username && (
<>
<IconButton
className={styles.swapButton}
tooltip={t('sign_in_exp.sign_up_and_sign_in.sign_in.auth_swap_tip')}
onClick={onToggleVerificationPrimary}
<Tooltip
anchorClassName={styles.swapButton}
content={t('sign_in_exp.sign_up_and_sign_in.sign_in.auth_swap_tip')}
>
<SwitchArrowIcon />
</IconButton>
<IconButton onClick={onToggleVerificationPrimary}>
<SwitchArrowIcon />
</IconButton>
</Tooltip>
<Checkbox
className={styles.checkBox}
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
@ -94,9 +96,8 @@ const SignInMethodItem = ({
)}
</div>
</div>
<IconButton
disabled={!isDeletable}
tooltip={conditional(
<Tooltip
content={conditional(
!isDeletable &&
t('sign_in_exp.sign_up_and_sign_in.tip.delete_sign_in_method', {
identifier: t('sign_in_exp.sign_up_and_sign_in.identifiers', {
@ -104,10 +105,11 @@ const SignInMethodItem = ({
}).toLocaleLowerCase(),
})
)}
onClick={onDelete}
>
<Minus />
</IconButton>
<IconButton disabled={!isDeletable} onClick={onDelete}>
<Minus />
</IconButton>
</Tooltip>
</div>
{errorMessage && <div className={styles.errorMessage}>{errorMessage}</div>}
<ConnectorSetupWarning requiredConnectors={requiredConnectors} />