From 6af1d79cd69d5e64a5261c11b9bfb468363daf4b Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:05:58 +0200 Subject: [PATCH] [web] reset all css variables impacting the comment field (#35681) GitOrigin-RevId: b66f612e186434208a658d64ccc6346c37f23182 --- .../source-editor/extensions/mentions.ts | 89 +------------- .../js/shared/components/mentions-input.tsx | 70 +++++++++-- .../web/frontend/stylesheets/pages/all.scss | 1 + .../pages/editor/mentions-autocomplete.scss | 109 ++++++++++++++++++ .../pages/editor/review-panel.scss | 62 +++------- .../review-panel/review-panel.spec.tsx | 37 ++++-- 6 files changed, 212 insertions(+), 156 deletions(-) create mode 100644 services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss diff --git a/services/web/frontend/js/features/source-editor/extensions/mentions.ts b/services/web/frontend/js/features/source-editor/extensions/mentions.ts index 004d761324..7eb1a35566 100644 --- a/services/web/frontend/js/features/source-editor/extensions/mentions.ts +++ b/services/web/frontend/js/features/source-editor/extensions/mentions.ts @@ -68,92 +68,13 @@ export function mentionCompletions(context: CompletionContext) { // Marker class added to the mentions autocomplete tooltip (via the // `tooltipClass` option in mentions-input.tsx). The tooltip is parented to -// `document.body`, so the source editor's global autocomplete base theme -// (auto-complete.ts) also lands on it. Scoping every rule under this class -// both isolates the mentions styling from the LaTeX autocomplete and raises -// specificity enough to win over those global `li[role="option"]` / -// `.cm-completionDetail` rules. The `li[role="option"]` selector is used for -// the same reason. +// `document.body`, so it lives in the light DOM even when the editor is mounted +// in a shadow root. Its styling therefore lives in document-level SCSS +// (stylesheets/pages/editor/mentions-autocomplete.scss), scoped under this +// class so it can reach the body-parented tooltip regardless of where the +// editor is mounted. export const MENTIONS_TOOLTIP_CLASS = 'ol-cm-mentions-autocomplete' -export const mentionAutocompleteTheme = EditorView.baseTheme({ - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete': { - marginLeft: '0', - border: 'none', - background: 'transparent', - borderRadius: 'var(--border-radius-large)', - }, - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul': { - maxWidth: 'min(360px, 90vw)', - whiteSpace: 'normal', - padding: 'var(--spacing-02)', - border: '1px solid var(--border-divider)', - borderRadius: 'var(--border-radius-large)', - background: 'var(--bg-light-primary)', - // Matches the `shadow-md` elevation mixin (tooltips/dropdowns). - boxShadow: '0 4px 12px 0 rgb(30 37 48 / 12%), 0 2px 4px rgb(30 37 48 / 8%)', - fontFamily: 'var(--font-sans)', - }, - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul > li[role="option"]': - { - display: 'grid', - gridTemplateColumns: 'auto 1fr', - gridTemplateRows: 'auto auto', - alignItems: 'center', - columnGap: 'var(--spacing-04)', - padding: 'var(--spacing-03) var(--spacing-04)', - overflowWrap: 'anywhere', - borderRadius: 'var(--border-radius-medium)', - lineHeight: '1.3', - }, - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete > ul > li[role="option"][aria-selected]': - { - background: 'var(--bg-light-secondary)', - }, - - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .ol-cm-mention-avatar': - { - gridColumn: '1', - gridRow: '1 / span 2', - alignSelf: 'center', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '32px', - height: '32px', - borderRadius: 'var(--border-radius-full)', - color: 'var(--white)', - fontSize: 'var(--font-size-02)', - fontWeight: '600', - textTransform: 'uppercase', - boxSizing: 'border-box', - }, - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionLabel': - { - gridColumn: '2', - gridRow: '1', - fontSize: 'var(--font-size-03)', - color: 'var(--content-primary)', - }, - // The portion of the label matching what the user has typed. - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionMatchedText': - { - fontWeight: '700', - textDecoration: 'none', - }, - // details - '.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete .cm-completionDetail': - { - gridColumn: '2', - gridRow: '2', - margin: '0', - fontFamily: 'var(--font-sans)', - fontSize: 'var(--font-size-02)', - fontStyle: 'normal', - color: 'var(--content-secondary)', - }, -}) - // Renders a project member's avatar circle for the autocomplete option. The // background uses the same per-user hue as the rest of the editor's // collaborator colours (via getHueForUserId), so a user looks consistent diff --git a/services/web/frontend/js/shared/components/mentions-input.tsx b/services/web/frontend/js/shared/components/mentions-input.tsx index cc5e80ab95..be50b14b7b 100644 --- a/services/web/frontend/js/shared/components/mentions-input.tsx +++ b/services/web/frontend/js/shared/components/mentions-input.tsx @@ -12,7 +12,6 @@ import { autocompletion, completionStatus } from '@codemirror/autocomplete' import classNames from 'classnames' import mentions, { mentionCompletions, - mentionAutocompleteTheme, renderMentionAvatar, MENTIONS_TOOLTIP_CLASS, } from '@/features/source-editor/extensions/mentions' @@ -43,16 +42,20 @@ type MentionsInputProps = { className?: string } -// Shared editor styling for the mentions input. +// Styling for the mentions input's own CodeMirror instance. Mounted in a shadow +// root, so the document editor's selector-based CSS cannot reach it; only +// inherited custom properties cross the boundary. These rules override CM's +// built-in base theme, injected into the shadow root alongside the view. const mentionsInputTheme = EditorView.theme({ - // reset variables to override visual mode's .cm-content rules - '&.cm-editor': { - '--visual-font-family': 'var(--bs-body-font-family)', - '--visual-font-size': 'var(--font-size)', - }, - // double the specificity to override the base font family used by theme.ts regardless of extension load order - '.cm-content.cm-content': { + // Match the surrounding UI's body font rather than CM6's inherited defaults. + '&.cm-editor .cm-content': { fontFamily: 'var(--bs-body-font-family)', + fontSize: 'inherit', + lineHeight: 'inherit', + }, + // suppress CM6's default focus outline; the consumer's border/parent conveys focus + '&.cm-editor.cm-focused': { + outline: 'none', }, // when range in editor is focused and selected '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { @@ -67,6 +70,43 @@ const mentionsInputTheme = EditorView.theme({ }, }) +// Appearance for the review-panel inputs. These are mounted in a shadow root, +// so the light-DOM SCSS in review-panel.scss that used to style +// the inner CodeMirror via descendant selectors can no longer reach it. That +// styling lives here instead. The host
still gets its box styling +// (background/border/padding) from review-panel.scss, and CSS custom properties +// (e.g. --review-panel-color, --rp-input-min-height) cross the shadow boundary, +// so those rules resolve against the review-panel context as before. +const reviewPanelInputTheme = EditorView.theme({ + '&.cm-editor': { + background: 'transparent', + color: 'inherit', + maxHeight: '200px', + }, + '.cm-scroller': { + lineHeight: 'var(--line-height-02)', + // Hide scrollbars so they don't consume layout width on systems that use + // non-overlay scrollbars (e.g. macOS "always show scrollbars"), which would + // otherwise trigger extra remeasure cycles. Scrolling still works via + // trackpad/keyboard. + scrollbarWidth: 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + '.cm-content': { + // --rp-input-min-height is set per-variant on the host in review-panel.scss. + minHeight: 'var(--rp-input-min-height, 44px)', + padding: 'var(--spacing-01) var(--spacing-03)', + }, + '.cm-cursor, .cm-cursor-primary': { + borderLeftColor: 'var(--review-panel-color)', + }, + '.cm-placeholder': { + color: 'var(--content-placeholder-themed)', + }, +}) + // A small CodeMirror 6 input that supports `@mention` autocompletion. Shared by // the review panel's add-comment, reply, and edit fields so all of them get the // same mention behaviour. @@ -137,14 +177,23 @@ export const MentionsInput = forwardRef< return } + // Review-panel inputs are portaled inside the document editor's DOM, so + // mount inside a shadow root to keep the editor's selector-based CSS out. + // attachShadow throws if a root already exists (e.g. StrictMode remount), + // so reuse the existing one. + const parent = + containerRef.current.shadowRoot ?? + containerRef.current.attachShadow({ mode: 'open' }) + const view = new EditorView({ doc: initialValue, - parent: containerRef.current, + parent, extensions: [ projectMembersInfo, EditorView.lineWrapping, drawSelection(), mentionsInputTheme, + reviewPanelInputTheme, placeholderExt(placeholder), ...(mentionsEnabled ? [ @@ -157,7 +206,6 @@ export const MentionsInput = forwardRef< icons: false, addToOptions: [{ render: renderMentionAvatar, position: 20 }], }), - mentionAutocompleteTheme, ] : []), editableConf.current.of(EditorView.editable.of(!disabled)), diff --git a/services/web/frontend/stylesheets/pages/all.scss b/services/web/frontend/stylesheets/pages/all.scss index ef0db13379..4b0224b767 100644 --- a/services/web/frontend/stylesheets/pages/all.scss +++ b/services/web/frontend/stylesheets/pages/all.scss @@ -21,6 +21,7 @@ @import 'editor/file-view'; @import 'editor/figure-modal'; @import 'editor/chat'; +@import 'editor/mentions-autocomplete'; @import 'editor/toast'; @import 'editor/history'; @import 'editor/write-and-cite'; diff --git a/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss b/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss new file mode 100644 index 0000000000..d191cfcef0 --- /dev/null +++ b/services/web/frontend/stylesheets/pages/editor/mentions-autocomplete.scss @@ -0,0 +1,109 @@ +/* + * Styling for the @mention autocomplete tooltip used by the review-panel + * comment inputs (see mentions-input.tsx). The tooltip is + * parented to `document.body` (via `tooltips({ parent: document.body })`), so it + * lives in the light DOM even though the editor itself is mounted in a shadow + * root. Its styling must therefore live here, at document level, rather than in + * an in-editor CodeMirror theme. + * + * Because the shadow-hosted editor's CodeMirror base theme cannot reach a + * body-parented tooltip, this block is self-contained: it re-declares + * CodeMirror's autocomplete `baseTheme` rules for `.cm-tooltip` / + * `.cm-tooltip-autocomplete > ul` (z-index, list layout, scrolling), which CM + * injects into the editor's root (the shadow root here) and so never reach a + * `document.body`-parented tooltip. + * + * Every rule is scoped under the triple class to both isolate the mentions + * styling from the LaTeX autocomplete and win specificity over the source + * editor's global `li[role="option"]` / `.cm-completionDetail` rules. + */ +.cm-tooltip.cm-tooltip-autocomplete.ol-cm-mentions-autocomplete { + // Load-bearing CM base rules, re-declared so the tooltip is self-contained. + z-index: 500; + box-sizing: border-box; + margin-left: 0; + border: none; + background: transparent; + border-radius: var(--border-radius-large); + + > ul { + overflow: hidden auto; + max-height: 10em; + list-style: none; + margin: 0; + height: 100%; + max-width: min(360px, 90vw); + white-space: normal; + padding: var(--spacing-02); + border: 1px solid var(--border-divider); + border-radius: var(--border-radius-large); + background: var(--bg-light-primary); + + // Matches the `shadow-md` elevation mixin (tooltips/dropdowns). + box-shadow: + 0 4px 12px 0 rgb(30 37 48 / 12%), + 0 2px 4px rgb(30 37 48 / 8%); + font-family: var(--font-sans); + + > li[role='option'] { + cursor: pointer; + overflow-x: hidden; + text-overflow: ellipsis; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto; + align-items: center; + column-gap: var(--spacing-04); + padding: var(--spacing-03) var(--spacing-04); + overflow-wrap: anywhere; + border-radius: var(--border-radius-medium); + line-height: 1.3; + + &[aria-selected] { + background: var(--bg-light-secondary); + } + } + } + + .ol-cm-mention-avatar { + grid-column: 1; + grid-row: 1 / span 2; + align-self: center; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: var(--border-radius-full); + color: var(--white); + font-size: var(--font-size-02); + font-weight: 600; + text-transform: uppercase; + box-sizing: border-box; + } + + /* stylelint-disable selector-class-pattern -- CodeMirror completion classes */ + .cm-completionLabel { + grid-column: 2; + grid-row: 1; + font-size: var(--font-size-03); + color: var(--content-primary); + } + + // The portion of the label matching what the user has typed. + .cm-completionMatchedText { + font-weight: 700; + text-decoration: none; + } + + .cm-completionDetail { + grid-column: 2; + grid-row: 2; + margin: 0; + font-family: var(--font-sans); + font-size: var(--font-size-02); + font-style: normal; + color: var(--content-secondary); + } + /* stylelint-enable selector-class-pattern */ +} diff --git a/services/web/frontend/stylesheets/pages/editor/review-panel.scss b/services/web/frontend/stylesheets/pages/editor/review-panel.scss index 3caa599e45..b85d3963c2 100644 --- a/services/web/frontend/stylesheets/pages/editor/review-panel.scss +++ b/services/web/frontend/stylesheets/pages/editor/review-panel.scss @@ -488,7 +488,15 @@ del.review-panel-content-highlight { } } +// Host box for the review-panel comment inputs. The inner CodeMirror is mounted +// in a shadow root, so its styling lives in an +// in-shadow CM theme (reviewPanelInputTheme in mentions-input.tsx), not here. +// Only host-box rules and CSS custom properties (which cross the shadow +// boundary) belong in this block. --rp-input-min-height feeds the inner +// .cm-content min-height inside the shadow. .review-panel-add-comment-editor { + --rp-input-min-height: 44px; + background: var(--review-panel-entry-bg-color); color: var(--review-panel-color); border: 1px solid var(--border-primary-themed); @@ -497,57 +505,13 @@ del.review-panel-content-highlight { cursor: text; &.review-panel-comment-input { + // Default to two lines tall so long @mentions wrap into view + --rp-input-min-height: calc( + (2 * var(--line-height-02)) + (2 * var(--spacing-01)) + ); + font-size: var(--rp-base-font-size); color: var(--review-panel-color-secondary); - - .cm-content { - // Default to two lines tall so long @mentions wrap into view - min-height: calc((2 * var(--line-height-02)) + (2 * var(--spacing-01))); - } - } - - .cm-editor { - background: transparent; - color: inherit; - max-height: 200px; - - &.cm-focused { - outline: none; - } - } - - .cm-scroller { - font-family: inherit; - line-height: var(--line-height-02); - - // Hide scrollbars so they don't consume layout width on systems that use - // non-overlay scrollbars (e.g. macOS "always show scrollbars"). A visible - // scrollbar causes a width change mid-init that fires CM's ResizeObserver, - // which triggers extra remeasure/reposition cycles — visible as a "dance". - // Scrolling still works via trackpad/keyboard. - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - } - - .cm-content { - min-height: 44px; - } - - // extra specificity to override visual mode's .cm-content rules - &.review-panel-add-comment-editor .cm-content.cm-content { - padding: var(--spacing-01) var(--spacing-03); - } - - .cm-cursor, - .cm-cursor-primary { - border-left-color: var(--review-panel-color); - } - - .cm-placeholder { - color: var(--content-placeholder-themed); } } diff --git a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx index 7a47169f18..4714b3bca0 100644 --- a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx +++ b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx @@ -339,10 +339,14 @@ describe('', function () { cy.findByRole('menu').within(() => { cy.findByText('Edit').click({ scrollBehavior: false }) }) - cy.findByRole('textbox').type( - '{selectAll}edited comment text{enter}', - { scrollBehavior: false } - ) + // The edit input is mounted in a shadow root, so pierce it to + // reach the CodeMirror content. + cy.get('.review-panel-comment-edit') + .shadow() + .find('.cm-content') + .type('{selectAll}edited comment text{enter}', { + scrollBehavior: false, + }) cy.wait('@editComment') // TODO: Figure out a way to plumb the websocket response back through // to the test so we can verify the comment is resolved @@ -422,9 +426,14 @@ describe('', function () { it('adds new comment (replies) to a thread', function () { cy.get('@review-panel').within(() => { - cy.findByRole('textbox').type('a new reply{enter}', { - scrollBehavior: false, - }) + // The reply input is mounted in a shadow root, so pierce it to reach + // the CodeMirror content. + cy.get('.review-panel-add-comment-editor.review-panel-comment-input') + .shadow() + .find('.cm-content') + .type('a new reply{enter}', { + scrollBehavior: false, + }) }) cy.wait('@addReply') }) @@ -518,12 +527,16 @@ describe('', function () { cy.get('@add-comment-button').click({ scrollBehavior: false }) cy.get('@review-panel').within(() => { // The add-comment editor is the CM6 input that is not a reply/edit - // input (those also carry the review-panel-comment-input class). + // input (those also carry the review-panel-comment-input class). It is + // mounted in a shadow root, so pierce it to reach the content. cy.get( - '.review-panel-add-comment-editor:not(.review-panel-comment-input) .cm-content' - ).type('a new comment{enter}', { - scrollBehavior: false, - }) + '.review-panel-add-comment-editor:not(.review-panel-comment-input)' + ) + .shadow() + .find('.cm-content') + .type('a new comment{enter}', { + scrollBehavior: false, + }) }) cy.wait('@addNewComment') // TODO : Figure out a way to plumb the websocket response back through