@salesforce/ui-bundle-template-app-react-sample-b2x 12.6.3-canary.999.2.1.gd0b2ab15 → 12.7.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.
Files changed (44) hide show
  1. package/dist/CHANGELOG.md +9 -0
  2. package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +6 -6
  3. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +1 -2
  4. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/appLayout.tsx +5 -4
  5. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/VerticalNav.tsx +60 -27
  6. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +18 -17
  7. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +2 -2
  8. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyMap.tsx +10 -2
  9. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertySearchFilters.tsx +1 -1
  10. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/button.tsx +2 -2
  11. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/checkbox.tsx +1 -1
  12. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
  13. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/select.tsx +1 -1
  14. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +23 -6
  15. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +13 -2
  16. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +12 -3
  17. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/centered-page-layout.tsx +8 -40
  18. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/TenantRoute.tsx +28 -4
  19. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +1 -1
  20. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/menu/AuthMenu.tsx +43 -5
  21. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +1 -1
  22. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +8 -4
  23. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +1 -1
  24. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
  25. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +2 -1
  26. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +30 -7
  27. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/api/objectSearchService.ts +5 -10
  28. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/PaginationControls.tsx +38 -3
  29. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useReanchorReadingCursor.ts +54 -0
  30. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +30 -8
  31. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +95 -48
  32. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +34 -23
  33. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +5 -1
  34. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx +48 -16
  35. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +15 -5
  36. package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/styles/global.css +2 -2
  37. package/dist/force-app/main/default/uiBundles/propertyrentalapp/tsconfig.tsbuildinfo +1 -1
  38. package/dist/package-lock.json +2 -2
  39. package/dist/package.json +1 -1
  40. package/dist/scripts/network-senders.mjs +84 -0
  41. package/dist/scripts/org-setup-xml.mjs +11 -6
  42. package/dist/scripts/org-setup.mjs +13 -5
  43. package/package.json +1 -1
  44. package/dist/LICENSE.txt +0 -82
