Skip to content

Commit

Permalink
feat: create readaloud toggle
Browse files Browse the repository at this point in the history
  • Loading branch information
lublagg committed Nov 19, 2024
1 parent 8c5660d commit 6e00cda
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 4 deletions.
4 changes: 1 addition & 3 deletions src/components/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
background-color: white;
color: rgb(110,109,95);
text-align: center;
padding: 10px;

div {
margin: 10px;
}
h1 {
font-size: 12px;
font-weight: bold;
Expand Down
18 changes: 17 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { initializePlugin } from "@concord-consortium/codap-plugin-api";

import { ReadAloudMenu } from "./readaloud-menu";
import { ChatInputComponent } from "./chat-input";
import { ChatTranscriptComponent } from "./chat-transcript";
import { ChatTranscript, ChatMessage } from "../types";
Expand All @@ -27,11 +27,21 @@ const mockAiResponse = (): ChatMessage => {
export const App = () => {
const greeting = "Hello! I'm DAVAI, your Data Analysis through Voice and Artificial Intelligence partner.";
const [chatTranscript, setChatTranscript] = useState<ChatTranscript>({messages: [{speaker: "DAVAI", content: greeting, timestamp: timeStamp()}]});
const [readAloudEnabled, setReadAloudEnabled] = useState(false);
const [playbackSpeed, setPlaybackSpeed] = useState(1);

useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions});
}, []);

const handleSetReadAloudEnabled = () => {
setReadAloudEnabled(!readAloudEnabled);
};

const handleSetPlaybackSpeed = (speed: number) => {
setPlaybackSpeed(speed);
};

