Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ export const Check = () => (
<path d="M20 6 9 17l-5-5" />
</Icon>
);

export const ChevronLeft = () => (
<Icon title="Previous">
<path d="m15 18-6-6 6-6" />
</Icon>
);

export const ChevronRight = () => (
<Icon title="Next">
<path d="m9 18 6-6-6-6" />
</Icon>
);
38 changes: 37 additions & 1 deletion src/sileo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
import {
ArrowRight,
Check,
ChevronLeft,
ChevronRight,
CircleAlert,
LifeBuoy,
LoaderCircle,
Expand Down Expand Up @@ -68,6 +70,9 @@ interface SileoProps {
onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
onMouseLeave?: MouseEventHandler<HTMLButtonElement>;
onDismiss?: () => void;
navIndex?: number;
navTotal?: number;
onNavigate?: (dir: -1 | 1) => void;
}

/* ---------------------------------- Icons --------------------------------- */
Expand Down Expand Up @@ -136,6 +141,9 @@ export const Sileo = memo(function Sileo({
onMouseEnter,
onMouseLeave,
onDismiss,
navIndex,
navTotal,
onNavigate,
}: SileoProps) {
const next: View = useMemo(
() => ({ title, description, state, icon, styles, button, fill }),
Expand Down Expand Up @@ -385,6 +393,8 @@ export const Sileo = memo(function Sileo({

/* ------------------------------ Derived values ---------------------------- */

const showNav = navTotal !== undefined && navTotal > 1;

const minExpanded = HEIGHT * MIN_EXPAND_RATIO;
const rawExpanded = hasDesc
? Math.max(minExpanded, HEIGHT + contentHeight)
Expand All @@ -398,7 +408,8 @@ export const Sileo = memo(function Sileo({
const expanded = open ? rawExpanded : frozenExpandedRef.current;
const svgHeight = hasDesc ? Math.max(expanded, minExpanded) : HEIGHT;
const expandedContent = Math.max(0, expanded - HEIGHT);
const resolvedPillWidth = Math.max(pillWidth || HEIGHT, HEIGHT);
const navExtra = showNav ? 50 : 0;
const resolvedPillWidth = Math.max(pillWidth || HEIGHT, HEIGHT) + navExtra;
const pillHeight = HEIGHT + blur * 3;

const pillX =
Expand Down Expand Up @@ -548,6 +559,7 @@ export const Sileo = memo(function Sileo({
if (exiting || !onDismiss) return;
const target = e.target as HTMLElement;
if (target.closest("[data-sileo-button]")) return;
if (target.closest("[data-sileo-nav]")) return;
pointerStartRef.current = e.clientY;
e.currentTarget.setPointerCapture(e.pointerId);
const el = buttonRef.current;
Expand Down Expand Up @@ -656,6 +668,30 @@ export const Sileo = memo(function Sileo({
</div>
)}
</div>
{showNav && (
<div data-sileo-nav>
<button
type="button"
onClick={() => onNavigate?.(-1)}
disabled={navIndex === 0}
aria-label="Previous notification"
>
<ChevronLeft />
</button>
<button
type="button"
onClick={() => onNavigate?.(1)}
disabled={
navIndex !== undefined &&
navTotal !== undefined &&
navIndex >= navTotal - 1
}
aria-label="Next notification"
>
<ChevronRight />
</button>
</div>
)}
</div>

{hasDesc && (
Expand Down
40 changes: 38 additions & 2 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@
overflow: hidden;
left: var(--_px, 0px);
transform: var(--_ht);
max-width: var(--_pw);
width: var(--_pw);
}

[data-sileo-toast][data-ready="true"] [data-sileo-header] {
transition:
transform var(--sileo-duration) var(--sileo-spring-easing),
left var(--sileo-duration) var(--sileo-spring-easing),
max-width var(--sileo-duration) var(--sileo-spring-easing);
width var(--sileo-duration) var(--sileo-spring-easing);
}

[data-sileo-header][data-edge="top"] {
Expand Down Expand Up @@ -448,6 +448,42 @@
align-items: center;
}

/* ------------------------------ Navigation -------------------------------- */

[data-sileo-nav] {
display: flex;
align-items: center;
gap: 0.125rem;
margin-left: auto;
flex-shrink: 0;
color: #fff;
pointer-events: auto;
user-select: none;
}

[data-sileo-nav] button {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border: 0;
border-radius: 9999px;
background: transparent;
cursor: pointer;
color: inherit;
padding: 0;
}

[data-sileo-nav] button:not(:disabled):hover {
background-color: rgba(255, 255, 255, 0.12);
}

[data-sileo-nav] button:disabled {
opacity: 0.3;
cursor: default;
}

@media (prefers-reduced-motion: no-preference) {
[data-sileo-toast][data-ready="true"]:hover,
[data-sileo-toast][data-ready="true"][data-exiting="true"] {
Expand Down
55 changes: 54 additions & 1 deletion src/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface SileoToasterProps {
offset?: SileoOffsetValue | SileoOffsetConfig;
options?: Partial<SileoOptions>;
theme?: "light" | "dark" | "system";
navigation?: boolean;
}

/* ------------------------------ Global State ------------------------------ */
Expand Down Expand Up @@ -132,7 +133,7 @@ const createToast = (options: InternalSileoOptions) => {
const live = store.toasts.filter((t) => !t.exiting);
const merged = mergeOptions(options);

const id = merged.id ?? "sileo-default";
const id = merged.id ?? generateId();
const prev = live.find((t) => t.id === id);
const item = buildSileoItem(merged, id, prev?.position);

Expand Down Expand Up @@ -252,15 +253,18 @@ export function Toaster({
offset,
options,
theme,
navigation = false,
}: SileoToasterProps) {
const resolvedTheme = useResolvedTheme(theme);
const [toasts, setToasts] = useState<SileoItem[]>(store.toasts);
const [activeId, setActiveId] = useState<string>();
const [selectedIds, setSelectedIds] = useState<Partial<Record<SileoPosition, string>>>({});

const hoverRef = useRef(false);
const timersRef = useRef(new Map<string, number>());
const listRef = useRef(toasts);
const latestRef = useRef<string | undefined>(undefined);
const prevLiveIdsRef = useRef(new Set<string>());
const handlersCache = useRef(
new Map<
string,
Expand Down Expand Up @@ -325,6 +329,20 @@ export function Toaster({
if (!toastIds.has(id)) handlersCache.current.delete(id);
}

const currentLive = toasts.filter((t) => !t.exiting);
const newToasts = currentLive.filter((t) => !prevLiveIdsRef.current.has(t.id));
if (newToasts.length > 0) {
setSelectedIds((prev) => {
const next = { ...prev };
for (const t of newToasts) {
const pos = t.position ?? store.position;
delete next[pos];
}
return next;
});
}
prevLiveIdsRef.current = new Set(currentLive.map((t) => t.id));

schedule(toasts);
}, [toasts, schedule]);

Expand Down Expand Up @@ -420,13 +438,42 @@ export function Toaster({
return map;
}, [toasts, position]);

const navigate = useCallback(
(pos: SileoPosition, dir: -1 | 1) => {
const items = activePositions.get(pos) ?? [];
const live = items.filter((t) => !t.exiting);
setSelectedIds((prev) => {
const rawSel = prev[pos];
const curId =
rawSel && live.find((t) => t.id === rawSel)
? rawSel
: live.at(-1)?.id;
const curIdx = live.findIndex((t) => t.id === curId);
const nextIdx = Math.max(0, Math.min(live.length - 1, curIdx + dir));
const nextId = live[nextIdx]?.id;
if (!nextId || nextId === prev[pos]) return prev;
return { ...prev, [pos]: nextId };
});
},
[activePositions],
);

return (
<>
{children}
{Array.from(activePositions, ([pos, items]) => {
const pill = pillAlign(pos);
const expand = expandDir(pos);

const live = items.filter((t) => !t.exiting);
const rawSel = selectedIds[pos];
const selId =
rawSel && items.find((t) => t.id === rawSel)
? rawSel
: (live.at(-1) ?? items.at(-1))?.id;
const displayItem = items.find((t) => t.id === selId);
const showNav = live.length > 1 && !displayItem?.exiting;

return (
<section
key={pos}
Expand All @@ -437,7 +484,10 @@ export function Toaster({
style={getViewportStyle(pos)}
>
{items.map((item) => {
const isSelected = item.id === selId;
if (!isSelected && !item.exiting) return null;
const h = getHandlers(item.id);
const selIdx = live.findIndex((t) => t.id === selId);
return (
<Sileo
key={item.id}
Expand All @@ -460,6 +510,9 @@ export function Toaster({
onMouseEnter={h.enter}
onMouseLeave={h.leave}
onDismiss={h.dismiss}
navIndex={navigation && showNav && isSelected ? selIdx : undefined}
navTotal={navigation && showNav && isSelected ? live.length : undefined}
onNavigate={(dir) => navigate(pos, dir)}
/>
);
})}
Expand Down