@remit/ui 0.0.146 → 0.0.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -102,6 +102,12 @@ export interface FolderTreePickerProps {
|
|
|
102
102
|
) => Promise<FolderTreeNode>;
|
|
103
103
|
/** Escape. The picker never owns its presentation, so it cannot close itself. */
|
|
104
104
|
onCancel?: () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Takes focus on mount. A picker that opens away from where it was triggered
|
|
107
|
+
* — a portalled popover — leaves the keyboard behind on the trigger, so Tab
|
|
108
|
+
* walks the surrounding toolbar instead of entering the tree.
|
|
109
|
+
*/
|
|
110
|
+
autoFocusFilter?: boolean;
|
|
105
111
|
/** The provider's hierarchy separator. */
|
|
106
112
|
delimiter?: string;
|
|
107
113
|
labels?: FolderTreePickerLabels;
|
|
@@ -147,6 +153,7 @@ export const FolderTreePicker = ({
|
|
|
147
153
|
onCancel,
|
|
148
154
|
delimiter = "/",
|
|
149
155
|
labels,
|
|
156
|
+
autoFocusFilter = false,
|
|
150
157
|
}: FolderTreePickerProps) => {
|
|
151
158
|
const text = { ...defaultLabels, ...labels };
|
|
152
159
|
const [query, setQuery] = useState("");
|
|
@@ -163,10 +170,16 @@ export const FolderTreePicker = ({
|
|
|
163
170
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
164
171
|
|
|
165
172
|
const rowRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
173
|
+
const filterRef = useRef<HTMLInputElement>(null);
|
|
166
174
|
const roving = useRef(false);
|
|
167
175
|
const createAbort = useRef<AbortController | null>(null);
|
|
168
176
|
useEffect(() => () => createAbort.current?.abort(), []);
|
|
169
177
|
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!autoFocusFilter) return;
|
|
180
|
+
filterRef.current?.focus();
|
|
181
|
+
}, [autoFocusFilter]);
|
|
182
|
+
|
|
170
183
|
const trimmedQuery = query.trim().toLowerCase();
|
|
171
184
|
const ordered = useMemo(
|
|
172
185
|
() => orderFolderNodes(folders, delimiter),
|
|
@@ -413,6 +426,7 @@ export const FolderTreePicker = ({
|
|
|
413
426
|
return (
|
|
414
427
|
<div className="flex min-h-0 w-full min-w-0 flex-col">
|
|
415
428
|
<Input
|
|
429
|
+
ref={filterRef}
|
|
416
430
|
variant="inline"
|
|
417
431
|
className="border-b border-line px-3 py-2"
|
|
418
432
|
icon={<Search className="size-4" aria-hidden="true" />}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Mail, MailOpen, Tag } from "lucide-react";
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { PopoverMenu, PopoverMenuRow } from "./popover-menu.js";
|
|
4
5
|
|
|
5
6
|
const meta: Meta<typeof PopoverMenu> = {
|
|
6
7
|
title: "Kit/PopoverMenu",
|
|
@@ -78,6 +79,50 @@ export const ManyItems: Story = {
|
|
|
78
79
|
},
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
function GrowingMenu() {
|
|
83
|
+
const [rows, setRows] = useState(2);
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex h-screen items-end justify-end p-4">
|
|
86
|
+
<PopoverMenu
|
|
87
|
+
triggerLabel="More actions"
|
|
88
|
+
items={Array.from({ length: rows }, (_, i) => ({
|
|
89
|
+
key: `label-${i}`,
|
|
90
|
+
label: `Label ${i + 1}`,
|
|
91
|
+
icon: <Tag className="size-4" />,
|
|
92
|
+
onSelect: () => undefined,
|
|
93
|
+
}))}
|
|
94
|
+
>
|
|
95
|
+
<PopoverMenuRow
|
|
96
|
+
label="Show more"
|
|
97
|
+
onSelect={() => setRows((current) => current + 10)}
|
|
98
|
+
/>
|
|
99
|
+
</PopoverMenu>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A panel that grows after it has been placed — a list arriving from a fetch,
|
|
106
|
+
* a confirmation bar appearing on a pick. It stays anchored to its trigger as
|
|
107
|
+
* it grows, rather than keeping the position it was given at its opening size
|
|
108
|
+
* and running off the bottom of the screen.
|
|
109
|
+
*/
|
|
110
|
+
export const GrowsAfterOpening: Story = {
|
|
111
|
+
name: "Panel grows after it opens",
|
|
112
|
+
parameters: { layout: "fullscreen" },
|
|
113
|
+
render: () => <GrowingMenu />,
|
|
114
|
+
play: async ({ canvasElement }) => {
|
|
115
|
+
canvasElement
|
|
116
|
+
.querySelector<HTMLButtonElement>('[aria-label="More actions"]')
|
|
117
|
+
?.click();
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
119
|
+
// The panel is portalled onto the body, so it is outside the canvas.
|
|
120
|
+
Array.from(document.body.querySelectorAll<HTMLButtonElement>("button"))
|
|
121
|
+
.find((button) => button.textContent?.trim() === "Show more")
|
|
122
|
+
?.click();
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
81
126
|
export const WithNestedPicker: Story = {
|
|
82
127
|
args: {
|
|
83
128
|
triggerLabel: "More actions",
|
|
@@ -53,8 +53,11 @@ function clampToViewport(
|
|
|
53
53
|
/**
|
|
54
54
|
* Measures the anchor and the panel's own size, then keeps the panel's fixed
|
|
55
55
|
* position clamped to the viewport for as long as it is open — reset on every
|
|
56
|
-
* resize
|
|
57
|
-
*
|
|
56
|
+
* resize, on scroll anywhere in the ancestor chain (a fixed position does not
|
|
57
|
+
* follow a scrolled anchor on its own), and whenever the panel's own box
|
|
58
|
+
* changes. A panel placed once at the size it opened with runs off the bottom
|
|
59
|
+
* as soon as its content arrives: a loading line gives way to a full list, a
|
|
60
|
+
* confirmation bar appears on a pick, a filter shrinks it back.
|
|
58
61
|
*/
|
|
59
62
|
function useAnchoredPlacement(
|
|
60
63
|
panelRef: RefObject<HTMLElement | null>,
|
|
@@ -92,7 +95,14 @@ function useAnchoredPlacement(
|
|
|
92
95
|
place();
|
|
93
96
|
window.addEventListener("resize", place);
|
|
94
97
|
window.addEventListener("scroll", place, true);
|
|
98
|
+
const panel = panelRef.current;
|
|
99
|
+
let observer: ResizeObserver | null = null;
|
|
100
|
+
if (panel && typeof ResizeObserver !== "undefined") {
|
|
101
|
+
observer = new ResizeObserver(place);
|
|
102
|
+
observer.observe(panel);
|
|
103
|
+
}
|
|
95
104
|
return () => {
|
|
105
|
+
observer?.disconnect();
|
|
96
106
|
window.removeEventListener("resize", place);
|
|
97
107
|
window.removeEventListener("scroll", place, true);
|
|
98
108
|
};
|
|
@@ -116,6 +126,11 @@ export interface PopoverMenuPortalProps {
|
|
|
116
126
|
* page — the compose body, a card, anything with its own `overflow` — no
|
|
117
127
|
* matter how high its `z-index` climbs; escaping that ancestor takes leaving
|
|
118
128
|
* its DOM subtree, which only a portal does.
|
|
129
|
+
*
|
|
130
|
+
* The wrapper carries the menu layer's own `z-50`: as a body child it would
|
|
131
|
+
* otherwise stack by document order alone and lose to every fixed surface
|
|
132
|
+
* already on the page. `z-[60]` stays above it, for confirmation dialogs and
|
|
133
|
+
* error banners that must cover an open menu.
|
|
119
134
|
*/
|
|
120
135
|
export function PopoverMenuPortal({
|
|
121
136
|
open,
|
|
@@ -127,7 +142,10 @@ export function PopoverMenuPortal({
|
|
|
127
142
|
const style = useAnchoredPlacement(panelRef, open, align, getAnchor);
|
|
128
143
|
if (!open) return null;
|
|
129
144
|
return createPortal(
|
|
130
|
-
<div
|
|
145
|
+
<div
|
|
146
|
+
className="z-50"
|
|
147
|
+
style={style ?? { position: "fixed", visibility: "hidden" }}
|
|
148
|
+
>
|
|
131
149
|
{children}
|
|
132
150
|
</div>,
|
|
133
151
|
document.body,
|