feat(client): adjustable scrollbar width in code editor (#49975)

This commit is contained in:
Bruce Blaser
2023-04-18 15:36:03 +03:00
committed by GitHub
parent 14259121fc
commit 0dd8282cf8
8 changed files with 304 additions and 6 deletions
@@ -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",
+2
View File
@@ -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
+6
View File
@@ -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);
});
});
});