@apliteni/apliteni-ui 0.95.0 → 0.96.0
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 +1 -1
- package/react/dist/index.js +64 -1
- package/src/components/dropdown.js +243 -13
- package/src/index.d.ts +12 -0
- package/src/styles/dropdown.css +24 -0
- package/src/styles/topbar.css +2 -2
package/package.json
CHANGED
package/react/dist/index.js
CHANGED
|
@@ -1891,10 +1891,12 @@ import {
|
|
|
1891
1891
|
dropdownMatch,
|
|
1892
1892
|
dropdownFiltering,
|
|
1893
1893
|
filterPanelFit,
|
|
1894
|
-
transitionMs as transitionMs2
|
|
1894
|
+
transitionMs as transitionMs2,
|
|
1895
|
+
dropdownViewportFit
|
|
1895
1896
|
} from "@apliteni/apliteni-ui";
|
|
1896
1897
|
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1897
1898
|
var EXIT_SLACK_MS2 = 50;
|
|
1899
|
+
var SETTLE_MS = 450;
|
|
1898
1900
|
var inPanel = (node) => {
|
|
1899
1901
|
const el = node?.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
1900
1902
|
return !!el?.closest("[data-dropdown-panel]");
|
|
@@ -2139,6 +2141,67 @@ function Dropdown({
|
|
|
2139
2141
|
list.scrollTop = top + el.offsetHeight - list.clientHeight;
|
|
2140
2142
|
}
|
|
2141
2143
|
});
|
|
2144
|
+
useIsoLayoutEffect(() => {
|
|
2145
|
+
const el = panel.current;
|
|
2146
|
+
if (!el || el.closest(".ui-filter-bar")) return;
|
|
2147
|
+
const fit = () => {
|
|
2148
|
+
let got = dropdownViewportFit(el);
|
|
2149
|
+
if (!got) return;
|
|
2150
|
+
el.style.setProperty("--ui-dropdown-ceiling", got.max > 0 ? `${got.max}px` : "");
|
|
2151
|
+
if (got.width > got.max) got = dropdownViewportFit(el) ?? got;
|
|
2152
|
+
el.style.translate = got.shift ? `${got.shift}px` : "";
|
|
2153
|
+
};
|
|
2154
|
+
fit();
|
|
2155
|
+
const view = root.current?.ownerDocument?.defaultView;
|
|
2156
|
+
if (!view) return;
|
|
2157
|
+
const stop = [];
|
|
2158
|
+
if (typeof view.ResizeObserver === "function") {
|
|
2159
|
+
const ro = new view.ResizeObserver(() => fit());
|
|
2160
|
+
if (trigger.current) ro.observe(trigger.current);
|
|
2161
|
+
ro.observe(el);
|
|
2162
|
+
stop.push(() => ro.disconnect());
|
|
2163
|
+
}
|
|
2164
|
+
const on = (type, capture = false, handler = fit) => {
|
|
2165
|
+
view.addEventListener(type, handler, capture);
|
|
2166
|
+
stop.push(() => view.removeEventListener(type, handler, capture));
|
|
2167
|
+
};
|
|
2168
|
+
on("resize");
|
|
2169
|
+
on("scroll", true);
|
|
2170
|
+
let settling = null;
|
|
2171
|
+
const settle = () => {
|
|
2172
|
+
if (settling != null) clearTimeout(settling);
|
|
2173
|
+
settling = setTimeout(() => {
|
|
2174
|
+
settling = null;
|
|
2175
|
+
fit();
|
|
2176
|
+
}, SETTLE_MS);
|
|
2177
|
+
};
|
|
2178
|
+
view.addEventListener("transitionend", fit, true);
|
|
2179
|
+
view.addEventListener("animationend", fit, true);
|
|
2180
|
+
stop.push(() => {
|
|
2181
|
+
view.removeEventListener("transitionend", fit, true);
|
|
2182
|
+
view.removeEventListener("animationend", fit, true);
|
|
2183
|
+
if (settling != null) clearTimeout(settling);
|
|
2184
|
+
});
|
|
2185
|
+
on("transitionstart", true, settle);
|
|
2186
|
+
on("animationstart", true, settle);
|
|
2187
|
+
const html = view.document?.documentElement;
|
|
2188
|
+
if (typeof view.MutationObserver === "function" && html) {
|
|
2189
|
+
const mo = new view.MutationObserver((records) => {
|
|
2190
|
+
if (records.every((m) => inPanel(m.target))) return;
|
|
2191
|
+
fit();
|
|
2192
|
+
});
|
|
2193
|
+
mo.observe(html, {
|
|
2194
|
+
subtree: true,
|
|
2195
|
+
childList: true,
|
|
2196
|
+
attributes: true,
|
|
2197
|
+
characterData: true
|
|
2198
|
+
});
|
|
2199
|
+
stop.push(() => mo.disconnect());
|
|
2200
|
+
}
|
|
2201
|
+
return () => {
|
|
2202
|
+
stop.forEach((off) => off());
|
|
2203
|
+
};
|
|
2204
|
+
}, [open]);
|
|
2142
2205
|
const choose = (item, e) => {
|
|
2143
2206
|
if (item.disabled) {
|
|
2144
2207
|
e.preventDefault();
|
|
@@ -268,6 +268,26 @@ function ddGap(panel) {
|
|
|
268
268
|
return Number.isFinite(declared) ? declared : DD_GAP;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// The least a panel keeps between itself and the edge of its view. The number is
|
|
272
|
+
// --ui-dropdown-edge in src/styles/dropdown.css, and this is the fallback for a
|
|
273
|
+
// document that has not loaded the sheet, as DD_GAP is for the offset.
|
|
274
|
+
const DD_EDGE = 8;
|
|
275
|
+
|
|
276
|
+
function ddEdge(panel) {
|
|
277
|
+
const declared = parseFloat(ddViewOf(panel).getComputedStyle(panel).getPropertyValue('--ui-dropdown-edge'));
|
|
278
|
+
return Number.isFinite(declared) ? declared : DD_EDGE;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The width a panel has to stay inside: the LAYOUT viewport, which is what both
|
|
282
|
+
* `position: fixed` and a page's scrollable width are measured against. A classic
|
|
283
|
+
* scrollbar is in `innerWidth` and not in the box either of those means, so an
|
|
284
|
+
* end-anchored panel written from `innerWidth` sat a scrollbar's width inside its
|
|
285
|
+
* trigger. The fallback is for a view that lays nothing out, JSDOM among them. */
|
|
286
|
+
const ddViewWidth = (panel) => {
|
|
287
|
+
const view = ddViewOf(panel);
|
|
288
|
+
return view.document?.documentElement?.clientWidth || view.innerWidth || 0;
|
|
289
|
+
};
|
|
290
|
+
|
|
271
291
|
function ddItemsOf(dd) {
|
|
272
292
|
const panel = ddPanelOf(dd);
|
|
273
293
|
if (!panel) return [];
|
|
@@ -419,12 +439,14 @@ function ddRowOf(dd) {
|
|
|
419
439
|
return dd?.closest?.('.ui-filter-bar__chip')?.closest('.ui-filter-bar') || null;
|
|
420
440
|
}
|
|
421
441
|
|
|
422
|
-
/** Is this node inside some dropdown panel?
|
|
442
|
+
/** Is this node inside some dropdown panel? Both fits write to a panel and to
|
|
423
443
|
* nothing else, so a change inside one is never news — and a panel is
|
|
424
444
|
* `position: absolute`, so nothing in it can move a chip along its row. Any
|
|
425
445
|
* panel, not only the one being fitted: two open menus in one row would
|
|
426
|
-
* otherwise answer each other a mutation at a time
|
|
427
|
-
*
|
|
446
|
+
* otherwise answer each other a mutation at a time, and so did two <Dropdown>s
|
|
447
|
+
* on a page, until the browser gate timed out.
|
|
448
|
+
* why: docs/components.md#a-filter-row-holds-its-panels
|
|
449
|
+
* why: docs/components.md#the-dropdown-panel */
|
|
428
450
|
function ddOurs(node) {
|
|
429
451
|
const el = node?.nodeType === 1 ? node : node?.parentElement;
|
|
430
452
|
return !!el?.closest?.('[data-dropdown-panel]');
|
|
@@ -484,10 +506,31 @@ function ddUnwatchRow(dd) {
|
|
|
484
506
|
dd.__ddRowMo = null;
|
|
485
507
|
}
|
|
486
508
|
|
|
509
|
+
/* ---- Following a panel's anchor --------------------------------------------
|
|
510
|
+
* A position change need not resize anything and no event reports one, so the CAUSE
|
|
511
|
+
* is watched instead: the DOM change that moved the trigger. Measured on the first
|
|
512
|
+
* answer to #572, a flex row told to end-align its children left a panel at
|
|
513
|
+
* 237.6…477.6 on a 390px screen and an ancestor scrolled sideways left one at
|
|
514
|
+
* −108…132; a ResizeObserver sees neither. The limit, and why no end event is
|
|
515
|
+
* taken for the middle of an animation, is stated with the rule.
|
|
516
|
+
* why: docs/components.md#the-dropdown-panel */
|
|
517
|
+
|
|
518
|
+
/** Fit a panel again where it now is, whichever placement it uses. */
|
|
519
|
+
function ddPlace(dd) {
|
|
520
|
+
if (dd.__ddPanel) positionPortalPanel(dd, dd.__ddPanel);
|
|
521
|
+
else ddFitViewport(dd, ddPanelOf(dd));
|
|
522
|
+
}
|
|
523
|
+
|
|
487
524
|
/* A few frames past the sheet's own end, so the timer never cuts the fade's last
|
|
488
525
|
* frame. The number the dialogs' exit uses. */
|
|
489
526
|
const DD_EXIT_SLACK_MS = 50;
|
|
490
527
|
|
|
528
|
+
/* How long the settle fallback waits: past --dur-slow (400ms), the longest motion
|
|
529
|
+
* the kit ships, plus the same slack. A motion longer than this is answered by its
|
|
530
|
+
* own end event; this timer is for the end event that never comes.
|
|
531
|
+
* why: src/tokens/tokens.css */
|
|
532
|
+
const DD_SETTLE_MS = 450;
|
|
533
|
+
|
|
491
534
|
/**
|
|
492
535
|
* Hold a closing chip menu's open geometry until its fade has finished.
|
|
493
536
|
*
|
|
@@ -548,6 +591,100 @@ function ddResolveDirection(dd, panel) {
|
|
|
548
591
|
panel.classList.toggle('is-up', below < panel.offsetHeight + ddGap(panel) && t.top > below);
|
|
549
592
|
}
|
|
550
593
|
|
|
594
|
+
/** The shift a panel is already carrying, the one ddFitViewport() wrote. Taken
|
|
595
|
+
* off before the next measurement, so asking twice answers the same as asking
|
|
596
|
+
* once — the fit is measured from where the sheet puts the panel, not from where
|
|
597
|
+
* the last fit left it. */
|
|
598
|
+
const ddShiftOf = (panel) => parseFloat(panel.style?.translate) || 0;
|
|
599
|
+
|
|
600
|
+
/** The widest a panel may be: the view it is laid out in, less the keep-out it owes
|
|
601
|
+
* each edge. It is the bound a PORTALLED panel needs, whose room is the whole view
|
|
602
|
+
* because it is `position: fixed`; an in-place one is bounded by its trigger's own
|
|
603
|
+
* block, once `overflow-wrap` has let its long tokens break.
|
|
604
|
+
* why: src/styles/dropdown.css */
|
|
605
|
+
const ddCeiling = (panel) => Math.max(0, ddViewWidth(panel) - 2 * ddEdge(panel));
|
|
606
|
+
|
|
607
|
+
/** Write the ceiling, BEFORE the panel is measured: capping a panel's width
|
|
608
|
+
* changes the width a shift would be calculated from. The sheet reads the
|
|
609
|
+
* property and keeps its own 240px floor where the two disagree, which is a view
|
|
610
|
+
* narrower than 2 × edge + 240. why: src/styles/dropdown.css */
|
|
611
|
+
function ddWriteCeiling(panel) {
|
|
612
|
+
const max = ddCeiling(panel);
|
|
613
|
+
panel.style?.setProperty('--ui-dropdown-ceiling', max > 0 ? `${max}px` : '');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** A panel a filter row bounds to the row itself. It is nothing the viewport fit
|
|
617
|
+
* has to measure, and nothing the wiring has to watch for it.
|
|
618
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
619
|
+
const ddInFilterRow = (panel) => typeof panel?.closest === 'function' && !!panel.closest('.ui-filter-bar');
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* How wide a panel may be and how far along the inline axis it has to move to stay
|
|
623
|
+
* inside the view it is laid out in: `left` at or after the edge, `right` at or
|
|
624
|
+
* before the far one.
|
|
625
|
+
*
|
|
626
|
+
* `align` decides the edge of the trigger a panel hangs from, and that is all it
|
|
627
|
+
* decides — on a phone it moves the clipping from one side to the other rather
|
|
628
|
+
* than removing it, which is #572. So the preferred edge is measured and kept
|
|
629
|
+
* whenever it fits, and overridden only when it does not: the same shape
|
|
630
|
+
* `direction: 'auto'` gives the block axis.
|
|
631
|
+
*
|
|
632
|
+
* Measured, not derived from the classes: the panel's own box is where the sheet
|
|
633
|
+
* has actually put it, so one answer covers `left: 0`, `.is-end`'s `right: 0` and
|
|
634
|
+
* the topbar's bespoke menus alike, and a rule nobody here has read about cannot
|
|
635
|
+
* send it the wrong way.
|
|
636
|
+
*
|
|
637
|
+
* A panel inside a `.ui-filter-bar` is not this function's subject: a filter row
|
|
638
|
+
* bounds its panels to the row itself, which already holds them inside the view.
|
|
639
|
+
* why: docs/components.md#a-filter-row-holds-its-panels
|
|
640
|
+
*
|
|
641
|
+
* @param {Element} panel a `[data-dropdown-panel]`, as the sheet has placed it
|
|
642
|
+
* @param {{left: number, width: number}} [at] the box the caller is about to
|
|
643
|
+
* write, for a panel it places itself — the portal, whose own box is still the
|
|
644
|
+
* one the last call left it at
|
|
645
|
+
* @returns {{shift: number, left: number, width: number, view: number, edge: number,
|
|
646
|
+
* max: number}|null}
|
|
647
|
+
*/
|
|
648
|
+
export function dropdownViewportFit(panel, at) {
|
|
649
|
+
if (!panel || typeof panel.getBoundingClientRect !== 'function') return null;
|
|
650
|
+
if (!at && ddInFilterRow(panel)) return null;
|
|
651
|
+
const view = ddViewWidth(panel);
|
|
652
|
+
const box = panel.getBoundingClientRect();
|
|
653
|
+
const width = at ? at.width : box.width;
|
|
654
|
+
const left = at ? at.left : box.left - ddShiftOf(panel);
|
|
655
|
+
if (!(view > 0) || !(width > 0) || !Number.isFinite(left)) return null;
|
|
656
|
+
const slack = view - width;
|
|
657
|
+
/* The gap is what a panel can afford rather than what it asks for. A panel still
|
|
658
|
+
wider than its view — the floor, inside a view too narrow for it — has no room
|
|
659
|
+
for one and cannot honour both edges at all; it keeps its start, so the reader
|
|
660
|
+
meets the beginning of the rows and not the end. */
|
|
661
|
+
const edge = Math.max(0, Math.min(ddEdge(panel), slack / 2));
|
|
662
|
+
const want = Math.max(edge, Math.min(left, slack - edge));
|
|
663
|
+
return { shift: want - left, left: want, width, view, edge, max: ddCeiling(panel) };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Keep a panel in place inside its view, by moving it along the inline axis.
|
|
668
|
+
*
|
|
669
|
+
* `translate` and not `left`, `right` or a margin: an absolutely positioned box is
|
|
670
|
+
* shrink-to-fit, so its width is measured against the room left between its own
|
|
671
|
+
* offsets, and a shift written as one of those offsets changes the width the shift
|
|
672
|
+
* was calculated from. A transform moves no layout, so the number stays true. The
|
|
673
|
+
* entry travel keeps `transform` to itself, and the property is off the panel's
|
|
674
|
+
* transition list, so a panel already open lands at once rather than sliding.
|
|
675
|
+
*
|
|
676
|
+
* A portalled panel is placed by positionPortalPanel(), which writes its edges in
|
|
677
|
+
* viewport coordinates and clamps them there.
|
|
678
|
+
* why: docs/components.md#the-dropdown-panel
|
|
679
|
+
*/
|
|
680
|
+
function ddFitViewport(dd, panel) {
|
|
681
|
+
if (!panel?.style || dd?.__ddPanel === panel || ddInFilterRow(panel)) return;
|
|
682
|
+
ddWriteCeiling(panel);
|
|
683
|
+
const fit = dropdownViewportFit(panel);
|
|
684
|
+
if (!fit) return;
|
|
685
|
+
panel.style.translate = fit.shift ? `${fit.shift}px` : '';
|
|
686
|
+
}
|
|
687
|
+
|
|
551
688
|
// Nothing lays a portalled panel out any more, so these four inline values are
|
|
552
689
|
// its layout. Inline, so no rule in any sheet can pin the opposite edge.
|
|
553
690
|
function positionPortalPanel(dd, panel) {
|
|
@@ -564,12 +701,21 @@ function positionPortalPanel(dd, panel) {
|
|
|
564
701
|
s.bottom = 'auto';
|
|
565
702
|
s.top = `${t.bottom + gap}px`;
|
|
566
703
|
}
|
|
567
|
-
|
|
704
|
+
/* The inline axis is fitted rather than mirrored. Both of these edges put a
|
|
705
|
+
240px panel off the screen from a trigger near the one it is anchored to, and
|
|
706
|
+
#572 is that: the fit is asked with the box this function is about to write,
|
|
707
|
+
because the panel's own is still where the last call left it. */
|
|
708
|
+
const end = panel.classList.contains('is-end');
|
|
709
|
+
ddWriteCeiling(panel);
|
|
710
|
+
const width = panel.getBoundingClientRect().width;
|
|
711
|
+
const left = end ? t.right - width : t.left;
|
|
712
|
+
const shift = dropdownViewportFit(panel, { left, width })?.shift || 0;
|
|
713
|
+
if (end) {
|
|
568
714
|
s.left = 'auto';
|
|
569
|
-
s.right = `${
|
|
715
|
+
s.right = `${ddViewWidth(panel) - t.right - shift}px`;
|
|
570
716
|
} else {
|
|
571
717
|
s.right = 'auto';
|
|
572
|
-
s.left = `${t.left}px`;
|
|
718
|
+
s.left = `${t.left + shift}px`;
|
|
573
719
|
}
|
|
574
720
|
}
|
|
575
721
|
|
|
@@ -596,6 +742,11 @@ function closeDropdown(dd) {
|
|
|
596
742
|
if (panel?.style && !dd.__ddPanel && ddRowOf(dd)) ddHoldClose(dd, panel);
|
|
597
743
|
else if (panel?.style && dd.closest?.('.ui-filter-bar')) panel.style.minWidth = '';
|
|
598
744
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'false');
|
|
745
|
+
/* The anchor may have moved while the panel was open, and a shut panel is laid
|
|
746
|
+
out: its box goes on counting towards the page's scrollable width. One reading
|
|
747
|
+
on the way out, so a panel left behind at 237.6…477.6 does not keep the page
|
|
748
|
+
478px wide with nothing open. why: ddPlace() */
|
|
749
|
+
if (panel) ddFitViewport(dd, panel);
|
|
599
750
|
}
|
|
600
751
|
|
|
601
752
|
/** Every wired dropdown still in a tree, the ones in frames and shadow roots
|
|
@@ -622,7 +773,11 @@ function openDropdown(dd, focusIdx) {
|
|
|
622
773
|
ddCancelClose(dd, panel);
|
|
623
774
|
ddResolveDirection(dd, panel);
|
|
624
775
|
if (dd.__ddPanel) { positionPortalPanel(dd, panel); panel.classList.add('is-open'); }
|
|
625
|
-
|
|
776
|
+
/* Both fits, because each one answers for the panels the other declines:
|
|
777
|
+
filterPanelFit() is null outside a filter chip, and dropdownViewportFit()
|
|
778
|
+
is null inside a filter row, whose own row already bounds its panels.
|
|
779
|
+
why: docs/components.md#a-filter-row-holds-its-panels */
|
|
780
|
+
else { ddFitFilterPanel(dd, panel); ddWatchRow(dd, panel); ddFitViewport(dd, panel); }
|
|
626
781
|
}
|
|
627
782
|
dd.classList.add('open');
|
|
628
783
|
/* After `open`, and after the fit: ddResetSearch() pins the width it reads off
|
|
@@ -674,12 +829,14 @@ function ddListen(doc) {
|
|
|
674
829
|
const open = ddLive().find((dd) => dd.classList.contains('open') && dd.ownerDocument === doc);
|
|
675
830
|
if (open) { closeDropdown(open); open.querySelector('[data-dropdown-trigger]')?.focus(); }
|
|
676
831
|
});
|
|
677
|
-
|
|
678
|
-
|
|
832
|
+
/* Viewport coordinates go stale the moment anything scrolls, and an in-place
|
|
833
|
+
panel's fit is measured against the view as much as a portalled one's: a
|
|
834
|
+
trigger slid from x=260 to x=10 inside a row that scrolls sideways left its
|
|
835
|
+
panel at −108…132, with the first row's label off the screen. Capture, so a
|
|
836
|
+
scroll inside the rail the panel was lifted out of, or inside that row, counts
|
|
837
|
+
too. why: docs/components.md#the-dropdown-panel */
|
|
679
838
|
const reposition = () => {
|
|
680
|
-
for (const dd of ddLive())
|
|
681
|
-
if (dd.classList.contains('open') && dd.__ddPanel) positionPortalPanel(dd, dd.__ddPanel);
|
|
682
|
-
}
|
|
839
|
+
for (const dd of ddLive()) if (dd.classList.contains('open')) ddPlace(dd);
|
|
683
840
|
};
|
|
684
841
|
/* The fallback re-fit, for a view with no ResizeObserver. Not taken as well where
|
|
685
842
|
* ddWatchRow() is watching: a `resize` event is the worse measurement of the two.
|
|
@@ -691,11 +848,65 @@ function ddListen(doc) {
|
|
|
691
848
|
}
|
|
692
849
|
}
|
|
693
850
|
};
|
|
851
|
+
/* The shut ones, which the sweep above does not reach and which no reader is
|
|
852
|
+
looking at: a shut panel is laid out, so a panel fitted at 1280 and left shut
|
|
853
|
+
at 375 keeps the page scrolling sideways with nothing open. A shut PORTALLED
|
|
854
|
+
panel is not one of them — `position: fixed` puts it in no page's scrollable
|
|
855
|
+
width. why: docs/components.md#the-dropdown-panel */
|
|
856
|
+
const refitInPlace = () => {
|
|
857
|
+
for (const dd of ddLive()) {
|
|
858
|
+
if (!dd.__ddPanel && !dd.classList.contains('open')) ddFitViewport(dd, ddPanelOf(dd));
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
// The two together, which is every panel that owes anybody an answer.
|
|
862
|
+
const follow = () => { reposition(); refitInPlace(); };
|
|
694
863
|
const view = doc.defaultView;
|
|
695
864
|
if (!view) return;
|
|
696
|
-
|
|
865
|
+
/* Both states on a scroll, not the open one alone: a shut panel is laid out, so
|
|
866
|
+
an ancestor scrolled back to where it started left one at 258…498 on a 390px
|
|
867
|
+
screen with nothing open — #501's page width, in the state the open sweep
|
|
868
|
+
cannot reach. React's half has always answered both; this is that answer.
|
|
869
|
+
why: docs/components.md#the-dropdown-panel */
|
|
870
|
+
view.addEventListener('scroll', follow, true);
|
|
697
871
|
view.addEventListener('resize', reposition);
|
|
698
872
|
view.addEventListener('resize', refit);
|
|
873
|
+
view.addEventListener('resize', refitInPlace);
|
|
874
|
+
/* Where motion leaves a trigger, read once it has stopped. The style write that
|
|
875
|
+
starts a transition is a mutation measured in the frame it lands, when the
|
|
876
|
+
trigger has not moved yet: one given `transition: transform 200ms` and then
|
|
877
|
+
translated 220px left its panel 240px outside the view for good, not for 200ms.
|
|
878
|
+
The end events answer that, and one timer per document answers the end event
|
|
879
|
+
that never comes — a cancelled transition's, or one on a box this document
|
|
880
|
+
never hears from. Armed when motion starts and cleared before it is armed
|
|
881
|
+
again, so one is pending at a time, and a fired one walks ddLive(), which has
|
|
882
|
+
already dropped whatever was removed meanwhile. The frames in between are what
|
|
883
|
+
this declines: the reader is owed the final position, not a measurement per
|
|
884
|
+
frame per panel. why: docs/components.md#the-dropdown-panel */
|
|
885
|
+
let settling = null;
|
|
886
|
+
const settle = () => {
|
|
887
|
+
clearTimeout(settling);
|
|
888
|
+
settling = setTimeout(() => { settling = null; follow(); }, DD_SETTLE_MS);
|
|
889
|
+
};
|
|
890
|
+
view.addEventListener('transitionend', follow, true);
|
|
891
|
+
view.addEventListener('animationend', follow, true);
|
|
892
|
+
view.addEventListener('transitionstart', settle, true);
|
|
893
|
+
view.addEventListener('animationstart', settle, true);
|
|
894
|
+
if (typeof view.MutationObserver === 'function' && doc.documentElement) {
|
|
895
|
+
const mo = new view.MutationObserver((records) => {
|
|
896
|
+
if (records.every((m) => ddOurs(m.target))) return;
|
|
897
|
+
follow();
|
|
898
|
+
});
|
|
899
|
+
/* characterData as well as the two structural kinds: growing a sibling TEXT
|
|
900
|
+
NODE moves a trigger along a flex row without changing one observed box, and
|
|
901
|
+
neither a child list nor an attribute changes with it. 39 characters of 10px
|
|
902
|
+
monospace in front of a menu carried both layers' panels from 8…248 to
|
|
903
|
+
236.8…476.8 and left the page 477px wide. A change inside any panel is still
|
|
904
|
+
skipped, text included: ddOurs() reads a text node's parent.
|
|
905
|
+
why: docs/components.md#the-dropdown-panel */
|
|
906
|
+
mo.observe(doc.documentElement, {
|
|
907
|
+
subtree: true, childList: true, attributes: true, characterData: true,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
699
910
|
}
|
|
700
911
|
|
|
701
912
|
export function wireDropdown(root = document) {
|
|
@@ -828,6 +1039,25 @@ export function wireDropdown(root = document) {
|
|
|
828
1039
|
if (dd.classList.contains('open')) positionPortalPanel(dd, dd.__ddPanel);
|
|
829
1040
|
}).observe(trigger);
|
|
830
1041
|
}
|
|
1042
|
+
} else if (panel && !ddInFilterRow(panel)) {
|
|
1043
|
+
/* And an in-place panel is fitted here, before anything is clicked, because
|
|
1044
|
+
a shut panel is laid out and a laid-out box counts towards the page's
|
|
1045
|
+
scrollable width. The two boxes the fit is measured from are both watched:
|
|
1046
|
+
the trigger, because the edge an end-aligned panel hangs from is the
|
|
1047
|
+
trigger's, and the panel, because its own width is the other half of the
|
|
1048
|
+
sum. The view's observer and not the module's: a panel in a frame is laid
|
|
1049
|
+
out in that frame. A filter row's panels are skipped outright rather than
|
|
1050
|
+
watched for a fit that declines them: that row is their bound, and
|
|
1051
|
+
ddWatchRow() is the observer they get instead.
|
|
1052
|
+
why: docs/components.md#the-dropdown-panel */
|
|
1053
|
+
ddFitViewport(dd, panel);
|
|
1054
|
+
const view = dd.ownerDocument?.defaultView;
|
|
1055
|
+
if (typeof view?.ResizeObserver === 'function') {
|
|
1056
|
+
// Writing a translate moves no layout, so a fit cannot report itself back.
|
|
1057
|
+
const ro = new view.ResizeObserver(() => ddFitViewport(dd, panel));
|
|
1058
|
+
ro.observe(trigger);
|
|
1059
|
+
ro.observe(panel);
|
|
1060
|
+
}
|
|
831
1061
|
}
|
|
832
1062
|
});
|
|
833
1063
|
|
package/src/index.d.ts
CHANGED
|
@@ -46,6 +46,18 @@ export declare function filterPanelFit(
|
|
|
46
46
|
dd: Element | null | undefined,
|
|
47
47
|
floor?: number,
|
|
48
48
|
): { room: number; shift: number; floor: number; end: boolean } | null;
|
|
49
|
+
/** How wide a panel may be and how far along the inline axis it has to move to
|
|
50
|
+
* stay inside the view it is laid out in, measured from where the sheet has put
|
|
51
|
+
* it: `shift` is 0 when the edge `align` asked for already fits, and `max` is the
|
|
52
|
+
* view less `edge` at each side — write it before measuring again, because capping
|
|
53
|
+
* a panel changes the width a shift is calculated from. `at` gives the box a
|
|
54
|
+
* caller is about to write instead, which is how the portal's placement asks.
|
|
55
|
+
* `null` when there is nothing to measure, or inside a `.ui-filter-bar`, which
|
|
56
|
+
* bounds its own panels. why: docs/components.md#the-dropdown-panel */
|
|
57
|
+
export declare function dropdownViewportFit(
|
|
58
|
+
panel: Element | null | undefined,
|
|
59
|
+
at?: { left: number; width: number },
|
|
60
|
+
): { shift: number; left: number; width: number; view: number; edge: number; max: number } | null;
|
|
49
61
|
export declare function backLink(opts?: Record<string, unknown>): string;
|
|
50
62
|
export declare function statBand(opts?: Record<string, unknown>): string;
|
|
51
63
|
export declare function commandPalette(opts?: Record<string, unknown>): string;
|
package/src/styles/dropdown.css
CHANGED
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
the portal's JS, so an upward panel cannot drift from a downward one.
|
|
57
57
|
why: docs/components.md#the-dropdown-panel */
|
|
58
58
|
--ui-dropdown-gap: 9px;
|
|
59
|
+
/* The least a panel keeps between itself and the edge of the view it is laid out
|
|
60
|
+
in. The wiring reads it off the panel and moves the panel along the inline axis
|
|
61
|
+
when the edge `align` asked for does not fit; a page wanting a panel flush with
|
|
62
|
+
the screen's edge sets it to 0.
|
|
63
|
+
why: docs/components.md#the-dropdown-panel */
|
|
64
|
+
--ui-dropdown-edge: 8px;
|
|
59
65
|
/* Named for the same reason the offset is: a block pinned to an edge has to
|
|
60
66
|
bleed back through it, and #306 is a page copying the number by hand.
|
|
61
67
|
why: docs/components.md#the-dropdown-panel */
|
|
@@ -72,6 +78,24 @@
|
|
|
72
78
|
top: calc(100% + var(--ui-dropdown-gap));
|
|
73
79
|
left: 0;
|
|
74
80
|
min-width: 240px;
|
|
81
|
+
/* …and no wider than the view it is laid out in, less the keep-out it owes each
|
|
82
|
+
edge. This bound is what holds a PORTALLED panel: that one is `position: fixed`,
|
|
83
|
+
so the room beside its own offsets is the whole view and a shrink-to-fit box
|
|
84
|
+
takes all of it — measured at 390px on a 390px screen with this line gone.
|
|
85
|
+
The floor above wins where the two disagree, which is a view under 2 × edge +
|
|
86
|
+
240. The wiring writes the property off the LAYOUT viewport; the fallback is
|
|
87
|
+
for a page whose JS has not run, and is a classic scrollbar's width too
|
|
88
|
+
generous, because `100vw` counts a scrollbar the panel's own box does not.
|
|
89
|
+
why: docs/components.md#the-dropdown-panel */
|
|
90
|
+
max-width: var(--ui-dropdown-ceiling, calc(100vw - 2 * var(--ui-dropdown-edge)));
|
|
91
|
+
/* And this is what holds an IN-PLACE one, whose room is its trigger's block: a
|
|
92
|
+
shrink-to-fit box is its min-content wide at the least, and one 70-character
|
|
93
|
+
token's min-content was 508px on a 390px screen. Breaking anywhere takes that
|
|
94
|
+
to one character, so the panel settles on the floor above — and keeps the token
|
|
95
|
+
inside its own box, since overflowing inline text widens the page whatever the
|
|
96
|
+
box says. The treatment a filter row's panels and the rail's account block
|
|
97
|
+
already take. why: src/styles/filter-bar.css */
|
|
98
|
+
overflow-wrap: anywhere;
|
|
75
99
|
/* The floating step, not the sunken one — a panel held open over a card cannot be
|
|
76
100
|
painted the colour of an inset. why: docs/foundations.md#elevation */
|
|
77
101
|
background: var(--bg-elevated);
|
package/src/styles/topbar.css
CHANGED
|
@@ -192,10 +192,10 @@
|
|
|
192
192
|
transition: background var(--dur-fast) var(--ease);
|
|
193
193
|
}
|
|
194
194
|
/* Menu rows use the reading surface and an edge on hover — see
|
|
195
|
-
src/styles/dropdown.css:
|
|
195
|
+
src/styles/dropdown.css:207 `.ui-dropdown__item:hover`.
|
|
196
196
|
why: docs/foundations.md#elevation */
|
|
197
197
|
.vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* code-bg: inherit — control paint; the containing surface is the ground, for a chip and for a band alike. */ }
|
|
198
|
-
/* The row is a keyboard stop: src/components/dropdown.js:
|
|
198
|
+
/* The row is a keyboard stop: src/components/dropdown.js:1015
|
|
199
199
|
`items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
|
|
200
200
|
the arrow keys, and this is one. It takes the ring on its own 10px radius, the
|
|
201
201
|
same way `.amenu a` above does. #482 */
|