generated from digitalservicebund/remix-application-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Button component and url util to index page
Button and url are copies from https://github.com/digitalservicebund/a2j-rechtsantragstelle: - Button: https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/main/app/components/Button.tsx - url: https://github.com/digitalservicebund/a2j-rechtsantragstelle/blob/main/app/util/url.ts - tests will be added in a separate PR
- Loading branch information
1 parent
09e4719
commit d504e02
Showing
3 changed files
with
127 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import classNames from "classnames"; | ||
import { cloneElement, type ReactElement } from "react"; | ||
import { z } from "zod"; | ||
import { isExternalUrl, isFileDownloadUrl } from "~/util/url"; | ||
|
||
export const iconSchema = z | ||
.custom<ReactElement<{ className: string }>>() | ||
.optional(); | ||
|
||
export const ButtonPropsSchema = z.object({ | ||
text: z.string().optional(), | ||
look: z.enum(["primary", "secondary", "tertiary", "ghost"]).optional(), | ||
size: z.enum(["large", "medium", "small"]).optional(), | ||
href: z.string().optional(), | ||
iconLeft: iconSchema, | ||
iconRight: iconSchema, | ||
fullWidth: z.boolean().optional(), | ||
}); | ||
|
||
export type ButtonProps = z.infer<typeof ButtonPropsSchema>; | ||
|
||
type LinkProps = React.ComponentPropsWithoutRef<"a">; | ||
|
||
function formatIcon(icon: z.infer<typeof iconSchema>) { | ||
if (!icon) return undefined; | ||
const className = `ds-button-icon ${icon.props.className ?? ""}`; | ||
return cloneElement(icon, { className }); | ||
} | ||
|
||
function Button({ | ||
children, | ||
text, | ||
iconLeft, | ||
iconRight, | ||
fullWidth, | ||
look, | ||
size, | ||
href, | ||
...props | ||
}: ButtonProps & LinkProps & React.ComponentPropsWithoutRef<"button">) { | ||
const buttonClasses = classNames( | ||
"ds-button", | ||
{ | ||
"ds-button-secondary": look == "secondary", | ||
"ds-button-tertiary": look == "tertiary", | ||
"ds-button-ghost": look == "ghost", | ||
"ds-button-large": size == "large", | ||
"ds-button-small": size == "small", | ||
"ds-button-with-icon": iconLeft ?? iconRight, | ||
"ds-button-with-icon-only": (iconLeft ?? iconRight) && !children, | ||
"ds-button-full-width": fullWidth, | ||
}, | ||
"contrast-more:border-4 forced-colors:border-4 border-solid contrast-more:border-black", // TODO: move into angie? | ||
props.className, | ||
); | ||
|
||
const textSpan = text ? <span className="ds-button-label">{text}</span> : ""; | ||
const childrenSpan = <span className="ds-button-label">{children}</span>; | ||
iconLeft = formatIcon(iconLeft); | ||
iconRight = formatIcon(iconRight); | ||
|
||
// for links that look like buttons, we want to add an event handler so that it can | ||
// be activated with the space bar | ||
// see: https://github.com/digitalservicebund/a2j-rechtsantragstelle/commit/43710c9e7d59e06f304830cc7e6b92893e7c7aa1#commitcomment-144257987 | ||
const onKeyDown = (event: React.KeyboardEvent<HTMLAnchorElement>) => { | ||
if (event.code === "Space") { | ||
event.currentTarget.click(); | ||
event.preventDefault(); | ||
} | ||
}; | ||
|
||
if (href) { | ||
const isExternal = isExternalUrl(href); | ||
const isFile = isFileDownloadUrl(href); | ||
const opts = | ||
isFile || isExternal ? { target: "_blank", rel: "noopener" } : {}; | ||
if (isExternal) opts.rel = "noopener noreferrer"; | ||
|
||
return ( | ||
<a | ||
{...props} | ||
href={href} | ||
className={buttonClasses} | ||
onKeyDown={onKeyDown} | ||
{...opts} | ||
> | ||
{iconLeft} {children ? childrenSpan : textSpan} {iconRight} | ||
</a> | ||
); | ||
} | ||
|
||
return ( | ||
<button {...(props as ButtonProps)} className={buttonClasses}> | ||
{iconLeft} {children ? childrenSpan : textSpan} {iconRight} | ||
</button> | ||
); | ||
} | ||
|
||
export default Button; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export function isExternalUrl(url: string) { | ||
return url.startsWith("https://"); | ||
} | ||
|
||
export function isFileDownloadUrl(url: string) { | ||
return url.endsWith("/download/pdf"); | ||
} | ||
|
||
export function getYoutubeVideoId(url: string): string | undefined { | ||
const regex = /(?:youtu\.be\/|youtube\.com\/watch\?v=)(\w+)/; | ||
const match = url.match(regex); | ||
if (match) { | ||
return match[1]; | ||
} | ||
return undefined; | ||
} |