Skip to content

Commit 4f1bd4e

Browse files
committed
refactor: Reusable internal drag handle
1 parent d0ecb9a commit 4f1bd4e

File tree

22 files changed

+385
-178
lines changed

22 files changed

+385
-178
lines changed
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
6+
import DragHandle, { DragHandleProps } from '~components/internal/components/drag-handle';
7+
8+
import createPermutations from '../utils/permutations';
9+
import PermutationsView from '../utils/permutations-view';
10+
import ScreenshotArea from '../utils/screenshot-area';
11+
12+
const permutations = createPermutations<DragHandleProps>([
13+
{
14+
variant: ['drag-indicator', 'resize-area', 'resize-horizontal', 'resize-vertical'],
15+
ariaLabel: ['drag-handle'],
16+
},
17+
{
18+
variant: ['drag-indicator', 'resize-area', 'resize-horizontal', 'resize-vertical'],
19+
ariaLabel: ['drag-handle'],
20+
size: ['small'],
21+
},
22+
{
23+
variant: ['drag-indicator', 'resize-area', 'resize-horizontal', 'resize-vertical'],
24+
ariaLabel: ['drag-handle'],
25+
disabled: [true],
26+
},
27+
]);
28+
29+
export default function DragHandlePermutations() {
30+
return (
31+
<>
32+
<h1>Drag handle permutations</h1>
33+
<ScreenshotArea style={{ maxWidth: 600 }}>
34+
<PermutationsView permutations={permutations} render={permutation => <DragHandle {...permutation} />} />
35+
</ScreenshotArea>
36+
</>
37+
);
38+
}