@@ -1,5 +1,12 @@
1
+ /*
2
+ * Copyright (c) 2024, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { useEffect, useId, useRef } from "react";
1
8
  import { CircleUser, LogIn, LogOut, UserPen, UserPlus } from "lucide-react";
2
- import { Link } from "react-router";
9
+ import { Link, useLocation } from "react-router";
3
10
  import { useAuth } from "../context/AuthContext";
4
11
  import { ROUTES } from "../authenticationConfig";
5
12
  import { Button } from "../../../components/ui/button";
@@ -31,6 +38,13 @@ export interface AuthMenuProps {
31
38
 
32
39
  export function AuthMenu({ trigger, guestContent, menuItems, className }: AuthMenuProps) {
33
40
  const { user, isAuthenticated, loading, logout } = useAuth();
41
+ const { pathname } = useLocation();
42
+ const userLabelId = useId();
43
+ const openedPathname = useRef(pathname);
44
+ const currentPathname = useRef(pathname);
45
+ useEffect(() => {
46
+ currentPathname.current = pathname;
47
+ }, [pathname]);
34
48
 
35
49
  if (loading) {
36
50
  return <Skeleton className="size-8 rounded-full" />;
@@ -49,14 +63,38 @@ export function AuthMenu({ trigger, guestContent, menuItems, className }: AuthMe
49
63
  const triggerNode = (isAuthenticated && user && trigger?.(user)) || defaultTrigger;
50
64
 
51
65
  return (
52
- <DropdownMenu>
66
+ <DropdownMenu
67
+ onOpenChange={(open) => {
68
+ if (open) openedPathname.current = pathname;
69
+ }}
70
+ >
53
71
  <DropdownMenuTrigger asChild>{triggerNode}</DropdownMenuTrigger>
54
72
 
55
- <DropdownMenuContent align="end" className={className ?? "w-48"}>
73
+ <DropdownMenuContent
74
+ align="end"
75
+ aria-describedby={isAuthenticated ? userLabelId : undefined}
76
+ className={className ?? "w-48"}
77
+ onCloseAutoFocus={(event) => {
78
+ if (openedPathname.current === currentPathname.current) return;
79
+ event.preventDefault();
80
+ // Recover page focus intercepted by the closing menu trap.
81
+ const active = document.activeElement;
82
+ if (active && active !== document.body && active.isConnected) return;
83
+ const target =
84
+ document.querySelector<HTMLElement>("main h1, [role='main'] h1") ??
85
+ document.querySelector<HTMLElement>("main, [role='main']");
86
+ if (target) {
87
+ if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
88
+ target.focus();
89
+ }
90
+ }}
91
+ >
56
92
  {isAuthenticated ? (
57
93
  <>
58
- <DropdownMenuLabel className="truncate">{user?.name}</DropdownMenuLabel>
59
- <DropdownMenuSeparator />
94
+ <DropdownMenuLabel id={userLabelId} className="truncate" aria-hidden="true">
95
+ {user?.name}
96
+ </DropdownMenuLabel>
97
+ <DropdownMenuSeparator aria-hidden="true" />
60
98
  <DropdownMenuItem asChild>
61
99
  <Link to={ROUTES.PROFILE.PATH}>
62
100
  <UserPen className="size-4" />
@@ -50,7 +50,7 @@ export default function ChangePassword() {
50
50
  err.errors.length === 1 ? (
51
51
  err.errors[0]
52
52
  ) : (
53
- <ul>
53
+ <ul role="list">
54
54
  {err.errors.map((e, i) => (
55
55
  <li key={i}>{e}</li>
56
56
  ))}
@@ -4,11 +4,15 @@ import { CenteredPageLayout } from "../layout/centered-page-layout";
4
4
  import { AuthForm } from "../forms/auth-form";
5
5
  import { useAppForm } from "../hooks/form";
6
6
  import { createDataSDK } from "@salesforce/platform-sdk";
7
- import { ROUTES, AUTH_PLACEHOLDERS } from "../authenticationConfig";
7
+ import { ROUTES } from "../authenticationConfig";
8
8
  import { ApiError, handleApiResponse } from "../utils/helpers";
9
9
 
10
10
  const forgotPasswordSchema = z.object({
11
- username: z.string().trim().toLowerCase().email("Please enter a valid username"),
11
+ username: z
12
+ .string()
13
+ .trim()
14
+ .toLowerCase()
15
+ .email("Enter your username in email format, for example asmith@example.com"),
12
16
  });
13
17
 
14
18
  export default function ForgotPassword() {
@@ -42,7 +46,7 @@ export default function ForgotPassword() {
42
46
  err.errors.length === 1 ? (
43
47
  err.errors[0]
44
48
  ) : (
45
- <ul>
49
+ <ul role="list">
46
50
  {err.errors.map((e, i) => (
47
51
  <li key={i}>{e}</li>
48
52
  ))}
@@ -75,7 +79,7 @@ export default function ForgotPassword() {
75
79
  {(field) => (
76
80
  <field.TextField
77
81
  label="Username"
78
- placeholder={AUTH_PLACEHOLDERS.USERNAME}
82
+ placeholder="asmith@example.com"
79
83
  autoComplete="username"
80
84
  disabled={success}
81
85
  />
@@ -60,7 +60,7 @@ export default function Login() {
60
60
  err.errors.length === 1 ? (
61
61
  err.errors[0]
62
62
  ) : (
63
- <ul>
63
+ <ul role="list">
64
64
  {err.errors.map((e, i) => (
65
65
  <li key={i}>{e}</li>
66
66
  ))}
@@ -69,7 +69,7 @@ export default function Register() {
69
69
  console.error("Registration failed", err);
70
70
  if (err instanceof ApiError) {
71
71
  setSubmitError(
72
- <ul>
72
+ <ul role="list">
73
73
  {err.errors.map((e, i) => (
74
74
  <li key={i}>{e}</li>
75
75
  ))}
@@ -45,7 +45,7 @@ export default function ResetPassword() {
45
45
  err.errors.length === 1 ? (
46
46
  err.errors[0]
47
47
  ) : (
48
- <ul>
48
+ <ul role="list">
49
49
  {err.errors.map((e, i) => (
50
50
  <li key={i}>{e}</li>
51
51
  ))}
@@ -64,6 +64,7 @@ export default function ResetPassword() {
64
64
  return (
65
65
  <CenteredPageLayout title={ROUTES.RESET_PASSWORD.TITLE}>
66
66
  <CardLayout title="Reset Password">
67
+ <h1 className="sr-only">Reset Password</h1>
67
68
  <StatusAlert>
68
69
  Reset token is invalid or expired.{" "}
69
70
  <Link to={ROUTES.FORGOT_PASSWORD.PATH} className="underline underline-offset-4">
@@ -378,6 +378,7 @@ function SessionWarningModal({
378
378
  onCountdownExpire,
379
379
  }: SessionWarningModalProps) {
380
380
  const continueButtonRef = useRef<HTMLButtonElement>(null);
381
+ const previousFocusRef = useRef<HTMLElement | null>(null);
381
382
 
382
383
  // Countdown timer with accessibility
383
384
  const timer = useCountdownTimer({
@@ -399,13 +400,6 @@ function SessionWarningModal({
399
400
  start();
400
401
  }, [isOpen, timeRemaining, start, stop, reset]);
401
402
 
402
- // Focus the continue button when dialog opens
403
- useEffect(() => {
404
- if (isOpen) {
405
- continueButtonRef.current?.focus();
406
- }
407
- }, [isOpen]);
408
-
409
403
  // Handle "Continue Working" button click
410
404
  const handleContinue = useCallback(() => {
411
405
  stop();
@@ -421,6 +415,30 @@ function SessionWarningModal({
421
415
  return (
422
416
  <Dialog open={isOpen} onOpenChange={() => {}}>
423
417
  <DialogContent
418
+ aria-labelledby="session-warning-title"
419
+ aria-describedby="session-warning-description"
420
+ onOpenAutoFocus={(event) => {
421
+ event.preventDefault();
422
+ previousFocusRef.current =
423
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
424
+ continueButtonRef.current?.focus();
425
+ }}
426
+ onCloseAutoFocus={(event) => {
427
+ event.preventDefault();
428
+ const previous = previousFocusRef.current;
429
+ previousFocusRef.current = null;
430
+ if (previous?.isConnected && previous !== document.body) {
431
+ previous.focus();
432
+ if (document.activeElement === previous) return;
433
+ }
434
+ const target = document.querySelector<HTMLElement>("main, #main-content");
435
+ if (target) {
436
+ if (target.tabIndex < 0 && !target.hasAttribute("tabindex")) {
437
+ target.setAttribute("tabindex", "-1");
438
+ }
439
+ target.focus();
440
+ }
441
+ }}
424
442
  showCloseButton={false}
425
443
  onEscapeKeyDown={(e) => e.preventDefault()}
426
444
  onPointerDownOutside={(e) => e.preventDefault()}
@@ -578,6 +596,11 @@ export default function SessionTimeoutValidator({
578
596
  */
