Skip to content

Commit 973e0b4

Browse files
Merge pull request #150 from linked-planet/dev
Dev
2 parents 003a270 + f5371fe commit 973e0b4

File tree

11 files changed

+1380
-801
lines changed

11 files changed

+1380
-801
lines changed

library/src/components/Calendar.tsx

Lines changed: 294 additions & 205 deletions
Large diffs are not rendered by default.

library/src/components/Modal.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type ModalDialogProps = {
3535
testId?: string
3636
triggerTestId?: string
3737
/** The accessible description for the dialog, is visually hidden but announced by screen readers */
38+
accessibleDialogTitle: string
3839
accessibleDialogDescription: string
3940
role?: RDialog.DialogContentProps["role"]
4041
tabIndex?: RDialog.DialogContentProps["tabIndex"]
@@ -65,6 +66,7 @@ function Container({
6566
triggerTestId,
6667
role = "dialog",
6768
accessibleDialogDescription,
69+
accessibleDialogTitle,
6870
tabIndex = undefined,
6971
ref,
7072
}: ModalDialogProps) {
@@ -117,8 +119,13 @@ function Container({
117119
}}
118120
role={role}
119121
tabIndex={tabIndex}
122+
aria-describedby={accessibleDialogDescription}
123+
title={accessibleDialogDescription}
120124
>
121125
<VisuallyHidden>
126+
<RDialog.DialogTitle>
127+
{accessibleDialogTitle}
128+
</RDialog.DialogTitle>
122129
<RDialog.DialogDescription>
123130
{accessibleDialogDescription}
124131
</RDialog.DialogDescription>
@@ -140,6 +147,7 @@ function Container({
140147
accessibleDialogDescription,
141148
role,
142149
tabIndex,
150+
accessibleDialogTitle,
143151
],
144152
)
145153

@@ -157,7 +165,11 @@ function Container({
157165
asChild
158166
ref={triggerRef}
159167
>
160-
<Button>{"Open Modal"}</Button>
168+
{typeof trigger === "string" ? (
169+
<Button>{trigger}</Button>
170+
) : (
171+
trigger
172+
)}
161173
</RDialog.Trigger>
162174
)}
163175

@@ -234,7 +246,6 @@ function Title({
234246
style,
235247
id,
236248
testId,
237-
accessibleDialogTitle,
238249
}: {
239250
children: ReactNode
240251
className?: string
@@ -251,11 +262,6 @@ function Title({
251262
id={id}
252263
data-testid={testId}
253264
>
254-
<VisuallyHidden>
255-
<RDialog.DialogTitle>
256-
{accessibleDialogTitle}
257-
</RDialog.DialogTitle>
258-
</VisuallyHidden>
259265
{children}
260266
</RDialog.Title>
261267
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { type IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner"
2+
import { useCallback, useRef } from "react"
3+
import { Modal } from "./Modal"
4+
import { Button } from "./Button"
5+
import { XIcon } from "lucide-react"
6+
7+
// react-qr-scanner does not work in SSR, so we need to use a client component
8+
9+
/**
10+
* A simple QR code reader component using {@link @yudiel/react-qr-scanner}.
11+
* Requires https to access the camera if not on localhost!
12+
*
13+
* @param className - The class name to apply to the component.
14+
* @param style - The style to apply to the component.
15+
* @param onScan - The function to call when a QR code is scanned.
16+
* @param allowMultiple - Whether to allow multiple QR codes to be scanned.
17+
* @param sound - Whether to play a sound when a QR code is scanned.
18+
* @param onError - The function to call when an error occurs.
19+
* @returns A QR code reader component.
20+
*/
21+
export function QrReader({
22+
className,
23+
style,
24+
onScan,
25+
allowMultiple = false,
26+
sound = true,
27+
onError,
28+
}: {
29+
className?: string
30+
style?: React.CSSProperties
31+
onScan: (result: string[]) => void
32+
allowMultiple?: boolean
33+
sound?: boolean
34+
onError?: (error: Error) => void
35+
}) {
36+
const onScanCB = useCallback(
37+
(result: IDetectedBarcode[]) => {
38+
const resultStrings = result.map((r) => r.rawValue)
39+
onScan(resultStrings)
40+
},
41+
[onScan],
42+
)
43+
44+
const onErrorCB = useCallback(
45+
(error: unknown) => {
46+
console.log("QReader Error", error)
47+
if (error instanceof Error) {
48+
onError?.(error)
49+
}
50+
},
51+
[onError],
52+
)
53+
54+
return (
55+
<div className={className} style={style}>
56+
<Scanner
57+
onScan={onScanCB}
58+
allowMultiple={allowMultiple}
59+
sound={sound}
60+
components={{
61+
torch: true,
62+
finder: true,
63+
zoom: true,
64+
onOff: false,
65+
}}
66+
scanDelay={0}
67+
onError={onErrorCB}
68+
/>
69+
</div>
70+
)
71+
}
72+
73+
export function QrReaderDialog({
74+
title,
75+
description,
76+
open,
77+
defaultOpen,
78+
onOpenChange,
79+
shouldCloseOnEscapePress = true,
80+
shouldCloseOnOverlayClick = true,
81+
shouldCloseOnScan,
82+
accessibleDialogTitle,
83+
accessibleDialogDescription,
84+
trigger,
85+
usePortal = true,
86+
onScan,
87+
className,
88+
style,
89+
}: {
90+
title?: React.ReactNode
91+
description?: React.ReactNode
92+
open?: boolean
93+
defaultOpen?: boolean
94+
onOpenChange?: (open: boolean) => void
95+
shouldCloseOnEscapePress?: boolean
96+
shouldCloseOnOverlayClick?: boolean
97+
shouldCloseOnScan: boolean
98+
accessibleDialogTitle: string
99+
accessibleDialogDescription: string
100+
trigger: React.ReactNode
101+
usePortal?: boolean
102+
onScan: (result: string[]) => void
103+
className?: string
104+
style?: React.CSSProperties
105+
}) {
106+
const closeTriggerRef = useRef<HTMLButtonElement>(null)
107+
108+
const onScanCB = useCallback(
109+
(result: string[]) => {
110+
onScan(result)
111+
if (shouldCloseOnScan) {
112+
closeTriggerRef.current?.click()
113+
}
114+
},
115+
[shouldCloseOnScan, onScan],
116+
)
117+
118+
return (
119+
<Modal.Container
120+
trigger={
121+
typeof trigger === "string" ? (
122+
<Button className={className} style={style}>
123+
{trigger}
124+
</Button>
125+
) : (
126+
trigger
127+
)
128+
}
129+
usePortal={usePortal}
130+
accessibleDialogTitle={accessibleDialogTitle}
131+
accessibleDialogDescription={accessibleDialogDescription}
132+
open={open}
133+
defaultOpen={defaultOpen}
134+
onOpenChange={onOpenChange}
135+
shouldCloseOnEscapePress={shouldCloseOnEscapePress}
136+
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
137+
>
138+
<Modal.Header>
139+
{title && (
140+
<Modal.Title accessibleDialogTitle={accessibleDialogTitle}>
141+
{title}
142+
</Modal.Title>
143+
)}
144+
<Modal.CloseTrigger asChild>
145+
<Button
146+
appearance="subtle"
147+
type="button"
148+
ref={closeTriggerRef}
149+
className="ml-auto"
150+
>
151+
<XIcon aria-label="Close popup" size="16" />
152+
</Button>
153+
</Modal.CloseTrigger>
154+
</Modal.Header>
155+
<Modal.Body>
156+
{description && <>{description}</>}
157+
<QrReader onScan={onScanCB} />
158+
</Modal.Body>
159+
</Modal.Container>
160+
)
161+
}

library/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ export type { DynamicFormTypes } from "./form"
4242
export * from "./EventList"
4343
export * from "./HighlightedText"
4444
export * from "./SlideOpen"
45+
export * from "./QrReader"

library/src/components/timetable/CreateNewItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const CreateNewTimeTableItemDialog = function CreateNewTimeTableItemDialog({
3030
return (
3131
<Modal.Container
3232
defaultOpen
33+
accessibleDialogTitle="Create New Booking"
3334
accessibleDialogDescription="A dialog to create a new time slot booking."
3435
>
3536
<form

0 commit comments

Comments
 (0)