Skip to content

Commit

Permalink
chore: Add internal drag handle UAP support
Browse files Browse the repository at this point in the history
  • Loading branch information
avinashbot committed Feb 21, 2025
1 parent 22d7bca commit afb2143
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 0 deletions.
44 changes: 44 additions & 0 deletions pages/drag-handle/wrapper.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import Box from '~components/box';
import Button from '~components/button';
import DragHandleWrapper from '~components/internal/components/drag-handle-wrapper';

import ScreenshotArea from '../utils/screenshot-area';

export default function GridPage() {
const [open, setOpen] = useState(false);

return (
<>
<h1>Drag handle demo</h1>
<ScreenshotArea>
<Box padding="l" textAlign="center">
<DragHandleWrapper
open={open}
directions={{
'block-start': 'visible',
'block-end': 'visible',
'inline-start': 'disabled',
'inline-end': 'visible',
}}
onPress={direction => console.log(direction)}
onClose={() => setOpen(false)}
>
<Button
variant="icon"
iconName="drag-indicator"
onClick={event => {
console.log({ button: event.detail.button });
setOpen(show => !show);
}}
ariaLabel="Drag"
/>
</DragHandleWrapper>
</Box>
</ScreenshotArea>
</>
);
}
123 changes: 123 additions & 0 deletions src/internal/components/drag-handle-wrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useEffect, useRef, useState } from 'react';
import clsx from 'clsx';

import { nodeContains } from '@cloudscape-design/component-toolkit/dom';
import { getIsRtl } from '@cloudscape-design/component-toolkit/internal';

import { IconProps } from '../../../icon/interfaces';
import InternalIcon from '../../../icon/internal';
import Tooltip from '../tooltip';
import { Transition } from '../transition';

import styles from './styles.css.js';

type Direction = 'block-start' | 'block-end' | 'inline-start' | 'inline-end';
type DirectionState = 'visible' | 'hidden' | 'disabled';

interface DragHandleWrapperProps {
open: boolean;
directions: Record<Direction, DirectionState>;
children: React.ReactNode;

onPress: (direction: Direction) => void;
onClose: () => void;
}

export default function DragHandleWrapper({ open, directions, children, onPress, onClose }: DragHandleWrapperProps) {
// TODO: fix up tooltip logic
// TODO: is onClick good enough? or should the buttons also appear when the mouse is dragged _juust_ a little bit?
// TODO: provide some functionality for drag implementations to distinguish between button onClick (which includes keyboard activation) on(Mouse)Click?
// TODO: i18nStrings-ify the labels (tooltip and cardinal buttons)

const wrapperRef = useRef<HTMLSpanElement | null>(null);
const dragHandleRef = useRef<HTMLSpanElement | null>(null);
const rtl = getIsRtl(dragHandleRef.current);

const [showTooltip, setShowTooltip] = useState(false);
useEffect(() => {
const controller = new AbortController();

document.addEventListener(
'click',
event => {
if (!nodeContains(wrapperRef.current, event.target)) {
onClose();
}
},
{ signal: controller.signal }
);

return () => {
controller.abort();
};
}, [onClose]);

const dragButtonProps = { open, rtl, onPress };
return (
<span className={clsx(styles['drag-handle-wrapper'], open && styles['drag-handle-wrapper-open'])} ref={wrapperRef}>
<DragButton direction="block-start" state={directions['block-start']} {...dragButtonProps} />
<DragButton direction="block-end" state={directions['block-end']} {...dragButtonProps} />
<DragButton direction="inline-start" state={directions['inline-start']} {...dragButtonProps} />
<DragButton direction="inline-end" state={directions['inline-end']} {...dragButtonProps} />

{!open && showTooltip && (
<Tooltip trackRef={dragHandleRef} value="Drag or select to move" onDismiss={() => setShowTooltip(false)} />
)}

<span
className={styles['drag-handle']}
ref={dragHandleRef}
onMouseEnter={() => setShowTooltip(true)}
onBlur={() => setShowTooltip(false)}
onMouseDown={() => setShowTooltip(false)}
onMouseLeave={() => setShowTooltip(false)}
>
{children}
</span>
</span>
);
}

interface DragButtonProps {
direction: Direction;
state: DirectionState;
open: boolean;
rtl: boolean;
onPress: DragHandleWrapperProps['onPress'];
}

const IconNameMap: Record<Direction, IconProps.Name> = {
'block-start': 'arrow-up',
'block-end': 'arrow-down',
'inline-start': 'arrow-left',
'inline-end': 'arrow-right',
};

function DragButton({ direction, state, open, rtl, onPress }: DragButtonProps) {
return (
<Transition in={open}>
{(transitionState, ref) => (
<button
ref={ref}
tabIndex={-1}
className={clsx(
styles['drag-button'],
styles[`drag-button-${direction}`],
rtl && styles[`drag-button-rtl`],
state === 'disabled' && styles['drag-button-disabled'],
(state === 'hidden' || transitionState === 'exited') && styles['drag-button-hidden'],
styles[`drag-button-motion-${transitionState}`]
)}
disabled={state === 'disabled'}
aria-label={'Resize (direction) (icon: ' + IconNameMap[direction] + ')'}
onClick={() => onPress(direction)}
>
<InternalIcon name={IconNameMap[direction]} size="small" />
</button>
)}
</Transition>
);
}
190 changes: 190 additions & 0 deletions src/internal/components/drag-handle-wrapper/motion.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
@use '../../styles' as styles;
@use '../../styles/tokens' as awsui;

.drag-button {
@include styles.with-motion {
@include styles.animation-fade-in;
@include styles.animation-fade-out-0;
}
}

.drag-button-motion-enter,
.drag-button-motion-entering,
.drag-button-motion-exit,
.drag-button-motion-exiting {
@include styles.with-motion {
pointer-events: none;
}
}

.drag-button-block-start {
&.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-up awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-up-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
}

.drag-button-block-end {
&.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-down awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-down-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
}

.drag-button-inline-start {
&.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-left awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-rtl.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-right awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-left-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-rtl.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-right-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
}

.drag-button-inline-end {
&.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-right awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-rtl.drag-button-motion-entering {
@include styles.with-motion {
animation:
slide-left awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-in awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-right-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
&.drag-button-rtl.drag-button-motion-exiting {
@include styles.with-motion {
animation:
slide-left-exit awsui.$motion-duration-fast awsui.$motion-easing-expressive,
awsui-motion-fade-out-0 awsui.$motion-duration-fast awsui.$motion-easing-expressive;
}
}
}

@keyframes slide-up {
0% {
transform: translate(0, 20px);
}
100% {
transform: translate(0, 0);
}
}

@keyframes slide-up-exit {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(0, 20px);
}
}

@keyframes slide-down {
0% {
transform: translate(0, -20px);
}
100% {
transform: translate(0, 0);
}
}

@keyframes slide-down-exit {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(0, -20px);
}
}

@keyframes slide-left {
0% {
transform: translate(20px, 0);
}
100% {
transform: translate(0, 0);
}
}

@keyframes slide-left-exit {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(20px, 0);
}
}

@keyframes slide-right {
0% {
transform: translate(-20px, 0);
}
100% {
transform: translate(0, 0);
}
}

@keyframes slide-right-exit {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(-20px, 0);
}
}
Loading

0 comments on commit afb2143

Please sign in to comment.