mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
[web] reset all css variables impacting the comment field (#35681)
GitOrigin-RevId: b66f612e186434208a658d64ccc6346c37f23182
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <div> 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)),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -339,10 +339,14 @@ describe('<ReviewPanel />', 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('<ReviewPanel />', 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('<ReviewPanel />', 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
|
||||
|
||||
Reference in New Issue
Block a user