mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
fix(console): tip bubble position (#2674)
This commit is contained in:
parent
bc5f4b541a
commit
57a28be292
13 changed files with 130 additions and 122 deletions
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||
import type { ReactNode } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import type { HorizontalAlignment } from '@/hooks/use-position';
|
||||
import type { HorizontalAlignment } from '@/types/positioning';
|
||||
|
||||
import type { Props as ButtonProps } from '../Button';
|
||||
import Dropdown from '../Dropdown';
|
||||
|
|
|
@ -3,8 +3,8 @@ import type { 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 { HorizontalAlignment } from '@/types/positioning';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
|
|
@ -29,7 +29,7 @@ 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 && (
|
||||
<ToggleTip anchorClassName={styles.toggleTipButton} content={tip}>
|
||||
<ToggleTip anchorClassName={styles.toggleTipButton} content={tip} horizontalAlign="start">
|
||||
<IconButton size="small">
|
||||
<Tip />
|
||||
</IconButton>
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.tipBubble {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
border-radius: 8px;
|
||||
background: var(--color-tooltip-background);
|
||||
color: var(--color-tooltip-text);
|
||||
box-shadow: var(--shadow-1);
|
||||
box-shadow: var(--shadow-2);
|
||||
padding: _.unit(2) _.unit(3);
|
||||
font: var(--font-body-medium);
|
||||
max-width: 300px;
|
||||
|
||||
&.invisible {
|
||||
opacity: 0%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #cabeff;
|
||||
|
||||
|
@ -18,8 +22,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
.arrow {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
|
@ -29,34 +32,29 @@
|
|||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&.top::after {
|
||||
top: 100%;
|
||||
&.top {
|
||||
.arrow {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.right::after {
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
&.right {
|
||||
.arrow {
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom::after {
|
||||
top: 0%;
|
||||
&.bottom {
|
||||
.arrow {
|
||||
top: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
&.left::after {
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
&.start::after {
|
||||
left: _.unit(10);
|
||||
}
|
||||
|
||||
|
||||
&.center::after {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.end::after {
|
||||
right: _.unit(7.5);
|
||||
&.left {
|
||||
.arrow {
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,64 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ForwardedRef, ReactNode, HTMLProps } from 'react';
|
||||
import type { ForwardedRef, ReactNode, HTMLProps, RefObject } from 'react';
|
||||
|
||||
import type { HorizontalAlignment } from '@/hooks/use-position';
|
||||
import type { HorizontalAlignment, Position } from '@/types/positioning';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type TipBubblePosition = 'top' | 'right' | 'bottom' | 'left';
|
||||
export type TipBubblePlacement = 'top' | 'right' | 'bottom' | 'left';
|
||||
|
||||
type Props = HTMLProps<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
position?: TipBubblePosition;
|
||||
position?: Position;
|
||||
anchorRef: RefObject<Element>;
|
||||
placement?: TipBubblePlacement;
|
||||
horizontalAlignment?: HorizontalAlignment;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const supportHorizontalAlignmentPositions = new Set<TipBubblePosition>(['top', 'bottom']);
|
||||
const supportHorizontalAlignmentPlacements = new Set<TipBubblePlacement>(['top', 'bottom']);
|
||||
|
||||
const TipBubble = (
|
||||
{ children, position = 'bottom', horizontalAlignment = 'center', className, ...rest }: Props,
|
||||
{
|
||||
children,
|
||||
position,
|
||||
placement = 'bottom',
|
||||
horizontalAlignment = 'center',
|
||||
className,
|
||||
anchorRef,
|
||||
...rest
|
||||
}: Props,
|
||||
reference: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
if (!anchorRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchorRect = anchorRef.current.getBoundingClientRect();
|
||||
|
||||
const arrowPosition = conditional(
|
||||
supportHorizontalAlignmentPlacements.has(placement) &&
|
||||
position && {
|
||||
left: anchorRect.x + anchorRect.width / 2 - Number(position.left),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
ref={reference}
|
||||
className={classNames(
|
||||
styles.tipBubble,
|
||||
styles[position],
|
||||
conditional(
|
||||
supportHorizontalAlignmentPositions.has(position) && styles[horizontalAlignment]
|
||||
),
|
||||
styles[placement],
|
||||
!position && styles.invisible,
|
||||
className
|
||||
)}
|
||||
style={{ ...position }}
|
||||
>
|
||||
{children}
|
||||
<div className={styles.arrow} style={{ ...arrowPosition }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { HorizontalAlignment, VerticalAlignment } from '@/hooks/use-position';
|
||||
import type { HorizontalAlignment, VerticalAlignment } from '@/types/positioning';
|
||||
|
||||
import type { TipBubblePosition } from '.';
|
||||
import type { TipBubblePlacement } from '.';
|
||||
|
||||
export const getVerticalOffset = (position: TipBubblePosition) => {
|
||||
switch (position) {
|
||||
export const getVerticalOffset = (placement: TipBubblePlacement) => {
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
return -16;
|
||||
case 'bottom':
|
||||
|
@ -14,10 +14,10 @@ export const getVerticalOffset = (position: TipBubblePosition) => {
|
|||
};
|
||||
|
||||
export const getHorizontalOffset = (
|
||||
tooltipPosition: TipBubblePosition,
|
||||
placement: TipBubblePlacement,
|
||||
horizontalAlignment: HorizontalAlignment
|
||||
): number => {
|
||||
if (tooltipPosition === 'top' || tooltipPosition === 'bottom') {
|
||||
if (placement === 'top' || placement === 'bottom') {
|
||||
switch (horizontalAlignment) {
|
||||
case 'start':
|
||||
return -32;
|
||||
|
@ -27,12 +27,12 @@ export const getHorizontalOffset = (
|
|||
return 0;
|
||||
}
|
||||
} else {
|
||||
return tooltipPosition === 'left' ? -32 : 32;
|
||||
return placement === 'left' ? -32 : 32;
|
||||
}
|
||||
};
|
||||
|
||||
export const getVerticalAlignment = (position: TipBubblePosition): VerticalAlignment => {
|
||||
switch (position) {
|
||||
export const getVerticalAlignment = (placement: TipBubblePlacement): VerticalAlignment => {
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
return 'top';
|
||||
case 'bottom':
|
||||
|
@ -43,10 +43,10 @@ export const getVerticalAlignment = (position: TipBubblePosition): VerticalAlign
|
|||
};
|
||||
|
||||
export const getHorizontalAlignment = (
|
||||
position: TipBubblePosition,
|
||||
placement: TipBubblePlacement,
|
||||
fallback: HorizontalAlignment
|
||||
): HorizontalAlignment => {
|
||||
switch (position) {
|
||||
switch (placement) {
|
||||
case 'right':
|
||||
return 'start';
|
||||
case 'left':
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.content {
|
||||
box-shadow: var(--shadow-2);
|
||||
position: absolute;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: transparent;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ 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 type { HorizontalAlignment } from '@/types/positioning';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import type { TipBubblePosition } from '../TipBubble';
|
||||
import type { TipBubblePlacement } from '../TipBubble';
|
||||
import TipBubble from '../TipBubble';
|
||||
import {
|
||||
getVerticalAlignment,
|
||||
|
@ -20,7 +20,7 @@ export type Props = {
|
|||
children: ReactNode;
|
||||
className?: string;
|
||||
anchorClassName?: string;
|
||||
position?: TipBubblePosition;
|
||||
placement?: TipBubblePlacement;
|
||||
horizontalAlign?: HorizontalAlignment;
|
||||
content?: ((closeTip: () => void) => ReactNode) | ReactNode;
|
||||
};
|
||||
|
@ -29,11 +29,11 @@ const ToggleTip = ({
|
|||
children,
|
||||
className,
|
||||
anchorClassName,
|
||||
position = 'top',
|
||||
placement = 'top',
|
||||
horizontalAlign = 'center',
|
||||
content,
|
||||
}: Props) => {
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const tipBubbleRef = useRef<HTMLDivElement>(null);
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
@ -47,14 +47,14 @@ const ToggleTip = ({
|
|||
positionState,
|
||||
mutate,
|
||||
} = usePosition({
|
||||
verticalAlign: getVerticalAlignment(position),
|
||||
horizontalAlign: getHorizontalAlignment(position, horizontalAlign),
|
||||
verticalAlign: getVerticalAlignment(placement),
|
||||
horizontalAlign: getHorizontalAlignment(placement, horizontalAlign),
|
||||
offset: {
|
||||
vertical: getVerticalOffset(position),
|
||||
horizontal: getHorizontalOffset(position, horizontalAlign),
|
||||
vertical: getVerticalOffset(placement),
|
||||
horizontal: getHorizontalOffset(placement, horizontalAlign),
|
||||
},
|
||||
anchorRef,
|
||||
overlayRef,
|
||||
overlayRef: tipBubbleRef,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -77,20 +77,16 @@ const ToggleTip = ({
|
|||
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}
|
||||
ref={tipBubbleRef}
|
||||
anchorRef={anchorRef}
|
||||
position={layoutPosition}
|
||||
placement={placement}
|
||||
className={className}
|
||||
horizontalAlignment={positionState.horizontalAlign}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
|
||||
.content {
|
||||
@include _.multi-line-ellipsis(6);
|
||||
}
|
||||
.content {
|
||||
@include _.multi-line-ellipsis(6);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ import type { ReactNode } from 'react';
|
|||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { HorizontalAlignment } from '@/hooks/use-position';
|
||||
import usePosition from '@/hooks/use-position';
|
||||
import type { HorizontalAlignment } from '@/types/positioning';
|
||||
|
||||
import TipBubble from '../TipBubble';
|
||||
import type { TipBubblePosition } from '../TipBubble';
|
||||
import type { TipBubblePlacement } from '../TipBubble';
|
||||
import {
|
||||
getVerticalAlignment,
|
||||
getHorizontalAlignment,
|
||||
|
@ -18,7 +18,7 @@ import * as styles from './index.module.scss';
|
|||
type Props = {
|
||||
className?: string;
|
||||
isKeepOpen?: boolean;
|
||||
position?: TipBubblePosition;
|
||||
placement?: TipBubblePlacement;
|
||||
horizontalAlign?: HorizontalAlignment;
|
||||
anchorClassName?: string;
|
||||
children?: ReactNode;
|
||||
|
@ -28,7 +28,7 @@ type Props = {
|
|||
const Tooltip = ({
|
||||
className,
|
||||
isKeepOpen = false,
|
||||
position = 'top',
|
||||
placement = 'top',
|
||||
horizontalAlign = 'center',
|
||||
anchorClassName,
|
||||
children,
|
||||
|
@ -38,16 +38,12 @@ const Tooltip = ({
|
|||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
position: layoutPosition,
|
||||
positionState,
|
||||
mutate,
|
||||
} = usePosition({
|
||||
verticalAlign: getVerticalAlignment(position),
|
||||
horizontalAlign: getHorizontalAlignment(position, horizontalAlign),
|
||||
const { position, positionState, mutate } = usePosition({
|
||||
verticalAlign: getVerticalAlignment(placement),
|
||||
horizontalAlign: getHorizontalAlignment(placement, horizontalAlign),
|
||||
offset: {
|
||||
vertical: getVerticalOffset(position),
|
||||
horizontal: getHorizontalOffset(position, horizontalAlign),
|
||||
vertical: getVerticalOffset(placement),
|
||||
horizontal: getHorizontalOffset(placement, horizontalAlign),
|
||||
},
|
||||
anchorRef,
|
||||
overlayRef: tooltipRef,
|
||||
|
@ -132,17 +128,16 @@ const Tooltip = ({
|
|||
{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>,
|
||||
<TipBubble
|
||||
ref={tooltipRef}
|
||||
anchorRef={anchorRef}
|
||||
className={className}
|
||||
position={position}
|
||||
placement={placement}
|
||||
horizontalAlignment={positionState.horizontalAlign}
|
||||
>
|
||||
<div className={styles.content}>{content}</div>
|
||||
</TipBubble>,
|
||||
tooltipDom
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export type VerticalAlignment = 'top' | 'middle' | 'bottom';
|
||||
|
||||
export type HorizontalAlignment = 'start' | 'center' | 'end';
|
||||
|
||||
type Offset = {
|
||||
vertical: number;
|
||||
horizontal: number;
|
||||
};
|
||||
import type { HorizontalAlignment, Offset, Position, VerticalAlignment } from '@/types/positioning';
|
||||
|
||||
type Props = {
|
||||
verticalAlign: VerticalAlignment;
|
||||
|
@ -18,11 +11,6 @@ type Props = {
|
|||
overlayRef: RefObject<Element>;
|
||||
};
|
||||
|
||||
type Position = {
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
// Leave space for box-shadow effect.
|
||||
const windowSafePadding = 12;
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ const GuideHeader = ({ appName, selectedSdk, isCompact = false, onClose }: Props
|
|||
/>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
placement="bottom"
|
||||
anchorClassName={styles.githubToolTipAnchor}
|
||||
content={t('applications.guide.get_sample_file')}
|
||||
>
|
||||
|
|
13
packages/console/src/types/positioning.ts
Normal file
13
packages/console/src/types/positioning.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type Position = {
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
export type Offset = {
|
||||
horizontal: number;
|
||||
vertical: number;
|
||||
};
|
||||
|
||||
export type HorizontalAlignment = 'start' | 'center' | 'end';
|
||||
|
||||
export type VerticalAlignment = 'top' | 'middle' | 'bottom';
|
Loading…
Reference in a new issue