Repository: code-dot-org/code-dot-org
Pull request #64822: [Codebridge] Increase text size for code editor
Author: login: fisher-alice
Labels: name: accessibility
Created: 2025-03-26T16:40:23Z | Merged: 2025-04-02T17:37:26Z

<!--
  A summary of the change, including any relevant background, motivation, and context.
  If relevant, include a description, screenshots, and/or video of the existing and new behavior.
-->
This PR adds the ability to increase text size in the Codebridge code editor and includes:
- adding an icon-only settings button to the workspace header,
- displaying a custom dropdown dialog when the settings button is clicked that allows a user to select a pre-defined text size (tiny, small (current default), medium, large, huge),
- selected text size persists during a session per signed-in user per app (will be updated in a follow-up so that it persists across multiple sessions), 
- selected text size persists for a signed-out user across levels, but resets to default when signed-out user refreshes browser.

Note that I debated adding a tooltip to the 'settings' button because:
- there is an existing bug with the DSCO tooltip - the tooltip remains displayed even after clicking on the settings button,
- there are already tooltips on 'version history' and 'feedback' buttons so navigating is becoming a little hindered if another tooltip is added,
- the gear icon to represent 'settings' is well-known.

However, after consulting with @hannahbergam (who wrote 'i think even if the settings button is fairly universal, it’s good practice!'), I'm adding back the tooltip. Thanks Hannah! [Slack thread](https://codedotorg.slack.com/archives/C06JELCAPT9/p1743100604308069?thread_ts=1742576084.787909&cid=C06JELCAPT9)

A couple notes on implementation:
- I decided to add a `lab2View` slice to store `editorFontSizeKey`. Although `CodeEditor` is primarily a Codebridge component, it is housed as a `lab2` component and being used experimentally in `music`. Also - the value does quite fit in existing `lab2` slices - `lab2ProjectRedux`, `predictLevelRedux`, `systemRedux`.
-  Initially, I used a dialog for settings but Product/Design wanted 'Settings' implemented very similar to 'Version History'.   I pulled the logic to position the dropdown based on the location of the trigger button in a custom hook `UseDropdownPosition`. Note that we plan to add the ability to update the console font size, and may add other input to 'Settings'.


## After update

Screencast video showing a signed-in user selecting text size in `pythonlab` and `weblab2`:



https://github.com/user-attachments/assets/efbbc8a8-2d53-4077-aa45-7e7b3fa5ca86





Screencast video showing a signed-out user selecting text size in `pythonlab`:


https://github.com/user-attachments/assets/cf1540b8-d3bb-4a04-87eb-d4dd03198683




Screencast video using keyboard navigation:


https://github.com/user-attachments/assets/5d542a21-d706-45ad-9efc-3308bf8d55bd



