Skip to content

Commit

Permalink
Merge pull request #1657 from danskernesdigitalebibliotek/DDFBRA-331-…
Browse files Browse the repository at this point in the history
…log-ud-giver-meddelelse-beklager-du-har-ikke-adgang-til-denne-side-v2

Don't allow double clicking on logout button
  • Loading branch information
Adamik10 authored Feb 4, 2025
2 parents 948a343 + f51de6a commit a2f81d6
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 212 deletions.
2 changes: 1 addition & 1 deletion src/apps/loan-list/materials/utils/warning-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const WarningBar: FC<WarningBarProps> = ({
variant="filled"
ariaLabelledBy={labelId}
>
{rightButtonText}
{rightButtonText || ""}
</LinkButton>
</>
)}
Expand Down
24 changes: 17 additions & 7 deletions src/apps/menu/menu-logged-in/MenuLoggedInContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import useReservations from "../../../core/utils/useReservations";
import useLoans from "../../../core/utils/useLoans";
import { usePatronData } from "../../../core/utils/helpers/usePatronData";
import { resetPersistedData } from "../../../core/store";
import { Button } from "../../../components/Buttons/Button";
import { redirectTo } from "../../../core/utils/helpers/url";

interface MenuLoggedInContentProps {
pageSize: number;
Expand Down Expand Up @@ -67,6 +69,11 @@ const MenuLoggedInContent: FC<MenuLoggedInContentProps> = ({ pageSize }) => {
loansSoonOverdue.length !== 0 ||
reservations.length !== 0;

const handleOnClick = () => {
resetPersistedData();
redirectTo(logoutUrl);
};

return (
<div className="modal-login modal-login--authenticated">
<div className="modal-login__container">
Expand Down Expand Up @@ -111,13 +118,16 @@ const MenuLoggedInContent: FC<MenuLoggedInContentProps> = ({ pageSize }) => {
</ul>
</nav>
<div className="modal-profile__btn-logout mx-32">
<Link
className="btn-primary btn-filled btn-large arrow__hover--right-small"
onClick={() => resetPersistedData()}
href={logoutUrl}
>
{t("menuLogOutText")}
</Link>
<Button
label={t("menuLogOutText")}
buttonType="none"
size="large"
variant="filled"
collapsible={false}
onClick={handleOnClick}
canOnlyBeClickedOnce
dataCy="menu-logout-button"
/>
</div>
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/apps/menu/menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,7 @@ describe("Menu (authenticated))", () => {
.and("have.text", "2");

// 2.e. “Log ud” knappen. Engelsk tekst: "Log out"
cy.get(".modal-profile__btn-logout")
.find("a")
cy.getBySel("menu-logout-button")
.should("exist")
.and("have.text", "Log out");
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { FC } from "react";
import ExternalLinkIcon from "@danskernesdigitalebibliotek/dpl-design-system/build/icons/buttons/icon-btn-external-link.svg";
import { useText } from "../../../../core/utils/text";
import { MaterialProps } from "../../../loan-list/materials/utils/material-fetch-hoc";
import { ReservationType } from "../../../../core/utils/types/reservation-type";
Expand Down Expand Up @@ -44,9 +43,10 @@ const ReservationDetailsRedirect: FC<
url={externalLink}
variant="filled"
id="go-to-ereolen-button"
iconClassNames="btn-icon invert"
buttonType="external-link"
>
{t("reservationDetailsDigitalReservationGoToEreolenText")}
<img src={ExternalLinkIcon} className="btn-icon invert" alt="" />
</LinkButton>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,7 @@ describe("Reservation details modal", () => {
cy.get(".modal")
.find("[data-cy='go-to-ereolen-button']")
.eq(0)
.should("have.text", "Go to eReolen")
.should("have.attr", "href")
// ID 17 2.d.i. link to "ereolen.dk"
.should("include", "ereolen.dk");
.should("have.text", "Go to eReolen");

cy.get(".modal")
.find(".status-label")
Expand Down
21 changes: 15 additions & 6 deletions src/components/Buttons/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef } from "react";
import {
ButtonSize,
ButtonType,
Expand All @@ -9,41 +9,50 @@ import { ButtonIcon } from "./ButtonIcon";
export type ButtonProps = {
label: string;
buttonType: ButtonType;
disabled: boolean;
collapsible: boolean;
size: ButtonSize;
variant: ButtonVariant;
disabled?: boolean;
onClick?: () => void;
iconClassNames?: string;
id?: string;
classNames?: string;
dataCy?: string;
ariaDescribedBy?: string;
canOnlyBeClickedOnce?: boolean;
};

export const Button: React.FC<ButtonProps> = ({
label,
buttonType,
disabled,
collapsible,
size,
variant,
disabled = false,
onClick,
iconClassNames,
id,
classNames,
dataCy,
ariaDescribedBy
ariaDescribedBy,
canOnlyBeClickedOnce = false
}) => {
const isLoadingRef = useRef(false);
const handleClick = () => {
if (isLoadingRef.current) return;
if (canOnlyBeClickedOnce) isLoadingRef.current = true;
if (onClick) onClick();
};

return (
<button
data-cy={dataCy || "button"}
type="button"
className={`btn-primary btn-${variant} btn-${size} ${
disabled ? "btn-outline" : ""
} arrow__hover--right-small ${classNames ?? ""}`}
disabled={disabled}
onClick={onClick}
disabled={disabled || isLoadingRef.current}
onClick={handleClick}
id={id}
aria-describedby={ariaDescribedBy}
>
Expand Down
38 changes: 17 additions & 21 deletions src/components/Buttons/LinkButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import clsx from "clsx";
import React from "react";
import {
ButtonSize,
ButtonType,
ButtonVariant
} from "../../core/utils/types/button";
import Link from "../atoms/links/Link";
import { ButtonIcon } from "./ButtonIcon";
import { Button } from "./Button";
import { redirectTo } from "../../core/utils/helpers/url";

export interface LinkButtonProps {
buttonType?: ButtonType;
children: React.ReactNode;
children: string;
classNames?: string;
dataCy?: string;
iconClassNames?: string;
Expand Down Expand Up @@ -38,26 +37,23 @@ const LinkButton: React.FC<LinkButtonProps> = ({
id
}) => {
return (
<Link
href={url}
isNewTab={isNewTab}
className={clsx(
"btn-primary",
`btn-${variant}`,
`btn-${size}`,
"arrow__hover--right-small",
"hide-linkstyle",
classNames
)}
trackClick={trackClick}
<Button
variant={variant}
size={size}
buttonType={buttonType || "none"}
classNames={classNames}
iconClassNames={iconClassNames}
onClick={() => {
if (trackClick) trackClick?.().then(() => redirectTo(url, isNewTab));
if (!trackClick) redirectTo(url, isNewTab);
}}
dataCy={dataCy}
ariaLabelledBy={ariaLabelledBy}
ariaDescribedBy={ariaLabelledBy}
id={id}
canOnlyBeClickedOnce
>
{children}
<ButtonIcon buttonType={buttonType} iconClassNames={iconClassNames} />
</Link>
label={children}
collapsible={false}
/>
);
};

Expand Down
44 changes: 3 additions & 41 deletions src/components/atoms/links/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,36 @@
import React, { useState } from "react";
import React from "react";
import { getLinkHandler } from "./getLinkHandler";

export interface LinkProps {
href: URL;
onClick?: () => Promise<void>;
children: React.ReactNode;
isNewTab?: boolean;
className?: string;
id?: string;
trackClick?: () => Promise<unknown>;
dataCy?: string;
ariaLabelledBy?: string;
stopPropagation?: boolean;
isHiddenFromScreenReaders?: boolean;
canOnlyBeClickedOnce?: boolean;
}

const Link: React.FC<LinkProps> = ({
href,
onClick,
children,
isNewTab = false,
className,
id,
trackClick,
dataCy,
ariaLabelledBy,
stopPropagation = false,
isHiddenFromScreenReaders,
canOnlyBeClickedOnce = false
isHiddenFromScreenReaders
}) => {
const [isLoading, setIsLoading] = useState(false);

const handleClick = getLinkHandler({
type: "click",
isNewTab,
stopPropagation,
url: href,
trackClick
});

const handleKeyUp = getLinkHandler({
type: "keyup",
isNewTab,
stopPropagation,
url: href,
trackClick
url: href
});

// We need to use a custom onClick & onKeyUp handlers as opposed to just native <a> behaviour
// because we in some cases have to track these clicks. So we need to wait to fire a call
// before we can redirect the user.
const onclickHandler = onClick
? async (
e:
| React.MouseEvent<HTMLAnchorElement>
| React.KeyboardEvent<HTMLAnchorElement>
) => {
if (canOnlyBeClickedOnce && isLoading) return; // Prevent further clicks
if (canOnlyBeClickedOnce) setIsLoading(true);
try {
await onClick(); // Await the provided onClick
handleClick(e); // Call handleClick after onClick resolves
} finally {
if (canOnlyBeClickedOnce) setIsLoading(false);
}
}
: handleClick;

return (
<a
id={id}
Expand All @@ -76,7 +39,6 @@ const Link: React.FC<LinkProps> = ({
target={isNewTab ? "_blank" : undefined}
rel="noreferrer"
className={className}
onClick={onclickHandler}
onKeyUp={handleKeyUp}
aria-labelledby={ariaLabelledBy}
tabIndex={isHiddenFromScreenReaders ? -1 : 0}
Expand Down
3 changes: 0 additions & 3 deletions src/components/atoms/links/LinkNoStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export interface LinkNoStyleProps {
children: React.ReactNode;
isNewTab?: boolean;
className?: string;
trackClick?: () => Promise<unknown>;
dataCy?: string;
ariaLabelledBy?: string;
isHiddenFromScreenReaders?: boolean;
Expand All @@ -17,7 +16,6 @@ const LinkNoStyle: React.FC<LinkNoStyleProps> = ({
children,
isNewTab = false,
className,
trackClick,
dataCy = "link-no-style",
ariaLabelledBy,
isHiddenFromScreenReaders
Expand All @@ -27,7 +25,6 @@ const LinkNoStyle: React.FC<LinkNoStyleProps> = ({
href={url}
isNewTab={isNewTab}
className={`hide-linkstyle ${className || ""}`}
trackClick={trackClick}
dataCy={dataCy}
ariaLabelledBy={ariaLabelledBy}
isHiddenFromScreenReaders={isHiddenFromScreenReaders}
Expand Down
55 changes: 41 additions & 14 deletions src/components/campaign/Campaign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { FC } from "react";
import { CampaignMatchPOST200Data } from "../../core/dpl-cms/model";
import { statistics } from "../../core/statistics/statistics";
import { useStatistics } from "../../core/statistics/useStatistics";
import LinkNoStyle from "../atoms/links/LinkNoStyle";
import CampaignBody from "./CampaignBody";
import { redirectTo } from "../../core/utils/helpers/url";

export interface CampaignProps {
campaignData: CampaignMatchPOST200Data;
Expand All @@ -24,18 +23,46 @@ const Campaign: FC<CampaignProps> = ({ campaignData }) => {
trackedData: campaignData.title as string
});
};
if (campaignData.url) {
return (
<LinkNoStyle
url={new URL(campaignData.url)}
trackClick={trackClick}
className="cursor-pointer"
>
<CampaignBody campaignData={campaignData} />
</LinkNoStyle>
);
}
return <CampaignBody campaignData={campaignData} />;

const onClick = () => {
if (campaignData.url) {
const url = new URL(campaignData.url);
trackClick().then(() => redirectTo(url, true));
}
};

const onKeyUp = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
onClick();
}
};

return (
<section
className="campaign mt-35"
data-cy="campaign-body"
onClick={onClick}
onKeyUp={onKeyUp}
role="link"
tabIndex={campaignData.url ? 0 : undefined}
>
{campaignData.image && campaignData.image.url && (
<img
data-cy="campaign-image"
className={`campaign__image ${
!campaignData.text ? "campaign__image--full-width" : ""
}`}
src={campaignData.image.url}
alt={campaignData.image.alt}
/>
)}
{campaignData.text && (
<h4 className="campaign__title campaign__title--ellipsis">
{campaignData.text}
</h4>
)}
</section>
);
};

export default Campaign;
Loading

0 comments on commit a2f81d6

Please sign in to comment.