From 46c47bb853ecfa2cd3c12469d9cce36d92365c03 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 19 Aug 2025 17:00:27 +0200 Subject: [PATCH 01/12] Add reusable LocalSwitcher and ThemeModeSelector --- .../translations/LocaleSwitcher.tsx | 51 +++++++++++++++++++ .../ui/theme/ThemeModeSelector.tsx | 37 ++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx create mode 100644 application/shared-webapp/ui/theme/ThemeModeSelector.tsx diff --git a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx new file mode 100644 index 0000000000..51dd65fd85 --- /dev/null +++ b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx @@ -0,0 +1,51 @@ +import { useLingui } from "@lingui/react"; +import type { Key } from "@react-types/shared"; +import { Button } from "@repo/ui/components/Button"; +import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu"; +import { CheckIcon, GlobeIcon } from "lucide-react"; +import { use, useMemo } from "react"; +import { preferredLocaleKey } from "./constants"; +import { type Locale, translationContext } from "./TranslationContext"; + +export function LocaleSwitcher({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { + const { setLocale, getLocaleInfo, locales } = use(translationContext); + const { i18n } = useLingui(); + + const items = useMemo( + () => + locales.map((locale) => ({ + id: locale, + label: getLocaleInfo(locale).label + })), + [locales, getLocaleInfo] + ); + + const handleLocaleChange = (key: Key) => { + const locale = key.toString() as Locale; + if (locale !== currentLocale) { + setLocale(locale).then(() => { + localStorage.setItem(preferredLocaleKey, locale); + }); + } + }; + + const currentLocale = i18n.locale as Locale; + + return ( + + + + {items.map((item) => ( + +
+ {item.label} + {item.id === currentLocale && } +
+
+ ))} +
+
+ ); +} diff --git a/application/shared-webapp/ui/theme/ThemeModeSelector.tsx b/application/shared-webapp/ui/theme/ThemeModeSelector.tsx new file mode 100644 index 0000000000..29bb896963 --- /dev/null +++ b/application/shared-webapp/ui/theme/ThemeModeSelector.tsx @@ -0,0 +1,37 @@ +import { Button } from "@repo/ui/components/Button"; +import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip"; +import { MoonIcon, MoonStarIcon, SunIcon, SunMoonIcon } from "lucide-react"; +import { toggleThemeMode, useThemeMode } from "./mode/ThemeMode"; +import { SystemThemeMode, ThemeMode } from "./mode/utils"; + +/** + * A button that toggles the theme mode between system, light and dark. + */ +export function ThemeModeSelector({ "aria-label": ariaLabel }: Readonly<{ "aria-label": string }>) { + const { themeMode, resolvedThemeMode, setThemeMode } = useThemeMode(); + + const tooltipText = getTooltipText(themeMode, resolvedThemeMode); + + return ( + + + {tooltipText} + + ); +} + +function getTooltipText(themeMode: ThemeMode, resolvedThemeMode: SystemThemeMode): string { + if (resolvedThemeMode === SystemThemeMode.Dark) { + return themeMode === ThemeMode.System ? "System mode (dark)" : "Dark mode"; + } + return themeMode === ThemeMode.System ? "System mode (light)" : "Light mode"; +} + +function ThemeModeIcon({ themeMode, resolvedThemeMode }: { themeMode: ThemeMode; resolvedThemeMode: SystemThemeMode }) { + if (resolvedThemeMode === SystemThemeMode.Dark) { + return themeMode === ThemeMode.System ? : ; + } + return themeMode === ThemeMode.System ? : ; +} From 2d295dd3c48abe025a4c31fe1dc4ae0d36ded03f Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Tue, 19 Aug 2025 17:01:17 +0200 Subject: [PATCH 02/12] Add MultiSelect component for multiple selection dropdowns --- .../ui/components/MultiSelect.tsx | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 application/shared-webapp/ui/components/MultiSelect.tsx diff --git a/application/shared-webapp/ui/components/MultiSelect.tsx b/application/shared-webapp/ui/components/MultiSelect.tsx new file mode 100644 index 0000000000..72eeedf34e --- /dev/null +++ b/application/shared-webapp/ui/components/MultiSelect.tsx @@ -0,0 +1,206 @@ +/** + * MultiSelect component for multiple selection dropdowns + * Based on the Select component but optimized for multiple selection + */ +import { ChevronDown, XIcon } from "lucide-react"; +import type React from "react"; +import { useContext, useEffect, useRef, useState } from "react"; +import { + Button, + DialogTrigger, + FormValidationContext, + type Key, + ListBox, + type ListBoxItemProps, + type ListBoxProps, + type Selection, + type ValidationResult +} from "react-aria-components"; +import { tv } from "tailwind-variants"; +import { Description } from "./Description"; +import { DropdownItem, DropdownSection, type DropdownSectionProps } from "./Dropdown"; +import { FieldError } from "./FieldError"; +import { focusRing } from "./focusRing"; +import { Label } from "./Label"; +import { Popover } from "./Popover"; + +const buttonStyles = tv({ + extend: focusRing, + base: "flex h-10 w-full min-w-[150px] cursor-default items-center gap-4 rounded-md border border-input bg-input-background py-2 pr-2 pl-3 text-start text-foreground transition", + variants: { + isInvalid: { + true: "border-destructive group-invalid:border-destructive forced-colors:group-invalid:border-[Mark]" + }, + isDisabled: { + false: "pressed:bg-active-background pressed:text-accent-foreground hover:bg-hover-background", + true: "opacity-50 forced-colors:border-[GrayText] forced-colors:text-[GrayText]" + } + } +}); + +export type MultiSelectItemShape = { + id: Key; + label: string; +}; + +export interface MultiSelectProps extends Omit, "children"> { + label?: string; + description?: string; + errorMessage?: string | ((validation: ValidationResult) => string); + items: Iterable; + selectedKeys: Selection; + onSelectionChange: (keys: Selection) => void; + children: React.ReactNode | ((item: T) => React.ReactNode); + className?: string; + placeholder?: string; + isReadOnly?: boolean; + name?: string; // for validation context +} + +export function MultiSelect({ + items, + selectedKeys, + onSelectionChange, + children, + label, + description, + errorMessage, + className, + placeholder = "Select options...", + isReadOnly = false, + name, + ...listBoxProps +}: MultiSelectProps) { + const errors = useContext(FormValidationContext); + const isInvalid = Boolean(name != null && name in errors ? errors?.[name] : undefined); + + const buttonRef = useRef(null); + const [popoverWidth, setPopoverWidth] = useState(); + + useEffect(() => { + if (!buttonRef.current) { + return; + } + const observer = new window.ResizeObserver(() => { + if (buttonRef.current) { + setPopoverWidth(buttonRef.current.offsetWidth); + } + }); + observer.observe(buttonRef.current); + setPopoverWidth(buttonRef.current.offsetWidth); + return () => observer.disconnect(); + }, []); + + const itemsArr = items ? Array.from(items) : []; + const hasItems = itemsArr.length > 0; + + return ( +
+ + {label && } + + {description && {description}} + {errorMessage} + {hasItems && ( + + + {children} + + + )} + +
+ ); +} + +export function MultiSelectItem(props: Readonly) { + // Only wrap string children in the span for truncation + return ( + + {typeof props.children === "string" ? ( + {props.children} + ) : ( + props.children + )} + + ); +} + +export function MultiSelectSection(props: Readonly>) { + return ; +} + +interface MultiSelectValueDisplayProps { + items: Iterable; + selectedKeys: Selection; + onRemove: (key: Key) => void; + placeholder?: string; +} + +export function MultiSelectValueDisplay({ + items, + selectedKeys, + onRemove, + placeholder +}: MultiSelectValueDisplayProps) { + const itemsArr = Array.isArray(items) ? items : Array.from(items); + const selectedItems = itemsArr.filter((item) => (selectedKeys instanceof Set ? selectedKeys.has(item.id) : false)); + + if (selectedItems.length === 0) { + return {placeholder}; + } + + return ( +
+ + {selectedItems[0].label} + { + e.stopPropagation(); + onRemove(selectedItems[0].id); + }} + aria-label="Remove" + /> + + {selectedItems.length > 1 && ( + +{selectedItems.length - 1} + )} +
+ ); +} From 5eb875355ef216d77fd4142780f275a58130f1ff Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Wed, 15 Oct 2025 16:36:34 +0200 Subject: [PATCH 03/12] Add fixed dialog width utilities to prevent layout inconsistencies across languages --- .../common/AcceptInvitationDialog.tsx | 2 +- .../federated-modules/common/SupportDialog.tsx | 2 +- .../common/UserProfileModal.tsx | 5 ++++- .../users/-components/InviteUserDialog.tsx | 2 +- .../-components/TenantNameRequiredDialog.tsx | 2 +- .../ui/components/AlertDialog.tsx | 8 +++++++- .../shared-webapp/ui/components/Modal.tsx | 6 +++--- .../shared-webapp/ui/tailwind-preset.ts | 18 +++++++++++++++++- 8 files changed, 35 insertions(+), 10 deletions(-) diff --git a/application/account-management/WebApp/federated-modules/common/AcceptInvitationDialog.tsx b/application/account-management/WebApp/federated-modules/common/AcceptInvitationDialog.tsx index 73ceb5589a..6c0844d0e7 100644 --- a/application/account-management/WebApp/federated-modules/common/AcceptInvitationDialog.tsx +++ b/application/account-management/WebApp/federated-modules/common/AcceptInvitationDialog.tsx @@ -54,7 +54,7 @@ export function AcceptInvitationDialog({ return ( - + onOpenChange(false)} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" diff --git a/application/account-management/WebApp/federated-modules/common/SupportDialog.tsx b/application/account-management/WebApp/federated-modules/common/SupportDialog.tsx index 4507bd9146..47f0970adc 100644 --- a/application/account-management/WebApp/federated-modules/common/SupportDialog.tsx +++ b/application/account-management/WebApp/federated-modules/common/SupportDialog.tsx @@ -16,7 +16,7 @@ export function SupportDialog({ children }: Readonly) { {children} - + {({ close }) => ( <> diff --git a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx index 3604092bf3..b0eef0123f 100644 --- a/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx +++ b/application/account-management/WebApp/federated-modules/common/UserProfileModal.tsx @@ -135,7 +135,10 @@ export default function UserProfileModal({ isOpen, onOpenChange }: Readonly ) : ( - + Update your profile picture and personal details here.}> diff --git a/application/account-management/WebApp/routes/admin/users/-components/InviteUserDialog.tsx b/application/account-management/WebApp/routes/admin/users/-components/InviteUserDialog.tsx index 85a05227ca..145d212bb5 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/InviteUserDialog.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/InviteUserDialog.tsx @@ -34,7 +34,7 @@ export default function InviteUserDialog({ isOpen, onOpenChange }: Readonly - + onOpenChange(false)} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" diff --git a/application/account-management/WebApp/routes/admin/users/-components/TenantNameRequiredDialog.tsx b/application/account-management/WebApp/routes/admin/users/-components/TenantNameRequiredDialog.tsx index 13e11c6213..797e8a5450 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/TenantNameRequiredDialog.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/TenantNameRequiredDialog.tsx @@ -16,7 +16,7 @@ interface TenantNameRequiredDialogProps { export function TenantNameRequiredDialog({ isOpen, onOpenChange }: Readonly) { return ( - + {({ close }) => ( <> diff --git a/application/shared-webapp/ui/components/AlertDialog.tsx b/application/shared-webapp/ui/components/AlertDialog.tsx index 3058e7132f..7ce615227c 100644 --- a/application/shared-webapp/ui/components/AlertDialog.tsx +++ b/application/shared-webapp/ui/components/AlertDialog.tsx @@ -2,6 +2,7 @@ import { AlertCircleIcon, InfoIcon } from "lucide-react"; import { type ReactNode, useId } from "react"; import { chain } from "react-aria"; import type { DialogProps } from "react-aria-components"; +import { twMerge } from "tailwind-merge"; import { tv } from "tailwind-variants"; import { Button } from "./Button"; /** @@ -45,7 +46,12 @@ export function AlertDialog({ }: Readonly) { const contentId = useId(); return ( - + {({ close }) => ( <> {title} diff --git a/application/shared-webapp/ui/components/Modal.tsx b/application/shared-webapp/ui/components/Modal.tsx index 7495fd7455..5ddd6eacef 100644 --- a/application/shared-webapp/ui/components/Modal.tsx +++ b/application/shared-webapp/ui/components/Modal.tsx @@ -43,7 +43,7 @@ const overlayStyles = tv({ }); const modalStyles = tv({ - base: "flex w-full flex-col overflow-hidden bg-popover bg-clip-padding text-left align-middle text-foreground shadow-2xl dark:backdrop-blur-2xl dark:backdrop-saturate-200 forced-colors:bg-[Canvas]", + base: "flex flex-col overflow-hidden bg-popover bg-clip-padding text-left align-middle text-foreground shadow-2xl dark:backdrop-blur-2xl dark:backdrop-saturate-200 forced-colors:bg-[Canvas]", variants: { isEntering: { true: "zoom-in-105 animate-in duration-200 ease-out" @@ -52,8 +52,8 @@ const modalStyles = tv({ true: "zoom-out-95 animate-out duration-200 ease-in" }, isFullScreenMobile: { - true: "max-sm:h-full max-sm:max-h-full max-sm:rounded-none max-sm:border-0 sm:max-h-[calc(100vh-2rem)] sm:w-fit sm:rounded-lg sm:border sm:border-border", - false: "max-h-[calc(100vh-2rem)] rounded-lg border border-border sm:w-fit" + true: "max-sm:h-full max-sm:max-h-full max-sm:w-full max-sm:rounded-none max-sm:border-0 sm:max-h-[calc(100vh-2rem)] sm:rounded-lg sm:border sm:border-border", + false: "max-h-[calc(100vh-2rem)] rounded-lg border border-border" } }, defaultVariants: { diff --git a/application/shared-webapp/ui/tailwind-preset.ts b/application/shared-webapp/ui/tailwind-preset.ts index 9ba0b4c442..07f2dc7855 100644 --- a/application/shared-webapp/ui/tailwind-preset.ts +++ b/application/shared-webapp/ui/tailwind-preset.ts @@ -37,6 +37,22 @@ export default { plugins: [ require("tailwindcss-react-aria-components"), require("tailwindcss-animate"), - require("@tailwindcss/container-queries") + require("@tailwindcss/container-queries"), + function ({ addUtilities }: { addUtilities: (utilities: Record>) => void }) { + addUtilities({ + ".w-dialog-md": { + "width": "28rem" + }, + ".w-dialog-lg": { + "width": "36rem" + }, + ".w-dialog-xl": { + "width": "44rem" + }, + ".w-dialog-2xl": { + "width": "52rem" + } + }); + } ] } satisfies Config; From 271d0148889ebbc8ea94f01746e1dab0eb873afe Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 9 Nov 2025 19:13:11 +0100 Subject: [PATCH 04/12] Migrate custom dialog utilities from addUtilities to @utility directive --- .../shared-webapp/ui/tailwind-preset.ts | 18 +----------------- application/shared-webapp/ui/tailwind.css | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/application/shared-webapp/ui/tailwind-preset.ts b/application/shared-webapp/ui/tailwind-preset.ts index 07f2dc7855..9ba0b4c442 100644 --- a/application/shared-webapp/ui/tailwind-preset.ts +++ b/application/shared-webapp/ui/tailwind-preset.ts @@ -37,22 +37,6 @@ export default { plugins: [ require("tailwindcss-react-aria-components"), require("tailwindcss-animate"), - require("@tailwindcss/container-queries"), - function ({ addUtilities }: { addUtilities: (utilities: Record>) => void }) { - addUtilities({ - ".w-dialog-md": { - "width": "28rem" - }, - ".w-dialog-lg": { - "width": "36rem" - }, - ".w-dialog-xl": { - "width": "44rem" - }, - ".w-dialog-2xl": { - "width": "52rem" - } - }); - } + require("@tailwindcss/container-queries") ] } satisfies Config; diff --git a/application/shared-webapp/ui/tailwind.css b/application/shared-webapp/ui/tailwind.css index c8c7a400a4..cb6fe5fa5b 100644 --- a/application/shared-webapp/ui/tailwind.css +++ b/application/shared-webapp/ui/tailwind.css @@ -127,6 +127,23 @@ --radius-sm: calc(var(--radius) - 4px); } +/* Custom dialog width utilities */ +@utility w-dialog-md { + width: 28rem; +} + +@utility w-dialog-lg { + width: 36rem; +} + +@utility w-dialog-xl { + width: 44rem; +} + +@utility w-dialog-2xl { + width: 52rem; +} + @layer base { * { @apply border-border; From 21d734e2be3feb499a00903855488dad40b8d95f Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 16 Nov 2025 11:19:11 +0100 Subject: [PATCH 05/12] Fix Modal component to pass props only to ModalOverlay --- application/shared-webapp/ui/components/Modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/shared-webapp/ui/components/Modal.tsx b/application/shared-webapp/ui/components/Modal.tsx index 5ddd6eacef..dc2c343aeb 100644 --- a/application/shared-webapp/ui/components/Modal.tsx +++ b/application/shared-webapp/ui/components/Modal.tsx @@ -74,7 +74,7 @@ export function Modal({ position, fullSize, blur, zIndex, ...props }: Readonly overlayStyles({ position, fullSize, blur, zIndex, ...renderProps })} > - + {props.children} ); } From d9d4397c64a4cf5713f59148622b97fa59596a84 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 16 Nov 2025 13:21:29 +0100 Subject: [PATCH 06/12] Lock verification code input after pasting to prevent extra characters --- .../ui/components/OneTimeCodeInput.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/application/shared-webapp/ui/components/OneTimeCodeInput.tsx b/application/shared-webapp/ui/components/OneTimeCodeInput.tsx index 4d0bb3cbf4..43b7c39aeb 100644 --- a/application/shared-webapp/ui/components/OneTimeCodeInput.tsx +++ b/application/shared-webapp/ui/components/OneTimeCodeInput.tsx @@ -24,6 +24,7 @@ export const OneTimeCodeInput = forwardRef(new Array(length).fill("")); + const [isLocked, setIsLocked] = useState(false); const id = useId(); const digitRefs = useMemo(() => new Array(length).fill(id).map((id, i) => `${id}_${i}`), [id, length]); const inputValue = digits.join(""); @@ -52,6 +53,7 @@ export const OneTimeCodeInput = forwardRef ({ reset: () => { setDigits(new Array(length).fill("")); + setIsLocked(false); setFocus(0); }, focus: () => { @@ -63,15 +65,23 @@ export const OneTimeCodeInput = forwardRef { + const onChangeHandler = (value: string, i: number, _isPaste: boolean = false): void => { let newDigits: string[]; if (value.length > 1) { // If the user pastes more than one digit const pastedDigits = value.substring(0, length).split(""); - newDigits = [...pastedDigits]; + // Pad with empty strings to maintain the full length + newDigits = new Array(length).fill(""); + pastedDigits.forEach((digit, index) => { + newDigits[index] = digit; + }); setDigits(newDigits); setFocus(pastedDigits.length); + // Only lock if the pasted code is complete + if (pastedDigits.length === length) { + setIsLocked(true); + } } else { newDigits = [...digits]; newDigits[i] = value; @@ -96,7 +106,7 @@ export const OneTimeCodeInput = forwardRef onChangeHandler(value, i)} autoFocus={i === inputValue.length && autoFocus} - disabled={disabled} + disabled={disabled || isLocked} /> ))} From 679341b6a9d9759f96d2f57ec8bc07aa2933af70 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 16 Nov 2025 14:10:16 +0100 Subject: [PATCH 07/12] Remove incorrect aria-hidden attributes from Table wrapper divs --- application/shared-webapp/ui/components/Table.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/application/shared-webapp/ui/components/Table.tsx b/application/shared-webapp/ui/components/Table.tsx index 216eb2f929..6f6e8707ea 100644 --- a/application/shared-webapp/ui/components/Table.tsx +++ b/application/shared-webapp/ui/components/Table.tsx @@ -40,14 +40,13 @@ export function Table({ disableHorizontalScroll, ...props }: Readonly -
+
+
From 80bc84384a26b911e8ef6b70bce87fba4c08349e Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 16 Nov 2025 14:17:33 +0100 Subject: [PATCH 08/12] Add aria-label to newsletter email input for accessibility --- .../WebApp/routes/(index)/-components/FooterSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/account-management/WebApp/routes/(index)/-components/FooterSection.tsx b/application/account-management/WebApp/routes/(index)/-components/FooterSection.tsx index 1002258f90..b41ddf5f12 100644 --- a/application/account-management/WebApp/routes/(index)/-components/FooterSection.tsx +++ b/application/account-management/WebApp/routes/(index)/-components/FooterSection.tsx @@ -12,7 +12,7 @@ export function FooterSection() {
Technology that has your back.
- + {/* Button component is used to display a call to action */}
From b3e23aee7720632eef23a474ee239a36bc202d75 Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sun, 16 Nov 2025 18:10:50 +0100 Subject: [PATCH 09/12] Add tooltip support to all form field components --- .../shared-webapp/ui/components/ComboBox.tsx | 4 ++- .../ui/components/DatePicker.tsx | 4 ++- .../ui/components/DateRangePicker.tsx | 4 ++- .../shared-webapp/ui/components/Label.tsx | 34 +++++++++++++++++-- .../ui/components/MultiSelect.tsx | 10 +++--- .../ui/components/NumberField.tsx | 5 +-- .../ui/components/SearchField.tsx | 12 +++++-- .../shared-webapp/ui/components/Select.tsx | 4 ++- .../shared-webapp/ui/components/TextArea.tsx | 13 +++++-- .../shared-webapp/ui/components/TextField.tsx | 4 ++- 10 files changed, 76 insertions(+), 18 deletions(-) diff --git a/application/shared-webapp/ui/components/ComboBox.tsx b/application/shared-webapp/ui/components/ComboBox.tsx index e5094ec198..4478f2fc8c 100644 --- a/application/shared-webapp/ui/components/ComboBox.tsx +++ b/application/shared-webapp/ui/components/ComboBox.tsx @@ -25,6 +25,7 @@ export interface ComboBoxProps extends Omit string); + tooltip?: string; isOpen?: boolean; placeholder?: string; children: React.ReactNode | ((item: T) => React.ReactNode); @@ -34,6 +35,7 @@ export function ComboBox({ label, description, errorMessage, + tooltip, isOpen, children, items, @@ -41,7 +43,7 @@ export function ComboBox({ }: Readonly>) { return ( - {label && } + {label && } + {tooltip} + + + + ); } diff --git a/application/shared-webapp/ui/components/MultiSelect.tsx b/application/shared-webapp/ui/components/MultiSelect.tsx index 72eeedf34e..96c699ed71 100644 --- a/application/shared-webapp/ui/components/MultiSelect.tsx +++ b/application/shared-webapp/ui/components/MultiSelect.tsx @@ -47,6 +47,7 @@ export interface MultiSelectProps extends Omit string); + tooltip?: string; items: Iterable; selectedKeys: Selection; onSelectionChange: (keys: Selection) => void; @@ -65,12 +66,13 @@ export function MultiSelect({ label, description, errorMessage, + tooltip, className, placeholder = "Select options...", isReadOnly = false, name, ...listBoxProps -}: MultiSelectProps) { +}: Readonly>) { const errors = useContext(FormValidationContext); const isInvalid = Boolean(name != null && name in errors ? errors?.[name] : undefined); @@ -81,7 +83,7 @@ export function MultiSelect({ if (!buttonRef.current) { return; } - const observer = new window.ResizeObserver(() => { + const observer = new globalThis.ResizeObserver(() => { if (buttonRef.current) { setPopoverWidth(buttonRef.current.offsetWidth); } @@ -97,7 +99,7 @@ export function MultiSelect({ return (
- {label && } + {label && }