src/app-layout/__tests__/split-panel-provider.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe.each(['bottom', 'side'] as const)('position=%s', position => {
4141
onToggle: () => {},
4242
refs: {
4343
slider: { current: null },
44+
handle: { current: null },
4445
toggle: { current: null },
4546
preferences: { current: null },
4647
},

src/app-layout/drawer/resizable-drawer.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const ResizableDrawer = ({
4343
const sizeControlProps: SizeControlProps = {
4444
position: 'side',
4545
panelRef: drawerRefObject,
46-
handleRef: refs.slider,
46+
handleRef: refs.handle,
4747
onResize: setSidePanelWidth,
4848
};
4949

@@ -60,15 +60,17 @@ export const ResizableDrawer = ({
6060
resizeHandle={
6161
!isMobile &&
6262
activeDrawer?.resizable && (
63-
<PanelResizeHandle
64-
ref={refs.slider}
65-
position="side"
66-
className={testutilStyles['drawers-slider']}
67-
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
68-
ariaValuenow={relativeSize}
69-
onKeyDown={onKeyDown}
70-
onPointerDown={onSliderPointerDown}
71-
/>
63+
<div ref={refs.handle}>
64+
<PanelResizeHandle
65+
ref={refs.slider}
66+
position="side"
67+
className={testutilStyles['drawers-slider']}
68+
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
69+
ariaValuenow={relativeSize}
70+
onKeyDown={onKeyDown}
71+
onPointerDown={onSliderPointerDown}
72+
/>
73+
</div>
7274
)
7375
}
7476
ariaLabels={{

src/app-layout/utils/use-focus-control.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export interface Focusable {
99
export interface FocusControlRefs {
1010
toggle: RefObject<Focusable>;
1111
close: RefObject<Focusable>;
12-
slider: RefObject<HTMLDivElement>;
12+
slider: RefObject<Focusable>;
13+
handle: RefObject<HTMLDivElement>;
1314
}
1415

1516
export interface FocusControlState {
@@ -35,7 +36,8 @@ export function useMultipleFocusControl(
3536
refs.current[drawerId] = {
3637
toggle: createRef<Focusable>(),
3738
close: createRef<Focusable>(),
38-
slider: createRef<HTMLDivElement>(),
39+
slider: createRef<Focusable>(),
40+
handle: createRef<HTMLDivElement>(),
3941
};
4042
}
4143
});
@@ -101,7 +103,8 @@ export function useFocusControl(
101103
const refs = {
102104
toggle: useRef<Focusable>(null),
103105
close: useRef<Focusable>(null),
104-
slider: useRef<HTMLDivElement>(null),
106+
slider: useRef<Focusable>(null),
107+
handle: useRef<HTMLDivElement>(null),
105108
};
106109
const previousFocusedElement = useRef<HTMLElement>();
107110
const shouldFocus = useRef(false);

src/app-layout/utils/use-resize.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,25 @@ function useResize(
6161
const sizeControlProps: SizeControlProps = {
6262
position: 'side',
6363
panelRef: drawerRefObject,
64-
handleRef: drawersRefs.slider,
64+
handleRef: drawersRefs.handle,
6565
onResize: setSidePanelWidth,
6666
};
6767

6868
const onSliderPointerDown = usePointerEvents(sizeControlProps);
6969
const onKeyDown = useKeyboardEvents(sizeControlProps);
7070

7171
const resizeHandle = (
72-
<PanelResizeHandle
73-
ref={drawersRefs.slider}
74-
position="side"
75-
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
76-
ariaValuenow={relativeSize}
77-
className={testutilStyles['drawers-slider']}
78-
onKeyDown={onKeyDown}
79-
onPointerDown={onSliderPointerDown}
80-
/>
72+
<div ref={drawersRefs.handle}>
73+
<PanelResizeHandle
74+
ref={drawersRefs.slider}
75+
position="side"
76+
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
77+
ariaValuenow={relativeSize}
78+
className={testutilStyles['drawers-slider']}
79+
onKeyDown={onKeyDown}
80+
onPointerDown={onSliderPointerDown}
81+
/>
82+
</div>
8183
);
8284

8385
return { resizeHandle, drawerSize };

src/app-layout/utils/use-split-panel-focus-control.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ type SplitPanelLastInteraction = { type: 'open' } | { type: 'close' } | { type:
88

99
export interface SplitPanelFocusControlRefs {
1010
toggle: RefObject<Focusable>;
11-
slider: RefObject<HTMLDivElement>;
11+
slider: RefObject<Focusable>;
12+
handle: RefObject<HTMLDivElement>;
1213
preferences: RefObject<Focusable>;
1314
}
1415
export interface SplitPanelFocusControlState {
@@ -19,7 +20,8 @@ export interface SplitPanelFocusControlState {
1920
export function useSplitPanelFocusControl(dependencies: DependencyList): SplitPanelFocusControlState {
2021
const refs = {
2122
toggle: useRef<Focusable>(null),
22-
slider: useRef<HTMLDivElement>(null),
23+
slider: useRef<Focusable>(null),
24+
handle: useRef<HTMLDivElement>(null),
2325
preferences: useRef<Focusable>(null),
2426
};
2527
const lastInteraction = useRef<SplitPanelLastInteraction | null>(null);

src/app-layout/visual-refresh-toolbar/drawer/global-drawer.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function AppLayoutGlobalDrawerImplementation({
5858
minWidth: minDrawerSize,
5959
maxWidth: maxDrawerSize,
6060
panelRef: drawerRef,
61-
handleRef: refs?.slider,
61+
handleRef: refs?.handle,
6262
onResize: size => onActiveDrawerResize({ id: activeDrawerId!, size }),
6363
});
6464
const size = getLimitedValue(minDrawerSize, activeDrawerSize, maxDrawerSize);
@@ -111,15 +111,17 @@ function AppLayoutGlobalDrawerImplementation({
111111
>
112112
{!isMobile && activeGlobalDrawer?.resizable && (
113113
<div className={styles['drawer-slider']}>
114-
<PanelResizeHandle
115-
ref={refs?.slider}
116-
position="side"
117-
className={testutilStyles['drawers-slider']}
118-
ariaLabel={activeGlobalDrawer?.ariaLabels?.resizeHandle}
119-
ariaValuenow={resizeProps.relativeSize}
120-
onKeyDown={resizeProps.onKeyDown}
121-
onPointerDown={resizeProps.onPointerDown}
122-
/>
114+
<div ref={refs.handle}>
115+
<PanelResizeHandle
116+
ref={refs?.slider}
117+
position="side"
118+
className={testutilStyles['drawers-slider']}
119+
ariaLabel={activeGlobalDrawer?.ariaLabels?.resizeHandle}
120+
ariaValuenow={resizeProps.relativeSize}
121+
onKeyDown={resizeProps.onKeyDown}
122+
onPointerDown={resizeProps.onPointerDown}
123+
/>
124+
</div>
123125
</div>
124126
)}
125127
<div

src/app-layout/visual-refresh-toolbar/drawer/local-drawer.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
5555
minWidth: minDrawerSize,
5656
maxWidth: maxDrawerSize,
5757
panelRef: drawerRef,
58-
handleRef: drawersFocusControl.refs.slider,
58+
handleRef: drawersFocusControl.refs.handle,
5959
onResize: size => onActiveDrawerResize({ id: activeDrawerId!, size }),
6060
});
6161
// temporary handle a situation when app-layout is old, but this component come as a widget
@@ -98,15 +98,17 @@ export function AppLayoutDrawerImplementation({ appLayoutInternals }: AppLayoutD
9898
>
9999
{!isMobile && activeDrawer?.resizable && (
100100
<div className={styles['drawer-slider']}>
101-
<PanelResizeHandle
102-
ref={drawersFocusControl.refs.slider}
103-
position="side"
104-
className={testutilStyles['drawers-slider']}
105-
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
106-
ariaValuenow={resizeProps.relativeSize}
107-
onKeyDown={resizeProps.onKeyDown}
108-
onPointerDown={resizeProps.onPointerDown}
109-
/>
101+
<div ref={drawersFocusControl.refs.handle}>
102+
<PanelResizeHandle
103+
ref={drawersFocusControl.refs.slider}
104+
position="side"
105+
className={testutilStyles['drawers-slider']}
106+
ariaLabel={activeDrawer?.ariaLabels?.resizeHandle}
107+
ariaValuenow={resizeProps.relativeSize}
108+
onKeyDown={resizeProps.onKeyDown}
109+
onPointerDown={resizeProps.onPointerDown}
110+
/>
111+
</div>
110112
</div>
111113
)}
112114
<div className={clsx(styles['drawer-content-container'], sharedStyles['with-motion-horizontal'])}>

src/collection-preferences/content-display/content-display-option.scss

+4
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@
3535
@include styles.text-wrapping;
3636
padding-inline-end: awsui.$space-l;
3737
}
38+
39+
.drag-handle-wrapper {
40+
margin-inline: awsui.$space-scaled-xxs;
41+
}

src/collection-preferences/content-display/content-display-option.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ const ContentDisplayOption = forwardRef(
2424
const controlId = `${idPrefix}-control-${option.id}`;
2525
return (
2626
<div ref={ref} className={getClassName('content')}>
27-
<DragHandle {...dragHandleProps} />
27+
<div className={styles['drag-handle-wrapper']}>
28+
<DragHandle {...dragHandleProps} />
29+
</div>
2830

2931
<label className={getClassName('label')} htmlFor={controlId}>
3032
{option.label}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
import { render, screen } from '@testing-library/react';
6+
7+
import DragHandle from '../../../../../lib/components/internal/components/drag-handle';
8+
9+
import styles from '../../../../../lib/components/internal/components/drag-handle/styles.css.js';
10+
11+
const allVariants = ['drag-indicator', 'resize-area', 'resize-horizontal', 'resize-vertical'] as const;
12+
const allSizes = ['small', 'normal'] as const;
13+
14+
test('renders default variant and size', () => {
15+
render(<DragHandle ariaLabel="drag handle" />);
16+
17+
expect(document.querySelector(`.${styles.handle}`)).toBeInTheDocument();
18+
expect(document.querySelector(`.${styles['handle-drag-indicator']}`)).toBeInTheDocument();
19+
expect(document.querySelector(`.${styles['handle-size-normal']}`)).toBeInTheDocument();
20+
});
21+
22+
test('renders all variants and sizes', () => {
23+
for (const variant of allVariants) {
24+
for (const size of allSizes) {
25+
render(<DragHandle ariaLabel="drag handle" variant={variant} size={size} />);
26+
27+
expect(document.querySelector(`.${styles.handle}`)).toBeInTheDocument();
28+
expect(document.querySelector(`.${styles[`handle-${variant}`]}`)).toBeInTheDocument();
29+
expect(document.querySelector(`.${styles[`handle-size-${size}`]}`)).toBeInTheDocument();
30+
}
31+
}
32+
});
33+
34+
test('assigns aria label and aria description', () => {
35+
render(
36+
<div>
37+
<DragHandle ariaLabel="drag" ariaDescribedby="description" />
38+
<div id="description">handle</div>
39+
</div>
40+
);
41+
expect(document.querySelector(`.${styles.handle}`)).toHaveAccessibleName('drag');
42+
expect(document.querySelector(`.${styles.handle}`)).toHaveAccessibleDescription('handle');
43+
});
44+
45+
test('has role="button" by default', () => {
46+
render(<DragHandle ariaLabel="drag handle" />);
47+
48+
expect(screen.getByRole('button')).toHaveAccessibleName('drag handle');
49+
});
50+
51+
test('has role="slider" and aria-value attributes when ariaValue is set', () => {
52+
render(<DragHandle ariaLabel="drag handle" ariaValue={{ valueMin: -1, valueMax: 1, valueNow: 0 }} />);
53+
54+
const handle = screen.getByRole('slider');
55+
expect(handle).toHaveAccessibleName('drag handle');
56+
expect(handle).toHaveAttribute('aria-valuemin', '-1');
57+
expect(handle).toHaveAttribute('aria-valuemax', '1');
58+
expect(handle).toHaveAttribute('aria-valuenow', '0');
59+
});
60+
61+
test('sets aria-disabled when disabled', () => {
62+
const { rerender } = render(<DragHandle ariaLabel="drag handle" disabled={false} />);
63+
64+
expect(document.querySelector(`.${styles.handle}`)).toHaveAttribute('aria-disabled', 'false');
65+
expect(document.querySelector(`.${styles['handle-disabled']}`)).not.toBeInTheDocument();
66+
67+
rerender(<DragHandle ariaLabel="drag handle" disabled={true} />);
68+
69+
expect(document.querySelector(`.${styles.handle}`)).toHaveAttribute('aria-disabled');
70+
expect(document.querySelector(`.${styles['handle-disabled']}`)).toBeInTheDocument();
71+
});

0 commit comments

Comments
 (0)