const handleChatInputSubmit = (messageText: string) => {
setChatTranscript(prevTranscript => ({
messages: [...prevTranscript.messages, { speaker: "User", content: messageText, timestamp: timeStamp() }]
Expand All @@ -52,6 +62,12 @@ export const App = () => {
</h1>
<ChatTranscriptComponent chatTranscript={chatTranscript} />
<ChatInputComponent onSubmit={handleChatInputSubmit} />
<ReadAloudMenu
enabled={readAloudEnabled}
onToggle={handleSetReadAloudEnabled}
playbackSpeed={playbackSpeed}
onPlaybackSpeedSelect={handleSetPlaybackSpeed}
/>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/chat-input.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@use "vars" as *;

.chat-input {
margin-top: 10px;
fieldset {
border: none;
display: flex;
Expand Down
102 changes: 102 additions & 0 deletions src/components/readaloud-menu.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
@import "./vars.scss";

.readaloud-controls {
display: flex;
align-items: center;
width: 100%;
gap: 10px;

.toggle {
display: flex;
align-items: center;
gap: 8px;
color: $dark-gray;

&.disabled {
color: rgba(121, 121, 121, 0.35);

input {
background-color: rgba(121, 121, 121, 0.35);

&:hover {
&::before {
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.35);
}
}
}
}

label {
cursor: pointer;
line-height: 1.4;
font-size: pxToRem(16);
user-select: none;
font-weight: bold;
white-space: nowrap;
}

input {
appearance: none;
min-width: 42px;
min-height: 16px;
margin: 0;

position: relative;
background-color: $medium-gray;
border-radius: 12px;

cursor: pointer;
user-select: none;
transition: background-color 0.3s;

&:checked {
background-color: $dark-teal;

&::before {
background-color: white;
left: unset;
transform: translateX(calc(100% - 10px));
}
}

&::before {
content: '';
position: absolute;
height: 24px;
width: 24px;
top: -5px;
left: 0;
bottom: 0;
object-fit: contain;
box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.35);
border: solid 1.5px #797979;
background-color: white;
transition: all 0.3s;
border-radius: 50%;
cursor: pointer;
}

&:hover {
&::before {
box-shadow: 0 0px 8px 2px $dark-teal;
}
}
}
}

select {
appearance: none;
background-color: $light-teal-3;
color: $dark-gray;
height: 32px;
width: 32px;
border-radius: 6px;
text-align: center;
font-family: 'Montserrat', sans-serif;
font-size: 12px;

option {
color: $dark-gray;
}
}
}
65 changes: 65 additions & 0 deletions src/components/readaloud-menu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react";
import { act, render, screen, within } from "@testing-library/react";

import { ReadAloudMenu } from "./readaloud-menu";

describe("test read aloud menu component", () => {
const mockHandleToggle = jest.fn();
const mockHandleSelect = jest.fn();

beforeEach(() => {
mockHandleToggle.mockClear();
mockHandleSelect.mockClear();
render(

Check failure on line 13 in src/components/readaloud-menu.test.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Forbidden usage of `render` within testing framework `beforeEach` setup
<ReadAloudMenu
enabled={false}
onToggle={mockHandleToggle}
playbackSpeed={1}
onPlaybackSpeedSelect={mockHandleSelect}
/>
);
});

it("renders a toggle switch that lets a user turn read-aloud mode on and off", () => {
const readAloudMenu = screen.getByRole("menu");
expect(readAloudMenu).toBeInTheDocument();
const readAloudToggle = screen.getByTestId("readaloud-toggle");
expect(readAloudToggle).toBeInTheDocument();
expect(readAloudToggle).toHaveAttribute("type", "checkbox");
expect(readAloudToggle).toHaveAttribute("role", "switch");
expect(readAloudToggle).toHaveAttribute("aria-checked", "false");
expect(readAloudToggle).not.toBeChecked();
act(() => readAloudToggle.click());
expect(mockHandleToggle).toHaveBeenCalledTimes(1);
act(() => readAloudToggle.click());
expect(mockHandleToggle).toHaveBeenCalledTimes(2);
});

it("renders a select dropdown that allows a user to select the playback speed of the readaloud", () => {
const readAloudPlaybackSpeed = screen.getByTestId("readaloud-playback-speed");
expect(readAloudPlaybackSpeed).toBeInTheDocument();
expect(readAloudPlaybackSpeed).toHaveAttribute("id", "readaloud-playback-speed");
expect(readAloudPlaybackSpeed).toHaveValue("1");
act(() => {
readAloudPlaybackSpeed.click();
const option3 = screen.getByTestId("playback-speed-option-3") as HTMLOptionElement;
expect(option3).toHaveValue("1.5");
option3.selected = true;
readAloudPlaybackSpeed.dispatchEvent(new Event('change', { bubbles: true }));

Check failure on line 48 in src/components/readaloud-menu.test.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Strings must use doublequote
});
expect(mockHandleSelect).toHaveBeenCalled();
expect(readAloudPlaybackSpeed).toHaveValue("1.5");
});

it("renders all playback speed options correctly", () => {
const readAloudPlaybackSpeed = screen.getByTestId("readaloud-playback-speed");
expect(readAloudPlaybackSpeed).toBeInTheDocument();
const options = within(readAloudPlaybackSpeed).getAllByRole("option");
expect(options).toHaveLength(4);
expect(options[0]).toHaveValue("0.5");
expect(options[1]).toHaveValue("1");
expect(options[2]).toHaveValue("1.5");
expect(options[3]).toHaveValue("2");
});

});

Check warning on line 65 in src/components/readaloud-menu.test.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Newline required at end of file but not found
57 changes: 57 additions & 0 deletions src/components/readaloud-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";

import "./readaloud-menu.scss";

interface IReadAloudMenuProps {
enabled: boolean;
onToggle: () => void;
playbackSpeed: number;
onPlaybackSpeedSelect: (speed: number) => void;
}

export const ReadAloudMenu = (props: IReadAloudMenuProps) => {
const { enabled, onToggle, playbackSpeed, onPlaybackSpeedSelect } = props;

const handleSelect = (event: React.ChangeEvent<HTMLSelectElement>) => {
onPlaybackSpeedSelect(parseFloat(event.target.value));
};

return (
<div className="readaloud-controls" role="menu">
<div role="menuitem" className="toggle">
<label htmlFor="readaloud-toggle" data-testid="toggle-label">
Tap text to listen
</label>
<input
data-testid="readaloud-toggle"
id="readaloud-toggle"
type="checkbox"
role="switch"
checked={enabled}
aria-checked={enabled}
onChange={onToggle}
/>
</div>
<div role="menuitem">
<label
data-testid="speed-label"
className="visually-hidden"
htmlFor="readaloud-playback-speed"
>
Select playback speed
</label>
<select
onChange={handleSelect}
data-testid="readaloud-playback-speed"
defaultValue={playbackSpeed}
id="readaloud-playback-speed"
>
<option data-testid="playback-speed-option-1" value={0.5}>.5x</option>
<option data-testid="playback-speed-option-2" value={1}>1x</option>
<option data-testid="playback-speed-option-3" value={1.5}>1.5x</option>
<option data-testid="playback-speed-option-4" value={2}>2x</option>
</select>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/vars.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
$light-teal-3: #dceff2;
$light-teal-2: #eef7f9;
$light-teal: #72bfca;
$dark-teal: #177991;
Expand Down

0 comments on commit 6e00cda

Please sign in to comment.