Could not refresh this commune. Showing its last loaded activity.
: null}
-
+
-
+
@@ -148,6 +150,43 @@ function ProductShell() {
);
}
+// Focus moves to an element that only holds it until the next step away.
+function focusOnce(element: HTMLElement) {
+ if (!element.hasAttribute("tabindex")) {
+ element.tabIndex = -1;
+ element.addEventListener("blur", () => element.removeAttribute("tabindex"), { once: true });
+ }
+ element.focus({ preventScroll: true });
+}
+
+function skipToContent(event: MouseEvent) {
+ const main = document.getElementById("main-content");
+ if (!main) return;
+ event.preventDefault();
+ focusOnce(main);
+}
+
+// After moving to another page or commune, start at the new page's heading
+// rather than on a control that has gone. A window, the note reader or a
+// search result places focus itself; if one was opening, wait until it closes
+// and only step in when focus would otherwise be lost.
+function usePageFocus(screen: ScreenId, hubId: string, held: boolean) {
+ const shown = useRef({ screen, hubId });
+ const waiting = useRef(false);
+ useEffect(() => {
+ const previous = shown.current;
+ shown.current = { screen, hubId };
+ const moved = previous.screen !== screen || (Boolean(previous.hubId) && previous.hubId !== hubId);
+ if (moved) waiting.current = true;
+ if (!waiting.current || held) return;
+ waiting.current = false;
+ const active = document.activeElement;
+ if (!moved && active && active !== document.body) return;
+ const heading = document.querySelector(".a-header-title h1");
+ if (heading) focusOnce(heading);
+ }, [screen, hubId, held]);
+}
+
function ConnectionCheck() {
const { hubId } = useApp();
const [running, setRunning] = useState(false);
diff --git a/apps/web/src/auth/AuthLayout.tsx b/apps/web/src/auth/AuthLayout.tsx
index 817423df..f6520017 100644
--- a/apps/web/src/auth/AuthLayout.tsx
+++ b/apps/web/src/auth/AuthLayout.tsx
@@ -5,7 +5,7 @@ export function AuthLayout({ children }: { children: ReactNode }) {
const year = new Date().getFullYear();
return (
-
+
Commune
@@ -16,26 +16,29 @@ export function AuthLayout({ children }: { children: ReactNode }) {
Track group spending, settle balances, and keep shared money clear.
+ {/* Phones hide the brand panel, so its heading is repeated here for
+ screen readers: every page keeps one h1. */}
+ Communefor people who do life, together.
-
-
{children}
-
+
Effortlessly manage your shared expenses.
+
+ {children}
+
-
+
+
+
);
diff --git a/apps/web/src/auth/Login.tsx b/apps/web/src/auth/Login.tsx
index 3927ba56..c8e3b97a 100644
--- a/apps/web/src/auth/Login.tsx
+++ b/apps/web/src/auth/Login.tsx
@@ -6,6 +6,7 @@ import { LIMITS } from "../data/limits";
import { keepSearch } from "../nav";
import { GoogleMark } from "../ui/Icon";
import { readStored, removeStored } from "../live/storage";
+import { useFormError } from "../ui/components";
export function LoginPage() {
const { signInWithPassword, signInWithGoogle, signOutUnconfirmed, retrySignOut, sessionNotice } = useApp();
@@ -13,7 +14,7 @@ export function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [busy, setBusy] = useState(false);
- const [error, setError] = useState("");
+ const { error, setError, invalid, messageProps } = useFormError();
const [signOutRetry, setSignOutRetry] = useState<"idle" | "busy" | "failed">("idle");
async function onRetrySignOut() {
@@ -25,7 +26,7 @@ export function LoginPage() {
event.preventDefault();
setError("");
if (!email.includes("@") || !password) {
- setError("Enter your email and password.");
+ setError("Enter your email and password.", email.includes("@") ? "login-password" : "login-email");
return;
}
setBusy(true);
@@ -68,18 +69,18 @@ export function LoginPage() {
) : sessionNotice && !error ?
{sessionNotice}
: null}
{error || new URLSearchParams(window.location.search).has("oauth")
- ?
{error || "Google sign-in did not complete. Please try again."}
+ ?
{error || "Google sign-in did not complete. Please try again."}
{item.title}Announcement · please confirm you have seen it
diff --git a/apps/web/src/screens/Members.tsx b/apps/web/src/screens/Members.tsx
index f97b3967..18636342 100644
--- a/apps/web/src/screens/Members.tsx
+++ b/apps/web/src/screens/Members.tsx
@@ -159,6 +159,8 @@ export function MembersScreen() {
);
const me = member.userId === session?.userId;
const lastAdmin = profile?.role === "admin" && admins.length <= 1;
+ // Why an action is unavailable is shown, not left to a hover tooltip.
+ const hintId = `member-hint-${member.userId}`;
return (
{ setMessage(""); receipts.report({ shareId: share.id, payerName: payer?.name ?? "the bill payer", expenseName: expense.name, outstandingMinor: outstanding, currency: hub.currency }); }}>
- I’ve sent it
+ I’ve sent it{" "}to {payer?.name ?? "the bill payer"} for {expense.name}