@salesforce/ui-bundle-template-app-react-sample-b2x 12.6.3 → 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.
- package/dist/CHANGELOG.md +9 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/package.json +6 -6
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/api/graphql-operations-types.ts +1 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/appLayout.tsx +5 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/layout/VerticalNav.tsx +60 -27
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +18 -17
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +2 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertyMap.tsx +10 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/properties/PropertySearchFilters.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/button.tsx +2 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/checkbox.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/input.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/components/ui/select.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/context/TenantAccessContext.tsx +23 -6
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/forms/auth-form.tsx +13 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/hooks/form.tsx +12 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layout/centered-page-layout.tsx +8 -40
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/TenantRoute.tsx +28 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/layouts/privateRouteLayout.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/menu/AuthMenu.tsx +43 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ChangePassword.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ForgotPassword.tsx +8 -4
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Login.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/pages/ResetPassword.tsx +2 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +30 -7
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/api/objectSearchService.ts +5 -10
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/features/object-search/components/PaginationControls.tsx +38 -3
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/hooks/useReanchorReadingCursor.ts +54 -0
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Application.tsx +30 -8
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Contact.tsx +95 -48
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Home.tsx +34 -23
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/Maintenance.tsx +5 -1
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertyDetails.tsx +48 -16
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/pages/PropertySearch.tsx +15 -5
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/src/styles/global.css +2 -2
- package/dist/force-app/main/default/uiBundles/propertyrentalapp/tsconfig.tsbuildinfo +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/network-senders.mjs +84 -0
- package/dist/scripts/org-setup-xml.mjs +11 -6
- package/dist/scripts/org-setup.mjs +13 -5
- package/package.json +1 -1
|
@@ -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
|
|
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">
|
|
59
|
-
|
|
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" />
|
|
@@ -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
|
|
7
|
+
import { ROUTES } from "../authenticationConfig";
|
|
8
8
|
import { ApiError, handleApiResponse } from "../utils/helpers";
|
|
9
9
|
|
|
10
10
|
const forgotPasswordSchema = z.object({
|
|
11
|
-
username: z
|
|
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=
|
|
82
|
+
placeholder="asmith@example.com"
|
|
79
83
|
autoComplete="username"
|
|
80
84
|
disabled={success}
|
|
81
85
|
/>
|
|
@@ -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
|
-
|
|
33
|
-
| undefined;
|
|
32
|
+
Record<string, unknown> | undefined;
|
|
34
33
|
const queryResult = (uiapi?.query as Record<string, unknown> | undefined)?.[objectName] as
|
|
35
|
-
|
|
|
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
|
-
|
|
60
|
-
| undefined;
|
|
57
|
+
Record<string, unknown> | undefined;
|
|
61
58
|
const aggregate = (uiapi?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
90
|
+
open={pageSizeOpen}
|
|
91
|
+
onOpenChange={(open) => setPageSizeOpen(!disabled && open)}
|
|
64
92
|
>
|
|
65
93
|
<SelectTrigger
|
|
66
94
|
id="page-size-select"
|
|
67
|
-
className=
|
|
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
|
-
|
|
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
|
|
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]"
|
|
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/
|
|
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
|
|
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">
|
|
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/
|
|
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"}
|