## Links
[jira](https://codedotorg.atlassian.net/browse/CT-1147)
[spec doc](https://docs.google.com/document/d/127wCZr1fRebYj2QT7uthm2IKaTb6etgeOTs30BVYotQ/edit?tab=t.y4gf5o33mhbs#heading=h.qi8xxyq72bct)
<!--
  Links to relevant external resources; ie, specification documents, Jira tickets, related PRs, Honeybadger errors, etc.
-->

<!--
- spec: []()
- jira ticket: []()
-->

## Testing story
Tested locally in `pythonlab` and `weblab2` levels - both signed in and signed out.

<!--
  Does your change include appropriate tests?
  If so, please describe how the tests included in this PR are sufficient.
  If not, please explain why this change does not need to be tested.
-->

<!-- Other aspects to consider. Delete any sections that are not relevant to your change. -->

## Deployment strategy

## Follow-up work
- Update so that signed-in user selected font size persist across multiple sessions, i.e., store in user settings.
- Add statsig analytics [from spec doc](https://docs.google.com/document/d/127wCZr1fRebYj2QT7uthm2IKaTb6etgeOTs30BVYotQ/edit?tab=t.y4gf5o33mhbs#heading=h.ghfocbs7b58g) - P0 https://codedotorg.atlassian.net/browse/CT-1165
- Add ability to change text size in console - P1 https://codedotorg.atlassian.net/browse/CT-1166
- Localize strings associated with font size increase settings (editor and console) https://codedotorg.atlassian.net/browse/CT-1167
- Fix DSCO tooltip bug - https://codedotorg.atlassian.net/browse/CT-1164

<!--
  List (ideally with Jira links) any clean-up or technical debt that will be addressed in future work.
-->

## Privacy

<!--
  1.	Does this change involve the collection, use, or sharing of new Personal Data?
  2.	Does this change involve a new or changed use or sharing of existing Personal Data?
-->

## Security

<!-- Link to Jira task(s) where sensitive security issues are discussed privately. -->

## Caching

## PR Checklist:

<!--
  The final step! Before you create your PR, double-check that everything is in order.
  Change [ ] to [X] during creation to check boxes.
-->

- [ ] Tests provide adequate coverage
- [ ] Privacy and Security impacts have been assessed
- [ ] Code is well-commented
- [ ] New features are translatable or updates will not break translations
- [ ] Relevant documentation has been added or updated
- [ ] User impact is well-understood and desirable
- [ ] Pull Request is labeled appropriately
- [ ] Follow-up work items (including potential tech debt) are tracked and linked



Diff:
diff --git a/apps/i18n/codebridge/en_us.json b/apps/i18n/codebridge/en_us.json
index d172be2cf3649..52165b5d3a457 100644
--- a/apps/i18n/codebridge/en_us.json
+++ b/apps/i18n/codebridge/en_us.json
@@ -90,6 +90,7 @@
   "systemCodeError": "We're sorry, we hit an issue running your program. Please refresh the page and try again.",
   "testName": "Test Name",
   "testsDidNotPass": "Tests did not all pass.",
+  "textEditorFontSize": "Text editor font size",
   "toggleFileBrowser": "Toggle File Browser",
   "uploadFile": "Upload",
   "validate": "Validate",
diff --git a/apps/src/code-studio/redux.js b/apps/src/code-studio/redux.js
index f0ec76574d9ed..b2bdaae97f337 100644
--- a/apps/src/code-studio/redux.js
+++ b/apps/src/code-studio/redux.js
@@ -6,6 +6,7 @@ import javalabEditor from '@cdo/apps/javalab/redux/editorRedux';
 import javalab from '@cdo/apps/javalab/redux/javalabRedux';
 import lab from '@cdo/apps/lab2/lab2Redux';
 import lab2Project from '@cdo/apps/lab2/redux/lab2ProjectRedux';
+import lab2View from '@cdo/apps/lab2/redux/lab2ViewRedux';
 import predictLevel from '@cdo/apps/lab2/redux/predictLevelRedux';
 import lab2System from '@cdo/apps/lab2/redux/systemRedux';
 import {getStore, registerReducers} from '@cdo/apps/redux';
@@ -54,6 +55,7 @@ registerReducers({
   microBit,
   lab,
   lab2Project,
+  lab2View,
   javalabEditor,
   javalab,
   predictLevel,
diff --git a/apps/src/codebridge/Editor/Editor.tsx b/apps/src/codebridge/Editor/Editor.tsx
index bc13592a108fc..b8c9e80a4c4f8 100644
--- a/apps/src/codebridge/Editor/Editor.tsx
+++ b/apps/src/codebridge/Editor/Editor.tsx
@@ -17,7 +17,7 @@ interface EditorProps {
 }
 
 export const Editor = ({langMapping, editableFileTypes}: EditorProps) => {
-  const {source, saveFile} = useCodebridgeContext();
+  const {source, saveFile, levelProperties} = useCodebridgeContext();
 
   const file = getActiveFileForSource(source);
 
@@ -61,6 +61,7 @@ export const Editor = ({langMapping, editableFileTypes}: EditorProps) => {
           darkMode={true}
           onCodeChange={onChange}
           startCode={file.contents}
+          appName={levelProperties.appName}
           editorConfigExtensions={editorConfigExtensions}
         />
       ) : (
diff --git a/apps/src/codebridge/Settings/SettingsButton.tsx b/apps/src/codebridge/Settings/SettingsButton.tsx
new file mode 100644
index 0000000000000..2f0edd8b488e5
--- /dev/null
+++ b/apps/src/codebridge/Settings/SettingsButton.tsx
@@ -0,0 +1,54 @@
+import {Button} from '@code-dot-org/component-library/button';
+import {
+  WithTooltip,
+  TooltipProps,
+} from '@code-dot-org/component-library/tooltip';
+import React, {useCallback, useRef, useState} from 'react';
+
+import commonI18n from '@cdo/locale';
+
+import SettingsDropdown from './SettingsDropdown';
+
+import darkModeStyles from '@cdo/apps/lab2/styles/dark-mode.module.scss';
+
+const SettingsButton: React.FunctionComponent = () => {
+  const [isOpen, setIsOpen] = useState(false);
+  const buttonContainerRef = useRef<HTMLDivElement>(null);
+
+  const closeSettings = useCallback(() => {
+    setIsOpen(false);
+  }, []);
+
+  const settingsTooltipProps: TooltipProps = {
+    text: commonI18n.settings(),
+    direction: 'onLeft',
+    tooltipId: 'settings-tooltip',
+    size: 'xs',
+    className: darkModeStyles.tooltipLeft,
+  };
+
+  return (
+    <div ref={buttonContainerRef}>
+      <WithTooltip tooltipProps={settingsTooltipProps}>
+        <Button
+          isIconOnly
+          icon={{iconStyle: 'solid', iconName: 'gear'}}
+          color="white"
+          size="xs"
+          onClick={() => setIsOpen(true)}
+          type="tertiary"
+          className={darkModeStyles.tertiaryButton}
+          ariaLabel="Settings"
+        />
+      </WithTooltip>
+      {isOpen && (
+        <SettingsDropdown
+          closeDropdown={closeSettings}
+          buttonRef={buttonContainerRef}
+        />
+      )}
+    </div>
+  );
+};
+
+export default React.memo(SettingsButton);
diff --git a/apps/src/codebridge/Settings/SettingsDropdown.tsx b/apps/src/codebridge/Settings/SettingsDropdown.tsx
new file mode 100644
index 0000000000000..cb7f0aeee6476
--- /dev/null
+++ b/apps/src/codebridge/Settings/SettingsDropdown.tsx
@@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
+            id="close-settings-dropdown"
+          />
+        </div>
+        <div className={moduleStyles.dropdownRow}>
+          {/* Customized label for dropdown */}
+          <label
+            htmlFor={codebridgeI18n.textEditorFontSize()}
+            className={moduleStyles.dropdownLabel}
+          >
+            {codebridgeI18n.textEditorFontSize()}
+          </label>
+          <SimpleDropdown
+            labelText={codebridgeI18n.textEditorFontSize()}
+            isLabelVisible={false}
+            onChange={event => onTextEditorDropdownChange(event.target.value)}
+            items={fontSizeOptions}
+            selectedValue={selectedValue}
+            name={'font-size'}
+            size="s"
+            color="white"
+          />
+        </div>
+        <div className={moduleStyles.footer}>
+          <Button
+            text="Cancel"
+            type="secondary"
+            size="s"
+            onClick={closeDropdown}
+            color={'white'}
+          />
+          <Button
+            text="Save"
+            type="primary"
+            size="s"
+            onClick={onSave}
+            color={'white'}
+          />
+        </div>
+      </div>
+    </FocusTrap>,
+    document.body
+  );
+};
+
+export default React.memo(SettingsDropdown);
diff --git a/apps/src/codebridge/Settings/settings-dropdown.module.scss b/apps/src/codebridge/Settings/settings-dropdown.module.scss
new file mode 100644
index 0000000000000..f6f63e380d435
--- /dev/null
+++ b/apps/src/codebridge/Settings/settings-dropdown.module.scss
@@ -0,0 +1,48 @@
+@import 'color.scss';
+
+.settingsDropdown {
+  position: absolute;
+  cursor: pointer;
+  border: 1px solid #54595E;
+  background-color: $light_gray_950;
+  scrollbar-color: $light_gray_200 $light_gray_950;
+  color: $white;
+  width: 400px;
+  text-align: left;
+  vertical-align: top;
+  z-index: 1000;
+  right: 0;
+  padding: 12px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+// This is duplicated to ensure it takes specificity over the default style.
+.settingsTitle.settingsTitle {
+  color: $white;
+}
+
+.dropdownRow {
+  display: flex;
+  align-items: center;
+  justify-content:space-between;
+  margin-bottom: 16px;
+}
+
+.dropdownLabel {
+  min-width: 150px;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  margin-top: 16px;
+}
diff --git a/apps/src/codebridge/Workspace/HeaderButtons.tsx b/apps/src/codebridge/Workspace/HeaderButtons.tsx
index 8c20348f25be2..1297c4a361ba5 100644
--- a/apps/src/codebridge/Workspace/HeaderButtons.tsx
+++ b/apps/src/codebridge/Workspace/HeaderButtons.tsx
@@ -3,6 +3,7 @@ import {
   TooltipProps,
   WithTooltip,
 } from '@code-dot-org/component-library/tooltip';
+import SettingsButton from '@codebridge/Settings/SettingsButton';
 import {sendCodebridgeAnalyticsEvent} from '@codebridge/utils/analyticsReporterHelper';
 import classNames from 'classnames';
 import React, {useCallback} from 'react';
@@ -79,6 +80,7 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
 
   return (
     <div className={moduleStyles.rightHeaderButtons}>
+      <SettingsButton />
       {enableMicroBit && (
         <Button
           iconRight={{iconStyle: 'solid', iconName: 'arrow-right-from-arc'}}
diff --git a/apps/src/codebridge/redux/workspaceRedux.ts b/apps/src/codebridge/redux/workspaceRedux.ts
index 50fcf337392cf..4083baac840c3 100644
--- a/apps/src/codebridge/redux/workspaceRedux.ts
+++ b/apps/src/codebridge/redux/workspaceRedux.ts
@@ -1,5 +1,4 @@
 import {createSlice, PayloadAction} from '@reduxjs/toolkit';
-
 export interface CodebridgeWorkspaceState {
   showLockedFilesBanner: boolean;
 }
diff --git a/apps/src/lab2/constants.ts b/apps/src/lab2/constants.ts
index bf050064d34e7..44eec9eac2dd9 100644
--- a/apps/src/lab2/constants.ts
+++ b/apps/src/lab2/constants.ts
@@ -35,3 +35,13 @@ export enum WARNING_BANNER_MESSAGES {
 
 // Default height of the predict question free response text area.
 export const PREDICT_FREE_RESPONSE_DEFAULT_HEIGHT = 50;
+
+export enum FontSize {
+  Tiny = 11,
+  Small = 14, // Default font size
+  Medium = 17,
+  Large = 22,
+  Huge = 27,
+}
+
+export const DEFAULT_FONT_SIZE_KEY = FontSize.Small;
diff --git a/apps/src/lab2/hooks/index.ts b/apps/src/lab2/hooks/index.ts
index 04c4fa2f4953a..2233adcdb589d 100644
--- a/apps/src/lab2/hooks/index.ts
+++ b/apps/src/lab2/hooks/index.ts
@@ -2,3 +2,4 @@ export * from './useFileUploader';
 export * from './useKeyboardTrap';
 export * from './useLifecycleNotifier';
 export * from './usePartialApply';
+export * from './useDropdownPosition';
diff --git a/apps/src/lab2/hooks/useDropdownPosition.ts b/apps/src/lab2/hooks/useDropdownPosition.ts
new file mode 100644
index 0000000000000..9c0d866883c26
--- /dev/null
+++ b/apps/src/lab2/hooks/useDropdownPosition.ts
@@ -0,0 +1,35 @@
+// hooks/useDropdownPosition.ts
+import {useLayoutEffect, useState} from 'react';
+
+const TOP_PADDING = 5;
+
+// A hook that calculates and updates the dropdown's position relative to the
+// trigger button - returns the inline styles to position the dropdown below and
+// to the right of the button.
+export default function useDropdownPosition(
+  buttonRef: React.RefObject<HTMLElement>,
+  dropdownRef: React.RefObject<HTMLElement>
+) {
+  const [dropdownStyles, setDropdownStyles] = useState<React.CSSProperties>({});
+
+  useLayoutEffect(() => {
+    const updateDropdownPosition = () => {
+      if (buttonRef.current && dropdownRef.current) {
+        const dropdownRect = dropdownRef.current.getBoundingClientRect();
+        const parentRect = buttonRef.current.getBoundingClientRect();
+        const top =
+          parentRect.top + parentRect.height + TOP_PADDING + window.scrollY;
+        const left = parentRect.right - dropdownRect.width + window.scrollX;
+        setDropdownStyles({top, left});
+      }
+    };
+
+    updateDropdownPosition();
+    window.addEventListener('resize', updateDropdownPosition);
+    return () => {
+      window.removeEventListener('resize', updateDropdownPosition);
+    };
+  }, [buttonRef, dropdownRef]);
+
+  return dropdownStyles;
+}
diff --git a/apps/src/lab2/redux/lab2ViewRedux.ts b/apps/src/lab2/redux/lab2ViewRedux.ts
new file mode 100644
index 0000000000000..27358493206fa
--- /dev/null
+++ b/apps/src/lab2/redux/lab2ViewRedux.ts
@@ -0,0 +1,26 @@
+import {PayloadAction, createSlice} from '@reduxjs/toolkit';
+
+import {FontSize} from '@cdo/apps/lab2/constants';
+
+export interface Lab2ViewState {
+  editorFontSizeKey: keyof typeof FontSize;
+}
+
+const initialState: Lab2ViewState = {
+  editorFontSizeKey: 'Small',
+};
+
+// SLICE
+const lab2ViewSlice = createSlice({
+  name: 'lab2View',
+  initialState,
+  reducers: {
+    setEditorFontSize(state, action: PayloadAction<keyof typeof FontSize>) {
+      state.editorFontSizeKey = action.payload;
+    },
+  },
+});
+
+export const {setEditorFontSize} = lab2ViewSlice.actions;
+
+export default lab2ViewSlice.reducer;
diff --git a/apps/src/lab2/views/components/editor/CodeEditor.tsx b/apps/src/lab2/views/components/editor/CodeEditor.tsx
index 0430faa0c835e..8e0a1c8199c57 100644
--- a/apps/src/lab2/views/components/editor/CodeEditor.tsx
+++ b/apps/src/lab2/views/components/editor/CodeEditor.tsx
@@ -5,9 +5,14 @@ import classNames from 'classnames';
 import React, {useEffect, useMemo, useRef, useState} from 'react';
 import {useDispatch} from 'react-redux';
 
+import {FontSize, DEFAULT_FONT_SIZE_KEY} from '@cdo/apps/lab2/constants';
 import {isReadOnlyWorkspace} from '@cdo/apps/lab2/lab2Redux';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {AppName} from '@cdo/apps/lab2/types';
 import i18n from '@cdo/apps/pythonlab/locale';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
 import {useAppSelector} from '@cdo/apps/util/reduxHooks';
+import {tryGetSessionStorage} from '@cdo/apps/utils';
 
 import {editorConfig} from './editorConfig';
 import {darkMode as darkModeTheme} from './editorThemes';
@@ -18,6 +23,7 @@ interface CodeEditorProps {
   onCodeChange: (code: string) => void;
   editorConfigExtensions: Extension[];
   startCode: string;
+  appName: AppName;
   darkMode?: boolean;
 }
 
@@ -25,6 +31,7 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   onCodeChange,
   editorConfigExtensions,
   startCode,
+  appName,
   darkMode = true,
 }) => {
   const editorRef = useRef<HTMLDivElement>(null);
@@ -33,6 +40,27 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const fontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey || DEFAULT_FONT_SIZE_KEY
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+
+  // User preference for selected font size persists within a session
+  // per signed-in user per app type (currently either pythonlab or weblab).
+  // TODO: update so that selected font size will persist across sessions.
+  // Note that When the user selects a different font size from settings, fontSizeKey
+  // is updated alongside sessionStorage for sessionStorageKey.
+  useEffect(() => {
+    const sessionStorageKey = `${appName}CodeEditorFontSizeKey`;
+    const sessionStorage = tryGetSessionStorage(sessionStorageKey, false);
+    if (
+      sessionStorage &&
+      sessionStorage !== fontSizeKey &&
+      signInState === SignInState.SignedIn
+    ) {
+      dispatch(setEditorFontSize(sessionStorage));
+    }
+  }, [dispatch, signInState, fontSizeKey, appName]);
 
   // These two compartments control read-only settings.
   // Controls if you can type in the editor or not.
@@ -40,6 +68,16 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   // Controls if the dom is focusable or not (and therefore if a cursor is visible in the editor or not).
   const editorEditableCompartment = useMemo(() => new Compartment(), []);
 
+  // This compartment controls font size settings for the editor.
+  const fontSizeCompartment = useMemo(() => new Compartment(), []);
+
+  const getFontSizeTheme = (fontSize: number) => {
+    return EditorView.theme({
+      '&': {
+        fontSize: `${fontSize}px`,
+      },
+    });
+  };
   useEffect(() => {
     if (editorRef.current === null || didInit) {
       return;
@@ -61,7 +99,8 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
 
     editorExtensions.push(
       editorReadOnlyCompartment.of(EditorState.readOnly.of(isReadOnly)),
-      editorEditableCompartment.of(EditorView.editable.of(!isReadOnly))
+      editorEditableCompartment.of(EditorView.editable.of(!isReadOnly)),
+      fontSizeCompartment.of(getFontSizeTheme(FontSize[fontSizeKey]))
     );
     if (darkMode) {
       editorExtensions.push(darkModeTheme);
@@ -92,8 +131,23 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
     editorReadOnlyCompartment,
     isReadOnly,
     editorEditableCompartment,
+    fontSizeCompartment,
+    fontSizeKey,
   ]);
 
+  // When we have a new fontSizeKey, reset font size.
+  useEffect(() => {
+    if (editorView) {
+      editorView.dispatch({
+        effects: [
+          fontSizeCompartment.reconfigure(
+            getFontSizeTheme(FontSize[fontSizeKey])
+          ),
+        ],
+      });
+    }
+  }, [editorView, fontSizeCompartment, fontSizeKey]);
+
   // When we have a new channelId and/or start code, reset the editor with the start code.
   // A new channelId means we are loading a new project, and we need to reset the editor.
   useEffect(() => {
diff --git a/apps/src/lab2/views/components/versionHistory/VersionHistoryDropdown.tsx b/apps/src/lab2/views/components/versionHistory/VersionHistoryDropdown.tsx
index eedc4c535d45f..90683577dacfb 100644
--- a/apps/src/lab2/views/components/versionHistory/VersionHistoryDropdown.tsx
+++ b/apps/src/lab2/views/components/versionHistory/VersionHistoryDropdown.tsx
@@ -7,17 +7,11 @@ import Tags from '@code-dot-org/component-library/tags';
 import {Heading6} from '@code-dot-org/component-library/typography';
 import classNames from 'classnames';
 import FocusTrap from 'focus-trap-react';
-import React, {
-  useCallback,
-  useEffect,
-  useLayoutEffect,
-  useMemo,
-  useRef,
-  useState,
-} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {createPortal} from 'react-dom';
 
 import {sendCodebridgeAnalyticsEvent} from '@cdo/apps/codebridge/utils/analyticsReporterHelper';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
 import Lab2Registry from '@cdo/apps/lab2/Lab2Registry';
 import lab2I18n from '@cdo/apps/lab2/locale';
 import {
@@ -54,7 +48,6 @@ interface VersionHistoryDropdownProps {
 }
 
 const INITIAL_VERSION_ID = 'initial-version';
-const TOP_PADDING = 5;
 
 /**
  * Dropdown that displays a list of versions for the current project.
@@ -89,39 +82,13 @@ const VersionHistoryDropdown: React.FunctionComponent<
   const viewingOldVersion = useAppSelector(
     state => state.lab2Project.viewingOldVersion
   );
-  const [dropdownStyles, setDropdownStyles] = useState<React.CSSProperties>({});
 
   // If this is a teacher viewing a student's project, we hide the restore button,
   // but still allow viewing old versions.
   const viewAsUserId = useAppSelector(state => state.progress.viewAsUserId);
 
   const dialogControl = useDialogControl();
-
-  // Effect to position the dropdown relative to the button that opened it.
-  // We use a layout effect because it is guaranteed to run before the browser repaints
-  // the screen, so we can avoid a flash of the dropdown in the wrong position.
-  useLayoutEffect(() => {
-    const updateDropdownPositionIfShown = () => {
-      if (buttonRef.current && menuRef.current) {
-        const dropdownRect = menuRef.current.getBoundingClientRect();
-        const parentRect = buttonRef.current.getBoundingClientRect();
-        const top =
-          parentRect.top + parentRect.height + TOP_PADDING + window.scrollY;
-        const left = parentRect.right - dropdownRect.width + window.scrollX;
-        setDropdownStyles({
-          top,
-          left,
-        });
-      }
-    };
-
-    updateDropdownPositionIfShown();
-
-    window.addEventListener('resize', updateDropdownPositionIfShown);
-    return () => {
-      window.removeEventListener('resize', updateDropdownPositionIfShown);
-    };
-  }, [buttonRef, menuRef]);
+  const dropdownStyles = useDropdownPosition(buttonRef, menuRef);
 
   const dateFormatter = useMemo(() => {
     return new Intl.DateTimeFormat(locale, {
diff --git a/apps/src/music/views/MusicLabView.tsx b/apps/src/music/views/MusicLabView.tsx
index d52bb1d44e19f..7e990f05188b6 100644
--- a/apps/src/music/views/MusicLabView.tsx
+++ b/apps/src/music/views/MusicLabView.tsx
@@ -467,6 +467,7 @@ const MusicLabView: React.FunctionComponent<MusicLabViewProps> = ({
                 onCodeChange={executeCode}
                 startCode={''}
                 editorConfigExtensions={[javascript()]}
+                appName="music"
               />
             )}
             <div role="application" id={blocklyDivId} />
diff --git a/apps/src/types/redux.ts b/apps/src/types/redux.ts
index ae8fae4eb534c..c9e14885a4cde 100644
--- a/apps/src/types/redux.ts
+++ b/apps/src/types/redux.ts
@@ -12,6 +12,7 @@ import {JavalabState} from '@cdo/apps/javalab/redux/javalabRedux';
 import {JavalabViewState} from '@cdo/apps/javalab/redux/viewRedux';
 import {LabState} from '@cdo/apps/lab2/lab2Redux';
 import {Lab2ProjectState} from '@cdo/apps/lab2/redux/lab2ProjectRedux';
+import {Lab2ViewState} from '@cdo/apps/lab2/redux/lab2ViewRedux';
 import {PredictLevelState} from '@cdo/apps/lab2/redux/predictLevelRedux';
 import {Lab2SystemState} from '@cdo/apps/lab2/redux/systemRedux';
 import {MazeState} from '@cdo/apps/maze/redux';
@@ -48,6 +49,7 @@ export interface RootState {
   lab: LabState;
   lab2Project: Lab2ProjectState;
   lab2System: Lab2SystemState;
+  lab2View: Lab2ViewState;
   layout: LayoutState;
   locales: LocaleState;
   mapbox: MapboxState;
diff --git a/apps/src/weblab2/Weblab2Editor.tsx b/apps/src/weblab2/Weblab2Editor.tsx
index acb0796c1ddf2..f31617dd7f87e 100644
--- a/apps/src/weblab2/Weblab2Editor.tsx
+++ b/apps/src/weblab2/Weblab2Editor.tsx
@@ -21,6 +21,7 @@ const Weblab2Editor: React.FunctionComponent = () => {
         onCodeChange={onCodeChange}
         startCode={'<!DOCTYPE HTML>\n<h1>Hello, world!</h1>'}
         editorConfigExtensions={editorExtensions}
+        appName="weblab2"
       />
     </div>
   );


Reviews:
user: login: molly-moen
body: My comments can all be handled as follow ups (besides removing a console log). Couple questions on persisting state and separation of concerns between codebridge/lab2. TBH I haven't used session storage before so I don't have much context on how that works!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T22:20:07Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:03:01Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:38:43Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:39:21Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:39:42Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T15:13:27Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T15:17:06Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T16:39:20Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T15:34:55Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:31:40Z
user: login: hannahbergam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T22:47:42Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T23:25:27Z
user: login: hannahbergam
body: The screenreader part works well! Two small things- I can't seem to get the font size dropdown to open via enter. Additionally, having the x button be the very first thing that receives focus seems a little backwards, but I'd say that one is non-blocking! 

I used the same allthethings level for testing that you had in your screenshot but didn't see it in all of pythonlab. Is this going to be enabled everywhere?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T23:48:50Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:41:37Z
user: login: molly-moen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:27:10Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:41:20Z
user: login: molly-moen
body: Looks good! A few comments that could be handled as follow-ups
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:07:51Z
user: login: hannahbergam
body: This is so so exciting. Thank you for this work! Going to give it an accessibility label too if that's okay:)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:37:07Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:24:05Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:28:29Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:28:57Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:35:09Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T18:57:25Z
user: login: fisher-alice
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T18:57:51Z

Review comments:
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: molly-moen
body: We should translate these strings (could be a follow-up)
created_at: 2025-03-27T22:04:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
+      message: 'Customize your text editor font size',
+      selectedValue: selectedFontSizeKey,
+      items: fontSizeOptions,
+      dropdownLabel: '',
+    });
+    const selectedKey = extractUserInput(results) as keyof typeof FontSize;
+    if (selectedKey && FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: molly-moen
body: We will want this setting to persist across sessions. Up to you if you want to leave this in and do that as a follow-up or rework it now. We can use [UserPreferences](https://github.com/code-dot-org/code-dot-org/blob/staging/apps/src/lib/util/UserPreferences.js) to persist a setting.
created_at: 2025-03-27T22:06:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: We can't rely on codebridge context here because this is a lab2 component. Maybe `appName` should be passed in as a prop?
created_at: 2025-03-27T22:07:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: Similarly we probably shouldn't be referencing a codebridge-specific redux. Maybe that's a reason to make this setting in a lab2 redux? Or we could argue that this component will only ever be used by codebridge and move it.
created_at: 2025-03-27T22:08:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const fontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+
+  // We want the user preference for selected font size to persist across a session
+  // per signed-in user per app type (currently either pythonlab or weblab).
+  // Note that When the user selects a different font size from settings, fontSizeKey
+  // is updated alongside sessionStorage for sessionStorageKey.
+  useEffect(() => {
+    const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+    const sessionStorage = tryGetSessionStorage(sessionStorageKey, false);
+    if (
+      sessionStorage &&
+      sessionStorage !== fontSizeKey &&
+      signInState === SignInState.SignedIn
+    ) {
+      dispatch(setEditorFontSize(sessionStorage));
+    }
+  }, [dispatch, levelProperties.appName, signInState, fontSizeKey]);
 
   // These two compartments control read-only settings.
   // Controls if you can type in the editor or not.
   const editorReadOnlyCompartment = useMemo(() => new Compartment(), []);
   // Controls if the dom is focusable or not (and therefore if a cursor is visible in the editor or not).
   const editorEditableCompartment = useMemo(() => new Compartment(), []);
 
+  // This compartment controls font size settings for the editor.
+  const fontSizeCompartment = useMemo(() => new Compartment(), []);
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: 🎉 
created_at: 2025-03-27T22:10:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const fontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+
+  // We want the user preference for selected font size to persist across a session
+  // per signed-in user per app type (currently either pythonlab or weblab).
+  // Note that When the user selects a different font size from settings, fontSizeKey
+  // is updated alongside sessionStorage for sessionStorageKey.
+  useEffect(() => {
+    const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+    const sessionStorage = tryGetSessionStorage(sessionStorageKey, false);
+    if (
+      sessionStorage &&
+      sessionStorage !== fontSizeKey &&
+      signInState === SignInState.SignedIn
+    ) {
+      dispatch(setEditorFontSize(sessionStorage));
+    }
+  }, [dispatch, levelProperties.appName, signInState, fontSizeKey]);
 
   // These two compartments control read-only settings.
   // Controls if you can type in the editor or not.
   const editorReadOnlyCompartment = useMemo(() => new Compartment(), []);
   // Controls if the dom is focusable or not (and therefore if a cursor is visible in the editor or not).
   const editorEditableCompartment = useMemo(() => new Compartment(), []);
 
+  // This compartment controls font size settings for the editor.
+  const fontSizeCompartment = useMemo(() => new Compartment(), []);
+
+  const getFontSizeTheme = (fontSize: number) => {
+    console.log('getFontSizeTheme fontSize', fontSize);
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: remove this?
created_at: 2025-03-27T22:10:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: nit: instead of having two variables `editorFontSizeKey` and `fontSizeKey` we could do this instead inside the app selector
```
state => state.codebridgeWorkspace.editorFontSizeKey || DEFAULT_FONT_SIZE_KEY
```
created_at: 2025-03-27T22:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,8 +131,23 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
     editorReadOnlyCompartment,
     isReadOnly,
     editorEditableCompartment,
+    fontSizeCompartment,
+    fontSizeKey,
   ]);
 
+  // When we have a new fontSizeKey, reset font size.
+  useEffect(() => {
+    if (editorView) {
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: Is there a risk of this firing more than we want (not sure if `editorView` or `fontSizeCompartment` will trigger unneeded calls here)?
created_at: 2025-03-27T22:17:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
+      message: 'Customize your text editor font size',
+      selectedValue: selectedFontSizeKey,
+      items: fontSizeOptions,
+      dropdownLabel: '',
+    });
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: molly-moen
body: When I talked about this with Sam, I imagined it being a dropdown similar to version history, rather than a modal. I think this is fine (certainly to start with), but it would be worth confirming we are good long-term with a modal!
created_at: 2025-03-27T22:19:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: fisher-alice
body: I initially thought about adding the setting in a lab2redux ([SIack thread](https://codedotorg.slack.com/archives/C03DBDN67B7/p1742934579582809)), and `aichat` doesn't currently have plans to increase font size. However, I do see that `CodeEditor` was very recently added to `MusicLabView` by https://github.com/code-dot-org/code-dot-org/pull/64649 so I think moving the setting to lab2 makes sense.
created_at: 2025-03-28T12:03:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: fisher-alice
body: https://codedotorg.atlassian.net/browse/CT-1167
created_at: 2025-03-28T13:38:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
+      message: 'Customize your text editor font size',
+      selectedValue: selectedFontSizeKey,
+      items: fontSizeOptions,
+      dropdownLabel: '',
+    });
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: fisher-alice
body: https://codedotorg.slack.com/archives/C06JELCAPT9/p1743161842018539?thread_ts=1742576084.787909&cid=C06JELCAPT9
created_at: 2025-03-28T13:39:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
+      message: 'Customize your text editor font size',
+      selectedValue: selectedFontSizeKey,
+      items: fontSizeOptions,
+      dropdownLabel: '',
+    });
+    const selectedKey = extractUserInput(results) as keyof typeof FontSize;
+    if (selectedKey && FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: fisher-alice
body: Thanks! I'll handle in a follow-up.
created_at: 2025-03-28T13:39:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,8 +131,23 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
     editorReadOnlyCompartment,
     isReadOnly,
     editorEditableCompartment,
+    fontSizeCompartment,
+    fontSizeKey,
   ]);
 
+  // When we have a new fontSizeKey, reset font size.
+  useEffect(() => {
+    if (editorView) {
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: fisher-alice
body: I added a log inside this font size change `useEffect`. Within the `useEffect` for the Editor (above), I added a log immediately before the `setEditorView` is called. I saw that the font size `useEffect` was triggered when font size changed or when the layout was swapped (horizontal <-> vertical). When I ran the program or edited the code, the font size `useEffect` was not triggered.

https://github.com/user-attachments/assets/545228a5-812b-44c3-9bd2-c51f906a6931


This makes sense since I see that `fontSizeCompartment` is assigned with `useMemo`, and `setEditorView` is only called when `editorRef.current === null || didInit` is `false`, i.e., when the component unmounts/mounts. So I think this is okay. Thanks for checking. Definitely got me to look at the rest of the code in this component more carefully.

created_at: 2025-03-28T15:13:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,13 +38,46 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
   const [editorView, setEditorView] = useState<EditorView | null>(null);
   const channelId = useAppSelector(state => state.lab.channel?.id);
   const isReadOnly = useAppSelector(isReadOnlyWorkspace);
+  const editorFontSizeKey = useAppSelector(
+    state => state.codebridgeWorkspace.editorFontSizeKey
+  );
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: fisher-alice
body: Moved redux state to new `lab2ViewRedux` - https://github.com/code-dot-org/code-dot-org/pull/64822/commits/566f2ffda55c0ebf328d749d52c7b300bfe3f5ce
created_at: 2025-03-28T15:17:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: fisher-alice
body: And I added to jira ticket translating backpack-related strings as well. thanks!
created_at: 2025-03-28T16:39:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,8 +109,40 @@ const WorkspaceHeaderButtons: React.FunctionComponent = () => {
     sendPythonCodeToMicroBit(pythonCode);
   };
 
+  const onClickSettings = async () => {
+    const results = await dialogControl?.showDialog({
+      type: DialogType.GenericDropdown,
+      title: 'Settings',
+      message: 'Customize your text editor font size',
+      selectedValue: selectedFontSizeKey,
+      items: fontSizeOptions,
+      dropdownLabel: '',
+    });
path: apps/src/codebridge/Workspace/HeaderButtons.tsx
user: login: fisher-alice
body: Thanks Molly! I just chatted with @samantha-code , and confirmed still would like something similar to the version history dropdown and not a dialog/modal. Converting to draft to re-do UI. Thanks!
created_at: 2025-04-01T15:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const menuRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, menuRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={menuRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
+            id="close-settings-dropdown"
+          />
+        </div>
+        <div className={moduleStyles.dropdownRow}>
+          {/* Customized label for dropdown */}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: @hannahbergam - would you mind taking a look at this section with a11y in mind? Thank you!
created_at: 2025-04-01T22:31:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const menuRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, menuRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={menuRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
+            id="close-settings-dropdown"
+          />
+        </div>
+        <div className={moduleStyles.dropdownRow}>
+          {/* Customized label for dropdown */}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: hannahbergam
body: Loving the focus trap and the labels! I can do this branch with my screenreader if that's what you have in mind?
created_at: 2025-04-01T22:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const menuRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, menuRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={menuRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
+            id="close-settings-dropdown"
+          />
+        </div>
+        <div className={moduleStyles.dropdownRow}>
+          {/* Customized label for dropdown */}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: Yes thank you! Just want to make sure I am following best a11y practices. Appreciate you!
created_at: 2025-04-01T23:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,7 +21,8 @@
   justify-content: space-between;
 }
 
-.versionHistoryTitle {
+// This is duplicated to ensure it takes specificity over the default style.
+.versionHistoryTitle.versionHistoryTitle {
path: apps/src/lab2/views/components/versionHistory/version-history.module.scss
user: login: fisher-alice
body: @molly-moen - I noticed on `prod` in `pythonlab` projects that the color of the 'Version History' title is not white.
![Screenshot 2025-04-02 at 9 14 39 AM](https://github.com/user-attachments/assets/cf205528-70ca-49de-80a4-d4e2f71af873)

So updated style here. After update:

![Screenshot 2025-04-02 at 9 41 18 AM](https://github.com/user-attachments/assets/ec586adc-9f56-4ef8-b491-25864bc12d49)

created_at: 2025-04-02T14:41:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,7 +21,8 @@
   justify-content: space-between;
 }
 
-.versionHistoryTitle {
+// This is duplicated to ensure it takes specificity over the default style.
+.versionHistoryTitle.versionHistoryTitle {
path: apps/src/lab2/views/components/versionHistory/version-history.module.scss
user: login: molly-moen
body: Yep I noticed this was happening too, see [this thread](https://codedotorg.slack.com/archives/C0532LCK125/p1743547172298639). I have a branch in progress to convert this to use the dark theme, so I'll handle it there!
created_at: 2025-04-02T15:27:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,7 +21,8 @@
   justify-content: space-between;
 }
 
-.versionHistoryTitle {
+// This is duplicated to ensure it takes specificity over the default style.
+.versionHistoryTitle.versionHistoryTitle {
path: apps/src/lab2/views/components/versionHistory/version-history.module.scss
user: login: fisher-alice
body: Oh gotcha! I'll revert this commit then. Thanks!
created_at: 2025-04-02T15:41:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,26 @@
+import {PayloadAction, createSlice} from '@reduxjs/toolkit';
+
+import {FontSize} from '@cdo/apps/lab2/constants';
+
+export interface Lab2ViewState {
+  editorFontSizeKey: keyof typeof FontSize;
+}
+
+const initialState: Lab2ViewState = {
+  editorFontSizeKey: 'Small',
path: apps/src/lab2/redux/lab2ViewRedux.ts
user: login: molly-moen
body: Would it make things easier to store the font size number here instead? Then we could store this as `FontSize.Small` right?
created_at: 2025-04-02T15:59:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: molly-moen
body: do we need `|| DEFAULT_FONT_SIZE_KEY` since the redux value has a default of `Small`?
created_at: 2025-04-02T16:00:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: molly-moen
body: can be a follow-up, but I think you can apply `data-theme="Dark"` and avoid some custom css/setting button color to white, similar to [this PR](https://github.com/code-dot-org/code-dot-org/pull/64972)
created_at: 2025-04-02T16:03:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: molly-moen
body: labels should be translated too right?
created_at: 2025-04-02T16:04:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,4 @@
 import {createSlice, PayloadAction} from '@reduxjs/toolkit';
-
path: apps/src/codebridge/redux/workspaceRedux.ts
user: login: molly-moen
body: intended change?
created_at: 2025-04-02T16:05:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+// hooks/useDropdownPosition.ts
+import {useLayoutEffect, useState} from 'react';
path: apps/src/lab2/hooks/useDropdownPosition.ts
user: login: molly-moen
body: nice hook!
created_at: 2025-04-02T16:06:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,8 +131,23 @@ const CodeEditor: React.FunctionComponent<CodeEditorProps> = ({
     editorReadOnlyCompartment,
     isReadOnly,
     editorEditableCompartment,
+    fontSizeCompartment,
+    fontSizeKey,
   ]);
 
+  // When we have a new fontSizeKey, reset font size.
+  useEffect(() => {
+    if (editorView) {
path: apps/src/lab2/views/components/editor/CodeEditor.tsx
user: login: molly-moen
body: Nice!
created_at: 2025-04-02T16:06:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
+      >
+        <div className={moduleStyles.header}>
+          <Heading6 className={moduleStyles.settingsTitle}>
+            {commonI18n.settings()}
+          </Heading6>
+          <CloseButton
+            onClick={closeDropdown}
+            aria-label="Close settings"
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: Yes - I used existing loc strings, and translate a couple in this PR, but will include labels in follow-up loc  ticket - https://codedotorg.atlassian.net/browse/CT-1167
created_at: 2025-04-02T17:24:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,26 @@
+import {PayloadAction, createSlice} from '@reduxjs/toolkit';
+
+import {FontSize} from '@cdo/apps/lab2/constants';
+
+export interface Lab2ViewState {
+  editorFontSizeKey: keyof typeof FontSize;
+}
+
+const initialState: Lab2ViewState = {
+  editorFontSizeKey: 'Small',
path: apps/src/lab2/redux/lab2ViewRedux.ts
user: login: fisher-alice
body: Hmmm...I was debating this, but I think I'll keep as-is for now. Maybe when we start storing in backend, I'll see what makes more sense then. Thanks!
created_at: 2025-04-02T17:28:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: Cool - will definitely do this as follow-up - thanks!
created_at: 2025-04-02T17:28:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
+  const {signInState} = useAppSelector(state => state.currentUser);
+  const {levelProperties} = useCodebridgeContext();
+  const dispatch = useAppDispatch();
+  const [selectedValue, setSelectedValue] = useState(selectedFontSizeKey);
+
+  const getSelectedKey = (value: string) => value as keyof typeof FontSize;
+
+  const dropdownStyles = useDropdownPosition(buttonRef, dropdownRef);
+
+  const onTextEditorDropdownChange = async (value: string) => {
+    setSelectedValue(getSelectedKey(value));
+  };
+
+  const onSave = () => {
+    const selectedKey = getSelectedKey(selectedValue);
+    if (FontSize[selectedKey]) {
+      // We want the user preference for selected font size to persist across a session
+      // for signed-in users per app type.
+      if (signInState === SignInState.SignedIn) {
+        const sessionStorageKey = `${levelProperties.appName}CodeEditorFontSizeKey`;
+        trySetSessionStorage(sessionStorageKey, selectedKey);
+      }
+      dispatch(setEditorFontSize(selectedKey));
+    }
+    closeDropdown();
+  };
+
+  return createPortal(
+    <FocusTrap
+      focusTrapOptions={{
+        onDeactivate: closeDropdown,
+        clickOutsideDeactivates: true,
+      }}
+    >
+      <div
+        className={moduleStyles.settingsDropdown}
+        ref={dropdownRef}
+        role="dialog"
+        style={dropdownStyles}
+        aria-modal="true"
+        aria-label={commonI18n.settings()}
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: I'll also address the other small comments in a fast follow-up (remove default constant, extra space). Thanks!
created_at: 2025-04-02T17:35:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import {Button} from '@code-dot-org/component-library/button';
+import CloseButton from '@code-dot-org/component-library/closeButton';
+import SimpleDropdown, {
+  SimpleDropdownProps,
+} from '@code-dot-org/component-library/dropdown/simpleDropdown';
+import {Heading6} from '@code-dot-org/component-library/typography';
+import FocusTrap from 'focus-trap-react';
+import React, {useState} from 'react';
+import {createPortal} from 'react-dom';
+
+import codebridgeI18n from '@cdo/apps/codebridge/locale';
+import {DEFAULT_FONT_SIZE_KEY, FontSize} from '@cdo/apps/lab2/constants';
+import useDropdownPosition from '@cdo/apps/lab2/hooks/useDropdownPosition';
+import {setEditorFontSize} from '@cdo/apps/lab2/redux/lab2ViewRedux';
+import {SignInState} from '@cdo/apps/templates/currentUserRedux';
+import useOutsideClick from '@cdo/apps/util/hooks/useOutsideClick';
+import {useAppSelector, useAppDispatch} from '@cdo/apps/util/reduxHooks';
+import {trySetSessionStorage} from '@cdo/apps/utils';
+import commonI18n from '@cdo/locale';
+
+import {useCodebridgeContext} from '../codebridgeContext';
+
+import moduleStyles from './settings-dropdown.module.scss';
+
+// fontSizeOptions contains a list of value/text from the FontSize enum,
+// e.g., [{value: 'Tiny', text: 'Tiny'}, {value: 'Small', text: 'Small'}, ...]
+const fontSizeOptions: SimpleDropdownProps['items'] = Object.keys(FontSize)
+  .filter(key => isNaN(Number(key))) // Filters out the reverse enum keys.
+  .map(key => ({
+    value: key,
+    text: key,
+  }));
+interface SettingsDropdownProps {
+  closeDropdown: () => void;
+  buttonRef: React.RefObject<HTMLDivElement>;
+}
+
+const SettingsDropdown: React.FunctionComponent<SettingsDropdownProps> = ({
+  closeDropdown,
+  buttonRef,
+}) => {
+  const dropdownRef = useOutsideClick<HTMLDivElement>(closeDropdown);
+  const editorFontSizeKey = useAppSelector(
+    state => state.lab2View.editorFontSizeKey
+  );
+  const selectedFontSizeKey = editorFontSizeKey || DEFAULT_FONT_SIZE_KEY;
path: apps/src/codebridge/Settings/SettingsDropdown.tsx
user: login: fisher-alice
body: https://github.com/code-dot-org/code-dot-org/pull/64981/files#r2025414415
created_at: 2025-04-02T18:57:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,4 @@
 import {createSlice, PayloadAction} from '@reduxjs/toolkit';
-
path: apps/src/codebridge/redux/workspaceRedux.ts
user: login: fisher-alice
body: https://github.com/code-dot-org/code-dot-org/pull/64981/files#r2025415053
created_at: 2025-04-02T18:57:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: fisher-alice
created_at: 2025-04-02T14:26:52Z
body: > The screenreader part works well! Two small things- I can't seem to get the font size dropdown to open via enter. Additionally, having the x button be the very first thing that receives focus seems a little backwards, but I'd say that one is non-blocking!

The font size dropdown not opening via enter (only on up/down keys) is an existing issue with the DSCO `SimpleDropdown`, so I logged a jira ticket for this as a follow-up - thanks! https://codedotorg.atlassian.net/browse/CT-1168

> 
> I used the same allthethings level for testing that you had in your screenshot but didn't see it in all of pythonlab. Is this going to be enabled everywhere?

Oh interesting! Which level were you looking at? Yes, it should be enabled everywhere. I see it on `pythonlab` standalone projects, and levels in '/allthethings'. I also searched through `pythonlab` at '/levels' and see the settings button on the levels I've visited.
author_association: CONTRIBUTOR
user: login: hannahbergam
created_at: 2025-04-02T16:26:33Z
body: > > The screenreader part works well! Two small things- I can't seem to get the font size dropdown to open via enter. Additionally, having the x button be the very first thing that receives focus seems a little backwards, but I'd say that one is non-blocking!
> 
> The font size dropdown not opening via enter (only on up/down keys) is an existing issue with the DSCO `SimpleDropdown`, so I logged a jira ticket for this as a follow-up - thanks! https://codedotorg.atlassian.net/browse/CT-1168
> 
> > I used the same allthethings level for testing that you had in your screenshot but didn't see it in all of pythonlab. Is this going to be enabled everywhere?
> 
> Oh interesting! Which level were you looking at? Yes, it should be enabled everywhere. I see it on `pythonlab` standalone projects, and levels in '/allthethings'. I also searched through `pythonlab` at '/levels' and see the settings button on the levels I've visited.

Makes sense that this is a DSCO issue- thanks for logging! And my apologies- false alarm about it not loading on every level. I did a bunch of restarts and see it now:)
author_association: CONTRIBUTOR
user: login: fisher-alice
created_at: 2025-04-02T17:25:15Z
body: > This is so so exciting. Thank you for this work! Going to give it an accessibility label too if that's okay:)

Oh wow - didn't know about this a11y label - thanks!
author_association: CONTRIBUTOR