mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
feat(client): adjustable scrollbar width in code editor (#49975)
This commit is contained in:
@@ -114,6 +114,7 @@
|
||||
"for": "Account Settings for {{username}}",
|
||||
"sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.",
|
||||
"sound-volume": "Campfire Volume:",
|
||||
"scrollbar-width": "Editor Scrollbar Width",
|
||||
"username": {
|
||||
"contains invalid characters": "Username \"{{username}}\" contains invalid characters",
|
||||
"is too short": "Username \"{{username}}\" is too short",
|
||||
|
||||
@@ -18,6 +18,7 @@ import ThemeSettings, { Themes } from './theme';
|
||||
import UsernameSettings from './username';
|
||||
import KeyboardShortcutsSettings from './keyboard-shortcuts';
|
||||
import SectionHeader from './section-header';
|
||||
import ScrollbarWidthSettings from './scrollbar-width';
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
@@ -288,6 +289,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
/>
|
||||
<ScrollbarWidthSettings />
|
||||
</FullWidthRow>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Form } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Spacer } from '../helpers';
|
||||
|
||||
import ToggleButtonSetting from './toggle-button-setting';
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function KeyboardShortcutsSettings({
|
||||
toggleKeyboardShortcuts(keyboardShortcuts ? false : true);
|
||||
}}
|
||||
/>
|
||||
<Spacer size='medium'></Spacer>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
label[for='scrollbar-width-slider'] {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range'] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
height: calc(1rem + 6px);
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']::-webkit-slider-thumb {
|
||||
/* box-shadow is needed to allow left hand side of slider track to have
|
||||
same color as right side. */
|
||||
box-shadow: 1px 1px 1px #3d3d3d;
|
||||
background: var(--secondary-color);
|
||||
-webkit-appearance: none;
|
||||
width: calc(1rem + 6px);
|
||||
height: calc(1rem + 6px);
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']:focus-visible::-webkit-slider-thumb {
|
||||
outline: 3px solid var(--focus-outline-color);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
background: var(--secondary-color);
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']::-moz-range-progress {
|
||||
background: var(--secondary-color);
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']::-moz-range-thumb {
|
||||
box-shadow: 1px 1px 1px #3d3d3d;
|
||||
border: 3px solid var(--secondary-color);
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
background: var(--secondary-color);
|
||||
position: relative;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
input.scrollbar-width[type='range']:focus-visible::-moz-range-thumb {
|
||||
outline: 3px solid var(--focus-outline-color);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers span:nth-child(1) {
|
||||
position: relative;
|
||||
inset-inline-start: 0.375rem;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers span:nth-child(2) {
|
||||
position: relative;
|
||||
inset-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers span:nth-child(3) {
|
||||
position: relative;
|
||||
inset-inline-start: 0.325rem;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers span:nth-child(4) {
|
||||
position: relative;
|
||||
inset-inline-start: 0.15rem;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers span:nth-child(5) {
|
||||
position: relative;
|
||||
inset-inline-start: -0.05rem;
|
||||
}
|
||||
|
||||
.scrollbar-width-numbers .selected {
|
||||
font-weight: 900;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-width-container .scrollbar-width-ticks {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.scrollbar-width-container .tick {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border: 2px solid var(--secondary-color);
|
||||
background: var(--secondary-background);
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 3px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scrollbar-width-container .tick[data-current='true'] {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:nth-child(1),
|
||||
[dir='rtl'] .scrollbar-width-ticks .tick:nth-child(5) {
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:nth-child(2),
|
||||
[dir='rtl'] .scrollbar-width-ticks .tick:nth-child(4) {
|
||||
left: calc(25% - 0.25rem + 2px);
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:nth-child(3) {
|
||||
left: calc(50% - 0.5rem + 0.5px);
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:nth-child(4),
|
||||
[dir='rtl'] .scrollbar-width-ticks .tick:nth-child(2) {
|
||||
left: calc(75% - 0.5rem - 5px);
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:nth-child(5),
|
||||
[dir='rtl'] .scrollbar-width-ticks .tick:nth-child(1) {
|
||||
left: calc(100% - 1rem - 2px);
|
||||
}
|
||||
|
||||
.scrollbar-width-ticks .tick:hover,
|
||||
.scrollbar-width-numbers span:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scrollbar-width-preview {
|
||||
display: inline-block;
|
||||
background: var(--secondary-color);
|
||||
height: 1.5rem;
|
||||
position: absolute;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Form } from '@freecodecamp/react-bootstrap';
|
||||
import React, { ChangeEvent, useState, useRef } from 'react';
|
||||
import store from 'store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Spacer } from '../helpers';
|
||||
import { getScrollbarWidth } from '../../utils/scrollbar-width';
|
||||
import './scrollbar-width.css';
|
||||
|
||||
const ticks = [5, 10, 15, 20, 25];
|
||||
|
||||
export default function ScrollbarWidthSettings(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [scrollbarWidth, setScrollbarWidth] = useState(getScrollbarWidth());
|
||||
const rangeRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const inputValue = Number(event.target.value);
|
||||
setScrollbarWidth(inputValue);
|
||||
store.set('monacoScrollbarWidth', inputValue);
|
||||
}
|
||||
|
||||
function handleClick(event: React.MouseEvent<HTMLSpanElement>) {
|
||||
const target = event.target as HTMLSpanElement;
|
||||
const newScrollbarWidth = Number(target.dataset.value);
|
||||
|
||||
if (!rangeRef?.current || !ticks.includes(newScrollbarWidth)) return;
|
||||
rangeRef.current.focus();
|
||||
|
||||
if (newScrollbarWidth === scrollbarWidth) return;
|
||||
rangeRef.current.value = String(newScrollbarWidth);
|
||||
setScrollbarWidth(newScrollbarWidth);
|
||||
store.set('monacoScrollbarWidth', newScrollbarWidth);
|
||||
}
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
|
||||
return (
|
||||
<Form inline={true} onSubmit={(e: React.FormEvent) => e.preventDefault()}>
|
||||
<label htmlFor='scrollbar-width-slider'>
|
||||
{t('settings.scrollbar-width')}:
|
||||
<span
|
||||
className='scrollbar-width-preview'
|
||||
style={{ width: `${scrollbarWidth}px` } as React.CSSProperties}
|
||||
/>
|
||||
</label>
|
||||
<div className='scrollbar-width-container'>
|
||||
<div className='scrollbar-width-ticks' aria-hidden='true'>
|
||||
{ticks.map(tick => (
|
||||
<span
|
||||
className='tick'
|
||||
onClick={handleClick}
|
||||
data-value={tick}
|
||||
key={`tick${tick}`}
|
||||
{...(scrollbarWidth === tick && { 'data-current': true })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type='range'
|
||||
min='5'
|
||||
max='25'
|
||||
step='5'
|
||||
id='scrollbar-width-slider'
|
||||
className='scrollbar-width'
|
||||
defaultValue={scrollbarWidth}
|
||||
onInput={handleChange}
|
||||
ref={rangeRef}
|
||||
/>
|
||||
<div className='scrollbar-width-numbers' aria-hidden='true'>
|
||||
{ticks.map(tick => (
|
||||
<span
|
||||
onClick={handleClick}
|
||||
data-value={tick}
|
||||
key={`num${tick}`}
|
||||
{...(scrollbarWidth === tick && { className: 'selected' })}
|
||||
>
|
||||
{tick}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer size='medium'></Spacer>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollbarWidthSettings.displayName = 'ScrollbarWidthSettings';
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
} from '../redux/selectors';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { enhancePrismAccessibility } from '../utils/index';
|
||||
import { getScrollbarWidth } from '../../../utils/scrollbar-width';
|
||||
import LowerJaw from './lower-jaw';
|
||||
|
||||
import './editor.css';
|
||||
@@ -286,7 +287,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
vertical: 'visible',
|
||||
verticalHasArrows: false,
|
||||
useShadows: false,
|
||||
verticalScrollbarSize: 5
|
||||
verticalScrollbarSize: getScrollbarWidth()
|
||||
},
|
||||
parameterHints: {
|
||||
enabled: false
|
||||
@@ -623,7 +624,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
|
||||
// make sure the overlayWidget has resized before using it to set the height
|
||||
|
||||
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
domNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
|
||||
// We have to wait for the viewZone to finish rendering before adjusting the
|
||||
// position of the content widget (i.e. trigger it via onDomNodeTop). If
|
||||
@@ -693,7 +694,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
editor: editor.IStandaloneCodeEditor
|
||||
) => {
|
||||
// make sure the overlayWidget has resized before using it to set the height
|
||||
outputNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
// We have to wait for the viewZone to finish rendering before adjusting the
|
||||
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
|
||||
// not the editor may report the wrong value for position of the lines.
|
||||
@@ -753,20 +754,25 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
domNode.style.userSelect = 'text';
|
||||
|
||||
domNode.style.left = `${editor.getLayoutInfo().contentLeft}px`;
|
||||
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
domNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
|
||||
domNode.style.top = getDescriptionZoneTop();
|
||||
dataRef.current.descriptionNode = domNode;
|
||||
return domNode;
|
||||
}
|
||||
|
||||
// Take the current scrollbar width into account
|
||||
function getEditorContentWidth(editor: editor.IStandaloneCodeEditor) {
|
||||
return editor.getLayoutInfo().contentWidth - getScrollbarWidth();
|
||||
}
|
||||
|
||||
function createOutputNode(editor: editor.IStandaloneCodeEditor) {
|
||||
if (dataRef.current.outputNode) return dataRef.current.outputNode;
|
||||
const outputNode = document.createElement('div');
|
||||
outputNode.classList.add('editor-lower-jaw');
|
||||
outputNode.setAttribute('id', 'editor-lower-jaw');
|
||||
outputNode.style.left = `${editor.getLayoutInfo().contentLeft}px`;
|
||||
outputNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
outputNode.style.top = getOutputZoneTop();
|
||||
dataRef.current.outputNode = outputNode;
|
||||
return outputNode;
|
||||
@@ -974,7 +980,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
const getDomNode = () => domNode;
|
||||
const getPosition = () => {
|
||||
if (getTop) {
|
||||
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
|
||||
domNode.style.width = `${getEditorContentWidth(editor)}px`;
|
||||
domNode.style.top = getTop();
|
||||
}
|
||||
// must return null, so that Monaco knows the widget will position
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import store from 'store';
|
||||
|
||||
export function getScrollbarWidth(): number {
|
||||
const storedWidth = parseInt(store.get('monacoScrollbarWidth'));
|
||||
return storedWidth >= 5 || storedWidth <= 25 ? storedWidth : 5;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
describe('Editor scrollbar width', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/settings');
|
||||
});
|
||||
|
||||
let upperJawWidth;
|
||||
|
||||
it('Default editor scrollbar width should be 5px', () => {
|
||||
cy.get('#scrollbar-width-slider').should('have.value', '5');
|
||||
cy.visit(
|
||||
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-2'
|
||||
);
|
||||
cy.get('.editor-upper-jaw').then($editorUpperJaw => {
|
||||
upperJawWidth = Number($editorUpperJaw.outerWidth());
|
||||
});
|
||||
cy.get('#editor-lower-jaw').should($editorLowerJaw => {
|
||||
expect(upperJawWidth).to.equal(Number($editorLowerJaw.outerWidth()));
|
||||
});
|
||||
cy.get('.monaco-scrollable-element').should($scrollable => {
|
||||
expect(upperJawWidth).to.equal(Number($scrollable.outerWidth()) - 5);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should allow you to change editor scrollbar width to 25px', () => {
|
||||
cy.get('.scrollbar-width-numbers > [data-value="25"]').click();
|
||||
cy.get('#scrollbar-width-slider').should('have.value', '25');
|
||||
cy.visit(
|
||||
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-2'
|
||||
);
|
||||
cy.get('.editor-upper-jaw').then($editorUpperJaw => {
|
||||
upperJawWidth = Number($editorUpperJaw.outerWidth());
|
||||
});
|
||||
cy.get('#editor-lower-jaw').should($editorLowerJaw => {
|
||||
expect(upperJawWidth).to.equal(Number($editorLowerJaw.outerWidth()));
|
||||
});
|
||||
cy.get('.monaco-scrollable-element').should($scrollable => {
|
||||
expect(upperJawWidth).to.equal(Number($scrollable.outerWidth()) - 25);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user