mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(console): implement tooltip and integrate with copy (#323)
This commit is contained in:
parent
fc113fba99
commit
d035047ee8
9 changed files with 195 additions and 12 deletions
|
@ -9,7 +9,7 @@
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"start": "parcel src/index.html",
|
"start": "parcel src/index.html",
|
||||||
"dev": "PORT=5002 parcel src/index.html --public-url /console --no-hmr",
|
"dev": "PORT=5002 parcel src/index.html --public-url /console --no-hmr --no-cache",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --public-url /console",
|
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --public-url /console",
|
||||||
"lint": "eslint --ext .ts --ext .tsx src",
|
"lint": "eslint --ext .ts --ext .tsx src",
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/phrases": "^0.1.0",
|
"@logto/phrases": "^0.1.0",
|
||||||
"@logto/schemas": "^0.1.0",
|
"@logto/schemas": "^0.1.0",
|
||||||
|
"@silverhand/essentials": "^1.1.6",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"i18next": "^21.6.12",
|
"i18next": "^21.6.12",
|
||||||
"i18next-browser-languagedetector": "^6.1.3",
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
|
@ -51,7 +52,22 @@
|
||||||
"@/*": "./src/$1"
|
"@/*": "./src/$1"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@silverhand/react"
|
"extends": "@silverhand/react",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"unicorn/no-useless-undefined": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"checkArguments": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"stylelint": {
|
"stylelint": {
|
||||||
"extends": "@silverhand/eslint-config-react/.stylelintrc"
|
"extends": "@silverhand/eslint-config-react/.stylelintrc"
|
||||||
|
|
|
@ -25,3 +25,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.successTooltip {
|
||||||
|
background: #008a71;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-top-color: #008a71;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,46 @@
|
||||||
import React, { MouseEventHandler, SVGProps } from 'react';
|
import classNames from 'classnames';
|
||||||
|
import React, { forwardRef, MouseEventHandler, SVGProps, useEffect, useRef, useState } from 'react';
|
||||||
|
import { TFuncKey, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Tooltip from '../Tooltip';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CopyIcon = (props: SVGProps<SVGSVGElement>) => (
|
const CopyIcon = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
(props: SVGProps<SVGSVGElement>, reference) => (
|
||||||
|
<svg
|
||||||
|
ref={reference}
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<path d="M11.6667 18.3332H3.33335C2.88675 18.3487 2.45375 18.1781 2.13776 17.8621C1.82177 17.5461 1.65116 17.1131 1.66668 16.6665V8.33317C1.65116 7.88656 1.82177 7.45356 2.13776 7.13758C2.45375 6.82159 2.88675 6.65098 3.33335 6.6665H6.66668V3.33317C6.65117 2.88656 6.82177 2.45356 7.13776 2.13757C7.45375 1.82159 7.88675 1.65098 8.33335 1.6665H16.6667C17.1133 1.65098 17.5463 1.82159 17.8623 2.13757C18.1783 2.45356 18.3489 2.88656 18.3334 3.33317V11.6665C18.3486 12.113 18.1779 12.5459 17.862 12.8618C17.5461 13.1778 17.1132 13.3484 16.6667 13.3332H13.3334V16.6665C13.3486 17.113 13.1779 17.5459 12.862 17.8618C12.5461 18.1778 12.1132 18.3484 11.6667 18.3332ZM3.33335 8.33317V16.6665H11.6667V13.3332H8.33335C7.88682 13.3484 7.45396 13.1778 7.13803 12.8618C6.8221 12.5459 6.65141 12.113 6.66668 11.6665V8.33317H3.33335ZM8.33335 3.33317V11.6665H16.6667V3.33317H8.33335Z" />
|
<path d="M11.6667 18.3332H3.33335C2.88675 18.3487 2.45375 18.1781 2.13776 17.8621C1.82177 17.5461 1.65116 17.1131 1.66668 16.6665V8.33317C1.65116 7.88656 1.82177 7.45356 2.13776 7.13758C2.45375 6.82159 2.88675 6.65098 3.33335 6.6665H6.66668V3.33317C6.65117 2.88656 6.82177 2.45356 7.13776 2.13757C7.45375 1.82159 7.88675 1.65098 8.33335 1.6665H16.6667C17.1133 1.65098 17.5463 1.82159 17.8623 2.13757C18.1783 2.45356 18.3489 2.88656 18.3334 3.33317V11.6665C18.3486 12.113 18.1779 12.5459 17.862 12.8618C17.5461 13.1778 17.1132 13.3484 16.6667 13.3332H13.3334V16.6665C13.3486 17.113 13.1779 17.5459 12.862 17.8618C12.5461 18.1778 12.1132 18.3484 11.6667 18.3332ZM3.33335 8.33317V16.6665H11.6667V13.3332H8.33335C7.88682 13.3484 7.45396 13.1778 7.13803 12.8618C6.8221 12.5459 6.65141 12.113 6.66668 11.6665V8.33317H3.33335ZM8.33335 3.33317V11.6665H16.6667V3.33317H8.33335Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type CopyState = TFuncKey<'translation', 'admin_console.copy'>;
|
||||||
|
|
||||||
const CopyToClipboard = ({ value }: Props) => {
|
const CopyToClipboard = ({ value }: Props) => {
|
||||||
const copy: MouseEventHandler<SVGSVGElement> = () => {
|
const copyIconReference = useRef<SVGSVGElement>(null);
|
||||||
// TO-DO: toast after finished
|
const [copyState, setCopyState] = useState<CopyState>('pending');
|
||||||
void navigator.clipboard.writeText(value);
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.copy' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
copyIconReference.current?.addEventListener('mouseleave', () => {
|
||||||
|
setCopyState('pending');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copy: MouseEventHandler<SVGSVGElement> = async () => {
|
||||||
|
setCopyState('copying');
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
setCopyState('copied');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -27,7 +52,12 @@ const CopyToClipboard = ({ value }: Props) => {
|
||||||
>
|
>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
{value}
|
{value}
|
||||||
<CopyIcon onClick={copy} />
|
<CopyIcon ref={copyIconReference} onClick={copy} />
|
||||||
|
<Tooltip
|
||||||
|
className={classNames(copyState === 'copied' && styles.successTooltip)}
|
||||||
|
domRef={copyIconReference}
|
||||||
|
content={t(copyState)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
25
packages/console/src/components/Tooltip/index.module.scss
Normal file
25
packages/console/src/components/Tooltip/index.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #191c1d;
|
||||||
|
color: #e0e3e3;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 8%);
|
||||||
|
padding: _.unit(2) _.unit(3);
|
||||||
|
font: var(--font-body);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
border-width: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #191c1d transparent transparent;
|
||||||
|
left: 50%;
|
||||||
|
top: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 8%);
|
||||||
|
}
|
||||||
|
}
|
83
packages/console/src/components/Tooltip/index.tsx
Normal file
83
packages/console/src/components/Tooltip/index.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { Nullable } from '@silverhand/essentials';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ReactNode, RefObject, useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
content: ReactNode;
|
||||||
|
domRef: RefObject<Nullable<T>>;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Position = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tooltip = <T extends Element>({ content, domRef, className }: Props<T>) => {
|
||||||
|
const [tooltipDom, setTooltipDom] = useState<HTMLDivElement>();
|
||||||
|
const [position, setPosition] = useState<Position>();
|
||||||
|
const isVisible = position !== undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!domRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = domRef.current;
|
||||||
|
|
||||||
|
const enterHandler = () => {
|
||||||
|
if (domRef.current) {
|
||||||
|
const { top, left, width } = domRef.current.getBoundingClientRect();
|
||||||
|
const { scrollTop, scrollLeft } = document.documentElement;
|
||||||
|
setPosition({ top: top + scrollTop - 12, left: left + scrollLeft + width / 2 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveHandler = () => {
|
||||||
|
setPosition(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
dom.addEventListener('mouseenter', enterHandler);
|
||||||
|
dom.addEventListener('mouseleave', leaveHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dom.removeEventListener('mouseenter', enterHandler);
|
||||||
|
dom.removeEventListener('mouseleave', leaveHandler);
|
||||||
|
};
|
||||||
|
}, [domRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible) {
|
||||||
|
if (tooltipDom) {
|
||||||
|
tooltipDom.remove();
|
||||||
|
setTooltipDom(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tooltipDom) {
|
||||||
|
const dom = document.createElement('div');
|
||||||
|
document.body.append(dom);
|
||||||
|
setTooltipDom(dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => tooltipDom?.remove();
|
||||||
|
}, [isVisible, tooltipDom]);
|
||||||
|
|
||||||
|
if (!tooltipDom || !position) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className={classNames(styles.container, className)} style={{ ...position }}>
|
||||||
|
{content}
|
||||||
|
</div>,
|
||||||
|
tooltipDom
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tooltip;
|
|
@ -14,6 +14,11 @@ const translation = {
|
||||||
},
|
},
|
||||||
admin_console: {
|
admin_console: {
|
||||||
title: 'Admin Console',
|
title: 'Admin Console',
|
||||||
|
copy: {
|
||||||
|
pending: 'Copy',
|
||||||
|
copying: 'Copying',
|
||||||
|
copied: 'Copied',
|
||||||
|
},
|
||||||
tab_sections: {
|
tab_sections: {
|
||||||
overview: 'Overview',
|
overview: 'Overview',
|
||||||
resource_management: 'Resource Management',
|
resource_management: 'Resource Management',
|
||||||
|
|
|
@ -16,6 +16,11 @@ const translation = {
|
||||||
},
|
},
|
||||||
admin_console: {
|
admin_console: {
|
||||||
title: '管理面板',
|
title: '管理面板',
|
||||||
|
copy: {
|
||||||
|
pending: '拷贝',
|
||||||
|
copying: '拷贝中',
|
||||||
|
copied: '已拷贝',
|
||||||
|
},
|
||||||
tab_sections: {
|
tab_sections: {
|
||||||
overview: '概览',
|
overview: '概览',
|
||||||
resource_management: '资源管理',
|
resource_management: '资源管理',
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"start": "parcel src/index.html",
|
"start": "parcel src/index.html",
|
||||||
"dev": "PORT=5001 parcel src/index.html --no-hmr",
|
"dev": "PORT=5001 parcel src/index.html --no-hmr --no-cache",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall",
|
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall",
|
||||||
"lint": "eslint --ext .ts --ext .tsx src",
|
"lint": "eslint --ext .ts --ext .tsx src",
|
||||||
|
|
|
@ -26,6 +26,7 @@ importers:
|
||||||
'@parcel/transformer-sass': ^2.3.1
|
'@parcel/transformer-sass': ^2.3.1
|
||||||
'@silverhand/eslint-config': ^0.9.1
|
'@silverhand/eslint-config': ^0.9.1
|
||||||
'@silverhand/eslint-config-react': ^0.9.3
|
'@silverhand/eslint-config-react': ^0.9.3
|
||||||
|
'@silverhand/essentials': ^1.1.6
|
||||||
'@silverhand/ts-config': ^0.9.1
|
'@silverhand/ts-config': ^0.9.1
|
||||||
'@silverhand/ts-config-react': ^0.9.3
|
'@silverhand/ts-config-react': ^0.9.3
|
||||||
'@types/lodash.kebabcase': ^4.1.6
|
'@types/lodash.kebabcase': ^4.1.6
|
||||||
|
@ -51,6 +52,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
|
'@silverhand/essentials': 1.1.6
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
i18next: 21.6.12
|
i18next: 21.6.12
|
||||||
i18next-browser-languagedetector: 6.1.3
|
i18next-browser-languagedetector: 6.1.3
|
||||||
|
@ -3127,6 +3129,14 @@ packages:
|
||||||
lodash.pick: 4.4.0
|
lodash.pick: 4.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@silverhand/essentials/1.1.6:
|
||||||
|
resolution: {integrity: sha512-Vl57z4HnEW/iSTHlFCPeJDLbg3OegrJy3gg0dbZsZwPifK0O5l5uBnrcpIYIPl3IhLSWw1ZqTyP7QWFpKadpEQ==}
|
||||||
|
engines: {node: '>=14.15.0', pnpm: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
lodash.orderby: 4.6.0
|
||||||
|
lodash.pick: 4.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@silverhand/ts-config-react/0.9.3_typescript@4.5.5:
|
/@silverhand/ts-config-react/0.9.3_typescript@4.5.5:
|
||||||
resolution: {integrity: sha512-oKI9/xxrqcLzCjoNxhDRKhk1HRfLKw1nvoWm4RQUHJWCSGBLHovuUB/qwwQXEPrJnwweRGfatFnbM5dTSKPwGA==}
|
resolution: {integrity: sha512-oKI9/xxrqcLzCjoNxhDRKhk1HRfLKw1nvoWm4RQUHJWCSGBLHovuUB/qwwQXEPrJnwweRGfatFnbM5dTSKPwGA==}
|
||||||
engines: {node: '>=14.15.0'}
|
engines: {node: '>=14.15.0'}
|
||||||
|
|
Loading…
Reference in a new issue