diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..227a2f3de --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,35 @@ +# pie-lib + +## What this is + +A monorepo of shared React components/utilities for the **PIE (Portable Interactions & Elements)** assessment framework — math rendering/input, drag-and-drop, charting, rich-text editing, config UI, rubric/scoring UI, icons, style utils. Published under the `@pie-lib/*` npm scope. + +Consumed by the sibling repo **`pie-elements`** (`../pie-elements`), which installs these as normal npm dependencies (not a local link) — `pie-elements` pins exact versions in its root `package.json` `resolutions` block. + +## Repo structure + +- Lerna (independent versioning, conventional-commits driven) + Yarn workspaces (`packages/*`). +- `packages/` — 28 library packages + `demo` (a Next.js app for local preview of all packages, deployed to now.sh on `develop`/`master` merges). +- Every package: `src/` (with `__tests__/`) → `lib/` (compiled output, checked in). +- No TypeScript anywhere — plain JS/JSX with PropTypes. + +Notable packages: `render-ui` (most widely consumed — preview layout, feedback, collapsible, response indicators), `drag` (dnd-kit based), `math-input`/`math-rendering`/`math-toolbar` (MathQuill, mid-migration to MathLive — see `docs/mathquill-to-mathlive-migration.md`), `charting`/`plot` (visx), `config-ui`, `controller-utils`, `test-utils` (shared test helpers/mocks). + +## Commands + +- `npm run build` — build all packages +- `npm test` — run all tests; to test a single package: `./node_modules/.bin/jest packages/pkg-name/src/` +- `npm run lint` — ESLint +- `scripts/dev --scope $package-name` — run the demo site on localhost:3000 (`--scope` optional, defaults to all) +- `npm run release` — release + deploy (merging to `develop` → `next` dist-tag / pie-lib-next.now.sh; merging to `master` → `latest` / pie-lib.now.sh) + +## Conventions + +- **Conventional commits syntax** on commit messages — Lerna uses this to detect the appropriate independent version bump per package. +- Styling has migrated to MUI v7 + Emotion (older packages may still show JSS-era patterns). +- If test setup gets out of sync: `npm run build`, `rm -fr packages/test-utils/node_modules`, then retry. +- Node >=18 required; there's a known Jest/Node quirk documented in `.cursor/skills/nvm-jest-v22/SKILL.md` (use `nvm use v22` before running jest directly if you hit a syntax error). + +## Working preferences + +- **Do not create git commits unless explicitly asked.** The user commits their own changes — leave the working tree staged/unstaged as appropriate and let them review and commit themselves. diff --git a/packages/editable-html-tip-tap/src/components/EditableHtml.jsx b/packages/editable-html-tip-tap/src/components/EditableHtml.jsx index b6d93144e..13f764c9f 100644 --- a/packages/editable-html-tip-tap/src/components/EditableHtml.jsx +++ b/packages/editable-html-tip-tap/src/components/EditableHtml.jsx @@ -243,7 +243,7 @@ export const EditableHtml = (props) => { let cb; if (scheduled && result) { - // finish editing only on success + // finish editing only on success cb = props.onChange; } @@ -424,10 +424,12 @@ const StyledEditorContent = styled(EditorContent, { }, // Out of flow so the caret stays at the start of the block; in-flow ::before pushes the caret after the hint text. - '& p.is-editor-empty, & div.is-editor-empty': { + // :only-child ensures the placeholder is hidden whenever the editor has other content (images, upload nodes, etc.) + // and covers the type+backspace edge case where Tiptap only adds is-empty (not is-editor-empty). + '& p[data-placeholder].is-empty:only-child, & div[data-placeholder].is-empty:only-child': { position: 'relative', }, - '& p.is-editor-empty::before, & div.is-editor-empty::before': { + '& p[data-placeholder].is-empty:only-child::before, & div[data-placeholder].is-empty:only-child::before': { content: 'attr(data-placeholder)', position: 'absolute', left: 0, diff --git a/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/__tests__/choice.test.jsx b/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/__tests__/choice.test.jsx new file mode 100644 index 000000000..e572b577d --- /dev/null +++ b/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/__tests__/choice.test.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import DragDropChoice from '../choice'; + +// Mock @dnd-kit hooks to avoid DndContext requirement +jest.mock('@dnd-kit/core', () => ({ + useDraggable: jest.fn(() => ({ + attributes: {}, + listeners: {}, + setNodeRef: jest.fn(), + isDragging: false, + })), + useDroppable: jest.fn(() => ({ + setNodeRef: jest.fn(), + isOver: false, + active: null, + })), +})); + +jest.mock('@pie-lib/math-rendering', () => ({ + renderMath: jest.fn(), +})); + +// Collect the CSS rules emotion/MUI injected into the document (jsdom uses insertRule). +const collectEmotionRules = () => { + const rules = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + rules.push(rule.cssText); + } + } catch (e) { + /* inaccessible stylesheet */ + } + } + return rules; +}; + +describe('DragInTheBlank choice', () => { + const defaultProps = { + value: { id: '1', value: '1/2' }, + disabled: false, + instanceId: 'test-instance', + n: { index: 0 }, + onChange: jest.fn(), + removeResponse: jest.fn(), + duplicates: false, + }; + + it('renders without crashing', () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + describe('fraction math styling', () => { + it('enlarges numerator/denominator digits adjacent to a fraction to 120%', () => { + render(); + const rule = collectEmotionRules().find((r) => r.includes('mjx-mn') && r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/mjx-mn:has\(~\s*mjx-mfrac\)/); + expect(rule).toMatch(/mjx-mfrac\s*~\s*mjx-mn/); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + + it('keeps the existing mjx-frac 120% rule', () => { + render(); + const rule = collectEmotionRules().find((r) => /(^|[^-])mjx-frac/.test(r) && !r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + }); +}); diff --git a/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/choice.jsx b/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/choice.jsx index b7445835c..29bd8f02f 100644 --- a/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/choice.jsx +++ b/packages/editable-html-tip-tap/src/components/respArea/DragInTheBlank/choice.jsx @@ -10,6 +10,9 @@ import { GripIcon } from '../../icons/RespArea'; const StyledContent = styled('span')(({ theme }) => ({ border: `solid 0px ${theme.palette.primary.main}`, + '& mjx-mn:has(~ mjx-mfrac), mjx-mfrac ~ mjx-mn': { + fontSize: '120% !important', + }, '& mjx-frac': { fontSize: '120% !important', }, diff --git a/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx b/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx index 84216ceee..6ad818db1 100644 --- a/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx +++ b/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx @@ -39,6 +39,7 @@ describe('ImageComponent', () => { const createMockEditor = (selection = { from: 0, to: 1 }) => ({ _tiptapContainerEl: document.body, + isEditable: true, commands: { updateAttributes: jest.fn(), focus: jest.fn(), diff --git a/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js b/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js index b1f019c08..26279d0cb 100644 --- a/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js +++ b/packages/editable-html-tip-tap/src/extensions/__tests__/responseArea.test.js @@ -264,16 +264,16 @@ describe('ResponseAreaExtension', () => { jest.resetModules(); }); - const buildInsertCommand = () => { + const buildInsertCommand = (type = 'inline-dropdown') => { const { ResponseAreaExtension } = require('../responseArea'); const context = { options: { - type: 'inline-dropdown', + type, maxResponseAreas: 5, }, }; const commands = ResponseAreaExtension.addCommands.call(context); - return commands.insertResponseArea('inline-dropdown'); + return commands.insertResponseArea(type); }; const createDoc = (existingCount, typeName = 'inline_dropdown') => ({ @@ -391,6 +391,60 @@ describe('ResponseAreaExtension', () => { expect(create).not.toHaveBeenCalled(); expect(mockTr.insert).not.toHaveBeenCalled(); }); + + describe('selection after insert', () => { + const buildStateAndTr = (typeName, { withResolve = false } = {}) => { + const mockInlineNode = { nodeSize: 1 }; + const create = jest.fn(() => mockInlineNode); + const mockDoc = { + descendants: jest.fn(), + content: { size: 50 }, + ...(withResolve ? { resolve: jest.fn((pos) => ({ resolved: pos })) } : {}), + }; + const mockTr = { + insert: jest.fn(), + doc: mockDoc, + setSelection: jest.fn(), + }; + const state = { + schema: { nodes: { [typeName]: { create } } }, + doc: mockDoc, + selection: { from: 5 }, + }; + + return { mockTr, state }; + }; + + it.each(['inline-dropdown', 'explicit-constructed-response'])( + 'sets a NodeSelection on the inserted node for %s', + (type) => { + const insert = buildInsertCommand(type); + const typeName = type.replace(/-/g, '_'); + const { mockTr, state } = buildStateAndTr(typeName); + const { NodeSelection, TextSelection } = require('prosemirror-state'); + + insert({ tr: mockTr, state, dispatch: jest.fn(), commands: { focus: jest.fn() } }); + + expect(NodeSelection.create).toHaveBeenCalledWith(mockTr.doc, 5); + expect(TextSelection.near).not.toHaveBeenCalled(); + expect(mockTr.setSelection).toHaveBeenCalledWith({ type: 'node', pos: 5 }); + }, + ); + + it('sets a TextSelection just after the inserted node for math-templated (no NodeSelection)', () => { + const insert = buildInsertCommand('math-templated'); + const { mockTr, state } = buildStateAndTr('math_templated', { withResolve: true }); + const { NodeSelection, TextSelection } = require('prosemirror-state'); + + insert({ tr: mockTr, state, dispatch: jest.fn(), commands: { focus: jest.fn() } }); + + expect(NodeSelection.create).not.toHaveBeenCalled(); + // usedPos (5) + nodeSize (1) === 6 + expect(mockTr.doc.resolve).toHaveBeenCalledWith(6); + expect(TextSelection.near).toHaveBeenCalledWith({ resolved: 6 }, 1); + expect(mockTr.setSelection).toHaveBeenCalledWith({ type: 'text', pos: { resolved: 6 }, dir: 1 }); + }); + }); }); }); }); diff --git a/packages/editable-html-tip-tap/src/extensions/image-component.jsx b/packages/editable-html-tip-tap/src/extensions/image-component.jsx index 4e5db4e61..7c9d31c21 100644 --- a/packages/editable-html-tip-tap/src/extensions/image-component.jsx +++ b/packages/editable-html-tip-tap/src/extensions/image-component.jsx @@ -125,7 +125,7 @@ function ImageComponent(props) { ); const applySizeData = useCallback(() => { - if (!node.attrs.width || !imgRef.current) return; + if (!node.attrs.width || !imgRef.current || !imgRef.current.naturalWidth) return; const resizePercent = getPercentFromWidth(node.attrs.width); if (node.attrs.resizePercent === resizePercent) return; updateThisNode({ resizePercent }); @@ -138,7 +138,7 @@ function ImageComponent(props) { useEffect(() => { if (selected) { - if (onlyThisNodeSelected) { + if (onlyThisNodeSelected && editor.isEditable) { // Only open the upload UI for a fresh placeholder. Remounting after tab switch // would otherwise call insertImageRequested again and reopen the file modal. const hasImageSrc = String(node.attrs?.src ?? '').trim(); @@ -156,15 +156,17 @@ function ImageComponent(props) { } else { setShowToolbar(selected); } - }, [onlyThisNodeSelected, selected]); + }, [onlyThisNodeSelected, selected, editor.isEditable]); useEffect(() => { applySizeData(); - const resizeHandle = resizeRef.current; - if (resizeHandle) { + const resizeHandle = resizeRef?.current; + + if (resizeHandle && editor.isEditable) { resizeHandle.addEventListener('mousedown', initResize, false); } + return () => { if (resizeHandle) { resizeHandle.removeEventListener('mousedown', initResize, false); @@ -272,7 +274,7 @@ function ImageComponent(props) { onLoad={loadImage} alt={node.attrs.alt} /> - + diff --git a/packages/editable-html-tip-tap/src/extensions/responseArea.js b/packages/editable-html-tip-tap/src/extensions/responseArea.js index 29c722cd6..95d191385 100644 --- a/packages/editable-html-tip-tap/src/extensions/responseArea.js +++ b/packages/editable-html-tip-tap/src/extensions/responseArea.js @@ -195,7 +195,13 @@ export const ResponseAreaExtension = Extension.create({ // tr.setSelection(NodeSelection.create(tr.doc, usedPos)) // --- Cursor move behavior for certain types (Slate: moveFocusTo next text) --- - if (['math_templated', 'inline_dropdown', 'explicit_constructed_response'].includes(typeName)) { + // Only types whose node view opens its own toolbar on selection + // (inline_dropdown, explicit_constructed_response) benefit from a + // NodeSelection here — the toolbar is the visual feedback that it's + // selected. math_templated has no such UI, so a NodeSelection there + // just looks unfocused (a highlighted node, no blinking caret); + // moving the cursor to just after it gives a real, visible caret. + if (['inline_dropdown', 'explicit_constructed_response'].includes(typeName)) { tr.setSelection(NodeSelection.create(tr.doc, usedPos)); } else { const after = usedPos + newInline.nodeSize; diff --git a/packages/editable-html/src/plugins/respArea/drag-in-the-blank/__tests__/choice.test.js b/packages/editable-html/src/plugins/respArea/drag-in-the-blank/__tests__/choice.test.js new file mode 100644 index 000000000..b18e9cfc7 --- /dev/null +++ b/packages/editable-html/src/plugins/respArea/drag-in-the-blank/__tests__/choice.test.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import DragDropChoice from '../choice'; + +// Mock @dnd-kit hooks to avoid DndContext requirement +jest.mock('@dnd-kit/core', () => ({ + useDraggable: jest.fn(() => ({ + attributes: {}, + listeners: {}, + setNodeRef: jest.fn(), + isDragging: false, + })), + useDroppable: jest.fn(() => ({ + setNodeRef: jest.fn(), + isOver: false, + active: null, + })), +})); + +jest.mock('@pie-lib/math-rendering', () => ({ + renderMath: jest.fn(), +})); + +// Collect the CSS rules emotion/MUI injected into the document (jsdom uses insertRule). +const collectEmotionRules = () => { + const rules = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + rules.push(rule.cssText); + } + } catch (e) { + /* inaccessible stylesheet */ + } + } + return rules; +}; + +describe('drag-in-the-blank choice', () => { + const defaultProps = { + value: { id: '1', value: '1/2' }, + disabled: false, + instanceId: 'test-instance', + n: { key: 'key-0' }, + nodeProps: {}, + opts: { options: { duplicates: false } }, + }; + + it('renders without crashing', () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + describe('fraction math styling', () => { + it('enlarges numerator/denominator digits adjacent to a fraction to 120%', () => { + render(); + const rule = collectEmotionRules().find((r) => r.includes('mjx-mn') && r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/mjx-mn:has\(~\s*mjx-mfrac\)/); + expect(rule).toMatch(/mjx-mfrac\s*~\s*mjx-mn/); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + + it('keeps the existing mjx-frac 120% rule', () => { + render(); + const rule = collectEmotionRules().find((r) => /(^|[^-])mjx-frac/.test(r) && !r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + }); +}); diff --git a/packages/editable-html/src/plugins/respArea/drag-in-the-blank/choice.jsx b/packages/editable-html/src/plugins/respArea/drag-in-the-blank/choice.jsx index f07948498..f6fe21b77 100644 --- a/packages/editable-html/src/plugins/respArea/drag-in-the-blank/choice.jsx +++ b/packages/editable-html/src/plugins/respArea/drag-in-the-blank/choice.jsx @@ -14,6 +14,9 @@ const StyledContent = styled('span')(({ theme }) => ({ '& mjx-frac': { fontSize: '120% !important', }, + '& mjx-mn:has(~ mjx-mfrac), mjx-mfrac ~ mjx-mn': { + fontSize: '120% !important', + }, '&.chip': { minWidth: '90px', }, diff --git a/packages/mask-markup/src/choices/__tests__/index.test.js b/packages/mask-markup/src/choices/__tests__/index.test.js index 05af4f6e7..e018463d5 100644 --- a/packages/mask-markup/src/choices/__tests__/index.test.js +++ b/packages/mask-markup/src/choices/__tests__/index.test.js @@ -4,6 +4,21 @@ import Choice from '../choice'; import { choice } from '../../__tests__/utils'; import Choices from '../index'; +// Collect the CSS rules emotion/MUI injected into the document (jsdom uses insertRule). +const collectEmotionRules = () => { + const rules = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + rules.push(rule.cssText); + } + } catch (e) { + /* inaccessible stylesheet */ + } + } + return rules; +}; + // Mock @dnd-kit hooks to avoid DndContext requirement jest.mock('@dnd-kit/core', () => ({ useDraggable: jest.fn(() => ({ @@ -71,5 +86,24 @@ describe('index', () => { expect(container.firstChild).toBeInTheDocument(); }); }); + + describe('fraction math styling', () => { + it('enlarges numerator/denominator digits adjacent to a fraction to 120%', () => { + render(); + // The new rule targets mjx-mn digits that sit next to an mjx-mfrac. + const rule = collectEmotionRules().find((r) => r.includes('mjx-mn') && r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/mjx-mn:has\(~\s*mjx-mfrac\)/); + expect(rule).toMatch(/mjx-mfrac\s*~\s*mjx-mn/); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + + it('keeps the existing mjx-frac 120% rule', () => { + render(); + const rule = collectEmotionRules().find((r) => /(^|[^-])mjx-frac/.test(r) && !r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + }); }); }); diff --git a/packages/mask-markup/src/choices/choice.jsx b/packages/mask-markup/src/choices/choice.jsx index 4629a9cfc..3d3dad64f 100644 --- a/packages/mask-markup/src/choices/choice.jsx +++ b/packages/mask-markup/src/choices/choice.jsx @@ -50,6 +50,9 @@ const StyledChipLabel = styled('span')(() => ({ '& mjx-frac': { fontSize: '120% !important', }, + '& mjx-mn:has(~ mjx-mfrac), mjx-mfrac ~ mjx-mn': { + fontSize: '120% !important', + }, })); export default function Choice({ choice, disabled, instanceId }) { @@ -71,8 +74,8 @@ export default function Choice({ choice, disabled, instanceId }) { style={ isDragging ? { - width: rootRef.current?.offsetWidth || 90, // min-width of chip is 90px, so if we don't have the width, we can use 90px as a fallback - height: rootRef.current?.offsetHeight || 32, // min-height of chip is 32px, so if we don't have the height, we can use 32px as a fallback + width: rootRef.current?.offsetWidth || 90, // min-width of chip is 90px, so if we don't have the width, we can use 90px as a fallback + height: rootRef.current?.offsetHeight || 32, // min-height of chip is 32px, so if we don't have the height, we can use 32px as a fallback } : {} } diff --git a/packages/mask-markup/src/components/__tests__/blank.test.js b/packages/mask-markup/src/components/__tests__/blank.test.js index 65954e66d..cb31e1b0e 100644 --- a/packages/mask-markup/src/components/__tests__/blank.test.js +++ b/packages/mask-markup/src/components/__tests__/blank.test.js @@ -30,6 +30,21 @@ jest.mock('@pie-lib/math-rendering', () => ({ renderMath: jest.fn(), })); +// Collect the CSS rules emotion/MUI injected into the document (jsdom uses insertRule). +const collectEmotionRules = () => { + const rules = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + rules.push(rule.cssText); + } + } catch (e) { + /* inaccessible stylesheet */ + } + } + return rules; +}; + describe('Blank', () => { const { renderMath } = require('@pie-lib/math-rendering'); const onChange = jest.fn(); @@ -184,6 +199,24 @@ describe('Blank', () => { }); }); + describe('fraction math styling', () => { + it('enlarges numerator/denominator digits adjacent to a fraction to 120%', () => { + render(); + const rule = collectEmotionRules().find((r) => r.includes('mjx-mn') && r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/mjx-mn:has\(~\s*mjx-mfrac\)/); + expect(rule).toMatch(/mjx-mfrac\s*~\s*mjx-mn/); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + + it('keeps the existing mjx-frac 120% rule', () => { + render(); + const rule = collectEmotionRules().find((r) => /(^|[^-])mjx-frac/.test(r) && !r.includes('mjx-mfrac')); + expect(rule).toBeDefined(); + expect(rule).toMatch(/font-size:\s*120%\s*!important/i); + }); + }); + describe('drag and drop', () => { it('accepts drag item when not disabled', () => { render(); diff --git a/packages/mask-markup/src/components/blank.jsx b/packages/mask-markup/src/components/blank.jsx index 5c42afae8..1828391c4 100644 --- a/packages/mask-markup/src/components/blank.jsx +++ b/packages/mask-markup/src/components/blank.jsx @@ -76,6 +76,9 @@ const StyledChipLabel = styled('span')(() => ({ '& mjx-frac': { fontSize: '120% !important', }, + '& mjx-mn:has(~ mjx-mfrac), mjx-mfrac ~ mjx-mn': { + fontSize: '120% !important', + }, '&.over': { whiteSpace: 'nowrap', overflow: 'hidden', @@ -125,11 +128,25 @@ function BlankContent({ }; const getMeasureNode = () => { - if (!spanRef.current) return null; + if (!spanRef.current) { + return null; + } + const mjx = spanRef.current.querySelector('mjx-container'); - if (mjx && spanRef.current.parentElement) return spanRef.current.parentElement; + + if (mjx && spanRef.current.parentElement) { + return spanRef.current.parentElement; + } + const img = spanRef.current.querySelector('img'); - if (img) return img; + + if (img) { + // If there's text alongside the image, measure the full span to capture both dimensions + const hasTextContent = spanRef.current.textContent.trim().length > 0; + + return hasTextContent ? spanRef.current : img; + } + return spanRef.current; }; @@ -166,7 +183,6 @@ function BlankContent({ const adjustedWidth = widthWithPadding <= responseAreaWidth ? responseAreaWidth : widthWithPadding; const adjustedHeight = heightWithPadding <= responseAreaHeight ? responseAreaHeight : heightWithPadding; - setDimensions((prevState) => ({ width: adjustedWidth > responseAreaWidth ? adjustedWidth : prevState.width, height: adjustedHeight > responseAreaHeight ? adjustedHeight : prevState.height,