@databiosphere/findable-ui 50.2.0 → 50.4.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.
Files changed (38) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/lib/components/Export/components/ExportMethod/exportMethod.d.ts +1 -2
  4. package/lib/components/Export/components/ExportMethod/exportMethod.js +7 -7
  5. package/lib/components/Export/components/ExportMethod/exportMethod.styles.d.ts +2 -3
  6. package/lib/components/Export/components/ExportMethod/exportMethod.styles.js +26 -3
  7. package/lib/components/Export/components/ExportMethod/stories/exportMethod.stories.d.ts +6 -0
  8. package/lib/components/Export/components/ExportMethod/{exportMethod.stories.js → stories/exportMethod.stories.js} +1 -4
  9. package/lib/components/Layout/components/Footer/components/PoweredByCleverCanary/poweredByCleverCanary.js +1 -1
  10. package/lib/views/ResearchView/assistant/assistant.js +1 -1
  11. package/lib/views/ResearchView/assistant/components/Form/form.js +1 -9
  12. package/lib/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/hook.d.ts +6 -0
  13. package/lib/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/hook.js +26 -0
  14. package/lib/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/types.d.ts +9 -0
  15. package/lib/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/types.js +1 -0
  16. package/lib/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/hook.d.ts +3 -1
  17. package/lib/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/hook.js +6 -5
  18. package/lib/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/types.d.ts +3 -1
  19. package/lib/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/utils.d.ts +13 -7
  20. package/lib/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/utils.js +15 -11
  21. package/lib/views/ResearchView/assistant/components/Input/input.js +7 -5
  22. package/lib/views/ResearchView/assistant/components/Input/types.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/src/components/Export/components/ExportMethod/exportMethod.styles.ts +27 -4
  25. package/src/components/Export/components/ExportMethod/exportMethod.tsx +47 -43
  26. package/src/components/Export/components/ExportMethod/{exportMethod.stories.tsx → stories/exportMethod.stories.tsx} +4 -7
  27. package/src/components/Layout/components/Footer/components/PoweredByCleverCanary/poweredByCleverCanary.tsx +1 -0
  28. package/src/views/ResearchView/assistant/assistant.tsx +1 -1
  29. package/src/views/ResearchView/assistant/components/Form/form.tsx +1 -8
  30. package/src/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/hook.ts +33 -0
  31. package/src/views/ResearchView/assistant/components/Input/hooks/UseControlledInput/types.ts +12 -0
  32. package/src/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/hook.ts +10 -8
  33. package/src/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/types.ts +4 -1
  34. package/src/views/ResearchView/assistant/components/Input/hooks/UseKeyShortCuts/utils.ts +19 -17
  35. package/src/views/ResearchView/assistant/components/Input/input.tsx +15 -6
  36. package/src/views/ResearchView/assistant/components/Input/types.ts +2 -1
  37. package/tests/research.useKeyShortCuts.test.ts +26 -24
  38. package/lib/components/Export/components/ExportMethod/exportMethod.stories.d.ts +0 -22
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "50.2.0"
2
+ ".": "50.4.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [50.4.0](https://github.com/DataBiosphere/findable-ui/compare/v50.3.0...v50.4.0) (2026-03-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * refactor exportmethod to card-based layout ([#848](https://github.com/DataBiosphere/findable-ui/issues/848)) ([bfc09d3](https://github.com/DataBiosphere/findable-ui/commit/bfc09d35c4e3d5fa55a7ab6f9e50eb88b23f31f0))
9
+ * update export/download ui ([#848](https://github.com/DataBiosphere/findable-ui/issues/848)) ([#853](https://github.com/DataBiosphere/findable-ui/issues/853)) ([bfc09d3](https://github.com/DataBiosphere/findable-ui/commit/bfc09d35c4e3d5fa55a7ab6f9e50eb88b23f31f0))
10
+
11
+ ## [50.3.0](https://github.com/DataBiosphere/findable-ui/compare/v50.2.1...v50.3.0) (2026-03-27)
12
+
13
+
14
+ ### Features
15
+
16
+ * convert input to controlled component with inputprovider ([#815](https://github.com/DataBiosphere/findable-ui/issues/815)) ([#843](https://github.com/DataBiosphere/findable-ui/issues/843)) ([699f186](https://github.com/DataBiosphere/findable-ui/commit/699f186f8bff76a0806e2afe93bfd5815fea698b))
17
+
18
+ ## [50.2.1](https://github.com/DataBiosphere/findable-ui/compare/v50.2.0...v50.2.1) (2026-03-25)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * set poweredbyclevercanary image height to 32px ([#845](https://github.com/DataBiosphere/findable-ui/issues/845)) ([fd1cbcc](https://github.com/DataBiosphere/findable-ui/commit/fd1cbcc985f4b56d7cca53d5eddf0c710d889c92))
24
+
3
25
  ## [50.2.0](https://github.com/DataBiosphere/findable-ui/compare/v50.1.1...v50.2.0) (2026-03-25)
4
26
 
5
27
 
@@ -1,11 +1,10 @@
1
1
  import { JSX, ReactNode } from "react";
2
2
  import { TrackingProps } from "../../../types";
3
3
  export interface ExportMethodProps extends TrackingProps {
4
- buttonLabel: string;
5
4
  description: ReactNode;
6
5
  footnote?: ReactNode;
7
6
  isAccessible?: boolean;
8
7
  route: string;
9
8
  title: string;
10
9
  }
11
- export declare const ExportMethod: ({ buttonLabel, description, footnote, isAccessible, route, title, trackingId, }: ExportMethodProps) => JSX.Element;
10
+ export declare const ExportMethod: ({ description, footnote, isAccessible, route, title, trackingId, }: ExportMethodProps) => JSX.Element;
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Tooltip, Typography } from "@mui/material";
2
+ import { ChevronRightRounded } from "@mui/icons-material";
3
+ import { CardActionArea, CardActions, CardContent, Tooltip, Typography, } from "@mui/material";
3
4
  import Link from "next/link";
4
5
  import { useDownloadStatus } from "../../../../hooks/useDownloadStatus";
6
+ import { SVG_ICON_PROPS } from "../../../../styles/common/mui/svgIcon";
5
7
  import { TYPOGRAPHY_PROPS } from "../../../../styles/common/mui/typography";
6
- import { FluidPaper } from "../../../common/Paper/paper.styles";
7
- import { SectionTitle } from "../../../common/Section/components/SectionTitle/sectionTitle";
8
- import { Section, SectionActions, SectionContent, } from "../../../common/Section/section.styles";
9
- import { ExportButton } from "./exportMethod.styles";
10
- export const ExportMethod = ({ buttonLabel, description, footnote, isAccessible = true, route, title, trackingId, }) => {
8
+ import { FluidPaper } from "../../../common/Paper/components/FluidPaper/fluidPaper";
9
+ import { StyledCard } from "./exportMethod.styles";
10
+ export const ExportMethod = ({ description, footnote, isAccessible = true, route, title, trackingId, }) => {
11
11
  const { disabled, message } = useDownloadStatus();
12
- return (_jsx(FluidPaper, { children: _jsxs(Section, { children: [_jsxs(SectionContent, { children: [_jsx(SectionTitle, { title: title }), _jsx(Typography, { variant: TYPOGRAPHY_PROPS.VARIANT.BODY_400_2_LINES, children: description })] }), _jsx(SectionActions, { children: _jsx(Tooltip, { arrow: true, title: message, children: _jsx("span", { children: _jsx(ExportButton, { component: Link, disabled: disabled || !isAccessible, href: route, id: trackingId, children: buttonLabel }) }) }) }), footnote && (_jsx(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, component: "div", variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400_2_LINES, children: footnote }))] }) }));
12
+ return (_jsx(Tooltip, { arrow: true, title: message, children: _jsx(StyledCard, { component: FluidPaper, elevation: 1, children: _jsxs(CardActionArea, { component: Link, disabled: disabled || !isAccessible, href: route, id: trackingId, children: [_jsxs(CardContent, { children: [_jsx(Typography, { component: "h3", variant: TYPOGRAPHY_PROPS.VARIANT.HEADING_XSMALL, children: title }), _jsx(Typography, { component: "div", variant: TYPOGRAPHY_PROPS.VARIANT.BODY_400_2_LINES, children: description }), footnote && (_jsx(Typography, { color: TYPOGRAPHY_PROPS.COLOR.INK_LIGHT, component: "div", variant: TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400_2_LINES, children: footnote }))] }), _jsx(CardActions, { children: _jsx(ChevronRightRounded, { color: SVG_ICON_PROPS.COLOR.INK_LIGHT }) })] }) }) }));
13
13
  };
@@ -1,3 +1,2 @@
1
- export declare const ExportButton: import("@emotion/styled").StyledComponent<Omit<import("../../../common/Button/button").ButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement> & {
2
- theme?: import("@emotion/react").Theme;
3
- }, {}, {}>;
1
+ import { Card } from "@mui/material";
2
+ export declare const StyledCard: typeof Card;
@@ -1,5 +1,28 @@
1
1
  import styled from "@emotion/styled";
2
- import { ButtonPrimary } from "../../../common/Button/components/ButtonPrimary/buttonPrimary";
3
- export const ExportButton = styled(ButtonPrimary) `
4
- text-transform: none; // overrides MuiButton theme text-transform "capitalize".
2
+ import { Card } from "@mui/material";
3
+ import { sectionPadding } from "../../../common/Section/section.styles";
4
+ export const StyledCard = styled(Card) `
5
+ .MuiCardActionArea-root {
6
+ display: grid;
7
+ gap: 16px;
8
+ grid-template-columns: 1fr auto;
9
+ ${sectionPadding};
10
+ text-decoration: none;
11
+
12
+ &.Mui-disabled {
13
+ opacity: 0.6;
14
+ }
15
+
16
+ .MuiCardContent-root {
17
+ padding: 0;
18
+
19
+ h3 {
20
+ margin-bottom: 4px;
21
+ }
22
+ }
23
+
24
+ .MuiCardActions-root {
25
+ padding: 0;
26
+ }
27
+ }
5
28
  `;
@@ -0,0 +1,6 @@
1
+ import { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { ExportMethod } from "../exportMethod";
3
+ declare const meta: Meta<typeof ExportMethod>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const ExportMethodStory: Story;
@@ -1,18 +1,15 @@
1
- import { ExportMethod } from "./exportMethod";
1
+ import { ExportMethod } from "../exportMethod";
2
2
  const meta = {
3
3
  argTypes: {
4
- buttonLabel: { control: "text" },
5
4
  description: { control: "text" },
6
5
  route: { control: "text" },
7
6
  title: { control: "text" },
8
7
  },
9
8
  component: ExportMethod,
10
- title: "Components/Section/Export/ExportMethod",
11
9
  };
12
10
  export default meta;
13
11
  export const ExportMethodStory = {
14
12
  args: {
15
- buttonLabel: "Request curl Command",
16
13
  description: "Obtain a curl command for downloading the selected data.",
17
14
  route: "/request-curl-command",
18
15
  title: "Download Study Data and Metadata (Curl Command)",
@@ -13,5 +13,5 @@ import { Logo } from "../../../Header/components/Content/components/Logo/logo";
13
13
  export const PoweredByCleverCanary = ({ alt = "Powered by CleverCanary", className, ...props /* StaticImageProps */ }) => {
14
14
  if (!props.src)
15
15
  return null;
16
- return (_jsx(Logo, { alt: alt, className: className, link: "https://www.clevercanary.com", src: props.src, target: ANCHOR_TARGET.BLANK }));
16
+ return (_jsx(Logo, { alt: alt, className: className, height: 32, link: "https://www.clevercanary.com", src: props.src, target: ANCHOR_TARGET.BLANK }));
17
17
  };
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useChatState } from "../state/hooks/UseChatState/hook";
3
+ import { Drawer } from "./components/Drawer/drawer";
3
4
  import { Form } from "./components/Form/form";
4
5
  import { Input } from "./components/Input/input";
5
6
  import { getPlaceholder } from "./components/Input/utils";
6
7
  import { Messages } from "./components/Messages/messages";
7
- import { Drawer } from "./components/Drawer/drawer";
8
8
  import { ToggleButtonGroup } from "./components/ToggleButtonGroup/toggleButtonGroup";
9
9
  /**
10
10
  * Renders the research assistant drawer.
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { TEST_IDS } from "../../../../../tests/testIds";
3
3
  import { useQuery } from "../../../state/query/hooks/UseQuery/hook";
4
- import { FIELD_NAME } from "./constants";
5
4
  import { StyledForm } from "./form.styles";
6
5
  import { getPayload } from "./utils";
7
6
  /**
@@ -15,13 +14,6 @@ import { getPayload } from "./utils";
15
14
  export const Form = ({ children, className, status, }) => {
16
15
  const { onSubmit } = useQuery();
17
16
  return (_jsx(StyledForm, { className: className, "data-testid": TEST_IDS.RESEARCH_FORM, onSubmit: async (e) => {
18
- await onSubmit(e, getPayload(e), {
19
- onSettled: (form) => {
20
- const input = form.elements.namedItem(FIELD_NAME.AI_PROMPT);
21
- if (input instanceof HTMLElement)
22
- input.focus();
23
- },
24
- status,
25
- });
17
+ await onSubmit(e, getPayload(e), { status });
26
18
  }, children: children }));
27
19
  };
@@ -0,0 +1,6 @@
1
+ import { UseControlledInputProps } from "./types";
2
+ /**
3
+ * Manages controlled input state with automatic clearing on form reset.
4
+ * @returns The controlled input state including value, onChange, setValue, and inputRef.
5
+ */
6
+ export declare const useControlledInput: () => UseControlledInputProps;
@@ -0,0 +1,26 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ /**
3
+ * Manages controlled input state with automatic clearing on form reset.
4
+ * @returns The controlled input state including value, onChange, setValue, and inputRef.
5
+ */
6
+ export const useControlledInput = () => {
7
+ const inputRef = useRef(null);
8
+ const [value, setValue] = useState("");
9
+ const onChange = useCallback((e) => {
10
+ setValue(e.target.value);
11
+ }, []);
12
+ useEffect(() => {
13
+ const form = inputRef.current?.form;
14
+ if (!form)
15
+ return;
16
+ const onReset = () => {
17
+ setValue("");
18
+ inputRef.current?.focus();
19
+ };
20
+ form.addEventListener("reset", onReset);
21
+ return () => {
22
+ form.removeEventListener("reset", onReset);
23
+ };
24
+ }, []);
25
+ return { inputRef, onChange, setValue, value };
26
+ };
@@ -0,0 +1,9 @@
1
+ import { ChangeEvent, Dispatch, RefObject, SetStateAction } from "react";
2
+ export type InputElement = HTMLInputElement | HTMLTextAreaElement;
3
+ export type SetValue = Dispatch<SetStateAction<string>>;
4
+ export interface UseControlledInputProps {
5
+ inputRef: RefObject<InputElement | null>;
6
+ onChange: (e: ChangeEvent<InputElement>) => void;
7
+ setValue: SetValue;
8
+ value: string;
9
+ }
@@ -1,6 +1,8 @@
1
+ import { SetValue } from "../UseControlledInput/types";
1
2
  import { UseKeyShortCutsProps } from "./types";
2
3
  /**
3
4
  * Provides a keydown handler that implements keyboard shortcuts for the assistant input.
5
+ * @param setValue - Setter for the controlled input value.
4
6
  * @returns An object containing the onKeyDown handler.
5
7
  */
6
- export declare const useKeyShortCuts: () => UseKeyShortCutsProps;
8
+ export declare const useKeyShortCuts: (setValue: SetValue) => UseKeyShortCutsProps;
@@ -4,9 +4,10 @@ import { KEY } from "./constants";
4
4
  import { getHistory, handleArrowKey, handleEnterKey, handleEscapeKey, handleTabKey, } from "./utils";
5
5
  /**
6
6
  * Provides a keydown handler that implements keyboard shortcuts for the assistant input.
7
+ * @param setValue - Setter for the controlled input value.
7
8
  * @returns An object containing the onKeyDown handler.
8
9
  */
9
- export const useKeyShortCuts = () => {
10
+ export const useKeyShortCuts = (setValue) => {
10
11
  const { state } = useChatState();
11
12
  const { messages } = state;
12
13
  const history = getHistory(messages);
@@ -22,11 +23,11 @@ export const useKeyShortCuts = () => {
22
23
  if (e.key === KEY.ENTER)
23
24
  return handleEnterKey(e);
24
25
  if (e.key === KEY.ESCAPE)
25
- return handleEscapeKey(e, refs);
26
+ return handleEscapeKey(refs, setValue);
26
27
  if (e.key === KEY.ARROW_UP || e.key === KEY.ARROW_DOWN)
27
- return handleArrowKey(e, history, refs);
28
+ return handleArrowKey(e, history, refs, setValue);
28
29
  if (e.key === KEY.TAB)
29
- return handleTabKey(e);
30
- }, [history]);
30
+ return handleTabKey(e, setValue);
31
+ }, [history, setValue]);
31
32
  return { onKeyDown };
32
33
  };
@@ -1,8 +1,10 @@
1
1
  import { KeyboardEvent, RefObject } from "react";
2
+ import { InputElement } from "../UseControlledInput/types";
3
+ export type KeyboardInputEvent = KeyboardEvent<InputElement>;
2
4
  export interface Refs {
3
5
  draftRef: RefObject<string>;
4
6
  historyIndexRef: RefObject<number>;
5
7
  }
6
8
  export interface UseKeyShortCutsProps {
7
- onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
9
+ onKeyDown: (e: KeyboardInputEvent) => void;
8
10
  }
@@ -1,6 +1,6 @@
1
- import { KeyboardEvent } from "react";
2
1
  import { Message } from "../../../../../state/types";
3
- import { Refs } from "./types";
2
+ import { SetValue } from "../UseControlledInput/types";
3
+ import { KeyboardInputEvent, Refs } from "./types";
4
4
  /**
5
5
  * Extracts the text of user messages from a list of messages and returns them in reverse order.
6
6
  * @param messages - An array of Message objects to extract user messages from.
@@ -12,21 +12,27 @@ export declare function getHistory(messages: Message[]): string[];
12
12
  * @param e - The keyboard event.
13
13
  * @param history - The history entries to navigate.
14
14
  * @param refs - Refs for draft text and history index.
15
+ * @param setValue - Setter for the controlled input value.
16
+ * @returns Void.
15
17
  */
16
- export declare function handleArrowKey(e: KeyboardEvent<HTMLInputElement>, history: string[], refs: Refs): void;
18
+ export declare function handleArrowKey(e: KeyboardInputEvent, history: string[], refs: Refs, setValue: SetValue): void;
17
19
  /**
18
20
  * Handles the Enter key press to submit the form, or allows newline on Shift+Enter.
19
21
  * @param e - The keyboard event.
22
+ * @returns Void.
20
23
  */
21
- export declare function handleEnterKey(e: KeyboardEvent<HTMLInputElement>): void;
24
+ export declare function handleEnterKey(e: KeyboardInputEvent): void;
22
25
  /**
23
26
  * Handles the Escape key press to clear the input and reset history navigation.
24
- * @param e - The keyboard event.
25
27
  * @param refs - Refs for draft text and history index.
28
+ * @param setValue - Setter for the controlled input value.
29
+ * @returns Void.
26
30
  */
27
- export declare function handleEscapeKey(e: KeyboardEvent<HTMLInputElement>, refs: Refs): void;
31
+ export declare function handleEscapeKey(refs: Refs, setValue: SetValue): void;
28
32
  /**
29
33
  * Handles the Tab key press to auto-fill the input with the placeholder.
30
34
  * @param e - The keyboard event.
35
+ * @param setValue - Setter for the controlled input value.
36
+ * @returns Void.
31
37
  */
32
- export declare function handleTabKey(e: KeyboardEvent<HTMLInputElement>): void;
38
+ export declare function handleTabKey(e: KeyboardInputEvent, setValue: SetValue): void;
@@ -16,31 +16,33 @@ export function getHistory(messages) {
16
16
  * @param e - The keyboard event.
17
17
  * @param history - The history entries to navigate.
18
18
  * @param refs - Refs for draft text and history index.
19
+ * @param setValue - Setter for the controlled input value.
20
+ * @returns Void.
19
21
  */
20
- export function handleArrowKey(e, history, refs) {
22
+ export function handleArrowKey(e, history, refs, setValue) {
21
23
  const { draftRef, historyIndexRef } = refs;
22
- const inputEl = e.currentTarget;
23
24
  if (e.key === KEY.ARROW_DOWN && historyIndexRef.current === -1) {
24
25
  return;
25
26
  }
26
27
  if (historyIndexRef.current === -1) {
27
- draftRef.current = inputEl.value;
28
+ draftRef.current = e.currentTarget.value;
28
29
  }
29
30
  const currentIndex = historyIndexRef.current;
30
31
  const newIndex = e.key === KEY.ARROW_UP
31
32
  ? Math.min(currentIndex + 1, history.length - 1)
32
33
  : Math.max(currentIndex - 1, -1);
33
34
  if (newIndex === -1) {
34
- inputEl.value = draftRef.current;
35
+ setValue(draftRef.current);
35
36
  historyIndexRef.current = -1;
36
37
  return;
37
38
  }
38
- inputEl.value = history[newIndex] || "";
39
+ setValue(history[newIndex] || "");
39
40
  historyIndexRef.current = newIndex;
40
41
  }
41
42
  /**
42
43
  * Handles the Enter key press to submit the form, or allows newline on Shift+Enter.
43
44
  * @param e - The keyboard event.
45
+ * @returns Void.
44
46
  */
45
47
  export function handleEnterKey(e) {
46
48
  if (e.shiftKey)
@@ -51,24 +53,26 @@ export function handleEnterKey(e) {
51
53
  }
52
54
  /**
53
55
  * Handles the Escape key press to clear the input and reset history navigation.
54
- * @param e - The keyboard event.
55
56
  * @param refs - Refs for draft text and history index.
57
+ * @param setValue - Setter for the controlled input value.
58
+ * @returns Void.
56
59
  */
57
- export function handleEscapeKey(e, refs) {
60
+ export function handleEscapeKey(refs, setValue) {
58
61
  const { draftRef, historyIndexRef } = refs;
59
- const inputEl = e.currentTarget;
60
- inputEl.value = "";
62
+ setValue("");
61
63
  draftRef.current = "";
62
64
  historyIndexRef.current = -1;
63
65
  }
64
66
  /**
65
67
  * Handles the Tab key press to auto-fill the input with the placeholder.
66
68
  * @param e - The keyboard event.
69
+ * @param setValue - Setter for the controlled input value.
70
+ * @returns Void.
67
71
  */
68
- export function handleTabKey(e) {
72
+ export function handleTabKey(e, setValue) {
69
73
  const inputEl = e.currentTarget;
70
74
  if (inputEl.value)
71
75
  return;
72
76
  e.preventDefault();
73
- inputEl.value = inputEl.placeholder;
77
+ setValue(inputEl.placeholder);
74
78
  }
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { IconButton, InputBase, Stack } from "@mui/material";
3
- import { INPUT_BASE_PROPS } from "./constants";
4
- import { StyledBox, StyledPaper } from "./input.styles";
5
3
  import { UpArrowIcon } from "../../../../../components/common/CustomIcon/components/UpArrowIcon/upArrowIcon";
6
4
  import { ICON_BUTTON_PROPS } from "../../../../../styles/common/mui/iconButton";
7
- import { SVG_ICON_PROPS } from "../../../../../styles/common/mui/svgIcon";
8
5
  import { STACK_PROPS } from "../../../../../styles/common/mui/stack";
6
+ import { SVG_ICON_PROPS } from "../../../../../styles/common/mui/svgIcon";
7
+ import { INPUT_BASE_PROPS } from "./constants";
8
+ import { useControlledInput } from "./hooks/UseControlledInput/hook";
9
9
  import { useKeyShortCuts } from "./hooks/UseKeyShortCuts/hook";
10
+ import { StyledBox, StyledPaper } from "./input.styles";
10
11
  /**
11
12
  * Renders an input component for the research panel.
12
13
  * @param props - Component props.
@@ -14,6 +15,7 @@ import { useKeyShortCuts } from "./hooks/UseKeyShortCuts/hook";
14
15
  * @returns Research panel input component.
15
16
  */
16
17
  export const Input = ({ disabled, ...props }) => {
17
- const { onKeyDown } = useKeyShortCuts();
18
- return (_jsx(StyledBox, { children: _jsxs(StyledPaper, { elevation: 0, children: [_jsx(InputBase, { ...INPUT_BASE_PROPS, onKeyDown: onKeyDown, ...props }), _jsx(Stack, { direction: STACK_PROPS.DIRECTION.ROW, gap: 2, children: _jsx(IconButton, { color: ICON_BUTTON_PROPS.COLOR.SECONDARY, disabled: disabled, size: ICON_BUTTON_PROPS.SIZE.XSMALL, type: "submit", children: _jsx(UpArrowIcon, { fontSize: SVG_ICON_PROPS.FONT_SIZE.SMALL }) }) })] }) }));
18
+ const { inputRef, onChange, setValue, value } = useControlledInput();
19
+ const { onKeyDown } = useKeyShortCuts(setValue);
20
+ return (_jsx(StyledBox, { children: _jsxs(StyledPaper, { elevation: 0, children: [_jsx(InputBase, { ...INPUT_BASE_PROPS, inputRef: inputRef, onChange: onChange, onKeyDown: onKeyDown, value: value, ...props }), _jsx(Stack, { direction: STACK_PROPS.DIRECTION.ROW, gap: 2, children: _jsx(IconButton, { color: ICON_BUTTON_PROPS.COLOR.SECONDARY, disabled: disabled, size: ICON_BUTTON_PROPS.SIZE.XSMALL, type: "submit", children: _jsx(UpArrowIcon, { fontSize: SVG_ICON_PROPS.FONT_SIZE.SMALL }) }) })] }) }));
19
21
  };
@@ -1,2 +1,2 @@
1
1
  import { IconButtonProps, InputBaseProps } from "@mui/material";
2
- export type InputProps = InputBaseProps & Pick<IconButtonProps, "disabled">;
2
+ export type InputProps = Omit<InputBaseProps, "onChange" | "value"> & Pick<IconButtonProps, "disabled">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "50.2.0",
3
+ "version": "50.4.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
@@ -1,6 +1,29 @@
1
1
  import styled from "@emotion/styled";
2
- import { ButtonPrimary } from "../../../common/Button/components/ButtonPrimary/buttonPrimary";
2
+ import { Card } from "@mui/material";
3
+ import { sectionPadding } from "../../../common/Section/section.styles";
3
4
 
4
- export const ExportButton = styled(ButtonPrimary)`
5
- text-transform: none; // overrides MuiButton theme text-transform "capitalize".
6
- `;
5
+ export const StyledCard = styled(Card)`
6
+ .MuiCardActionArea-root {
7
+ display: grid;
8
+ gap: 16px;
9
+ grid-template-columns: 1fr auto;
10
+ ${sectionPadding};
11
+ text-decoration: none;
12
+
13
+ &.Mui-disabled {
14
+ opacity: 0.6;
15
+ }
16
+
17
+ .MuiCardContent-root {
18
+ padding: 0;
19
+
20
+ h3 {
21
+ margin-bottom: 4px;
22
+ }
23
+ }
24
+
25
+ .MuiCardActions-root {
26
+ padding: 0;
27
+ }
28
+ }
29
+ ` as typeof Card;
@@ -1,20 +1,21 @@
1
- import { Tooltip, Typography } from "@mui/material";
1
+ import { ChevronRightRounded } from "@mui/icons-material";
2
+ import {
3
+ CardActionArea,
4
+ CardActions,
5
+ CardContent,
6
+ Tooltip,
7
+ Typography,
8
+ } from "@mui/material";
2
9
  import Link from "next/link";
3
10
  import { JSX, ReactNode } from "react";
4
11
  import { useDownloadStatus } from "../../../../hooks/useDownloadStatus";
12
+ import { SVG_ICON_PROPS } from "../../../../styles/common/mui/svgIcon";
5
13
  import { TYPOGRAPHY_PROPS } from "../../../../styles/common/mui/typography";
6
- import { FluidPaper } from "../../../common/Paper/paper.styles";
7
- import { SectionTitle } from "../../../common/Section/components/SectionTitle/sectionTitle";
8
- import {
9
- Section,
10
- SectionActions,
11
- SectionContent,
12
- } from "../../../common/Section/section.styles";
14
+ import { FluidPaper } from "../../../common/Paper/components/FluidPaper/fluidPaper";
13
15
  import { TrackingProps } from "../../../types";
14
- import { ExportButton } from "./exportMethod.styles";
16
+ import { StyledCard } from "./exportMethod.styles";
15
17
 
16
18
  export interface ExportMethodProps extends TrackingProps {
17
- buttonLabel: string;
18
19
  description: ReactNode;
19
20
  footnote?: ReactNode;
20
21
  isAccessible?: boolean;
@@ -23,7 +24,6 @@ export interface ExportMethodProps extends TrackingProps {
23
24
  }
24
25
 
25
26
  export const ExportMethod = ({
26
- buttonLabel,
27
27
  description,
28
28
  footnote,
29
29
  isAccessible = true,
@@ -33,38 +33,42 @@ export const ExportMethod = ({
33
33
  }: ExportMethodProps): JSX.Element => {
34
34
  const { disabled, message } = useDownloadStatus();
35
35
  return (
36
- <FluidPaper>
37
- <Section>
38
- <SectionContent>
39
- <SectionTitle title={title} />
40
- <Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_400_2_LINES}>
41
- {description}
42
- </Typography>
43
- </SectionContent>
44
- <SectionActions>
45
- <Tooltip arrow title={message}>
46
- <span>
47
- <ExportButton
48
- component={Link}
49
- disabled={disabled || !isAccessible}
50
- href={route}
51
- id={trackingId}
36
+ <Tooltip arrow title={message}>
37
+ <StyledCard component={FluidPaper} elevation={1}>
38
+ <CardActionArea
39
+ component={Link}
40
+ disabled={disabled || !isAccessible}
41
+ href={route}
42
+ id={trackingId}
43
+ >
44
+ <CardContent>
45
+ <Typography
46
+ component="h3"
47
+ variant={TYPOGRAPHY_PROPS.VARIANT.HEADING_XSMALL}
48
+ >
49
+ {title}
50
+ </Typography>
51
+ <Typography
52
+ component="div"
53
+ variant={TYPOGRAPHY_PROPS.VARIANT.BODY_400_2_LINES}
54
+ >
55
+ {description}
56
+ </Typography>
57
+ {footnote && (
58
+ <Typography
59
+ color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
60
+ component="div"
61
+ variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400_2_LINES}
52
62
  >
53
- {buttonLabel}
54
- </ExportButton>
55
- </span>
56
- </Tooltip>
57
- </SectionActions>
58
- {footnote && (
59
- <Typography
60
- color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}
61
- component="div"
62
- variant={TYPOGRAPHY_PROPS.VARIANT.BODY_SMALL_400_2_LINES}
63
- >
64
- {footnote}
65
- </Typography>
66
- )}
67
- </Section>
68
- </FluidPaper>
63
+ {footnote}
64
+ </Typography>
65
+ )}
66
+ </CardContent>
67
+ <CardActions>
68
+ <ChevronRightRounded color={SVG_ICON_PROPS.COLOR.INK_LIGHT} />
69
+ </CardActions>
70
+ </CardActionArea>
71
+ </StyledCard>
72
+ </Tooltip>
69
73
  );
70
74
  };
@@ -1,16 +1,14 @@
1
- import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
- import { ExportMethod } from "./exportMethod";
1
+ import { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { ExportMethod } from "../exportMethod";
3
3
 
4
- const meta = {
4
+ const meta: Meta<typeof ExportMethod> = {
5
5
  argTypes: {
6
- buttonLabel: { control: "text" },
7
6
  description: { control: "text" },
8
7
  route: { control: "text" },
9
8
  title: { control: "text" },
10
9
  },
11
10
  component: ExportMethod,
12
- title: "Components/Section/Export/ExportMethod",
13
- } satisfies Meta<typeof ExportMethod>;
11
+ };
14
12
 
15
13
  export default meta;
16
14
 
@@ -18,7 +16,6 @@ type Story = StoryObj<typeof meta>;
18
16
 
19
17
  export const ExportMethodStory: Story = {
20
18
  args: {
21
- buttonLabel: "Request curl Command",
22
19
  description: "Obtain a curl command for downloading the selected data.",
23
20
  route: "/request-curl-command",
24
21
  title: "Download Study Data and Metadata (Curl Command)",
@@ -22,6 +22,7 @@ export const PoweredByCleverCanary = ({
22
22
  <Logo
23
23
  alt={alt}
24
24
  className={className}
25
+ height={32}
25
26
  link="https://www.clevercanary.com"
26
27
  src={props.src}
27
28
  target={ANCHOR_TARGET.BLANK}
@@ -1,10 +1,10 @@
1
1
  import { JSX } from "react";
2
2
  import { useChatState } from "../state/hooks/UseChatState/hook";
3
+ import { Drawer } from "./components/Drawer/drawer";
3
4
  import { Form } from "./components/Form/form";
4
5
  import { Input } from "./components/Input/input";
5
6
  import { getPlaceholder } from "./components/Input/utils";
6
7
  import { Messages } from "./components/Messages/messages";
7
- import { Drawer } from "./components/Drawer/drawer";
8
8
  import { ToggleButtonGroup } from "./components/ToggleButtonGroup/toggleButtonGroup";
9
9
 
10
10
  /**
@@ -1,7 +1,6 @@
1
1
  import { JSX } from "react";
2
2
  import { TEST_IDS } from "../../../../../tests/testIds";
3
3
  import { useQuery } from "../../../state/query/hooks/UseQuery/hook";
4
- import { FIELD_NAME } from "./constants";
5
4
  import { StyledForm } from "./form.styles";
6
5
  import { FormProps } from "./types";
7
6
  import { getPayload } from "./utils";
@@ -25,13 +24,7 @@ export const Form = ({
25
24
  className={className}
26
25
  data-testid={TEST_IDS.RESEARCH_FORM}
27
26
  onSubmit={async (e) => {
28
- await onSubmit(e, getPayload(e), {
29
- onSettled: (form) => {
30
- const input = form.elements.namedItem(FIELD_NAME.AI_PROMPT);
31
- if (input instanceof HTMLElement) input.focus();
32
- },
33
- status,
34
- });
27
+ await onSubmit(e, getPayload(e), { status });
35
28
  }}
36
29
  >
37
30
  {children}
@@ -0,0 +1,33 @@
1
+ import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
2
+ import { InputElement, UseControlledInputProps } from "./types";
3
+
4
+ /**
5
+ * Manages controlled input state with automatic clearing on form reset.
6
+ * @returns The controlled input state including value, onChange, setValue, and inputRef.
7
+ */
8
+ export const useControlledInput = (): UseControlledInputProps => {
9
+ const inputRef = useRef<InputElement>(null);
10
+ const [value, setValue] = useState<string>("");
11
+
12
+ const onChange = useCallback((e: ChangeEvent<InputElement>): void => {
13
+ setValue(e.target.value);
14
+ }, []);
15
+
16
+ useEffect(() => {
17
+ const form = inputRef.current?.form;
18
+ if (!form) return;
19
+
20
+ const onReset = (): void => {
21
+ setValue("");
22
+ inputRef.current?.focus();
23
+ };
24
+
25
+ form.addEventListener("reset", onReset);
26
+
27
+ return (): void => {
28
+ form.removeEventListener("reset", onReset);
29
+ };
30
+ }, []);
31
+
32
+ return { inputRef, onChange, setValue, value };
33
+ };
@@ -0,0 +1,12 @@
1
+ import { ChangeEvent, Dispatch, RefObject, SetStateAction } from "react";
2
+
3
+ export type InputElement = HTMLInputElement | HTMLTextAreaElement;
4
+
5
+ export type SetValue = Dispatch<SetStateAction<string>>;
6
+
7
+ export interface UseControlledInputProps {
8
+ inputRef: RefObject<InputElement | null>;
9
+ onChange: (e: ChangeEvent<InputElement>) => void;
10
+ setValue: SetValue;
11
+ value: string;
12
+ }
@@ -1,7 +1,8 @@
1
- import { KeyboardEvent, useCallback, useEffect, useRef } from "react";
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { SetValue } from "../UseControlledInput/types";
2
3
  import { useChatState } from "../../../../../state/hooks/UseChatState/hook";
3
4
  import { KEY } from "./constants";
4
- import { UseKeyShortCutsProps } from "./types";
5
+ import { KeyboardInputEvent, UseKeyShortCutsProps } from "./types";
5
6
  import {
6
7
  getHistory,
7
8
  handleArrowKey,
@@ -12,9 +13,10 @@ import {
12
13
 
13
14
  /**
14
15
  * Provides a keydown handler that implements keyboard shortcuts for the assistant input.
16
+ * @param setValue - Setter for the controlled input value.
15
17
  * @returns An object containing the onKeyDown handler.
16
18
  */
17
- export const useKeyShortCuts = (): UseKeyShortCutsProps => {
19
+ export const useKeyShortCuts = (setValue: SetValue): UseKeyShortCutsProps => {
18
20
  const { state } = useChatState();
19
21
  const { messages } = state;
20
22
 
@@ -30,15 +32,15 @@ export const useKeyShortCuts = (): UseKeyShortCutsProps => {
30
32
  }, [messages]);
31
33
 
32
34
  const onKeyDown = useCallback(
33
- (e: KeyboardEvent<HTMLInputElement>) => {
35
+ (e: KeyboardInputEvent) => {
34
36
  const refs = { draftRef, historyIndexRef };
35
37
  if (e.key === KEY.ENTER) return handleEnterKey(e);
36
- if (e.key === KEY.ESCAPE) return handleEscapeKey(e, refs);
38
+ if (e.key === KEY.ESCAPE) return handleEscapeKey(refs, setValue);
37
39
  if (e.key === KEY.ARROW_UP || e.key === KEY.ARROW_DOWN)
38
- return handleArrowKey(e, history, refs);
39
- if (e.key === KEY.TAB) return handleTabKey(e);
40
+ return handleArrowKey(e, history, refs, setValue);
41
+ if (e.key === KEY.TAB) return handleTabKey(e, setValue);
40
42
  },
41
- [history],
43
+ [history, setValue],
42
44
  );
43
45
 
44
46
  return { onKeyDown };
@@ -1,4 +1,7 @@
1
1
  import { KeyboardEvent, RefObject } from "react";
2
+ import { InputElement } from "../UseControlledInput/types";
3
+
4
+ export type KeyboardInputEvent = KeyboardEvent<InputElement>;
2
5
 
3
6
  export interface Refs {
4
7
  draftRef: RefObject<string>;
@@ -6,5 +9,5 @@ export interface Refs {
6
9
  }
7
10
 
8
11
  export interface UseKeyShortCutsProps {
9
- onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
12
+ onKeyDown: (e: KeyboardInputEvent) => void;
10
13
  }
@@ -1,8 +1,8 @@
1
- import { KeyboardEvent } from "react";
2
1
  import { isUserMessage } from "../../../../../state/guards/guards";
3
2
  import { Message } from "../../../../../state/types";
4
- import { Refs } from "./types";
3
+ import { SetValue } from "../UseControlledInput/types";
5
4
  import { KEY } from "./constants";
5
+ import { KeyboardInputEvent, Refs } from "./types";
6
6
 
7
7
  /**
8
8
  * Extracts the text of user messages from a list of messages and returns them in reverse order.
@@ -21,21 +21,23 @@ export function getHistory(messages: Message[]): string[] {
21
21
  * @param e - The keyboard event.
22
22
  * @param history - The history entries to navigate.
23
23
  * @param refs - Refs for draft text and history index.
24
+ * @param setValue - Setter for the controlled input value.
25
+ * @returns Void.
24
26
  */
25
27
  export function handleArrowKey(
26
- e: KeyboardEvent<HTMLInputElement>,
28
+ e: KeyboardInputEvent,
27
29
  history: string[],
28
30
  refs: Refs,
31
+ setValue: SetValue,
29
32
  ): void {
30
33
  const { draftRef, historyIndexRef } = refs;
31
- const inputEl = e.currentTarget;
32
34
 
33
35
  if (e.key === KEY.ARROW_DOWN && historyIndexRef.current === -1) {
34
36
  return;
35
37
  }
36
38
 
37
39
  if (historyIndexRef.current === -1) {
38
- draftRef.current = inputEl.value;
40
+ draftRef.current = e.currentTarget.value;
39
41
  }
40
42
 
41
43
  const currentIndex = historyIndexRef.current;
@@ -45,20 +47,21 @@ export function handleArrowKey(
45
47
  : Math.max(currentIndex - 1, -1);
46
48
 
47
49
  if (newIndex === -1) {
48
- inputEl.value = draftRef.current;
50
+ setValue(draftRef.current);
49
51
  historyIndexRef.current = -1;
50
52
  return;
51
53
  }
52
54
 
53
- inputEl.value = history[newIndex] || "";
55
+ setValue(history[newIndex] || "");
54
56
  historyIndexRef.current = newIndex;
55
57
  }
56
58
 
57
59
  /**
58
60
  * Handles the Enter key press to submit the form, or allows newline on Shift+Enter.
59
61
  * @param e - The keyboard event.
62
+ * @returns Void.
60
63
  */
61
- export function handleEnterKey(e: KeyboardEvent<HTMLInputElement>): void {
64
+ export function handleEnterKey(e: KeyboardInputEvent): void {
62
65
  if (e.shiftKey) return;
63
66
  e.preventDefault();
64
67
  const formEl = e.currentTarget.form;
@@ -67,16 +70,13 @@ export function handleEnterKey(e: KeyboardEvent<HTMLInputElement>): void {
67
70
 
68
71
  /**
69
72
  * Handles the Escape key press to clear the input and reset history navigation.
70
- * @param e - The keyboard event.
71
73
  * @param refs - Refs for draft text and history index.
74
+ * @param setValue - Setter for the controlled input value.
75
+ * @returns Void.
72
76
  */
73
- export function handleEscapeKey(
74
- e: KeyboardEvent<HTMLInputElement>,
75
- refs: Refs,
76
- ): void {
77
+ export function handleEscapeKey(refs: Refs, setValue: SetValue): void {
77
78
  const { draftRef, historyIndexRef } = refs;
78
- const inputEl = e.currentTarget;
79
- inputEl.value = "";
79
+ setValue("");
80
80
  draftRef.current = "";
81
81
  historyIndexRef.current = -1;
82
82
  }
@@ -84,10 +84,12 @@ export function handleEscapeKey(
84
84
  /**
85
85
  * Handles the Tab key press to auto-fill the input with the placeholder.
86
86
  * @param e - The keyboard event.
87
+ * @param setValue - Setter for the controlled input value.
88
+ * @returns Void.
87
89
  */
88
- export function handleTabKey(e: KeyboardEvent<HTMLInputElement>): void {
90
+ export function handleTabKey(e: KeyboardInputEvent, setValue: SetValue): void {
89
91
  const inputEl = e.currentTarget;
90
92
  if (inputEl.value) return;
91
93
  e.preventDefault();
92
- inputEl.value = inputEl.placeholder;
94
+ setValue(inputEl.placeholder);
93
95
  }
@@ -1,13 +1,14 @@
1
1
  import { IconButton, InputBase, Stack } from "@mui/material";
2
2
  import { JSX } from "react";
3
- import { INPUT_BASE_PROPS } from "./constants";
4
- import { StyledBox, StyledPaper } from "./input.styles";
5
3
  import { UpArrowIcon } from "../../../../../components/common/CustomIcon/components/UpArrowIcon/upArrowIcon";
6
4
  import { ICON_BUTTON_PROPS } from "../../../../../styles/common/mui/iconButton";
7
- import { SVG_ICON_PROPS } from "../../../../../styles/common/mui/svgIcon";
8
5
  import { STACK_PROPS } from "../../../../../styles/common/mui/stack";
9
- import { InputProps } from "./types";
6
+ import { SVG_ICON_PROPS } from "../../../../../styles/common/mui/svgIcon";
7
+ import { INPUT_BASE_PROPS } from "./constants";
8
+ import { useControlledInput } from "./hooks/UseControlledInput/hook";
10
9
  import { useKeyShortCuts } from "./hooks/UseKeyShortCuts/hook";
10
+ import { StyledBox, StyledPaper } from "./input.styles";
11
+ import { InputProps } from "./types";
11
12
 
12
13
  /**
13
14
  * Renders an input component for the research panel.
@@ -16,11 +17,19 @@ import { useKeyShortCuts } from "./hooks/UseKeyShortCuts/hook";
16
17
  * @returns Research panel input component.
17
18
  */
18
19
  export const Input = ({ disabled, ...props }: InputProps): JSX.Element => {
19
- const { onKeyDown } = useKeyShortCuts();
20
+ const { inputRef, onChange, setValue, value } = useControlledInput();
21
+ const { onKeyDown } = useKeyShortCuts(setValue);
20
22
  return (
21
23
  <StyledBox>
22
24
  <StyledPaper elevation={0}>
23
- <InputBase {...INPUT_BASE_PROPS} onKeyDown={onKeyDown} {...props} />
25
+ <InputBase
26
+ {...INPUT_BASE_PROPS}
27
+ inputRef={inputRef}
28
+ onChange={onChange}
29
+ onKeyDown={onKeyDown}
30
+ value={value}
31
+ {...props}
32
+ />
24
33
  <Stack direction={STACK_PROPS.DIRECTION.ROW} gap={2}>
25
34
  <IconButton
26
35
  color={ICON_BUTTON_PROPS.COLOR.SECONDARY}
@@ -1,3 +1,4 @@
1
1
  import { IconButtonProps, InputBaseProps } from "@mui/material";
2
2
 
3
- export type InputProps = InputBaseProps & Pick<IconButtonProps, "disabled">;
3
+ export type InputProps = Omit<InputBaseProps, "onChange" | "value"> &
4
+ Pick<IconButtonProps, "disabled">;
@@ -14,6 +14,7 @@ interface MockInputElement {
14
14
 
15
15
  // Mock useChatState
16
16
  const mockUseChatState = jest.fn();
17
+ const mockSetValue = jest.fn();
17
18
 
18
19
  jest.unstable_mockModule(
19
20
  "../src/views/ResearchView/state/hooks/UseChatState/hook",
@@ -96,12 +97,13 @@ function setupMockState(messages: Message[]): void {
96
97
  describe("useKeyShortCuts", () => {
97
98
  beforeEach(() => {
98
99
  mockUseChatState.mockReset();
100
+ mockSetValue.mockReset();
99
101
  setupMockState([]);
100
102
  });
101
103
 
102
104
  describe("enter key", () => {
103
105
  it("should prevent default and submit form on Enter", () => {
104
- const { result } = renderHook(() => useKeyShortCuts());
106
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
105
107
  const inputEl = createMockInputEl("some query");
106
108
  const event = createMockKeyEvent("Enter", inputEl);
107
109
 
@@ -112,7 +114,7 @@ describe("useKeyShortCuts", () => {
112
114
  });
113
115
 
114
116
  it("should not prevent default on Shift+Enter", () => {
115
- const { result } = renderHook(() => useKeyShortCuts());
117
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
116
118
  const inputEl = createMockInputEl("some query");
117
119
  const event = createMockKeyEvent("Enter", inputEl, true);
118
120
 
@@ -125,13 +127,13 @@ describe("useKeyShortCuts", () => {
125
127
 
126
128
  describe("escape key", () => {
127
129
  it("should clear input value on Escape", () => {
128
- const { result } = renderHook(() => useKeyShortCuts());
130
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
129
131
  const inputEl = createMockInputEl("some text");
130
132
  const event = createMockKeyEvent("Escape", inputEl);
131
133
 
132
134
  result.current.onKeyDown(event);
133
135
 
134
- expect(inputEl.value).toBe("");
136
+ expect(mockSetValue).toHaveBeenCalledWith("");
135
137
  });
136
138
  });
137
139
 
@@ -142,13 +144,13 @@ describe("useKeyShortCuts", () => {
142
144
  createPromptMessage("response"),
143
145
  createUserMessage("second query"),
144
146
  ]);
145
- const { result } = renderHook(() => useKeyShortCuts());
147
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
146
148
  const inputEl = createMockInputEl();
147
149
  const event = createMockKeyEvent("ArrowUp", inputEl);
148
150
 
149
151
  result.current.onKeyDown(event);
150
152
 
151
- expect(inputEl.value).toBe("second query");
153
+ expect(mockSetValue).toHaveBeenCalledWith("second query");
152
154
  });
153
155
 
154
156
  it("should navigate through multiple history entries on ArrowUp", () => {
@@ -156,25 +158,25 @@ describe("useKeyShortCuts", () => {
156
158
  createUserMessage("first query"),
157
159
  createUserMessage("second query"),
158
160
  ]);
159
- const { result } = renderHook(() => useKeyShortCuts());
161
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
160
162
  const inputEl = createMockInputEl();
161
163
 
162
164
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
163
- expect(inputEl.value).toBe("second query");
165
+ expect(mockSetValue).toHaveBeenLastCalledWith("second query");
164
166
 
165
167
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
166
- expect(inputEl.value).toBe("first query");
168
+ expect(mockSetValue).toHaveBeenLastCalledWith("first query");
167
169
  });
168
170
 
169
171
  it("should clamp at oldest history entry on ArrowUp", () => {
170
172
  setupMockState([createUserMessage("only query")]);
171
- const { result } = renderHook(() => useKeyShortCuts());
173
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
172
174
  const inputEl = createMockInputEl();
173
175
 
174
176
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
175
177
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
176
178
 
177
- expect(inputEl.value).toBe("only query");
179
+ expect(mockSetValue).toHaveBeenLastCalledWith("only query");
178
180
  });
179
181
 
180
182
  it("should navigate forward on ArrowDown and restore draft at index -1", () => {
@@ -182,63 +184,63 @@ describe("useKeyShortCuts", () => {
182
184
  createUserMessage("first query"),
183
185
  createUserMessage("second query"),
184
186
  ]);
185
- const { result } = renderHook(() => useKeyShortCuts());
187
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
186
188
  const inputEl = createMockInputEl("my draft");
187
189
 
188
190
  // Navigate up twice.
189
191
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
190
192
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
191
- expect(inputEl.value).toBe("first query");
193
+ expect(mockSetValue).toHaveBeenLastCalledWith("first query");
192
194
 
193
195
  // Navigate down once.
194
196
  result.current.onKeyDown(createMockKeyEvent("ArrowDown", inputEl));
195
- expect(inputEl.value).toBe("second query");
197
+ expect(mockSetValue).toHaveBeenLastCalledWith("second query");
196
198
 
197
199
  // Navigate down to restore draft.
198
200
  result.current.onKeyDown(createMockKeyEvent("ArrowDown", inputEl));
199
- expect(inputEl.value).toBe("my draft");
201
+ expect(mockSetValue).toHaveBeenLastCalledWith("my draft");
200
202
  });
201
203
 
202
204
  it("should not navigate on ArrowDown when not browsing history", () => {
203
205
  setupMockState([createUserMessage("some query")]);
204
- const { result } = renderHook(() => useKeyShortCuts());
206
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
205
207
  const inputEl = createMockInputEl("current text");
206
208
  const event = createMockKeyEvent("ArrowDown", inputEl);
207
209
 
208
210
  result.current.onKeyDown(event);
209
211
 
210
- expect(inputEl.value).toBe("current text");
212
+ expect(mockSetValue).not.toHaveBeenCalled();
211
213
  });
212
214
 
213
215
  it("should save draft before entering history", () => {
214
216
  setupMockState([createUserMessage("history entry")]);
215
- const { result } = renderHook(() => useKeyShortCuts());
217
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
216
218
  const inputEl = createMockInputEl("my draft text");
217
219
 
218
220
  // Navigate up to save draft and enter history.
219
221
  result.current.onKeyDown(createMockKeyEvent("ArrowUp", inputEl));
220
- expect(inputEl.value).toBe("history entry");
222
+ expect(mockSetValue).toHaveBeenLastCalledWith("history entry");
221
223
 
222
224
  // Navigate down to restore draft.
223
225
  result.current.onKeyDown(createMockKeyEvent("ArrowDown", inputEl));
224
- expect(inputEl.value).toBe("my draft text");
226
+ expect(mockSetValue).toHaveBeenLastCalledWith("my draft text");
225
227
  });
226
228
  });
227
229
 
228
230
  describe("tab key", () => {
229
231
  it("should fill input with placeholder when input is empty", () => {
230
- const { result } = renderHook(() => useKeyShortCuts());
232
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
231
233
  const inputEl = createMockInputEl("", "Search for studies...");
232
234
  const event = createMockKeyEvent("Tab", inputEl);
233
235
 
234
236
  result.current.onKeyDown(event);
235
237
 
236
238
  expect(event.preventDefault).toHaveBeenCalled();
237
- expect(inputEl.value).toBe("Search for studies...");
239
+ expect(mockSetValue).toHaveBeenCalledWith("Search for studies...");
238
240
  });
239
241
 
240
242
  it("should not prevent default when input has value", () => {
241
- const { result } = renderHook(() => useKeyShortCuts());
243
+ const { result } = renderHook(() => useKeyShortCuts(mockSetValue));
242
244
  const inputEl = createMockInputEl(
243
245
  "existing text",
244
246
  "Search for studies...",
@@ -248,7 +250,7 @@ describe("useKeyShortCuts", () => {
248
250
  result.current.onKeyDown(event);
249
251
 
250
252
  expect(event.preventDefault).not.toHaveBeenCalled();
251
- expect(inputEl.value).toBe("existing text");
253
+ expect(mockSetValue).not.toHaveBeenCalled();
252
254
  });
253
255
  });
254
256
  });
@@ -1,22 +0,0 @@
1
- import type { StoryObj } from "@storybook/nextjs-vite";
2
- declare const meta: {
3
- argTypes: {
4
- buttonLabel: {
5
- control: "text";
6
- };
7
- description: {
8
- control: "text";
9
- };
10
- route: {
11
- control: "text";
12
- };
13
- title: {
14
- control: "text";
15
- };
16
- };
17
- component: ({ buttonLabel, description, footnote, isAccessible, route, title, trackingId, }: import("./exportMethod").ExportMethodProps) => import("react").JSX.Element;
18
- title: string;
19
- };
20
- export default meta;
21
- type Story = StoryObj<typeof meta>;
22
- export declare const ExportMethodStory: Story;