Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/oc-docs/e2e/components/playground.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class PlaygroundComponent extends BaseComponent {
readonly runner = this.page.getByTestId('playground-runner');
readonly loadError = this.page.getByTestId('playground-load-error');
readonly sidebarPanel = this.page.getByTestId('playground-sidebar-panel');
readonly sidebarBackdrop = this.page.getByTestId('playground-sidebar-backdrop');
readonly collectionNode = this.page.getByTestId('sidebar-collection-root');
readonly collectionCollapseToggle = this.collectionNode.getByRole('button', {
name: /Collapse collection|Expand collection/,
Expand Down
25 changes: 25 additions & 0 deletions packages/oc-docs/e2e/tests/playground/playground.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,31 @@ test.describe('playground docks (desktop)', () => {
expect(viewBox!.x).toBeLessThanOrEqual(sideBox!.x + 5);
});

test('inline dock: clicking outside the overlay sidebar closes it', async ({ page, playground }) => {
await page.goto(openAt('inline'));
await playground.sidebarToggle.click();
await expect(playground.sidebarPanel).toBeVisible();
// Click the view area outside the sidebar (its backdrop), mirroring the docs
// navigation drawer: an outside click dismisses the overlay. The backdrop
// spans the whole dock from its left edge, so click just past the sidebar's
// right edge to land on the exposed view area, not the panel.
const side = await playground.sidebarPanel.boundingBox();
await playground.sidebarBackdrop.click({ position: { x: side!.width + 40, y: 30 } });
await expect(playground.sidebarPanel).toHaveCount(0);
});

test('inline dock: clicking outside the playground closes the overlay sidebar', async ({ page, playground }) => {
// Wide viewport so the docs column stays desktop and is clickable to the
// left of the inline dock (rather than collapsing to a drawer).
await page.setViewportSize({ width: 1920, height: 900 });
await page.goto(openAt('inline'));
await playground.sidebarToggle.click();
await expect(playground.sidebarPanel).toBeVisible();
// Click the docs page, entirely outside the playground dock.
await page.getByTestId('page').click({ position: { x: 100, y: 300 } });
await expect(playground.sidebarPanel).toHaveCount(0);
});

test('inline dock: selecting a request auto-closes the overlay sidebar', async ({ page, playground }) => {
await page.goto(openAt('inline'));
await playground.sidebarToggle.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { selectActiveEnvName } from '../../../store/slices/env';
import type { ExampleHighlight } from '../../Docs/Sidebar/SidebarTree/SidebarTree';
import { useNavModel } from '../../../routing/hooks';
import { usePlaygroundUrlState, useElementWidth } from '../../../hooks';
import { usePlaygroundUrlState, useElementWidth, useClickOutside } from '../../../hooks';
import { getItemUuid, findItemByUuid } from '../../../utils/itemUtils';
import { isFolder } from '../../../utils/schemaHelpers';
import { exampleIndexForSlug, exampleSlugForIndex } from '../../../routing/slug';
Expand Down Expand Up @@ -101,6 +101,19 @@ const PlaygroundBody: React.FC<PlaygroundBodyProps> = ({
const viewWidth = useElementWidth(viewRef);
const orientation = viewWidth > 0 && viewWidth < ORIENTATION_BREAKPOINT ? 'vertical' : 'horizontal';

// Close the inline-dock overlay when the pointer goes down anywhere outside
// the sidebar, including outside the playground. The backdrop still handles
// clicks over the view (so they don't reach a control underneath); this adds
// the rest of the page. The toggle is excluded so closing via it isn't undone
// by its own click reopening the sidebar.
const sidebarRef = useRef<HTMLElement>(null);
useClickOutside(
sidebarRef,
onCloseSidebar,
sidebarOpen && dock === 'inline',
'[data-testid="playground-sidebar-toggle"]'
);

// Reopen whatever the URL says was last open. `pgReq` holds a request, a
// folder, or the environments / collection-settings view, so a deep link, a
// Try, or a reload all bring back the same thing. Runs once per URL value.
Expand Down Expand Up @@ -225,8 +238,18 @@ const PlaygroundBody: React.FC<PlaygroundBodyProps> = ({

return (
<StyledWrapper data-testid="playground-runner" data-overlay-sidebar={dock === 'inline' ? 'true' : undefined}>
{sidebarOpen && dock === 'inline' && (
// In the inline dock the sidebar overlays the view, so a click outside it
// dismisses it, same as the docs navigation drawer's backdrop.
<div
className="sidebar-backdrop"
data-testid="playground-sidebar-backdrop"
aria-hidden="true"
onClick={onCloseSidebar}
/>
)}
Comment thread
sundram-bruno marked this conversation as resolved.
{sidebarOpen && (
<aside className="sidebar" data-testid="playground-sidebar-panel">
<aside className="sidebar" data-testid="playground-sidebar-panel" ref={sidebarRef}>
<PlaygroundSidebar
collection={collection}
activeSlug={activeSlug}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,21 @@ export const StyledWrapper = styled.div`
position: relative;
}

&[data-overlay-sidebar='true'] .sidebar-backdrop {
position: absolute;
inset: 0;
z-index: calc(var(--z-sidebar) - 1);
}

&[data-overlay-sidebar='true'] .sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 5;
z-index: var(--z-sidebar);
background-color: var(--oc-background-base);
box-shadow: 2px 0 8px color-mix(in srgb, var(--oc-text) 12%, transparent);
border-right: none;
box-shadow: var(--oc-shadow-md);
}

&[data-overlay-sidebar='true'] .view {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const StyledWrapper = styled.div`
z-index: calc(var(--z-overlay, 50) + 1);
background-color: var(--oc-background-base);
border-right: 1px solid var(--oc-border-border0);
box-shadow: var(--shadow-md);
box-shadow: var(--oc-shadow-md);
transform: translateX(-100%);
visibility: hidden;
pointer-events: none;
Expand Down
12 changes: 9 additions & 3 deletions packages/oc-docs/src/hooks/useClickOutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ import type { RefObject } from 'react';
* pointerdown (not mousedown) so it fires uniformly for mouse, touch and pen;
* on touch, mousedown is only a compatibility event and can be suppressed.
* Shared by the search panel and the filter dropdowns so the close-on-outside
* behaviour lives in one place.
* behaviour lives in one place. Pass `ignoreSelector` to keep a trigger that
* lives outside `ref` (e.g. a separate toggle button) from counting as outside,
* which would otherwise close then immediately reopen on the trigger's click.
*/
export const useClickOutside = (
ref: RefObject<HTMLElement | null>,
onClose: () => void,
enabled = true,
ignoreSelector?: string,
): void => {
useEffect(() => {
if (!enabled) return;
const onPointerDown = (e: PointerEvent) => {
if (!ref.current?.contains(e.target as Node)) onClose();
const target = e.target as Element | null;
if (ref.current?.contains(target)) return;
if (ignoreSelector && target?.closest(ignoreSelector)) return;
onClose();
};
document.addEventListener('pointerdown', onPointerDown);
return () => document.removeEventListener('pointerdown', onPointerDown);
}, [enabled, onClose, ref]);
}, [enabled, onClose, ref, ignoreSelector]);
};
Loading