Skip to content

Commit cc2369e

Browse files
authored
Merge branch 'main' into feat/widgetize-app-layout-skeleton-event-base
2 parents c30b2eb + f4b5334 commit cc2369e

File tree

9 files changed

+124
-41
lines changed

9 files changed

+124
-41
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,4 @@ jobs:
2929
with:
3030
skip-test: true
3131
role-to-assume: ${{ inputs.role-to-assume }}
32-
project-name: ${{ inputs.project-name }}
33-
34-
trigger-downstream:
35-
needs: [release]
36-
runs-on: ubuntu-latest
37-
if: github.ref == 'refs/heads/main'
38-
strategy:
39-
matrix:
40-
repository: [chat-components]
41-
steps:
42-
- name: Trigger release in downstream repository
43-
run: |
44-
curl -L \
45-
-X POST \
46-
-H "Accept: application/vnd.github+json" \
47-
-H "Authorization: Bearer ${{ secrets.WORKFLOW_DISPATCH_TOKEN }}" \
48-
-H "X-GitHub-Api-Version: 2022-11-28" \
49-
https://api.github.com/repos/cloudscape-design/${{ matrix.repository }}/dispatches \
50-
-d "{\"event_type\": \"run-release\"}"
51-
32+
project-name: ${{ inputs.project-name }}

src/internal/components/drag-handle-wrapper/__tests__/drag-handle-wrapper.test.tsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,15 @@ function expectDirectionButtonToBeVisible(direction: Direction) {
5353
).toBe(true);
5454
}
5555

