@apliteni/apliteni-ui 0.96.1 → 0.98.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/README.md +4 -3
- package/react/dist/index.js +24 -2
- package/src/components/dropdown.js +77 -4
- package/src/index.d.ts +15 -0
- package/src/index.js +1 -1
- package/src/logic/dropdown.js +23 -0
- package/src/styles/base.css +7 -0
- package/src/styles/dropdown.css +49 -3
- package/src/styles/table.css +6 -8
- package/src/styles/topbar.css +56 -8
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -658,9 +658,10 @@ size. Neither has a filled disc behind it.
|
|
|
658
658
|
there is no backdrop layer and no `backdrop` prop. Pass `title`, one short `body`
|
|
659
659
|
line, and React `actions`. There is no `eyebrow` prop: a confirmation carries one
|
|
660
660
|
title and at most one line under it, so put the outcome in the title rather than in a
|
|
661
|
-
label above it. A kit `Button` carries the focus ring already
|
|
662
|
-
a plain `<a
|
|
663
|
-
|
|
661
|
+
label above it. A kit `Button` carries the focus ring already, and so does
|
|
662
|
+
a router link or a plain `<a>`: the kit rings every anchor on `:focus-visible`. A
|
|
663
|
+
focusable that is neither — a `div` or `span` given `tabIndex` — still needs
|
|
664
|
+
`className="ui-focusable"`, or it falls back to the browser's own focus outline. An omitted or empty `actions`
|
|
664
665
|
leaves out the actions row. Hero and split default to h1; compact defaults to h2.
|
|
665
666
|
`level` overrides the heading rank, and a value outside 1–6 falls back to that layout
|
|
666
667
|
default. `confetti` enables the existing decorative animation. All three components
|
package/react/dist/index.js
CHANGED
|
@@ -1892,7 +1892,8 @@ import {
|
|
|
1892
1892
|
dropdownFiltering,
|
|
1893
1893
|
filterPanelFit,
|
|
1894
1894
|
transitionMs as transitionMs2,
|
|
1895
|
-
dropdownViewportFit
|
|
1895
|
+
dropdownViewportFit,
|
|
1896
|
+
dropdownHeightFit
|
|
1896
1897
|
} from "@apliteni/apliteni-ui";
|
|
1897
1898
|
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1898
1899
|
var EXIT_SLACK_MS2 = 50;
|
|
@@ -2102,6 +2103,27 @@ function Dropdown({
|
|
|
2102
2103
|
doc.removeEventListener("keydown", onKey2);
|
|
2103
2104
|
};
|
|
2104
2105
|
}, [open]);
|
|
2106
|
+
useIsoLayoutEffect(() => {
|
|
2107
|
+
if (!open) return;
|
|
2108
|
+
const el = panel.current;
|
|
2109
|
+
if (!el) return;
|
|
2110
|
+
const size = () => {
|
|
2111
|
+
const avail = dropdownHeightFit(root.current, el);
|
|
2112
|
+
if (avail == null) return;
|
|
2113
|
+
if (Number.isFinite(avail)) el.style.setProperty("--ui-dropdown-avail", `${avail}px`);
|
|
2114
|
+
else el.style.removeProperty("--ui-dropdown-avail");
|
|
2115
|
+
const cap2 = el.ownerDocument.defaultView?.getComputedStyle(el).getPropertyValue("--ui-dropdown-cap").trim();
|
|
2116
|
+
el.classList.toggle("is-unbounded", !Number.isFinite(avail) && !cap2);
|
|
2117
|
+
};
|
|
2118
|
+
size();
|
|
2119
|
+
const view = el.ownerDocument?.defaultView ?? window;
|
|
2120
|
+
view.addEventListener("scroll", size, true);
|
|
2121
|
+
view.addEventListener("resize", size);
|
|
2122
|
+
return () => {
|
|
2123
|
+
view.removeEventListener("scroll", size, true);
|
|
2124
|
+
view.removeEventListener("resize", size);
|
|
2125
|
+
};
|
|
2126
|
+
}, [open, direction]);
|
|
2105
2127
|
useIsoLayoutEffect(() => {
|
|
2106
2128
|
const want = landOn.current;
|
|
2107
2129
|
landOn.current = null;
|
|
@@ -2328,7 +2350,7 @@ function Dropdown({
|
|
|
2328
2350
|
section.label ?? `s${si}`
|
|
2329
2351
|
);
|
|
2330
2352
|
}) : (items || []).map((entry, i) => renderRow(entry, `i${i}`));
|
|
2331
|
-
const cap = scroll && scroll !== true ? {
|
|
2353
|
+
const cap = scroll && scroll !== true ? { "--ui-dropdown-cap": typeof scroll === "number" ? `${scroll}px` : scroll } : void 0;
|
|
2332
2354
|
const none = sx && filtering && !visible.length ? /* @__PURE__ */ jsxs16(Fragment5, { children: [
|
|
2333
2355
|
/* @__PURE__ */ jsx20("span", { className: "ui-dropdown__none-title", children: sx.empty.replace("{q}", () => query.trim()) }),
|
|
2334
2356
|
/* @__PURE__ */ jsx20("span", { className: "ui-dropdown__none-hint", children: sx.hint })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
|
|
2
|
-
export { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
|
|
1
|
+
import { dropdownMatch, dropdownFiltering, dropdownAvail } from '../logic/dropdown.js';
|
|
2
|
+
export { dropdownMatch, dropdownFiltering, dropdownAvail } from '../logic/dropdown.js';
|
|
3
3
|
// Dropdown — the kit's one popover-list primitive. A trigger opens a panel of
|
|
4
4
|
// item rows; two flavours share the same panel and the same open/close JS:
|
|
5
5
|
//
|
|
@@ -120,7 +120,10 @@ function ddSearchBody({ items, sections }, sx, name, scroll) {
|
|
|
120
120
|
const rows = ddBody({ items, sections }, true, sx);
|
|
121
121
|
const flat = (sections ? sections.flatMap((s) => s.items || []) : (items || [])).filter(ddIsRow);
|
|
122
122
|
const shown = flat.some((it) => dropdownMatch(it.label, sx.q));
|
|
123
|
-
|
|
123
|
+
// The cap property and not `max-height`: an inline height outranks the sheet's
|
|
124
|
+
// min() and would put the panel back past the viewport edge. #489
|
|
125
|
+
const cap = scroll && scroll !== true
|
|
126
|
+
? ` style="--ui-dropdown-cap:${typeof scroll === 'number' ? scroll + 'px' : esc(scroll)}"` : '';
|
|
124
127
|
const input = [
|
|
125
128
|
'class="ui-dropdown__search-input"', 'type="text"', 'role="combobox"',
|
|
126
129
|
'aria-autocomplete="list"', 'aria-expanded="true"', `aria-controls="${esc(listId)}"`,
|
|
@@ -202,7 +205,10 @@ export function dropdown({
|
|
|
202
205
|
// With search the panel holds a field and a list, which a listbox may not.
|
|
203
206
|
`role="${sx ? 'dialog' : listRole}"`,
|
|
204
207
|
sx ? `aria-label="${esc(name)}"` : (ariaLabel ? `aria-label="${esc(ariaLabel)}"` : ''),
|
|
205
|
-
|
|
208
|
+
// The cap property and not `max-height`: an inline height outranks the
|
|
209
|
+
// sheet's min() and would put the panel back past the viewport edge. #489
|
|
210
|
+
scroll && scroll !== true && !sx
|
|
211
|
+
? `style="--ui-dropdown-cap:${typeof scroll === 'number' ? scroll + 'px' : esc(scroll)}"` : '',
|
|
206
212
|
].filter(Boolean).join(' ');
|
|
207
213
|
|
|
208
214
|
// The block the sheet bleeds to the panel's bottom edge. It sits OUTSIDE the
|
|
@@ -278,6 +284,16 @@ function ddEdge(panel) {
|
|
|
278
284
|
return Number.isFinite(declared) ? declared : DD_EDGE;
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
// The least height a capped panel keeps, below which it would rather spend the
|
|
288
|
+
// edge inset than shrink further. The number is --ui-dropdown-min in
|
|
289
|
+
// src/styles/dropdown.css. #489
|
|
290
|
+
const DD_MIN = 120;
|
|
291
|
+
|
|
292
|
+
function ddMin(panel) {
|
|
293
|
+
const declared = parseFloat(ddViewOf(panel).getComputedStyle(panel).getPropertyValue('--ui-dropdown-min'));
|
|
294
|
+
return Number.isFinite(declared) ? declared : DD_MIN;
|
|
295
|
+
}
|
|
296
|
+
|
|
281
297
|
/** The width a panel has to stay inside: the LAYOUT viewport, which is what both
|
|
282
298
|
* `position: fixed` and a page's scrollable width are measured against. A classic
|
|
283
299
|
* scrollbar is in `innerWidth` and not in the box either of those means, so an
|
|
@@ -515,10 +531,65 @@ function ddUnwatchRow(dd) {
|
|
|
515
531
|
* taken for the middle of an animation, is stated with the rule.
|
|
516
532
|
* why: docs/components.md#the-dropdown-panel */
|
|
517
533
|
|
|
534
|
+
/**
|
|
535
|
+
* How tall a panel may be: the room between its trigger and the viewport edge,
|
|
536
|
+
* from dropdownAvail() in src/logic/dropdown.js — one calculation, so the
|
|
537
|
+
* vanilla wiring and the React `<Dropdown>` cannot disagree about where a panel
|
|
538
|
+
* ends. Measured from the trigger, not the panel, the way dropdownViewportFit()
|
|
539
|
+
* is measured from the panel and not the trigger: each is the box the number it
|
|
540
|
+
* answers actually depends on. `null` when there is nothing to measure.
|
|
541
|
+
* why: docs/components.md#the-dropdown-panel
|
|
542
|
+
*
|
|
543
|
+
* @param {Element} dd the `[data-dropdown]` container the trigger lives in
|
|
544
|
+
* @param {Element} panel a `[data-dropdown-panel]`, for its gap/edge/min and its
|
|
545
|
+
* `is-up` direction
|
|
546
|
+
*/
|
|
547
|
+
export function dropdownHeightFit(dd, panel) {
|
|
548
|
+
const trigger = dd?.querySelector?.('[data-dropdown-trigger]');
|
|
549
|
+
if (!trigger || typeof trigger.getBoundingClientRect !== 'function' || !panel) return null;
|
|
550
|
+
const t = trigger.getBoundingClientRect();
|
|
551
|
+
const view = ddViewOf(panel);
|
|
552
|
+
return dropdownAvail({
|
|
553
|
+
anchorTop: t.top,
|
|
554
|
+
anchorBottom: t.bottom,
|
|
555
|
+
viewport: view.innerHeight,
|
|
556
|
+
gap: ddGap(panel),
|
|
557
|
+
inset: ddEdge(panel),
|
|
558
|
+
min: ddMin(panel),
|
|
559
|
+
up: panel.classList.contains('is-up'),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function ddSizeHeight(dd, panel) {
|
|
564
|
+
if (!panel?.style) return;
|
|
565
|
+
const avail = dropdownHeightFit(dd, panel);
|
|
566
|
+
if (avail == null) return;
|
|
567
|
+
// Infinity means the floor could not be met even by spending the whole inset:
|
|
568
|
+
// the property is removed rather than written, so the sheet's own fallback — the
|
|
569
|
+
// viewport less the edge inset, not a trigger-sized sliver — takes over. why:
|
|
570
|
+
// dropdownAvail(), src/logic/dropdown.js
|
|
571
|
+
//
|
|
572
|
+
// `.is-unbounded` is only added where that leaves NO cap at all: a consumer's
|
|
573
|
+
// own `--ui-dropdown-cap` (`.is-scroll` or a numeric `scroll`) still clamps the
|
|
574
|
+
// panel, and that clamp is still real room to scroll. why: src/styles/dropdown.css
|
|
575
|
+
const cap = ddViewOf(panel).getComputedStyle(panel).getPropertyValue('--ui-dropdown-cap').trim();
|
|
576
|
+
panel.classList.toggle('is-unbounded', !Number.isFinite(avail) && !cap);
|
|
577
|
+
// A number that has not changed is not written again, the way the width fit's
|
|
578
|
+
// own write is guarded: a mutation inside the row that moved nothing vertically
|
|
579
|
+
// would otherwise answer its own write, over and over, through the document
|
|
580
|
+
// observer below. why: dropdownViewportFit()'s own "moved" guard, same file
|
|
581
|
+
const next = Number.isFinite(avail) ? `${avail}px` : '';
|
|
582
|
+
if (panel.style.getPropertyValue('--ui-dropdown-avail') === next) return;
|
|
583
|
+
if (next) panel.style.setProperty('--ui-dropdown-avail', next);
|
|
584
|
+
else panel.style.removeProperty('--ui-dropdown-avail');
|
|
585
|
+
}
|
|
586
|
+
|
|
518
587
|
/** Fit a panel again where it now is, whichever placement it uses. */
|
|
519
588
|
function ddPlace(dd) {
|
|
589
|
+
const panel = dd.__ddPanel || ddPanelOf(dd);
|
|
520
590
|
if (dd.__ddPanel) positionPortalPanel(dd, dd.__ddPanel);
|
|
521
591
|
else ddFitViewport(dd, ddPanelOf(dd));
|
|
592
|
+
ddSizeHeight(dd, panel);
|
|
522
593
|
}
|
|
523
594
|
|
|
524
595
|
/* A few frames past the sheet's own end, so the timer never cuts the fade's last
|
|
@@ -778,6 +849,7 @@ function openDropdown(dd, focusIdx) {
|
|
|
778
849
|
is null inside a filter row, whose own row already bounds its panels.
|
|
779
850
|
why: docs/components.md#a-filter-row-holds-its-panels */
|
|
780
851
|
else { ddFitFilterPanel(dd, panel); ddWatchRow(dd, panel); ddFitViewport(dd, panel); }
|
|
852
|
+
ddSizeHeight(dd, panel);
|
|
781
853
|
}
|
|
782
854
|
dd.classList.add('open');
|
|
783
855
|
/* After `open`, and after the fit: ddResetSearch() pins the width it reads off
|
|
@@ -949,6 +1021,7 @@ export function wireDropdown(root = document) {
|
|
|
949
1021
|
ddFitFilterPanel(dd, panel);
|
|
950
1022
|
ddWatchRow(dd, panel);
|
|
951
1023
|
}
|
|
1024
|
+
if (panel && dd.classList.contains('open')) ddSizeHeight(dd, panel);
|
|
952
1025
|
|
|
953
1026
|
trigger.addEventListener('click', (e) => {
|
|
954
1027
|
e.stopPropagation();
|
package/src/index.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export declare function wireDropdown(root?: Document | Element): void;
|
|
|
34
34
|
/** The kit's own match, asked rather than re-implemented by <Dropdown>. */
|
|
35
35
|
export declare function dropdownMatch(label: unknown, query: unknown): boolean;
|
|
36
36
|
export declare function dropdownFiltering(query: unknown): boolean;
|
|
37
|
+
/** How tall a floating panel may be: the room between its anchor and the
|
|
38
|
+
* viewport edge, less `gap` and `inset`. `min` is a floor that spends `inset`,
|
|
39
|
+
* down to nothing, rather than room past the edge, which does not exist. */
|
|
40
|
+
export declare function dropdownAvail(opts: {
|
|
41
|
+
anchorTop: number; anchorBottom: number; viewport: number;
|
|
42
|
+
gap: number; inset: number; min: number; up?: boolean;
|
|
43
|
+
}): number;
|
|
37
44
|
/** The kit's menu floor, the width `.ui-dropdown__panel` writes. */
|
|
38
45
|
export declare const DD_MENU_FLOOR: number;
|
|
39
46
|
/** Where a filter chip's open menu can sit: the room from the edge it is
|
|
@@ -58,6 +65,14 @@ export declare function dropdownViewportFit(
|
|
|
58
65
|
panel: Element | null | undefined,
|
|
59
66
|
at?: { left: number; width: number },
|
|
60
67
|
): { shift: number; left: number; width: number; view: number; edge: number; max: number } | null;
|
|
68
|
+
/** How tall a panel may be: the room between its trigger (inside `dd`) and the
|
|
69
|
+
* viewport edge, from the same dropdownAvail() dropdownViewportFit's `max`
|
|
70
|
+
* shares its calculation with. `null` when there is nothing to measure.
|
|
71
|
+
* why: docs/components.md#the-dropdown-panel */
|
|
72
|
+
export declare function dropdownHeightFit(
|
|
73
|
+
dd: Element | null | undefined,
|
|
74
|
+
panel: Element | null | undefined,
|
|
75
|
+
): number | null;
|
|
61
76
|
export declare function backLink(opts?: Record<string, unknown>): string;
|
|
62
77
|
export declare function statBand(opts?: Record<string, unknown>): string;
|
|
63
78
|
export declare function commandPalette(opts?: Record<string, unknown>): string;
|
package/src/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export * from './components/table-values.js';
|
|
|
27
27
|
export * from './components/segmented.js';
|
|
28
28
|
export * from './components/filter-bar.js';
|
|
29
29
|
|
|
30
|
-
export { dropdownMatch, dropdownFiltering } from './logic/dropdown.js';
|
|
30
|
+
export { dropdownMatch, dropdownFiltering, dropdownAvail } from './logic/dropdown.js';
|
|
31
31
|
export { SCORE, rankGroups, rankCommands, scoreCommand, paletteHotkey } from './logic/command-palette.js';
|
|
32
32
|
export { segmentedNextIndex } from './logic/segmented.js';
|
|
33
33
|
export { filterChipText, filterChipName, filterChipUnset, filterChipItems, nextFocusStop, focusNextStop } from './logic/filter-bar.js';
|
package/src/logic/dropdown.js
CHANGED
|
@@ -14,3 +14,26 @@ export const dropdownFiltering = (query) => fold(query).trim() !== '';
|
|
|
14
14
|
|
|
15
15
|
/** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
|
|
16
16
|
export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How tall a floating panel may be: the room between its anchor and the viewport
|
|
20
|
+
* edge, less the gap it keeps from the anchor and the edge inset it keeps from
|
|
21
|
+
* that edge. `up` measures the room above the anchor, otherwise below it.
|
|
22
|
+
*
|
|
23
|
+
* Public for the same reason the two above are: the vanilla wiring and the React
|
|
24
|
+
* <Dropdown> both have to arrive at the same number, and a panel that ends inside
|
|
25
|
+
* the viewport in one and past it in the other is the bug this answers (#489).
|
|
26
|
+
*
|
|
27
|
+
* `min` is a floor: a trigger near the edge has little room, and a panel reduced
|
|
28
|
+
* to a sliver is worse than one left to its content height. The floor never
|
|
29
|
+
* spends room PAST the edge to reach itself, because that room does not exist.
|
|
30
|
+
* Once even the whole inset cannot reach it, there is no cap a reader could
|
|
31
|
+
* use, so this returns `Infinity`: "nothing measured," the word a caller
|
|
32
|
+
* already reads from an unmeasured panel. The panel then keeps its content
|
|
33
|
+
* height, the document grows, and a reader reaches every row by scrolling the
|
|
34
|
+
* page — a 27px sliver hiding 446px of rows was danger 1 in #641's review.
|
|
35
|
+
*/
|
|
36
|
+
export const dropdownAvail = ({ anchorTop, anchorBottom, viewport, gap, inset, min, up = false }) => {
|
|
37
|
+
const room = Math.max(up ? anchorTop - gap : viewport - anchorBottom - gap, 0);
|
|
38
|
+
return room < min ? Infinity : Math.max(room - inset, min);
|
|
39
|
+
};
|
package/src/styles/base.css
CHANGED
|
@@ -141,6 +141,13 @@ svg:where(:not([width]):not([height])) {
|
|
|
141
141
|
.ui-focusable, in the vanilla topbar, React ThemeToggle and the React shell's bar
|
|
142
142
|
alike, so keyboard focus on it fell back to the browser's own outline in all three.
|
|
143
143
|
It has no class of its own to opt in with; it joins the list instead. Found on #385. */
|
|
144
|
+
/* `a` is here as an ELEMENT, not a class, because a link in running text is the one
|
|
145
|
+
control a consumer writes with no class on it at all — so it took the browser's
|
|
146
|
+
outline, which #457 refused. The net is over every anchor; a component that rings
|
|
147
|
+
its own anchor outranks this at two classes and keeps the shape it chose. The band
|
|
148
|
+
sits at whatever corner the link carries, which is the rule .ui-table's links
|
|
149
|
+
already take. Found on #586, decided on #587. */
|
|
150
|
+
a:focus-visible,
|
|
144
151
|
.ui-focusable:focus-visible,
|
|
145
152
|
.ui-snippet :focus-visible,
|
|
146
153
|
.ui-btn:focus-visible,
|
package/src/styles/dropdown.css
CHANGED
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
the screen's edge sets it to 0.
|
|
63
63
|
why: docs/components.md#the-dropdown-panel */
|
|
64
64
|
--ui-dropdown-edge: 8px;
|
|
65
|
+
/* The least height a capped panel keeps before it would rather spend the edge
|
|
66
|
+
inset above than shrink further — three rows or so. Below it the panel
|
|
67
|
+
still ends at or before the edge; it never spends room past the edge to
|
|
68
|
+
reach this floor, because that room does not exist. #489
|
|
69
|
+
why: docs/components.md#the-dropdown-panel */
|
|
70
|
+
--ui-dropdown-min: 120px;
|
|
65
71
|
/* Named for the same reason the offset is: a block pinned to an edge has to
|
|
66
72
|
bleed back through it, and #306 is a page copying the number by hand.
|
|
67
73
|
why: docs/components.md#the-dropdown-panel */
|
|
@@ -107,6 +113,24 @@
|
|
|
107
113
|
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
|
|
108
114
|
border-radius: var(--radius-lg);
|
|
109
115
|
padding: var(--ui-dropdown-pad);
|
|
116
|
+
/* The cap, and the whole of #489: a panel took its content's height until that
|
|
117
|
+
height ran off the screen, and then it took what was left. Two numbers decide
|
|
118
|
+
it and the smaller wins — --ui-dropdown-cap is what the consumer asked for
|
|
119
|
+
(the `scroll` option writes it), --ui-dropdown-avail is the room between the
|
|
120
|
+
trigger and the viewport edge, which the wiring measures on every open and
|
|
121
|
+
every scroll. Neither is declared here: an unset cap means the consumer wants
|
|
122
|
+
the whole list, and an unset avail means nothing has measured yet, which is a
|
|
123
|
+
server-rendered page before hydration — so the fallback promises the same
|
|
124
|
+
thing at a coarser grain, the viewport less the edge inset at each end.
|
|
125
|
+
why: docs/components.md#the-dropdown-panel */
|
|
126
|
+
max-height: min(
|
|
127
|
+
var(--ui-dropdown-cap, 100dvh),
|
|
128
|
+
var(--ui-dropdown-avail, calc(100dvh - 2 * var(--ui-dropdown-edge)))
|
|
129
|
+
);
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
/* The wheel stops at the panel's own end instead of carrying on down the page
|
|
132
|
+
underneath it — the second half of #489. */
|
|
133
|
+
overscroll-behavior: contain;
|
|
110
134
|
opacity: 0;
|
|
111
135
|
visibility: hidden;
|
|
112
136
|
/* Drawn for the whole of the fade out, so the rows do not vanish mid-fade — and a
|
|
@@ -123,7 +147,21 @@
|
|
|
123
147
|
z-index: var(--z-dropdown);
|
|
124
148
|
}
|
|
125
149
|
.ui-dropdown__panel.is-end { left: auto; right: 0; }
|
|
126
|
-
|
|
150
|
+
/* `.is-scroll` asks for 300px specifically; the panel above caps every panel at
|
|
151
|
+
the room the viewport left either way. #489 */
|
|
152
|
+
.ui-dropdown__panel.is-scroll { --ui-dropdown-cap: 300px; }
|
|
153
|
+
|
|
154
|
+
/* The wiring adds this where the floor (above) left the panel uncapped: below
|
|
155
|
+
120px even spending the whole edge inset, there is no usable cap, so the
|
|
156
|
+
panel keeps its content height and the page grows to hold it instead —
|
|
157
|
+
#641's danger 1. That panel has nothing of its own to scroll, so the
|
|
158
|
+
`overflow-y: auto` above still claims a wheel over it for nothing, and
|
|
159
|
+
`overscroll-behavior: contain` then stops the gesture from reaching the
|
|
160
|
+
page that is now the only way to the rows below the fold. `visible` lets it
|
|
161
|
+
through. A panel that also carries `.is-scroll` or a numeric cap keeps
|
|
162
|
+
`auto`: that cap is still real room to scroll inside. why: dropdownAvail(),
|
|
163
|
+
src/logic/dropdown.js */
|
|
164
|
+
.ui-dropdown__panel.is-unbounded { overflow-y: visible; }
|
|
127
165
|
|
|
128
166
|
/* Chrome makes a scroll container a keyboard stop on its own — no tabindex, no
|
|
129
167
|
author rule — so `.is-scroll` took the browser's outline, black in light mode.
|
|
@@ -286,7 +324,13 @@
|
|
|
286
324
|
while the list scrolls. Every rule below keys on a class only the search
|
|
287
325
|
variant emits, so the plain dropdown renders as it did.
|
|
288
326
|
why: docs/components.md#a-dropdown-with-a-search-field */
|
|
289
|
-
|
|
327
|
+
/* A column, so the panel's own avail-based max-height (above) lands on the rows
|
|
328
|
+
and not on the field: the field and the empty-state block keep their content
|
|
329
|
+
height, and the list — the child that can overflow — gives up the room they
|
|
330
|
+
take. #489 */
|
|
331
|
+
.ui-dropdown__panel--search { min-width: 280px; display: flex; flex-direction: column; }
|
|
332
|
+
.ui-dropdown__panel--search > * { flex: none; }
|
|
333
|
+
.ui-dropdown__panel--search > .ui-dropdown__list { flex: 1 1 auto; min-height: 0; }
|
|
290
334
|
|
|
291
335
|
.ui-dropdown__search { position: relative; display: flex; align-items: center; margin-bottom: 6px; }
|
|
292
336
|
.ui-dropdown__search-ic {
|
|
@@ -324,7 +368,9 @@
|
|
|
324
368
|
.ui-dropdown__search-input:hover { border-color: var(--field-edge-hover); }
|
|
325
369
|
.ui-dropdown__search-input:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
326
370
|
|
|
327
|
-
|
|
371
|
+
/* The rows' own cap, read from the same property the panel reads: the `scroll`
|
|
372
|
+
option lands on the list when there is a field above it. #489 */
|
|
373
|
+
.ui-dropdown__list { max-height: var(--ui-dropdown-cap, 300px); overflow-y: auto; overscroll-behavior: contain; }
|
|
328
374
|
/* Rows are `role="option" tabindex="-1"`, so the LIST takes the keyboard stop and
|
|
329
375
|
answered with the native outline. The band is drawn INWARD, on the list: an outset
|
|
330
376
|
one 6px inside a --radius-lg corner leaves the panel, and the panel is not the box
|
package/src/styles/table.css
CHANGED
|
@@ -145,14 +145,12 @@
|
|
|
145
145
|
top: 50%;
|
|
146
146
|
translate: 0 -50%;
|
|
147
147
|
}
|
|
148
|
-
/*
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
held by
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
outline-offset: var(--ring-offset);
|
|
155
|
-
}
|
|
148
|
+
/* No focus rule here any more. #510 gave a cell link the ring with a rule of this file's
|
|
149
|
+
own, because `base.css` reached no bare `a`; #587 put `a` on that shared list, so this
|
|
150
|
+
one painted the same two declarations a second time. The guarantee is unchanged and
|
|
151
|
+
still held by src/styles/table-focus.test.js — the corner the band follows is the
|
|
152
|
+
`border-radius` above, which is this file's and stays.
|
|
153
|
+
why: guidelines/state-set.md#focus-rings */
|
|
156
154
|
|
|
157
155
|
.ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
158
156
|
/* A numeric column is sized to its content while .ui-table__title claims the rest,
|
package/src/styles/topbar.css
CHANGED
|
@@ -141,6 +141,13 @@
|
|
|
141
141
|
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
|
|
142
142
|
border-radius: 16px;
|
|
143
143
|
padding: 6px;
|
|
144
|
+
/* The same cap `.ui-dropdown__panel` writes itself, in bespoke clothes: this
|
|
145
|
+
menu is `wireDropdown()` too, so wireDropdown() measures it the same way and
|
|
146
|
+
writes the same two properties. #489
|
|
147
|
+
why: docs/components.md#the-dropdown-panel */
|
|
148
|
+
max-height: min(var(--ui-dropdown-cap, 100dvh), var(--ui-dropdown-avail, calc(100dvh - 16px)));
|
|
149
|
+
overflow-y: auto;
|
|
150
|
+
overscroll-behavior: contain;
|
|
144
151
|
opacity: 0;
|
|
145
152
|
visibility: hidden;
|
|
146
153
|
/* Drawn for the whole of the fade out, so the rows do not vanish mid-fade — and a box
|
|
@@ -157,6 +164,13 @@
|
|
|
157
164
|
visibility var(--dur-med) linear;
|
|
158
165
|
z-index: var(--z-dropdown);
|
|
159
166
|
}
|
|
167
|
+
/* Chrome makes a scroll container a keyboard stop on its own — no tabindex, no
|
|
168
|
+
author rule — and #489's cap is what gave this menu the `overflow-y` that
|
|
169
|
+
makes it one, the same way `.ui-dropdown__panel` got it. `.vopt` is
|
|
170
|
+
`tabindex="-1"` and reached only by the arrow keys, so Tab lands on the menu
|
|
171
|
+
itself rather than a row inside it, and the ring belongs here for the same
|
|
172
|
+
reason `.ui-dropdown__panel` keeps it. why: docs/foundations.md#elevation */
|
|
173
|
+
.vsw__menu:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
160
174
|
/* Open, the menu is clickable and visible AT ONCE. Both menus in this file are
|
|
161
175
|
`wireDropdown()` in bespoke clothes — same hooks, same keyboard, same fade — so they
|
|
162
176
|
owe a panel's two rules as much as `.ui-dropdown__panel` does: `visibility` off the
|
|
@@ -192,10 +206,10 @@
|
|
|
192
206
|
transition: background var(--dur-fast) var(--ease);
|
|
193
207
|
}
|
|
194
208
|
/* Menu rows use the reading surface and an edge on hover — see
|
|
195
|
-
src/styles/dropdown.css:
|
|
209
|
+
src/styles/dropdown.css:245 `.ui-dropdown__item:hover`.
|
|
196
210
|
why: docs/foundations.md#elevation */
|
|
197
211
|
.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:
|
|
212
|
+
/* The row is a keyboard stop: src/components/dropdown.js:1088
|
|
199
213
|
`items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
|
|
200
214
|
the arrow keys, and this is one. It takes the ring on its own 10px radius, the
|
|
201
215
|
same way `.amenu a` above does. #482 */
|
|
@@ -277,10 +291,24 @@
|
|
|
277
291
|
border: 1px solid var(--float-edge);
|
|
278
292
|
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop);
|
|
279
293
|
border-radius: 14px;
|
|
280
|
-
/*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
/* #519 chose no clip over padding: `overflow: hidden` with no room cut the ring down to
|
|
295
|
+
the one pixel of border this panel had, so the rule took the clip away instead and let
|
|
296
|
+
the ring cross the edge. #489's cap takes that choice away — `overflow-y: auto` clips
|
|
297
|
+
again, because a menu that scrolls has to stop its content at its own edge — so this
|
|
298
|
+
reserves the room instead, the same 6px `.vsw__menu` and `.ui-dropdown__panel` already
|
|
299
|
+
keep rather than a new figure. why: docs/components.md#a-menu-panel-does-not-cut-off-its-rows-ring */
|
|
300
|
+
padding: 6px;
|
|
301
|
+
/* The same cap `.ui-dropdown__panel` writes itself — see .vsw__menu above. #489 */
|
|
302
|
+
max-height: min(var(--ui-dropdown-cap, 100dvh), var(--ui-dropdown-avail, calc(100dvh - 16px)));
|
|
303
|
+
overflow-y: auto;
|
|
304
|
+
overscroll-behavior: contain;
|
|
305
|
+
/* Chromium scrolls a focused row flush with the scrollport edge, which is 3px
|
|
306
|
+
short of the row's own ring: 1px of gap plus a 2px band. The padding above
|
|
307
|
+
reserves that room while the menu stands still; this keeps it reserved once
|
|
308
|
+
scrolling the menu — reaching the first row this PR made it do — puts a
|
|
309
|
+
focused row at either end. #641's danger 2.
|
|
310
|
+
why: docs/components.md#a-menu-panel-does-not-cut-off-its-rows-ring */
|
|
311
|
+
scroll-padding-block: 3px;
|
|
284
312
|
opacity: 0;
|
|
285
313
|
visibility: hidden;
|
|
286
314
|
pointer-events: none; /* see .vsw__menu above */
|
|
@@ -292,6 +320,10 @@
|
|
|
292
320
|
visibility var(--dur-med) linear;
|
|
293
321
|
z-index: var(--z-dropdown);
|
|
294
322
|
}
|
|
323
|
+
/* The same gap as `.vsw__menu` above, from the same cause: #489's cap. `.amenu
|
|
324
|
+
a` is `tabindex="-1"` too, so Tab still lands on the menu and not a row, and
|
|
325
|
+
the ring belongs here. why: docs/foundations.md#elevation */
|
|
326
|
+
.amenu:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
295
327
|
/* Clickable and visible at once — see .vsw.open .vsw__menu above. The row this one
|
|
296
328
|
costs is `.aout`: a stray click in the fade out ends the reader's session. */
|
|
297
329
|
.acct.open .amenu {
|
|
@@ -301,12 +333,28 @@
|
|
|
301
333
|
transform: translateY(0);
|
|
302
334
|
transition-property: opacity, transform;
|
|
303
335
|
}
|
|
304
|
-
|
|
336
|
+
/* `margin` bleeds the head back through the 6px panel padding above, the way
|
|
337
|
+
`.ui-dropdown__panel > .ui-dropdown__head` already does for its own panel:
|
|
338
|
+
the border-bottom hairline reaches the panel's edge instead of stopping a
|
|
339
|
+
notch short of it. #641's warning.
|
|
340
|
+
why: docs/components.md#the-dropdown-panel */
|
|
341
|
+
.amenu .ahead {
|
|
342
|
+
padding: 13px 15px;
|
|
343
|
+
margin: -6px -6px 0;
|
|
344
|
+
border-bottom: 1px solid var(--border);
|
|
345
|
+
border-radius: 14px 14px 0 0;
|
|
346
|
+
display: flex; align-items: center; gap: 11px;
|
|
347
|
+
}
|
|
305
348
|
.amenu .ahead .avatar { cursor: default; }
|
|
306
349
|
.amenu .ahead .aw { min-width: 0; display: flex; flex-direction: column; gap: 1px; line-height: 1.35; }
|
|
307
350
|
.amenu .ahead .anm { color: var(--strong); font-size: var(--text-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
308
351
|
.amenu .ahead .aem { color: var(--text); font-size: calc(var(--text-xs) + 0.5px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
309
|
-
|
|
352
|
+
/* `scroll-margin-block` on the row, beside `scroll-padding-block` on the panel
|
|
353
|
+
above: Chromium's own focus-follow scroll honoured the padding scrolling
|
|
354
|
+
toward the end of the list and not the start, so the row carries its own
|
|
355
|
+
reserve too rather than leaving the start of the list short. #641's danger 2.
|
|
356
|
+
why: docs/components.md#a-menu-panel-does-not-cut-off-its-rows-ring */
|
|
357
|
+
.amenu a { display: flex; align-items: center; gap: 11px; padding: 10px 15px; color: var(--text); text-decoration: none; font-family: var(--font-sans); font-size: var(--text-sm); cursor: pointer; scroll-margin-block: 3px; }
|
|
310
358
|
.amenu a: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. */ }
|
|
311
359
|
.amenu a:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); border-radius: var(--radius-xs); }
|
|
312
360
|
.amenu a.cur { color: var(--accent); }
|