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
9 changes: 8 additions & 1 deletion src/features/welcome/WelcomeSplitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export default function WelcomeSplitButton(props: WelcomeSplitButtonProps) {
const menuId = useId();
return (
<div className="welcome-split-button" ref={groupRef}>
<button className={props.className} onClick={props.onClick}>
<button
className={props.className}
onClick={() => {
setOpen(false);
props.onClick();
}}
>
{props.icon}
{props.label}
</button>
Expand All @@ -45,6 +51,7 @@ export default function WelcomeSplitButton(props: WelcomeSplitButtonProps) {
portal={false}
open={open}
anchor={triggerRef.current ?? undefined}
within={groupRef.current ?? undefined}
onClose={() => setOpen(false)}
className="welcome-split-menu"
role="menu"
Expand Down
3 changes: 3 additions & 0 deletions src/ui/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export interface PopupProps extends Pick<
readonly onEscape?: () => void;
readonly dismissible?: boolean;
readonly anchor?: HTMLElement | undefined;
// A press inside this element is not an outside press: the popup stays open and the click
// goes through, so a control beside the anchor (a split button's main half) acts at once.
readonly within?: HTMLElement | undefined;
readonly className?: string;
readonly style?: CSSProperties | undefined;
readonly role?: 'dialog' | 'alertdialog' | 'menu' | 'listbox' | 'tooltip';
Expand Down
12 changes: 8 additions & 4 deletions src/ui/popupDismiss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { inOwnedPopup, isTopPopupLayer } from './popupOwnership';
import { useEffect, type RefObject } from 'react';
import type { PopupProps, PopupActions } from './Popup';
import { toRecord } from '../../shared/core/record';
interface DismissOptions extends Pick<PopupProps, 'dismissible' | 'anchor' | 'onEscape'> {
interface DismissOptions extends Pick<
PopupProps,
'dismissible' | 'anchor' | 'within' | 'onEscape'
> {
readonly active: boolean;
readonly mode: 'modal' | 'popover';
}
Expand All @@ -11,7 +14,7 @@ export function usePopupDismiss(
options: DismissOptions,
{ close, finish }: PopupActions,
): void {
const { active, anchor, dismissible = true, mode, onEscape } = options;
const { active, anchor, within, dismissible = true, mode, onEscape } = options;
usePopupEnvironment(surface, options, close);
useEffect(() => {
if (!active || !dismissible) {
Expand All @@ -23,7 +26,8 @@ export function usePopupDismiss(
!(target instanceof window.Element && target.closest('.toast-stack')) &&
!surface.current?.contains(target) &&
!inOwnedPopup(target, surface.current ?? undefined) &&
!anchor?.contains(target);
!anchor?.contains(target) &&
!within?.contains(target);
const down = (event: MouseEvent): void => {
outsidePress = outside(event.target);
if (mode === 'popover' && outsidePress && popupIsTop(surface.current)) {
Expand Down Expand Up @@ -75,7 +79,7 @@ export function usePopupDismiss(
document.removeEventListener('keydown', key);
window.removeEventListener('message', message);
};
}, [active, anchor, close, finish, dismissible, mode, onEscape, surface]);
}, [active, anchor, within, close, finish, dismissible, mode, onEscape, surface]);
}

export function popupIsTop(surface: RefObject<HTMLDivElement>['current']): boolean {
Expand Down
14 changes: 14 additions & 0 deletions test/renderer/welcome/welcome.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ const PIXEL =
document.body.dispatchEvent(new dom.window.MouseEvent('mousedown', { bubbles: true }));
});
check('an outside click closes the menu', all('[role=menuitem]').length === 0);
// The main half beside an open menu is part of the same control: its press is not an
// outside press, so the first click opens the picker instead of only closing the menu.
await act(async () => arrows[0].click());
const openFromFolder = all('.actions button')[0];
await act(async () => {
openFromFolder.dispatchEvent(new dom.window.MouseEvent('mousedown', { bubbles: true }));
openFromFolder.click();
});
check(
'with its menu open, Open from folder opens the picker on the first click',
folderRequests === 2,
String(folderRequests),
);
check('and closes the menu', all('[role=menuitem]').length === 0);
check('nothing tells the user a picture is old', !/out of date/i.test(text()), text());
check('and nothing offers to fix it', all('.recent-refresh').length === 0);

Expand Down
Loading