56-
function renderDragHandle(props: Omit<DragHandleWrapperProps, 'children'>) {
56+
function renderDragHandle(props: Partial<Omit<DragHandleWrapperProps, 'children'>>) {
57+
const mergedProps: Omit<DragHandleWrapperProps, 'children'> = {
58+
directions: {},
59+
hideButtonsOnDrag: false,
60+
clickDragThreshold: 3,
61+
...props,
62+
};
5763
const { container } = render(
58-
<DragHandleWrapper {...props}>
64+
<DragHandleWrapper {...mergedProps}>
5965
<button type="button" id="drag-button">
6066
Drag
6167
</button>
@@ -343,7 +349,7 @@ test('shows direction buttons when dragged 2 pixels', () => {
343349
expect(getDirectionButton('block-start')).toBeVisible();
344350
});
345351

346-
test("doesn't show direction buttons when dragged more than 3 pixels", () => {
352+
test("doesn't show direction buttons when dragged more than 3 pixels (default threshold)", () => {
347353
const { dragHandle } = renderDragHandle({
348354
directions: { 'block-start': 'active' },
349355
tooltipText: 'Click me!',
@@ -355,6 +361,49 @@ test("doesn't show direction buttons when dragged more than 3 pixels", () => {
355361
expectDirectionButtonToBeHidden('block-start');
356362
});
357363

364+
test('shows direction buttons when dragged less than custom clickDragThreshold', () => {
365+
const { dragHandle } = renderDragHandle({
366+
directions: { 'block-start': 'active' },
367+
tooltipText: 'Click me!',
368+
clickDragThreshold: 10,
369+
});
370+
371+
fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
372+
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 });
373+
fireEvent.pointerUp(dragHandle);
374+
expectDirectionButtonToBeVisible('block-start');
375+
});
376+
377+
describe('hideButtonsOnDrag property', () => {
378+
test('hides direction buttons when dragging with hideButtonsOnDrag=true', () => {
379+
const { dragHandle, showButtons } = renderDragHandle({
380+
directions: { 'block-start': 'active' },
381+
tooltipText: 'Click me!',
382+
hideButtonsOnDrag: true,
383+
});
384+
385+
showButtons();
386+
expectDirectionButtonToBeVisible('block-start');
387+
fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
388+
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 });
389+
expectDirectionButtonToBeHidden('block-start');
390+
});
391+
392+
test('keeps direction buttons visible when dragging with hideButtonsOnDrag=false (default)', () => {
393+
const { dragHandle, showButtons } = renderDragHandle({
394+
directions: { 'block-start': 'active' },
395+
tooltipText: 'Click me!',
396+
hideButtonsOnDrag: false,
397+
});
398+
399+
showButtons();
400+
expectDirectionButtonToBeVisible('block-start');
401+
fireEvent.pointerDown(dragHandle, { clientX: 50, clientY: 50 });
402+
fireEvent.pointerMove(dragHandle, { clientX: 55, clientY: 55 }); // Move more than default threshold
403+
expectDirectionButtonToBeVisible('block-start');
404+
});
405+
});
406+
358407
test('hides direction buttons on Escape keypress', () => {
359408
const { dragHandle, showButtons } = renderDragHandle({
360409
directions: { 'block-start': 'active' },

src/internal/components/drag-handle-wrapper/index.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,15 @@ import PortalOverlay from './portal-overlay';
1414

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

17-
// The amount of distance after pointer down that the cursor is allowed to
18-
// jitter for a subsequent mouseup to still register as a "press" instead of
19-
// a drag. A little allowance is needed for usability reasons, but this number
20-
// isn't set in stone.
21-
const PRESS_DELTA_MAX = 3;
22-
2317
export default function DragHandleWrapper({
2418
directions,
2519
tooltipText,
2620
children,
2721
onDirectionClick,
2822
triggerMode = 'focus',
2923
initialShowButtons = false,
24+
hideButtonsOnDrag,
25+
clickDragThreshold,
3026
}: DragHandleWrapperProps) {
3127
const wrapperRef = useRef<HTMLDivElement | null>(null);
3228
const dragHandleRef = useRef<HTMLDivElement | null>(null);
@@ -70,25 +66,26 @@ export default function DragHandleWrapper({
7066
useEffect(() => {
7167
const controller = new AbortController();
7268

73-
// See `PRESS_DELTA_MAX` above. We need to differentiate between a "click"
74-
// and a "drag" action. We can say a "click" happens when a "pointerdown"
75-
// is followed by a "pointerup" with no "pointermove" between the two.
69+
// We need to differentiate between a "click" and a "drag" action.
70+
// We can say a "click" happens when a "pointerdown" is followed by
71+
// a "pointerup" with no "pointermove" between the two.
7672
// However, it would be a poor usability experience if a "click" isn't
7773
// registered because, while pressing my mouse, I moved it by just one
7874
// pixel, making it a "drag" instead. So we allow the pointer to move by
79-
// `PRESS_DELTA_MAX` pixels before setting `didPointerDrag` to true.
75+
// `clickDragThreshold` pixels before setting `didPointerDrag` to true.
8076
document.addEventListener(
8177
'pointermove',
8278
event => {
8379
if (
8480
isPointerDown.current &&
8581
initialPointerPosition.current &&
86-
(event.clientX > initialPointerPosition.current.x + PRESS_DELTA_MAX ||
87-
event.clientX < initialPointerPosition.current.x - PRESS_DELTA_MAX ||
88-
event.clientY > initialPointerPosition.current.y + PRESS_DELTA_MAX ||
89-
event.clientY < initialPointerPosition.current.y - PRESS_DELTA_MAX)
82+
(event.clientX > initialPointerPosition.current.x + clickDragThreshold ||
83+
event.clientX < initialPointerPosition.current.x - clickDragThreshold ||
84+
event.clientY > initialPointerPosition.current.y + clickDragThreshold ||
85+
event.clientY < initialPointerPosition.current.y - clickDragThreshold)
9086
) {
9187
didPointerDrag.current = true;
88+
hideButtonsOnDrag && setShowButtons(false);
9289
}
9390
},
9491
{ signal: controller.signal }
@@ -123,7 +120,7 @@ export default function DragHandleWrapper({
123120
);
124121

125122
return () => controller.abort();
126-
}, []);
123+
}, [clickDragThreshold, hideButtonsOnDrag]);
127124

128125
const onHandlePointerDown: React.PointerEventHandler = event => {
129126
// Tooltip behavior: the tooltip should appear on hover, but disappear when

src/internal/components/drag-handle-wrapper/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ export interface DragHandleWrapperProps {
1212
children: React.ReactNode;
1313
triggerMode?: TriggerMode;
1414
initialShowButtons?: boolean;
15+
hideButtonsOnDrag: boolean;
16+
clickDragThreshold: number;
1517
}

src/internal/components/drag-handle-wrapper/motion.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
&-motion-exiting {
5757
@include styles.with-motion {
5858
animation:
59-
drag-handle-exit awsui.$motion-duration-complex awsui.$motion-easing-responsive,
60-
awsui-motion-fade-out-0 awsui.$motion-duration-complex awsui.$motion-easing-responsive;
59+
drag-handle-exit awsui.$motion-duration-complex awsui.$motion-easing-responsive forwards,
60+
awsui-motion-fade-out-0 awsui.$motion-duration-complex awsui.$motion-easing-responsive forwards;
6161
}
6262
}
6363
}

src/internal/components/drag-handle/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const InternalDragHandle = forwardRef(
2626
onDirectionClick,
2727
triggerMode,
2828
initialShowButtons,
29+
hideButtonsOnDrag = false,
30+
clickDragThreshold = 3,
2931
...rest
3032
}: DragHandleProps,
3133
ref: React.Ref<Element>
@@ -39,6 +41,8 @@ const InternalDragHandle = forwardRef(
3941
onDirectionClick={onDirectionClick}
4042
triggerMode={triggerMode}
4143
initialShowButtons={initialShowButtons}
44+
hideButtonsOnDrag={hideButtonsOnDrag}
45+
clickDragThreshold={clickDragThreshold}
4246
>
4347
<DragHandleButton
4448
ref={ref}

src/internal/components/drag-handle/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export interface DragHandleProps {
2424
onDirectionClick?: (direction: DragHandleProps.Direction) => void;
2525
triggerMode?: TriggerMode;
2626
initialShowButtons?: boolean;
27+
/**
28+
* Hide the UAP buttons when dragging is active.
29+
*/
30+
hideButtonsOnDrag?: boolean;
31+
/**
32+
* Max cursor movement (in pixels) that still counts as a press rather than
33+
* a drag. Small threshold needed for usability.
34+
*/
35+
clickDragThreshold?: number;
2736
}
2837

2938
export namespace DragHandleProps {

src/tabs/__tests__/tabs.test.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React from 'react';
3+
import React, { useState } from 'react';
44
import { fireEvent, render } from '@testing-library/react';
55

66
import { KeyCode } from '@cloudscape-design/test-utils-core/utils';
@@ -940,6 +940,35 @@ describe('Tabs', () => {
940940
});
941941
});
942942

943+
describe('Dismissible mixed with non-dismissible', () => {
944+
test('moves focus from the last dismissible button to non-dismissible', () => {
945+
const TabsWrapper = () => {
946+
const [tabsDismissibles, setTabDismissibles] = useState([
947+
{
948+
label: 'First tab',
949+
id: 'first',
950+
content: <>first</>,
951+
},
952+
{
953+
label: 'Second tab',
954+
id: 'second',
955+
dismissible: true,
956+
dismissLabel: 'Dismiss second tab (dismissibles variant)',
957+
onDismiss: () => setTabDismissibles(prevTabs => prevTabs.slice(0, 1)),
958+
content: <>second</>,
959+
},
960+
]);
961+
962+
return <Tabs tabs={tabsDismissibles} />;
963+
};
964+
const { wrapper } = renderTabs(<TabsWrapper />);
965+
966+
wrapper.findDismissibleButtonByTabId('second')!.click();
967+
968+
expect(wrapper.findTabLinkById('first')!.getElement()).toHaveFocus();
969+
});
970+
});
971+
943972
describe('Actions', () => {
944973
test('renders action', () => {
945974
const actionButton = renderTabs(<Tabs tabs={actionDismissibleTabs} />).wrapper.findActionByTabIndex(2);

src/tabs/tab-header-bar.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '../internal/context/single-tab-stop-navigation-context';
2020
import { hasModifierKeys, isPlainLeftClick } from '../internal/events';
2121
import useHiddenDescription from '../internal/hooks/use-hidden-description';
22+
import { usePrevious } from '../internal/hooks/use-previous';
2223
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
2324
import { KeyCode } from '../internal/keycode';
2425
import { circleIndex } from '../internal/utils/circle-index';
@@ -112,6 +113,7 @@ export function TabHeaderBar({
112113
const [focusedTabId, setFocusedTabId] = useState(activeTabId);
113114
const [previousActiveTabId, setPreviousActiveTabId] = useState<string | undefined>(activeTabId);
114115
const hasActionOrDismissible = tabs.some(tab => tab.action || tab.dismissible);
116+
const hadActionOrDismissible = usePrevious(hasActionOrDismissible);
115117
const tabActionAttributes = hasActionOrDismissible
116118
? {
117119
role: 'application',
@@ -124,6 +126,16 @@ export function TabHeaderBar({
124126
role: 'tablist',
125127
};
126128

129+
useEffect(() => {
130+
if (hadActionOrDismissible && !hasActionOrDismissible) {
131+
// when tabs becomes non-dismissible (e.g. when all dismissible tabs are removed),
132+
// the hasActionOrDismissible is changing which causing tabs to re-mount to the React tree,
133+
// which, in turn, causes losing their refs, and the nextActive.focus() function inside handleDismiss does not focus on the next tab
134+
// so this code does
135+
getNextFocusTarget()?.focus();
136+
}
137+
}, [hasActionOrDismissible, hadActionOrDismissible]);
138+
127139
useEffect(() => {
128140
if (headerBarRef.current) {
129141
setHorizontalOverflow(hasHorizontalOverflow(headerBarRef.current, inlineStartOverflowButton));

0 commit comments

Comments
 (0)