Skip to content

Commit

Permalink
Implement useOverlayWithSubject() hook.
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrause committed Jan 20, 2025
1 parent a03dbe4 commit 135e016
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 12 deletions.
7 changes: 5 additions & 2 deletions src/components/overlays/DialogModal/DialogModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export const useConfirmationModal = <S,>(
children: 'Are you sure you want to perform this action?',
actions: (
<>
<Dialog.CancelAction label="Cancel"
<Dialog.CancelAction
label="Cancel"
onPress={() => {
const subject = modal.subject;
if (typeof subject === 'undefined') {
Expand All @@ -121,7 +122,9 @@ export const useConfirmationModal = <S,>(
onCancel?.(subject);
}}
/>
<Dialog.SubmitAction label={actionLabel || 'Confirm'}
<Dialog.SubmitAction
//autoFocus // Focus "confirm" by default?
label={actionLabel || 'Confirm'}
onPress={() => {
const subject = modal.subject;
if (typeof subject === 'undefined') {
Expand Down
30 changes: 30 additions & 0 deletions src/components/overlays/DialogOverlay/DialogOverlay.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ export const DialogOverlaySmall: Story = { args: { size: 'small' } };
export const DialogOverlayMedium: Story = { args: { size: 'medium' } };
export const DialogOverlayLarge: Story = { args: { size: 'large' } };

const DialogOverlayControlledWithSubject = (props: React.ComponentProps<typeof DialogOverlay>) => {
type Subject = { name: string };
const overlay = DialogOverlay.useOverlayWithSubject<Subject>();

return (
<article className="bk-body-text">
{overlay.subject &&
<DialogOverlay {...overlay.props} {...props} title={overlay.subject.name}>
Details about {overlay.subject.name} here.
</DialogOverlay>
}

<p>A single details overlay will be used, filled in with the subject based on which name was pressed.</p>

<p>
<Button variant="primary" label="Open: Alice" onPress={() => { overlay.activateWith({ name: 'Alice' }); }}/>
</p>
<p>
<Button variant="primary" label="Open: Bob" onPress={() => { overlay.activateWith({ name: 'Bob' }); }}/>
</p>
</article>
);
};
export const DialogModalWithSubject: Story = {
args: {
trigger: undefined,
},
render: (args) => <DialogOverlayControlledWithSubject {...args}/>,
};

export const DialogOverlayWithNestedModal: Story = {
args: {
display: 'slide-over',
Expand Down
34 changes: 34 additions & 0 deletions src/components/overlays/DialogOverlay/DialogOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import * as React from 'react';
import { flushSync } from 'react-dom';
import { mergeRefs } from '../../../util/reactUtil.ts';
import { classNames as cx } from '../../../util/componentUtil.ts';

Expand Down Expand Up @@ -51,6 +52,38 @@ export type DialogOverlayProps = Omit<React.ComponentProps<typeof Dialog>, 'chil
providerProps?: undefined | Omit<PopoverProviderPropsDialog, 'children'>,
};

export type OverlayWithSubject<S> = {
props: Partial<DialogOverlayProps>,
subject: undefined | S,
activateWith: (subject: S | (() => S)) => void,
};
/**
* Utility hook to get a reference to a `DialogOverlay` for imperative usage. To open, you can call `activate()`, or
* `activateWith()` if you want to include some subject data to be shown in the modal.
*/
export const useOverlayWithSubject = <S,>(
config?: undefined | {
subjectInitial?: undefined | S | (() => undefined | S),
},
): OverlayWithSubject<S> => {
const { subjectInitial } = config ?? {};

const popoverRef = PopoverProvider.useRef(null);
const [subject, setSubject] = React.useState<undefined | S>(subjectInitial);

return {
props: { popoverRef },
subject,
activateWith: (subject: S | (() => S)) => {
// Use flushSync() to force the modal to render, in case the modal rendering is conditional
// on the subject being set.
flushSync(() => { setSubject(subject); });

popoverRef.current?.activate();
},
};
};

/**
* A dialog component displayed as a popover when activating the given trigger.
*/
Expand Down Expand Up @@ -108,6 +141,7 @@ export const DialogOverlay = Object.assign(
},
{
usePopoverRef: PopoverProvider.useRef,
useOverlayWithSubject,
Action: Dialog.Action,
ActionIcon: Dialog.ActionIcon,
CancelAction: Dialog.CancelAction,
Expand Down
22 changes: 12 additions & 10 deletions src/components/overlays/ToastProvider/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,19 @@ export const Toaster = (props: ToasterProps) => {
}, [toastStore]);

// Pause auto-close when the page is not currently visible by the user
const handleVisibilityChange = React.useCallback(() => {
if (document.visibilityState === 'visible') {
toastStore.onPageVisible();
} else {
toastStore.onPageHide();
}
}, [toastStore]);
React.useEffect(() => {
window.document.addEventListener('visibilitychange', handleVisibilityChange, false);
return () => { window.document.removeEventListener('visibilitychange', handleVisibilityChange); };
}, [handleVisibilityChange]);
const controller = new AbortController();

document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
toastStore.onPageVisible();
} else {
toastStore.onPageHide();
}
}, { signal: controller.signal });

return () => { controller.abort(); };
}, [toastStore]);

const containerRef = React.useRef<null | React.ComponentRef<'section'>>(null);
const openPopover = React.useCallback((container: null | HTMLElement) => {
Expand Down

0 comments on commit 135e016

Please sign in to comment.