|
3 | 3 | import type { FloatingRootContext } from "../../hooks/use-floating-root-context.svelte.js";
|
4 | 4 | import type { FloatingContext } from "../../hooks/use-floating.svelte.js";
|
5 | 5 | import { getNodeName, isHTMLElement } from "@floating-ui/utils/dom";
|
6 |
| - import { getTabbableOptions } from "../../internal/tabbable.js"; |
| 6 | + import { |
| 7 | + getNextTabbable, |
| 8 | + getPreviousTabbable, |
| 9 | + getTabbableOptions, |
| 10 | + isOutsideEvent, |
| 11 | + } from "../../internal/tabbable.js"; |
7 | 12 | import { isTabbable, tabbable, type FocusableElement } from "tabbable";
|
8 | 13 | import { isTypeableCombobox } from "../../internal/is-typeable-element.js";
|
9 | 14 | import { markOthers, supportsInert } from "../../internal/mark-others.js";
|
|
28 | 33 | import type { Boxed, OpenChangeReason } from "../../types.js";
|
29 | 34 | import { useMergeRefs } from "../../hooks/use-merge-refs.svelte.js";
|
30 | 35 | import { watch } from "../../internal/watch.svelte.js";
|
31 |
| - import { HIDDEN_STYLES_STRING } from "../focus-guard.svelte"; |
| 36 | + import FocusGuard, { HIDDEN_STYLES_STRING } from "../focus-guard.svelte"; |
32 | 37 |
|
33 | 38 | const LIST_LIMIT = 20;
|
34 | 39 | let previouslyFocusedElements: Element[] = [];
|
|
141 | 146 | outsideElementsInert?: boolean;
|
142 | 147 | }
|
143 | 148 |
|
| 149 | + type DismissButtonSnippetProps = { |
| 150 | + ref: Boxed<HTMLElement>; |
| 151 | + }; |
| 152 | +
|
144 | 153 | export type { FloatingFocusManagerProps };
|
145 | 154 | </script>
|
146 | 155 |
|
147 | 156 | <script lang="ts">
|
148 | 157 | import VisuallyHiddenDismiss from "./visually-hidden-dismiss.svelte";
|
| 158 | + import { box } from "../../internal/box.svelte.js"; |
149 | 159 |
|
150 | 160 | let {
|
151 | 161 | context,
|
|
183 | 193 | const tree = useFloatingTree();
|
184 | 194 | const portalContext = usePortalContext();
|
185 | 195 |
|
186 |
| - let startDismissButton = $state<HTMLButtonElement | null>(null); |
187 |
| - let endDismissButton = $state<HTMLButtonElement | null>(null); |
| 196 | + const startDismissButtonRef = box<HTMLElement>(null!); |
| 197 | + const endDismissButtonRef = box<HTMLButtonElement>(null!); |
| 198 | +
|
188 | 199 | let preventReturnFocus = $state(false);
|
189 | 200 | let isPointerDown = $state(false);
|
190 | 201 | let tabbableIndex = $state(-1);
|
|
378 | 389 | }
|
379 | 390 | });
|
380 | 391 |
|
381 |
| - let beforeGuardRef = $state<Boxed<HTMLSpanElement | null>>({ |
382 |
| - current: null, |
383 |
| - }); |
384 |
| - let afterGuardRef = $state<Boxed<HTMLSpanElement | null>>({ |
385 |
| - current: null, |
386 |
| - }); |
| 392 | + const beforeGuardRef = box<HTMLSpanElement | null>(null); |
| 393 | + const afterGuardRef = box<HTMLSpanElement | null>(null); |
387 | 394 |
|
388 | 395 | const mergedBeforeGuardRef = useMergeRefs([
|
389 | 396 | beforeGuardRef,
|
|
415 | 422 | context.floating,
|
416 | 423 | ...portalNodes,
|
417 | 424 | ...ancestorFloatingNodes,
|
418 |
| - startDismissButton, |
419 |
| - endDismissButton, |
| 425 | + startDismissButtonRef, |
| 426 | + endDismissButtonRef, |
420 | 427 | beforeGuardRef.current,
|
421 | 428 | afterGuardRef.current,
|
422 | 429 | portalContext?.beforeOutsideRef.current,
|
|
663 | 670 | };
|
664 | 671 | });
|
665 | 672 |
|
666 |
| - type DismissButtonSnippetProps = { |
667 |
| - disabled: boolean; |
668 |
| - visuallyHiddenDismiss: boolean; |
669 |
| - modal: boolean; |
670 |
| - location: "start" | "end"; |
671 |
| - }; |
| 673 | + const shouldRenderGuards = $derived( |
| 674 | + !disabled && |
| 675 | + guards && |
| 676 | + (modal ? !isUntrappedTypeableCombobox : true) && |
| 677 | + (isInsidePortal || modal) |
| 678 | + ); |
672 | 679 | </script>
|
| 680 | + |
| 681 | +{#snippet DismissButton({ ref }: DismissButtonSnippetProps)} |
| 682 | + {#if !disabled && visuallyHiddenDismiss && modal} |
| 683 | + <VisuallyHiddenDismiss |
| 684 | + bind:ref={ref.current} |
| 685 | + onclick={(event) => context.onOpenChange(false, event)}> |
| 686 | + {typeof visuallyHiddenDismiss === "string" |
| 687 | + ? visuallyHiddenDismiss |
| 688 | + : "Dismiss"} |
| 689 | + </VisuallyHiddenDismiss> |
| 690 | + {/if} |
| 691 | +{/snippet} |
| 692 | + |
| 693 | +{#if shouldRenderGuards} |
| 694 | + <FocusGuard |
| 695 | + data-type="inside" |
| 696 | + bind:ref={mergedBeforeGuardRef.current} |
| 697 | + onfocus={(event) => { |
| 698 | + if (modal) { |
| 699 | + const els = getTabbableElements(); |
| 700 | + enqueueFocus( |
| 701 | + order[0] === "reference" ? els[0] : els[els.length - 1] |
| 702 | + ); |
| 703 | + } else if ( |
| 704 | + portalContext?.preserveTabOrder && |
| 705 | + portalContext.portalNode |
| 706 | + ) { |
| 707 | + preventReturnFocus = false; |
| 708 | + if (isOutsideEvent(event, portalContext.portalNode)) { |
| 709 | + const nextTabbable = |
| 710 | + getNextTabbable() || context.domReference; |
| 711 | + nextTabbable?.focus(); |
| 712 | + } else { |
| 713 | + portalContext.beforeOutsideRef.current?.focus(); |
| 714 | + } |
| 715 | + } |
| 716 | + }} /> |
| 717 | +{/if} |
| 718 | +<!-- |
| 719 | +Ensure the first swipe is the list item. The end of the listbox popup |
| 720 | +will have a dismiss button. |
| 721 | +--> |
| 722 | +{#if !isUntrappedTypeableCombobox} |
| 723 | + {@render DismissButton({ ref: startDismissButtonRef })} |
| 724 | +{/if} |
| 725 | +{@render children?.()} |
| 726 | +{@render DismissButton({ ref: endDismissButtonRef })} |
| 727 | +{#if shouldRenderGuards} |
| 728 | + <FocusGuard |
| 729 | + data-type="inside" |
| 730 | + bind:ref={mergedAfterGuardRef.current} |
| 731 | + onfocus={(event) => { |
| 732 | + if (modal) { |
| 733 | + enqueueFocus(getTabbableElements()[0]); |
| 734 | + } else if ( |
| 735 | + portalContext?.preserveTabOrder && |
| 736 | + portalContext.portalNode |
| 737 | + ) { |
| 738 | + if (closeOnFocusOut) { |
| 739 | + preventReturnFocus = true; |
| 740 | + } |
| 741 | + |
| 742 | + if (isOutsideEvent(event, portalContext.portalNode)) { |
| 743 | + const prevTabbable = |
| 744 | + getPreviousTabbable() || context.domReference; |
| 745 | + prevTabbable?.focus(); |
| 746 | + } else { |
| 747 | + portalContext.afterOutsideRef.current?.focus(); |
| 748 | + } |
| 749 | + } |
| 750 | + }} /> |
| 751 | +{/if} |
0 commit comments