diff --git a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/emoji-autocomplete-linux.png b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/emoji-autocomplete-linux.png index 3c3aae1e6f..e4b06f18fa 100644 Binary files a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/emoji-autocomplete-linux.png and b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/emoji-autocomplete-linux.png differ diff --git a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png index 008e450ca3..de157ac749 100644 Binary files a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png and b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png differ diff --git a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/narrow-linux.png b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/narrow-linux.png index 366271b095..ad6104ee09 100644 Binary files a/apps/web/playwright/snapshots/composer/CIDER.spec.ts/narrow-linux.png and b/apps/web/playwright/snapshots/composer/CIDER.spec.ts/narrow-linux.png differ diff --git a/apps/web/playwright/snapshots/composer/RTE.spec.ts/narrow-linux.png b/apps/web/playwright/snapshots/composer/RTE.spec.ts/narrow-linux.png index 455917ebe2..6fffd4825d 100644 Binary files a/apps/web/playwright/snapshots/composer/RTE.spec.ts/narrow-linux.png and b/apps/web/playwright/snapshots/composer/RTE.spec.ts/narrow-linux.png differ diff --git a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png index 460b54b845..88e258952d 100644 Binary files a/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png and b/apps/web/playwright/snapshots/crypto/history-sharing.spec.ts/shared-history-invite-accepted-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-collapse.spec.ts/room-list-collapse-default-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-collapse.spec.ts/room-list-collapse-default-linux.png index e9e4462317..5d4c860653 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-collapse.spec.ts/room-list-collapse-default-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-collapse.spec.ts/room-list-collapse-default-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png index 124cdc674d..2c931bc4e4 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/room-panel-empty-room-list-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index f19a6cb0c4..3435f26daf 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png index ef53f1d6fb..a635fd4824 100644 Binary files a/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png and b/apps/web/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png differ diff --git a/apps/web/res/css/views/rooms/RoomListPanel/_RoomListPanel.pcss b/apps/web/res/css/views/rooms/RoomListPanel/_RoomListPanel.pcss index eb1f6e5fe5..91b7bc8720 100644 --- a/apps/web/res/css/views/rooms/RoomListPanel/_RoomListPanel.pcss +++ b/apps/web/res/css/views/rooms/RoomListPanel/_RoomListPanel.pcss @@ -8,5 +8,4 @@ .mx_RoomListPanel { background-color: var(--cpd-color-bg-canvas-default); height: 100%; - border-right: 1px solid var(--cpd-color-bg-subtle-primary); } diff --git a/apps/web/src/viewmodels/structures/ResizerViewModel.ts b/apps/web/src/viewmodels/structures/ResizerViewModel.ts index 7e14523590..322db045f1 100644 --- a/apps/web/src/viewmodels/structures/ResizerViewModel.ts +++ b/apps/web/src/viewmodels/structures/ResizerViewModel.ts @@ -47,8 +47,15 @@ export class ResizerViewModel */ private panelHandle?: PanelImperativeHandle; + /** + * Needed to distinguish between a drag and a click on the separator. + */ + private readonly mouseClickHandler: MouseClickHandler; + public constructor() { super(undefined, getInitialState()); + // Run onSeparatorClick when the separator is clicked. + this.mouseClickHandler = new MouseClickHandler(this.onSeparatorClick); } public onLeftPanelResize = debounce((panelSize: PanelSize): void => { @@ -79,13 +86,25 @@ export class ResizerViewModel this.panelHandle = handle; }; - public onSeparatorClick = (): void => { + private onSeparatorClick = (): void => { if (this.panelHandle?.isCollapsed()) { const lastSize = SettingsStore.getValue("RoomList.panelSize"); this.panelHandle.resize(`${lastSize ?? 100}%`); } }; + public onPointerUp = (): void => { + this.mouseClickHandler.onPointerUp(); + }; + + public onPointerMove = (): void => { + this.mouseClickHandler.onPointerMove(); + }; + + public onPointerDown = (): void => { + this.mouseClickHandler.onPointerDown(); + }; + public onFocus = (): void => { /** * The intention here is to make the separator visible when it is focused by keyboard @@ -108,3 +127,26 @@ export class ResizerViewModel this.snapshot.merge({ isFocusedViaKeyboard: false }); }; } + +/** + * Dragging the separator will emit a click event. + * This class uses pointer event handlers to distinguish between a drag and a click + * on the separator. + */ +class MouseClickHandler { + public constructor(private readonly onClick: () => void) {} + + private isResize = false; + + public onPointerUp = (): void => { + if (!this.isResize) this.onClick(); + }; + + public onPointerDown = (): void => { + this.isResize = false; + }; + + public onPointerMove = (): void => { + this.isResize = true; + }; +} diff --git a/apps/web/test/viewmodels/structures/ResizerViewModel-test.ts b/apps/web/test/viewmodels/structures/ResizerViewModel-test.ts index c913eb6fc3..c8f3d5251e 100644 --- a/apps/web/test/viewmodels/structures/ResizerViewModel-test.ts +++ b/apps/web/test/viewmodels/structures/ResizerViewModel-test.ts @@ -63,12 +63,33 @@ describe("LeftPanelResizerViewModel", () => { }); }); - it("should noop on onSeparatorClick() when handle is not yet set", () => { + it("should noop on click when handle is not yet set", () => { const vm = new ResizerViewModel(); - expect(() => vm.onSeparatorClick()).not.toThrow(); + expect(() => { + // Click + vm.onPointerDown(); + vm.onPointerUp(); + }).not.toThrow(); }); - describe("should expand panel on onSeparatorClick()", () => { + it("should noop on mouse drag", () => { + const vm = new ResizerViewModel(); + SettingsStore.setValue("RoomList.panelSize", null, SettingLevel.DEVICE, 34); + const mockHandle = { + resize: jest.fn(), + isCollapsed: jest.fn().mockReturnValue(true), + } as unknown as PanelImperativeHandle; + vm.setPanelHandle(mockHandle); + + // Simulate drag + vm.onPointerDown(); + vm.onPointerMove(); + vm.onPointerUp(); + + expect(mockHandle.resize).not.toHaveBeenCalledWith("34%"); + }); + + describe("should expand panel on click()", () => { it("to last non-zero width that the user set", () => { const vm = new ResizerViewModel(); SettingsStore.setValue("RoomList.panelSize", null, SettingLevel.DEVICE, 34); @@ -78,7 +99,9 @@ describe("LeftPanelResizerViewModel", () => { } as unknown as PanelImperativeHandle; vm.setPanelHandle(mockHandle); - vm.onSeparatorClick(); + // Simulate click + vm.onPointerDown(); + vm.onPointerUp(); expect(mockHandle.resize).toHaveBeenCalledWith("34%"); }); @@ -91,7 +114,9 @@ describe("LeftPanelResizerViewModel", () => { } as unknown as PanelImperativeHandle; vm.setPanelHandle(mockHandle); - vm.onSeparatorClick(); + // Simulate click + vm.onPointerDown(); + vm.onPointerUp(); expect(mockHandle.resize).toHaveBeenCalledWith("100%"); }); diff --git a/packages/playwright-common/src/fixtures/panel.ts b/packages/playwright-common/src/fixtures/panel.ts index b7d6d4b5da..4d6c098b86 100644 --- a/packages/playwright-common/src/fixtures/panel.ts +++ b/packages/playwright-common/src/fixtures/panel.ts @@ -12,7 +12,7 @@ import { test as base } from "./services.js"; * Rounding this number to a whole number would mean updating a whole * bunch of screenshots. */ -const LEFT_PANEL_WIDTH = "369.6875px"; +const LEFT_PANEL_WIDTH = "368.6875px"; export const test = base.extend<{ /** diff --git a/packages/shared-components/__vis__/linux/__baselines__/resize/separator/SeparatorView.stories.tsx/left-panel-expanded-auto.png b/packages/shared-components/__vis__/linux/__baselines__/resize/separator/SeparatorView.stories.tsx/left-panel-expanded-auto.png index adfeaa7f5d..81f90b0ad1 100644 Binary files a/packages/shared-components/__vis__/linux/__baselines__/resize/separator/SeparatorView.stories.tsx/left-panel-expanded-auto.png and b/packages/shared-components/__vis__/linux/__baselines__/resize/separator/SeparatorView.stories.tsx/left-panel-expanded-auto.png differ diff --git a/packages/shared-components/src/resize/separator/SeparatorView.module.css b/packages/shared-components/src/resize/separator/SeparatorView.module.css index 17eaae0fce..ec418c398b 100644 --- a/packages/shared-components/src/resize/separator/SeparatorView.module.css +++ b/packages/shared-components/src/resize/separator/SeparatorView.module.css @@ -6,21 +6,25 @@ */ .separator { - /* Hide the separator by default */ - display: none; /* Necessary to avoid weird focus outlines (doubled on one side, absent on one side etc...) */ outline-offset: -2px; } -.separator[data-separator="hover"], +/* When type=border, we just render a 1px border */ +.separator[data-separator-type="border"] { + width: 0px; + border-right: 1px solid var(--cpd-color-bg-subtle-primary); +} + +/* When type=bar, we render the 12px separator */ +.separator[data-separator-type="bar"] { + width: 12px; + display: flex; + align-items: center; + border-right: 1px solid var(--cpd-color-bg-subtle-primary); +} + +.separator[data-separator-type="bar"][data-separator="hover"], .separator:focus-visible { background: var(--cpd-color-bg-action-tertiary-hovered); } - -.visible { - /* Show the separator when the left panel is collapsed */ - display: flex; - align-items: center; - width: 12px; - border-right: 1px solid var(--cpd-color-bg-subtle-primary); -} diff --git a/packages/shared-components/src/resize/separator/SeparatorView.stories.tsx b/packages/shared-components/src/resize/separator/SeparatorView.stories.tsx index a3366c591e..93a589f4b4 100644 --- a/packages/shared-components/src/resize/separator/SeparatorView.stories.tsx +++ b/packages/shared-components/src/resize/separator/SeparatorView.stories.tsx @@ -16,8 +16,15 @@ import { Flex } from "../../core/utils/Flex"; type SeparatorViewProps = ResizerViewSnapshot & SeparatorViewActions; -const Wrapper = ({ onFocus, onBlur, onSeparatorClick, ...snapshot }: SeparatorViewProps): JSX.Element => { - const vm = useMockedViewModel(snapshot, { onFocus, onBlur, onSeparatorClick }); +const Wrapper = ({ + onFocus, + onBlur, + onPointerDown, + onPointerMove, + onPointerUp, + ...snapshot +}: SeparatorViewProps): JSX.Element => { + const vm = useMockedViewModel(snapshot, { onFocus, onBlur, onPointerDown, onPointerMove, onPointerUp }); return ; }; @@ -30,7 +37,9 @@ const meta = { args: { onFocus: fn(), onBlur: fn(), - onSeparatorClick: fn(), + onPointerUp: fn(), + onPointerMove: fn(), + onPointerDown: fn(), isCollapsed: true, isFocusedViaKeyboard: false, }, diff --git a/packages/shared-components/src/resize/separator/SeparatorView.test.tsx b/packages/shared-components/src/resize/separator/SeparatorView.test.tsx index 1887acbb57..2202e42093 100644 --- a/packages/shared-components/src/resize/separator/SeparatorView.test.tsx +++ b/packages/shared-components/src/resize/separator/SeparatorView.test.tsx @@ -23,7 +23,9 @@ class MockViewModel extends BaseViewModel implemen } public onBlur: () => void = vi.fn(); public onFocus: () => void = vi.fn(); - public onSeparatorClick: () => void = vi.fn(); + public onPointerUp: () => void = vi.fn(); + public onPointerMove: () => void = vi.fn(); + public onPointerDown: () => void = vi.fn(); } function renderPanel(initialSnapshot?: Partial): MockViewModel { @@ -55,11 +57,12 @@ describe("", () => { expect(container).toMatchSnapshot(); }); - it("should call onSeparatorClick() when clicked", async () => { + it("should call onPointerDown and onPointerUp on pointer events", async () => { const vm = renderPanel(); const separator = screen.getByRole("separator"); await userEvent.click(separator); - expect(vm.onSeparatorClick).toHaveBeenCalledOnce(); + expect(vm.onPointerDown).toHaveBeenCalledOnce(); + expect(vm.onPointerUp).toHaveBeenCalledOnce(); }); it("should call onFocus and onBlur when receiving/loosing focus", async () => { diff --git a/packages/shared-components/src/resize/separator/SeparatorView.tsx b/packages/shared-components/src/resize/separator/SeparatorView.tsx index c77c38764a..2d95f1940e 100644 --- a/packages/shared-components/src/resize/separator/SeparatorView.tsx +++ b/packages/shared-components/src/resize/separator/SeparatorView.tsx @@ -18,9 +18,19 @@ import { useI18n } from "../../core/i18n/i18nContext"; export interface SeparatorViewActions { /** - * onClick handler for the separator. + * onPointerUp handler for separator. */ - onSeparatorClick: () => void; + onPointerUp: () => void; + + /** + * onPointerMove handler for separator. + */ + onPointerMove: () => void; + + /** + * onPointerDown handler for separator. + */ + onPointerDown: () => void; /** * onFocus handler for the separator. @@ -45,28 +55,39 @@ export function SeparatorView({ vm, className }: Props): React.ReactNode { const { translate: _t } = useI18n(); const { isCollapsed, isFocusedViaKeyboard } = useViewModel(vm); - const classes = classNames(styles.separator, className, { - [styles.visible]: isCollapsed || isFocusedViaKeyboard, - }); + /** + * There are two types of separator: + * - bar: This shows a thick bar separator with a resize icon in the middle; shown when the panel is collapsed. + * - border: This is just a 1px wide separator; shown when the panel is expanded. + */ + const type = isCollapsed || isFocusedViaKeyboard ? "bar" : "border"; + + const barContent = ( + + + + ); return ( - - - + {type === "bar" ? barContent : null} ); } diff --git a/packages/shared-components/src/resize/separator/__snapshots__/SeparatorView.test.tsx.snap b/packages/shared-components/src/resize/separator/__snapshots__/SeparatorView.test.tsx.snap index a901029401..695dde68da 100644 --- a/packages/shared-components/src/resize/separator/__snapshots__/SeparatorView.test.tsx.snap +++ b/packages/shared-components/src/resize/separator/__snapshots__/SeparatorView.test.tsx.snap @@ -36,8 +36,9 @@ exports[` > renders Default story 1`] = ` aria-valuemax="99.502" aria-valuemin="0" aria-valuenow="0" - class="SeparatorView-module_separator Separator SeparatorView-module_visible" + class="SeparatorView-module_separator Separator" data-separator="inactive" + data-separator-type="bar" data-testid="react-use-id-3" id="react-use-id-3" role="separator" @@ -115,8 +116,9 @@ exports[` > renders KeyboardFocused story 1`] = ` aria-valuemax="99.502" aria-valuemin="0" aria-valuenow="49.751" - class="SeparatorView-module_separator Separator SeparatorView-module_visible" + class="SeparatorView-module_separator Separator" data-separator="inactive" + data-separator-type="bar" data-testid="react-use-id-3" id="react-use-id-3" role="separator" @@ -188,29 +190,21 @@ exports[` > renders LeftPanelExpanded story 1`] = ` + />