@helloimjolopez-pathway/pathway-components 0.12.0 → 0.13.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.
|
@@ -60,6 +60,7 @@ import { Spinner } from "../spinner/spinner.jsx";
|
|
|
60
60
|
import { Badge } from "../badge/badge.jsx";
|
|
61
61
|
import { Avatar, initialsFrom } from "../avatar/avatar.jsx";
|
|
62
62
|
import { Button } from "../button/button.jsx";
|
|
63
|
+
import { Tooltip } from "../tooltip/tooltip.jsx";
|
|
63
64
|
|
|
64
65
|
// ─── TOKEN HELPERS ────────────────────────────────────────────────────────────
|
|
65
66
|
// SC = semantic-color (themes/*.css), SL = semantic-layout-units (layout.css),
|
|
@@ -191,6 +192,12 @@ export const TYPE = {
|
|
|
191
192
|
header: { fontSize: ST("font-size-xs"), lineHeight: ST("line-height-xs-single"), fontWeight: ST("weight-semibold"), letterSpacing: ST("letter-spacing-wide") },
|
|
192
193
|
};
|
|
193
194
|
|
|
195
|
+
/* The BottomSheet's tokens and measurements (bottom-sheet.jsx T and L), named
|
|
196
|
+
here rather than imported because BottomSheet imports this module. */
|
|
197
|
+
const SHEET_T = { surface: SC("fill-surface-elevated"), overlay: SC("scrim-subtle"), handle: SC("fill-static-neutral-strong") };
|
|
198
|
+
const SHEET_L = { radius: SL("cornerradius-xlarge"), pillRadius: SL("cornerradius-full"),
|
|
199
|
+
maxWidth: 640, topClearance: 56, handleZone: 20, pillWidth: 32, pillHeight: 4 };
|
|
200
|
+
|
|
194
201
|
const ITEM = ".pw-menu-item";
|
|
195
202
|
const live = (s) => `${ITEM}:not([data-disabled]):is(${s})`;
|
|
196
203
|
const HOVER = "[data-hovered],[data-focused],[data-open],[data-force=Hover],[data-force=Focus]";
|
|
@@ -206,6 +213,9 @@ const CSS = `
|
|
|
206
213
|
background:${T.panel};border:${L.panelBorder} solid ${T.panelBorder};border-radius:${L.panelRadius};box-shadow:${L.shadow};
|
|
207
214
|
font-family:${ST("family-brand")},sans-serif}
|
|
208
215
|
.pw-menu-panel[data-searchable]{height:${L.searchHeight}px}
|
|
216
|
+
/* React Aria stacks the panel at 100000 inline; a truncated label's Tooltip
|
|
217
|
+
(portalled to the body at 1000) has to sit above it. */
|
|
218
|
+
body:has(.pw-menu-panel) .pw-tooltip{z-index:100001}
|
|
209
219
|
.pw-menu-panel[data-entering]{animation:pw-menu-in ${M("duration-4")} ${M("easing-decelerate")} both}
|
|
210
220
|
.pw-menu-panel[data-placement=top][data-entering]{animation-name:pw-menu-in-up}
|
|
211
221
|
.pw-menu-panel[data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
|
|
@@ -304,7 +314,26 @@ ${ITEM}:is([data-selected],[data-current]) .pw-menu-item__stripe{background:${T.
|
|
|
304
314
|
border:0;border-radius:${L.itemRadius};background:${T.avatarFill};color:${T.avatarText};font-size:${ST("font-size-xxs")};font-weight:${ST("weight-semibold")}}
|
|
305
315
|
.pw-menu-trigger :is(.pw-menu-trigger__icon,.pw-menu-trigger__button,.pw-menu-trigger__avatar):focus-visible{outline:${L.focusWidth} solid ${T.focusRing}}
|
|
306
316
|
|
|
317
|
+
/* smallScreen="sheet" (G4, pending the overlay family decision): below 768 the
|
|
318
|
+
panel opens as a bottom sheet from the BottomSheet tokens, the way the
|
|
319
|
+
approved demo draws it. The scrim is a spread shadow, so a press outside
|
|
320
|
+
still reaches the page and closes the menu. */
|
|
321
|
+
@media (max-width: 767px){
|
|
322
|
+
.pw-menu-panel[data-small-screen=sheet]{position:fixed !important;top:auto !important;bottom:0 !important;left:0 !important;right:0 !important;
|
|
323
|
+
width:auto;max-width:${SHEET_L.maxWidth}px;margin:0 auto;max-height:calc(var(--visual-viewport-height,100vh) - ${SHEET_L.topClearance}px);
|
|
324
|
+
padding-top:${SHEET_L.handleZone}px;padding-bottom:env(safe-area-inset-bottom,0);
|
|
325
|
+
background:${SHEET_T.surface};border-bottom:0;border-radius:${SHEET_L.radius} ${SHEET_L.radius} 0 0;box-shadow:0 0 0 100vmax ${SHEET_T.overlay}}
|
|
326
|
+
.pw-menu-panel[data-small-screen=sheet]::before{content:"";position:absolute;top:${(SHEET_L.handleZone - SHEET_L.pillHeight) / 2}px;left:50%;
|
|
327
|
+
width:${SHEET_L.pillWidth}px;height:${SHEET_L.pillHeight}px;margin-left:-${SHEET_L.pillWidth / 2}px;border-radius:${SHEET_L.pillRadius};background:${SHEET_T.handle}}
|
|
328
|
+
.pw-menu-panel[data-small-screen=sheet][data-entering],.pw-menu-panel[data-small-screen=sheet][data-placement=top][data-entering]{animation:pw-menu-sheet-in ${M("duration-5")} ${M("easing-emphasized")} both}
|
|
329
|
+
.pw-menu-panel[data-small-screen=sheet][data-exiting]{animation:pw-menu-sheet-out ${M("duration-4")} ${M("easing-accelerate")} both}
|
|
330
|
+
@keyframes pw-menu-sheet-in{from{transform:translateY(100%)}to{transform:none}}
|
|
331
|
+
@keyframes pw-menu-sheet-out{from{transform:none}to{transform:translateY(100%)}}
|
|
332
|
+
}
|
|
333
|
+
|
|
307
334
|
@media (prefers-reduced-motion: reduce){
|
|
335
|
+
.pw-menu-panel[data-small-screen=sheet][data-entering],.pw-menu-panel[data-small-screen=sheet][data-placement=top][data-entering]{animation:pw-menu-fade ${M("duration-2")} ${M("easing-standard")} both}
|
|
336
|
+
.pw-menu-panel[data-small-screen=sheet][data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
|
|
308
337
|
.pw-menu-panel[data-entering],.pw-menu-panel[data-placement=top][data-entering]{animation:pw-menu-fade ${M("duration-2")} ${M("easing-standard")} both}
|
|
309
338
|
.pw-menu-panel[data-exiting]{animation:pw-menu-out ${M("duration-2")} ${M("easing-standard")} both}
|
|
310
339
|
@keyframes pw-menu-fade{from{opacity:0}to{opacity:1}}
|
|
@@ -325,8 +354,9 @@ function useStyles() {
|
|
|
325
354
|
|
|
326
355
|
/* Picks are seen before the panel goes: the row shows its new state for
|
|
327
356
|
Duration/3, then the panel fades (approved demo). */
|
|
328
|
-
const
|
|
329
|
-
&& parseFloat(getComputedStyle(document.documentElement).getPropertyValue(
|
|
357
|
+
const motionMs = (name, fallback) => (typeof document !== "undefined"
|
|
358
|
+
&& parseFloat(getComputedStyle(document.documentElement).getPropertyValue(name))) || fallback;
|
|
359
|
+
const pickDelay = () => motionMs("--motion-duration-3", 200);
|
|
330
360
|
|
|
331
361
|
// ─── CONTEXTS ─────────────────────────────────────────────────────────────────
|
|
332
362
|
|
|
@@ -379,6 +409,32 @@ function Trailing({ trailing, hasSubmenu }) {
|
|
|
379
409
|
return null;
|
|
380
410
|
}
|
|
381
411
|
|
|
412
|
+
/* A label cut off by the panel width shows the full text in the real Tooltip:
|
|
413
|
+
on hover after Duration/4, on keyboard focus at once (approved demo). The
|
|
414
|
+
item's accessible name already carries the full label, so the tooltip is
|
|
415
|
+
visual only and leaves the item's ARIA alone. */
|
|
416
|
+
function ItemLabel({ label, s }) {
|
|
417
|
+
const ref = useRef(null);
|
|
418
|
+
const [cut, setCut] = useState(false);
|
|
419
|
+
const [hoverOpen, setHoverOpen] = useState(false);
|
|
420
|
+
const engaged = !!(s.isHovered || s.isFocusVisible);
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
const el = ref.current;
|
|
423
|
+
setCut(!!el && el.scrollWidth > el.clientWidth + 1);
|
|
424
|
+
}, [engaged, label]);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (!s.isHovered) { setHoverOpen(false); return undefined; }
|
|
427
|
+
const t = setTimeout(() => setHoverOpen(true), motionMs("--motion-duration-4", 300));
|
|
428
|
+
return () => clearTimeout(t);
|
|
429
|
+
}, [s.isHovered]);
|
|
430
|
+
const open = cut && typeof label === "string" && (!!s.isFocusVisible || hoverOpen);
|
|
431
|
+
return (
|
|
432
|
+
<Tooltip content={typeof label === "string" ? label : undefined} open={open} manageAria={false} align="start">
|
|
433
|
+
<span ref={ref} className="pw-menu-item__label">{label}</span>
|
|
434
|
+
</Tooltip>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
382
438
|
/* The one item anatomy: [stripe] [indicator + visual] [label, description or
|
|
383
439
|
reason] [trailing]. The indicator is the real Checkbox or Radio, visual
|
|
384
440
|
only: the item is the control and owns the role, the state and the target. */
|
|
@@ -413,10 +469,10 @@ function ItemContent({ p, s }) {
|
|
|
413
469
|
<span className="pw-menu-item__text" data-inline={inline || undefined}>
|
|
414
470
|
{p.badges && p.badges.length ? (
|
|
415
471
|
<span className="pw-menu-item__label-row">
|
|
416
|
-
<
|
|
472
|
+
<ItemLabel label={p.label} s={s} />
|
|
417
473
|
{p.badges.map((b) => <Badge key={b.label} label={b.label} status={b.status || "Neutral"} size="Medium" badgeStyle="Outline & Fill" />)}
|
|
418
474
|
</span>
|
|
419
|
-
) : <
|
|
475
|
+
) : <ItemLabel label={p.label} s={s} />}
|
|
420
476
|
{inline ? <span className="pw-menu-item__desc">{p.inlineText}</span>
|
|
421
477
|
: p.description ? <span className="pw-menu-item__desc">{p.description}</span> : null}
|
|
422
478
|
{p.reason && (
|
|
@@ -634,6 +690,7 @@ export function PopoverMenuList({
|
|
|
634
690
|
* @param {"none"|"single"|"multiple"} selectionMode, selectedKeys, onSelectionChange, onAction
|
|
635
691
|
* @param {ReactNode} header, footer, emptyState See PopoverMenuList
|
|
636
692
|
* @param {boolean} searchable The panel opens at the fixed 360 height and keeps it while the list filters
|
|
693
|
+
* @param {"float"|"sheet"} smallScreen Below 768: float from the trigger (default), or open as a bottom sheet. Pending decision G4
|
|
637
694
|
*/
|
|
638
695
|
export function PopoverMenu({
|
|
639
696
|
type = "icon",
|
|
@@ -657,6 +714,7 @@ export function PopoverMenu({
|
|
|
657
714
|
footer,
|
|
658
715
|
emptyState,
|
|
659
716
|
searchable = false,
|
|
717
|
+
smallScreen = "float",
|
|
660
718
|
maxHeight,
|
|
661
719
|
className = "",
|
|
662
720
|
style,
|
|
@@ -736,7 +794,8 @@ export function PopoverMenu({
|
|
|
736
794
|
[RootMenuTriggerStateContext, state],
|
|
737
795
|
[PopoverContext, { trigger: "MenuTrigger", triggerRef: wrap, placement: `bottom ${align}` }],
|
|
738
796
|
]}>
|
|
739
|
-
<Popover className="pw-menu-panel" offset={L.offset} data-searchable={searchable || undefined}
|
|
797
|
+
<Popover className="pw-menu-panel" offset={L.offset} data-searchable={searchable || undefined}
|
|
798
|
+
data-small-screen={smallScreen === "sheet" ? "sheet" : undefined}>
|
|
740
799
|
{list}
|
|
741
800
|
</Popover>
|
|
742
801
|
</Provider>
|