0
Fork 0
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:
Xiao Yijun 2022-12-16 09:59:10 +08:00 committed by GitHub
parent bc5f4b541a
commit 57a28be292
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 130 additions and 122 deletions

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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%;
}
}
}

View file

@ -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>
);
};

View file

@ -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':

View file

@ -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;
}
}
}

View file

@ -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}
>

View file

@ -1,9 +1,5 @@
@use '@/scss/underscore' as _;
.tooltip {
position: absolute;
.content {
@include _.multi-line-ellipsis(6);
}
.content {
@include _.multi-line-ellipsis(6);
}

View file

@ -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
)}
</>

View file

@ -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;

View file

@ -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')}
>

View 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';