mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
feat(app): resize the expression picker and snap to emoji columns (#1924)
This commit is contained in:
@@ -52,6 +52,7 @@ interface FloatingState {
|
||||
isReady: boolean;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
placement: Placement;
|
||||
}
|
||||
|
||||
interface UseAntiShiftFloatingOptions {
|
||||
@@ -280,7 +281,7 @@ export function useAntiShiftFloating(
|
||||
const floatingRef = useRef<HTMLElement>(null);
|
||||
const [state, setState] = useState<FloatingState>(() => {
|
||||
const {x, y} = target ? getInitialGuess(target, placement, offsetMainAxis, offsetCrossAxis) : {x: -9999, y: -9999};
|
||||
return {x, y, isReady: false, offsetX: 0, offsetY: 0};
|
||||
return {x, y, isReady: false, offsetX: 0, offsetY: 0, placement};
|
||||
});
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
const isCalculatingRef = useRef(false);
|
||||
@@ -345,7 +346,11 @@ export function useAntiShiftFloating(
|
||||
}),
|
||||
);
|
||||
}
|
||||
const {x, y} = await computePosition(target, floating, {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
placement: resolvedPlacement,
|
||||
} = await computePosition(target, floating, {
|
||||
placement,
|
||||
middleware,
|
||||
});
|
||||
@@ -387,11 +392,18 @@ export function useAntiShiftFloating(
|
||||
prev.y === safeBasePosition.y &&
|
||||
prev.isReady &&
|
||||
prev.offsetX === nextOffset.x &&
|
||||
prev.offsetY === nextOffset.y
|
||||
prev.offsetY === nextOffset.y &&
|
||||
prev.placement === resolvedPlacement
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return {...safeBasePosition, isReady: true, offsetX: nextOffset.x, offsetY: nextOffset.y};
|
||||
return {
|
||||
...safeBasePosition,
|
||||
isReady: true,
|
||||
offsetX: nextOffset.x,
|
||||
offsetY: nextOffset.y,
|
||||
placement: resolvedPlacement,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error positioning floating element', error);
|
||||
|
||||
-41
@@ -87,47 +87,6 @@
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.resizeCorner {
|
||||
position: absolute;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
z-index: 5;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.resizeCorner:focus-visible {
|
||||
outline: 0.125rem solid var(--focus-primary);
|
||||
outline-offset: -0.125rem;
|
||||
}
|
||||
|
||||
.resizeCornerTopLeft {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.resizeCornerTopRight {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resizeCornerBottomLeft {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resizeCornerBottomRight {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
:global(html.expression-picker-resizing),
|
||||
:global(html.expression-picker-resizing) * {
|
||||
cursor: var(--expression-picker-resize-cursor, nwse-resize) !important;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {EmojiPicker} from '@app/features/channel/components/EmojiPicker';
|
||||
import {
|
||||
EMOJI_GRID_SCROLLER_PADDING_WIDTH,
|
||||
EMOJI_GRID_TRACK_WIDTH,
|
||||
} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
|
||||
import {GifPicker} from '@app/features/channel/components/pickers/gif/GifPicker';
|
||||
import {MemesPicker} from '@app/features/channel/components/pickers/memes/MemesPicker';
|
||||
import {StickersPicker} from '@app/features/channel/components/StickersPicker';
|
||||
@@ -20,10 +24,13 @@ import {
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import type {ResizeEdge} from '@app/features/ui/floating_pane/FloatingPaneMath';
|
||||
import {useResizablePane} from '@app/features/ui/hooks/useResizablePane';
|
||||
import {
|
||||
type ResizablePaneHandleLabels,
|
||||
ResizablePaneHandles,
|
||||
} from '@app/features/ui/resizable_pane/ResizablePaneHandles';
|
||||
import {getNextTabIndex, getTabNavigationDirection} from '@app/features/ui/tabs/TabKeyboardNavigation';
|
||||
import type {I18n, MessageDescriptor} from '@lingui/core';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
@@ -31,6 +38,22 @@ import {observer} from 'mobx-react-lite';
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const RESIZE_EXPRESSION_PICKER_TOP_DESCRIPTOR = msg({
|
||||
message: 'Resize expression picker from the top edge',
|
||||
comment: 'Accessible label for the top resize handle on the expression picker popout.',
|
||||
});
|
||||
const RESIZE_EXPRESSION_PICKER_BOTTOM_DESCRIPTOR = msg({
|
||||
message: 'Resize expression picker from the bottom edge',
|
||||
comment: 'Accessible label for the bottom resize handle on the expression picker popout.',
|
||||
});
|
||||
const RESIZE_EXPRESSION_PICKER_LEFT_DESCRIPTOR = msg({
|
||||
message: 'Resize expression picker from the left edge',
|
||||
comment: 'Accessible label for the left resize handle on the expression picker popout.',
|
||||
});
|
||||
const RESIZE_EXPRESSION_PICKER_RIGHT_DESCRIPTOR = msg({
|
||||
message: 'Resize expression picker from the right edge',
|
||||
comment: 'Accessible label for the right resize handle on the expression picker popout.',
|
||||
});
|
||||
const RESIZE_EXPRESSION_PICKER_TOP_LEFT_DESCRIPTOR = msg({
|
||||
message: 'Resize expression picker from top left',
|
||||
comment: 'Accessible label for the top-left resize handle on the expression picker popout.',
|
||||
@@ -48,18 +71,20 @@ const RESIZE_EXPRESSION_PICKER_BOTTOM_RIGHT_DESCRIPTOR = msg({
|
||||
comment: 'Accessible label for the bottom-right resize handle on the expression picker popout.',
|
||||
});
|
||||
|
||||
const EXPRESSION_PICKER_DEFAULT_SIZE = {width: 578, height: 690};
|
||||
const EXPRESSION_PICKER_MIN_SIZE = {width: 360, height: 360};
|
||||
export const EXPRESSION_PICKER_CATEGORY_RAIL_WIDTH = 46;
|
||||
export const EXPRESSION_PICKER_BORDER_WIDTH = 1;
|
||||
export const EXPRESSION_PICKER_GRID_CHROME_WIDTH =
|
||||
EXPRESSION_PICKER_CATEGORY_RAIL_WIDTH + EXPRESSION_PICKER_BORDER_WIDTH * 2 + EMOJI_GRID_SCROLLER_PADDING_WIDTH * 2;
|
||||
export const EXPRESSION_PICKER_WIDTH_SNAP = {
|
||||
step: EMOJI_GRID_TRACK_WIDTH,
|
||||
offset: EXPRESSION_PICKER_GRID_CHROME_WIDTH,
|
||||
};
|
||||
export const EXPRESSION_PICKER_DEFAULT_SIZE = {width: 592, height: 690};
|
||||
export const EXPRESSION_PICKER_MIN_SIZE = {width: 360, height: 360};
|
||||
const EXPRESSION_PICKER_VIEWPORT_PADDING = 16;
|
||||
const EXPRESSION_PICKER_RESIZING_CLASS = 'expression-picker-resizing';
|
||||
const EXPRESSION_PICKER_RESIZE_CURSOR_PROPERTY = '--expression-picker-resize-cursor';
|
||||
|
||||
interface ResizeHandleSpec {
|
||||
edge: ResizeEdge;
|
||||
className: string;
|
||||
label: MessageDescriptor;
|
||||
}
|
||||
|
||||
interface ExpressionPickerHeaderContextType {
|
||||
headerPortalElement: HTMLDivElement | null;
|
||||
}
|
||||
@@ -148,28 +173,16 @@ interface ExpressionPickerPopoutProps {
|
||||
onTabChange?: (tab: ExpressionPickerTabType) => void;
|
||||
}
|
||||
|
||||
const RESIZE_HANDLES: ReadonlyArray<ResizeHandleSpec> = [
|
||||
{
|
||||
edge: 'top-left',
|
||||
className: styles.resizeCornerTopLeft,
|
||||
label: RESIZE_EXPRESSION_PICKER_TOP_LEFT_DESCRIPTOR,
|
||||
},
|
||||
{
|
||||
edge: 'top-right',
|
||||
className: styles.resizeCornerTopRight,
|
||||
label: RESIZE_EXPRESSION_PICKER_TOP_RIGHT_DESCRIPTOR,
|
||||
},
|
||||
{
|
||||
edge: 'bottom-left',
|
||||
className: styles.resizeCornerBottomLeft,
|
||||
label: RESIZE_EXPRESSION_PICKER_BOTTOM_LEFT_DESCRIPTOR,
|
||||
},
|
||||
{
|
||||
edge: 'bottom-right',
|
||||
className: styles.resizeCornerBottomRight,
|
||||
label: RESIZE_EXPRESSION_PICKER_BOTTOM_RIGHT_DESCRIPTOR,
|
||||
},
|
||||
];
|
||||
const RESIZE_HANDLE_LABELS: ResizablePaneHandleLabels = {
|
||||
top: RESIZE_EXPRESSION_PICKER_TOP_DESCRIPTOR,
|
||||
bottom: RESIZE_EXPRESSION_PICKER_BOTTOM_DESCRIPTOR,
|
||||
left: RESIZE_EXPRESSION_PICKER_LEFT_DESCRIPTOR,
|
||||
right: RESIZE_EXPRESSION_PICKER_RIGHT_DESCRIPTOR,
|
||||
'top-left': RESIZE_EXPRESSION_PICKER_TOP_LEFT_DESCRIPTOR,
|
||||
'top-right': RESIZE_EXPRESSION_PICKER_TOP_RIGHT_DESCRIPTOR,
|
||||
'bottom-left': RESIZE_EXPRESSION_PICKER_BOTTOM_LEFT_DESCRIPTOR,
|
||||
'bottom-right': RESIZE_EXPRESSION_PICKER_BOTTOM_RIGHT_DESCRIPTOR,
|
||||
};
|
||||
|
||||
export const ExpressionPickerPopout = observer(
|
||||
({
|
||||
@@ -220,6 +233,7 @@ export const ExpressionPickerPopout = observer(
|
||||
storageKey: 'fluxer:ui:expression-picker-size',
|
||||
defaultSize: EXPRESSION_PICKER_DEFAULT_SIZE,
|
||||
minSize: EXPRESSION_PICKER_MIN_SIZE,
|
||||
widthSnap: EXPRESSION_PICKER_WIDTH_SNAP,
|
||||
viewportPadding: EXPRESSION_PICKER_VIEWPORT_PADDING,
|
||||
resizingClassName: EXPRESSION_PICKER_RESIZING_CLASS,
|
||||
cursorProperty: EXPRESSION_PICKER_RESIZE_CURSOR_PROPERTY,
|
||||
@@ -325,17 +339,11 @@ export const ExpressionPickerPopout = observer(
|
||||
<div className={styles.content} data-flx="expressions.expression-picker-popout.content">
|
||||
{selectedCategory.renderComponent({channelId, onSelect: handleEmojiSelect, onClose})}
|
||||
</div>
|
||||
{RESIZE_HANDLES.map(({edge, className, label}) => (
|
||||
<button
|
||||
key={edge}
|
||||
type="button"
|
||||
aria-label={i18n._(label)}
|
||||
aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown Home End Enter Backspace Delete"
|
||||
className={clsx(styles.resizeCorner, className)}
|
||||
data-flx="expressions.expression-picker-popout.resize-corner.button"
|
||||
{...getHandleProps(edge)}
|
||||
/>
|
||||
))}
|
||||
<ResizablePaneHandles
|
||||
getHandleProps={getHandleProps}
|
||||
labels={RESIZE_HANDLE_LABELS}
|
||||
data-flx="expressions.expression-picker-popout.resizable-pane-handles"
|
||||
/>
|
||||
</div>
|
||||
</ExpressionPickerHeaderContext.Provider>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,16 @@ export interface ResizablePaneSize {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface ResizablePaneWidthSnap {
|
||||
readonly step: number;
|
||||
readonly offset: number;
|
||||
}
|
||||
|
||||
export interface UseResizablePaneOptions {
|
||||
readonly storageKey: string;
|
||||
readonly defaultSize: ResizablePaneSize;
|
||||
readonly minSize: ResizablePaneSize;
|
||||
readonly widthSnap?: ResizablePaneWidthSnap | null;
|
||||
readonly viewportPadding: number;
|
||||
readonly resizingClassName: string;
|
||||
readonly cursorProperty: string;
|
||||
@@ -80,11 +86,22 @@ const RESIZABLE_PANE_SIZE_JSON_MAX_LENGTH =
|
||||
const TITLEBAR_SELECTOR = '[data-native-titlebar]';
|
||||
const logger = new Logger('useResizablePane');
|
||||
|
||||
function clampDimension(value: number, configuredMin: number, availableMax: number): number {
|
||||
export function clampResizablePaneDimension(
|
||||
value: number,
|
||||
configuredMin: number,
|
||||
availableMax: number,
|
||||
snap?: ResizablePaneWidthSnap | null,
|
||||
): number {
|
||||
const max = Math.max(1, Math.floor(availableMax));
|
||||
const min = Math.min(configuredMin, max);
|
||||
if (!Number.isFinite(value)) return min;
|
||||
return Math.max(min, Math.min(Math.round(value), max));
|
||||
const requested = Number.isFinite(value) ? Math.round(value) : min;
|
||||
const bounded = Math.max(min, Math.min(requested, max));
|
||||
if (snap == null || !(snap.step > 0)) return bounded;
|
||||
const fitted = snap.offset + Math.floor((bounded - snap.offset) / snap.step) * snap.step;
|
||||
if (fitted >= min) return fitted;
|
||||
const raised = fitted + snap.step;
|
||||
if (raised <= max) return raised;
|
||||
return fitted > 0 ? fitted : bounded;
|
||||
}
|
||||
|
||||
function getUsableAppZoomFactor(ownerDocument: Document): number {
|
||||
@@ -281,24 +298,25 @@ export function useResizablePane(
|
||||
edge: ResizeEdge | null,
|
||||
anchorRect: ResizeAnchorRect | null,
|
||||
): ResizablePaneSize => {
|
||||
const {minSize, viewportPadding} = optionsRef.current;
|
||||
const {minSize, viewportPadding, widthSnap} = optionsRef.current;
|
||||
const unbounded = Number.POSITIVE_INFINITY;
|
||||
if (ownerDocument == null) {
|
||||
return {
|
||||
width: Math.max(minSize.width, Math.round(size.width)),
|
||||
height: Math.max(minSize.height, Math.round(size.height)),
|
||||
width: clampResizablePaneDimension(size.width, minSize.width, unbounded, widthSnap),
|
||||
height: clampResizablePaneDimension(size.height, minSize.height, unbounded),
|
||||
};
|
||||
}
|
||||
const zoom = getUsableAppZoomFactor(ownerDocument);
|
||||
const maximumSize = getResizeMaximumSize(ownerDocument, viewportPadding, zoom, edge, anchorRect);
|
||||
if (maximumSize == null) {
|
||||
return {
|
||||
width: Math.max(minSize.width, Math.round(size.width)),
|
||||
height: Math.max(minSize.height, Math.round(size.height)),
|
||||
width: clampResizablePaneDimension(size.width, minSize.width, unbounded, widthSnap),
|
||||
height: clampResizablePaneDimension(size.height, minSize.height, unbounded),
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: clampDimension(size.width, minSize.width, maximumSize.width),
|
||||
height: clampDimension(size.height, minSize.height, maximumSize.height),
|
||||
width: clampResizablePaneDimension(size.width, minSize.width, maximumSize.width, widthSnap),
|
||||
height: clampResizablePaneDimension(size.height, minSize.height, maximumSize.height),
|
||||
};
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {PopoutPosition} from '@app/features/ui/popover';
|
||||
import {createContext, useContext} from 'react';
|
||||
|
||||
export interface PopoutResizePositionOffset {
|
||||
@@ -14,6 +15,7 @@ export interface PopoutResizePositionSession {
|
||||
|
||||
export interface PopoutResizePositionController {
|
||||
begin: () => PopoutResizePositionSession;
|
||||
placement?: PopoutPosition;
|
||||
}
|
||||
|
||||
export const PopoutResizePositionContext = createContext<PopoutResizePositionController | null>(null);
|
||||
@@ -25,3 +27,9 @@ export function usePopoutResizePositionController(): PopoutResizePositionControl
|
||||
}
|
||||
return controller;
|
||||
}
|
||||
|
||||
export function usePopoutResizePlacement(): PopoutPosition | null {
|
||||
const controller = useContext(PopoutResizePositionContext);
|
||||
if (controller == null || controller.placement == null) return null;
|
||||
return controller.placement;
|
||||
}
|
||||
|
||||
@@ -198,7 +198,10 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
|
||||
constrainHeight,
|
||||
enableSmartBoundary: true,
|
||||
});
|
||||
const resizePositionController = useMemo(() => ({begin: beginManualPositioning}), [beginManualPositioning]);
|
||||
const resizePositionController = useMemo(
|
||||
() => ({begin: beginManualPositioning, placement: state.placement}),
|
||||
[beginManualPositioning, state.placement],
|
||||
);
|
||||
const {refs: focusRefs, context: focusContext} = useFloating({open: true});
|
||||
useLayoutEffect(() => {
|
||||
focusRefs.setReference(target);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.handle:focus-visible {
|
||||
outline: 0.125rem solid var(--focus-primary);
|
||||
outline-offset: -0.125rem;
|
||||
}
|
||||
|
||||
.edgeTop {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0.5rem;
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.edgeBottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0.5rem;
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.edgeLeft {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.edgeRight {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.cornerTopLeft,
|
||||
.cornerTopRight,
|
||||
.cornerBottomLeft,
|
||||
.cornerBottomRight {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.cornerTopLeft {
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.cornerTopRight {
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.cornerBottomLeft {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.cornerBottomRight {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {ALL_RESIZE_EDGES, type ResizeEdge} from '@app/features/ui/floating_pane/FloatingPaneMath';
|
||||
import type {ResizablePaneHandleProps} from '@app/features/ui/hooks/useResizablePane';
|
||||
import type {PopoutPosition} from '@app/features/ui/popover';
|
||||
import {usePopoutResizePlacement} from '@app/features/ui/popover/PopoutResizePositionContext';
|
||||
import styles from '@app/features/ui/resizable_pane/ResizablePaneHandles.module.css';
|
||||
import type {MessageDescriptor} from '@lingui/core';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
|
||||
const EDGES_AWAY_FROM_ANCHOR: Readonly<Record<PopoutPosition, ReadonlyArray<ResizeEdge>>> = {
|
||||
top: ['top'],
|
||||
bottom: ['bottom'],
|
||||
left: ['left'],
|
||||
right: ['right'],
|
||||
'top-start': ['top', 'right', 'top-right'],
|
||||
'top-end': ['top', 'left', 'top-left'],
|
||||
'bottom-start': ['bottom', 'right', 'bottom-right'],
|
||||
'bottom-end': ['bottom', 'left', 'bottom-left'],
|
||||
'left-start': ['left', 'bottom', 'bottom-left'],
|
||||
'left-end': ['left', 'top', 'top-left'],
|
||||
'right-start': ['right', 'bottom', 'bottom-right'],
|
||||
'right-end': ['right', 'top', 'top-right'],
|
||||
};
|
||||
|
||||
export function getResizablePaneHandleEdges(placement: PopoutPosition | null): ReadonlyArray<ResizeEdge> {
|
||||
if (placement == null) return ALL_RESIZE_EDGES;
|
||||
return EDGES_AWAY_FROM_ANCHOR[placement];
|
||||
}
|
||||
|
||||
const RESIZABLE_PANE_HANDLE_KEY_SHORTCUTS = 'ArrowLeft ArrowRight ArrowUp ArrowDown Home End Enter Backspace Delete';
|
||||
|
||||
export type ResizablePaneHandleLabels = Readonly<Record<ResizeEdge, MessageDescriptor>>;
|
||||
|
||||
const EDGE_CLASS_NAMES: Readonly<Record<ResizeEdge, string>> = {
|
||||
top: styles.edgeTop,
|
||||
bottom: styles.edgeBottom,
|
||||
left: styles.edgeLeft,
|
||||
right: styles.edgeRight,
|
||||
'top-left': styles.cornerTopLeft,
|
||||
'top-right': styles.cornerTopRight,
|
||||
'bottom-left': styles.cornerBottomLeft,
|
||||
'bottom-right': styles.cornerBottomRight,
|
||||
};
|
||||
|
||||
export interface ResizablePaneHandlesProps {
|
||||
getHandleProps: (edge: ResizeEdge) => ResizablePaneHandleProps;
|
||||
labels: ResizablePaneHandleLabels;
|
||||
}
|
||||
|
||||
export function ResizablePaneHandles({getHandleProps, labels}: ResizablePaneHandlesProps) {
|
||||
const {i18n} = useLingui();
|
||||
const placement = usePopoutResizePlacement();
|
||||
return (
|
||||
<>
|
||||
{getResizablePaneHandleEdges(placement).map((edge) => (
|
||||
<button
|
||||
key={edge}
|
||||
type="button"
|
||||
aria-label={i18n._(labels[edge])}
|
||||
aria-keyshortcuts={RESIZABLE_PANE_HANDLE_KEY_SHORTCUTS}
|
||||
className={clsx(styles.handle, EDGE_CLASS_NAMES[edge])}
|
||||
data-resizable-pane-edge={edge}
|
||||
data-flx="ui.resizable-pane.resizable-pane-handles.handle.button"
|
||||
{...getHandleProps(edge)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user