579
597
  const handleDismissAlert = useCallback(() => {
580
598
  setShowExpiredAlert(false);
599
+ const main = document.querySelector<HTMLElement>("main, #main-content");
600
+ if (main) {
601
+ if (!main.hasAttribute("tabindex")) main.setAttribute("tabindex", "-1");
602
+ main.focus();
603
+ }
581
604
  }, []);
582
605
 
583
606
  return (
@@ -29,11 +29,9 @@ export async function searchObjects<TResult, TQuery, TVariables>(
29
29
  }
30
30
 
31
31
  const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
32
- | Record<string, unknown>
33
- | undefined;
32
+ Record<string, unknown> | undefined;
34
33
  const queryResult = (uiapi?.query as Record<string, unknown> | undefined)?.[objectName] as
35
- | TResult
36
- | undefined;
34
+ TResult | undefined;
37
35
 
38
36
  if (!queryResult) {
39
37
  throw new Error(`No ${objectName} data returned`);
@@ -56,18 +54,15 @@ export async function fetchDistinctValues<TQuery>(
56
54
  }
57
55
 
58
56
  const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
59
- | Record<string, unknown>
60
- | undefined;
57
+ Record<string, unknown> | undefined;
61
58
  const aggregate = (uiapi?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
62
- | { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
63
- | undefined;
59
+ { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> } | undefined;
64
60
 
65
61
  const edges = aggregate?.edges ?? [];
66
62
  return edges
67
63
  .map((edge) => {
68
64
  const field = edge?.node?.aggregate?.[fieldName] as
69
- | { value?: string | null; displayValue?: string | null; label?: string | null }
70
- | undefined;
65
+ { value?: string | null; displayValue?: string | null; label?: string | null } | undefined;
71
66
  const value = field?.value;
72
67
  if (!value) return null;
73
68
  return { value, label: field.label ?? field.displayValue ?? value };
@@ -13,6 +13,7 @@ import {
13
13
  SelectValue,
14
14
  } from "../../../components/ui/select";
15
15
  import { Label } from "../../../components/ui/label";
16
+ import { useEffect, useRef, useState, type KeyboardEvent } from "react";
16
17
 
17
18
  interface PaginationControlsProps {
18
19
  pageIndex: number;
@@ -37,7 +38,9 @@ export default function PaginationControls({
37
38
  onPageSizeChange,
38
39
  disabled = false,
39
40
  }: PaginationControlsProps) {
41
+ const [pageSizeOpen, setPageSizeOpen] = useState(false);
40
42
  const handlePageSizeChange = (newValue: string) => {
43
+ if (disabled) return;
41
44
  const newSize = parseInt(newValue, 10);
42
45
  if (!isNaN(newSize) && newSize !== pageSize) {
43
46
  onPageSizeChange(newSize);
@@ -46,9 +49,33 @@ export default function PaginationControls({
46
49
  const currentPage = pageIndex + 1;
47
50
  const prevDisabled = disabled || !hasPreviousPage;
48
51
  const nextDisabled = disabled || !hasNextPage;
52
+ const navRef = useRef<HTMLDivElement>(null);
53
+
54
+ useEffect(() => {
55
+ if (disabled) {
56
+ setPageSizeOpen(false);
57
+ return;
58
+ }
59
+ const buttons = navRef.current?.querySelectorAll<HTMLElement>('[role="button"]');
60
+ if (!buttons || buttons.length === 0) return;
61
+ const first = buttons[0];
62
+ const last = buttons[buttons.length - 1];
63
+ if (nextDisabled && !prevDisabled && document.activeElement === last) first.focus();
64
+ else if (prevDisabled && !nextDisabled && document.activeElement === first) last.focus();
65
+ }, [disabled, prevDisabled, nextDisabled]);
66
+
67
+ const handleActivationKey = (action: () => void) => (event: KeyboardEvent) => {
68
+ if (event.key === "Enter" || event.key === " ") {
69
+ event.preventDefault();
70
+ action();
71
+ }
72
+ };
49
73
 
50
74
  return (
51
- <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
75
+ <div
76
+ ref={navRef}
77
+ className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2"
78
+ >
52
79
  <div
53
80
  className="flex justify-center sm:justify-start items-center gap-2 shrink-0 row-2 sm:row-1"
54
81
  role="group"
@@ -60,11 +87,13 @@ export default function PaginationControls({
60
87
  <Select
61
88
  value={pageSize.toString()}
62
89
  onValueChange={handlePageSizeChange}
63
- disabled={disabled}
90
+ open={pageSizeOpen}
91
+ onOpenChange={(open) => setPageSizeOpen(!disabled && open)}
64
92
  >
65
93
  <SelectTrigger
66
94
  id="page-size-select"
67
- className="w-16"
95
+ className={`w-16 ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
96
+ aria-disabled={disabled || undefined}
68
97
  aria-label="Select number of results per page"
69
98
  >
70
99
  <SelectValue />
@@ -82,7 +111,10 @@ export default function PaginationControls({
82
111
  <PaginationContent>
83
112
  <PaginationItem>
84
113
  <PaginationPrevious
114
+ role="button"
115
+ tabIndex={prevDisabled ? -1 : 0}
85
116
  onClick={prevDisabled ? undefined : onPreviousPage}
117
+ onKeyDown={prevDisabled ? undefined : handleActivationKey(onPreviousPage)}
86
118
  aria-disabled={prevDisabled}
87
119
  className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
88
120
  />
@@ -97,7 +129,10 @@ export default function PaginationControls({
97
129
  </PaginationItem>
98
130
  <PaginationItem>
99
131
  <PaginationNext
132
+ role="button"
133
+ tabIndex={nextDisabled ? -1 : 0}
100
134
  onClick={nextDisabled ? undefined : onNextPage}
135
+ onKeyDown={nextDisabled ? undefined : handleActivationKey(onNextPage)}
101
136
  aria-disabled={nextDisabled}
102
137
  className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
103
138
  />
@@ -0,0 +1,54 @@
1
+ /*
2
+ * Copyright (c) 2026, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useEffect, useRef } from "react";
7
+
8
+ function isTextEntry(el: HTMLElement): boolean {
9
+ if (el.isContentEditable) return true;
10
+ if (el.tagName === "TEXTAREA") return true;
11
+ if (el.tagName !== "INPUT") return false;
12
+ const type = (el.getAttribute("type") ?? "text").toLowerCase();
13
+ return !["button", "submit", "reset", "checkbox", "radio", "range", "color", "file"].includes(
14
+ type,
15
+ );
16
+ }
17
+
18
+ /** Reanchors the reading cursor after reloads while preserving newer focus and text entry. */
19
+ export function useReanchorReadingCursor(loading: boolean) {
20
+ const previousLoading = useRef(loading);
21
+ const focusedOnReloadStart = useRef<HTMLElement | null>(null);
22
+
23
+ useEffect(() => {
24
+ const started = !previousLoading.current && loading;
25
+ const settled = previousLoading.current && !loading;
26
+ previousLoading.current = loading;
27
+
28
+ if (started) {
29
+ const active = document.activeElement;
30
+ focusedOnReloadStart.current =
31
+ active instanceof HTMLElement && active !== document.body && !isTextEntry(active)
32
+ ? active
33
+ : null;
34
+ return;
35
+ }
36
+ if (!settled) return;
37
+
38
+ const target = focusedOnReloadStart.current;
39
+ focusedOnReloadStart.current = null;
40
+ if (!target || !target.isConnected) return;
41
+ const active = document.activeElement;
42
+ if (active !== target && active !== document.body) return;
43
+
44
+ const raf = requestAnimationFrame(() => {
45
+ if (!target.isConnected) return;
46
+ const current = document.activeElement;
47
+ if (current !== target && current !== document.body) return;
48
+ // VoiceOver tracks the blur/refocus transition after the results render.
49
+ if (document.activeElement === target) target.blur();
50
+ target.focus();
51
+ });
52
+ return () => cancelAnimationFrame(raf);
53
+ }, [loading]);
54
+ }
@@ -144,10 +144,18 @@ export default function Application() {
144
144
  const [submitError, setSubmitError] = useState<string | null>(null);
145
145
  const [submittedName, setSubmittedName] = useState<string | null>(null);
146
146
  const successHeadingRef = useRef<HTMLHeadingElement>(null);
147
+ const formHeadingRef = useRef<HTMLHeadingElement>(null);
148
+ const returnToForm = useRef(false);
147
149
 
148
150
  useEffect(() => {
149
151
  if (submittedName) successHeadingRef.current?.focus();
150
- }, [submittedName]);
152
+ else if (!propertyId && !loading && returnToForm.current) {
153
+ returnToForm.current = false;
154
+ if (document.activeElement === document.body || document.activeElement?.matches("main")) {
155
+ formHeadingRef.current?.focus();
156
+ }
157
+ }
158
+ }, [submittedName, loading, propertyId]);
151
159
 
152
160
  const handleSubmit = useCallback(
153
161
  async (e: SubmitEvent<HTMLFormElement>) => {
@@ -174,7 +182,7 @@ export default function Application() {
174
182
  setSubmitting(false);
175
183
  }
176
184
  },
177
- [propertyId, contactId, moveInDate, employment, references, user?.id],
185
+ [propertyId, contactId, moveInDate, employment, references],
178
186
  );
179
187
 
180
188
  if (loading) {
@@ -183,17 +191,18 @@ export default function Application() {
183
191
 
184
192
  if (submittedName) {
185
193
  return (
186
- <div className="mx-auto max-w-[900px]" role="status">
194
+ <div className="mx-auto max-w-[900px]">
187
195
  <h1 className="sr-only">Application submitted</h1>
188
196
  <Card className="mb-6 rounded-2xl border border-border p-6 shadow-sm">
189
197
  <h2
190
198
  ref={successHeadingRef}
191
199
  tabIndex={-1}
200
+ aria-describedby="application-success-description"
192
201
  className="mb-2 text-2xl font-semibold text-foreground"
193
202
  >
194
203
  Application submitted
195
204
  </h2>
196
- <p className="text-sm text-muted-foreground">
205
+ <p id="application-success-description" className="text-sm text-muted-foreground">
197
206
  Your application
198
207
  {propertyName ? (
199
208
  <>
@@ -216,9 +225,20 @@ export default function Application() {
216
225
  <Button
217
226
  asChild
218
227
  size="sm"
219
- className="rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
228
+ className="rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 [a]:hover:bg-primary/95"
220
229
  >
221
- <Link to="/application">Submit another</Link>
230
+ <Link
231
+ to="/application"
232
+ onClick={() => {
233
+ returnToForm.current = true;
234
+ setSubmittedName(null);
235
+ setMoveInDate("");
236
+ setEmployment("");
237
+ setReferences("");
238
+ }}
239
+ >
240
+ Submit another
241
+ </Link>
222
242
  </Button>
223
243
  </div>
224
244
  </Card>
@@ -228,7 +248,9 @@ export default function Application() {
228
248
 
229
249
  return (
230
250
  <div className="mx-auto max-w-[900px]">
231
- <h1 className="sr-only">Apply for a property</h1>
251
+ <h1 ref={formHeadingRef} tabIndex={-1} className="sr-only">
252
+ Apply for a property
253
+ </h1>
232
254
  <div className="mb-4">
233
255
  <Link
234
256
  to={propertyId ? `/property/${propertyId}` : "/properties"}
@@ -308,7 +330,7 @@ export default function Application() {
308
330
  <Button
309
331
  type="submit"
310
332
  size="sm"
311
- className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90 disabled:opacity-50"
333
+ className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/95 disabled:opacity-50"
312
334
  disabled={submitting}
313
335
  >
314
336
  {submitting ? "Submitting…" : "Submit application"}