Skip to content

Commit 24440d6

Browse files
committed
Improve the styling of the variables list rows and number fields
- Leave a 2px gap on the sides of the rows, so that their rounded corners are not stuck to the borders of the panel or dialog containing the list. - Replace the native increment buttons of the number fields by custom ones: they can't be styled (Firefox in particular displays large buttons that don't fit in a dense list). Like the native ones, keeping a button pressed changes the value again and again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K4F4KBJuaJmEiNhRaq5tiq
1 parent 4182436 commit 24440d6

4 files changed

Lines changed: 209 additions & 4 deletions

File tree

newIDE/app/src/UI/SimpleTextField.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,38 @@ const styles = {
3838
},
3939
};
4040

41+
/**
42+
* Delay before the value starts being changed again and again, when an
43+
* increment button is kept pressed (like the native buttons of number inputs do).
44+
*/
45+
const INCREMENT_REPEAT_INITIAL_DELAY = 400;
46+
const INCREMENT_REPEAT_DELAY = 60;
47+
4148
// $FlowFixMe[missing-local-annot]
4249
const stopPropagation = e => e.stopPropagation();
4350

51+
const ChevronArrowTopSvg = (
52+
<svg viewBox="0 0 16 16" focusable="false" aria-hidden="true">
53+
<path
54+
fillRule="evenodd"
55+
clipRule="evenodd"
56+
d="M7.99998 6.66663C8.13902 6.66663 8.27177 6.72452 8.36638 6.8264L10.533 9.15973C10.7209 9.36209 10.7092 9.67845 10.5069 9.86636C10.3045 10.0543 9.98815 10.0425 9.80025 9.84018L7.99998 7.90143L6.19971 9.84018C6.01181 10.0425 5.69544 10.0543 5.49309 9.86636C5.29073 9.67845 5.27902 9.36209 5.46692 9.15973L7.63358 6.8264C7.72819 6.72452 7.86095 6.66663 7.99998 6.66663Z"
57+
fill="currentColor"
58+
/>
59+
</svg>
60+
);
61+
62+
const ChevronArrowBottomSvg = (
63+
<svg viewBox="0 0 16 16" focusable="false" aria-hidden="true">
64+
<path
65+
fillRule="evenodd"
66+
clipRule="evenodd"
67+
d="M5.49309 6.80023C5.69544 6.61233 6.01181 6.62405 6.19971 6.8264L7.99998 8.76515L9.80025 6.8264C9.98815 6.62405 10.3045 6.61233 10.5069 6.80023C10.7092 6.98813 10.7209 7.3045 10.533 7.50685L8.36638 9.84019C8.27177 9.94207 8.13902 9.99996 7.99998 9.99996C7.86095 9.99996 7.72819 9.94207 7.63358 9.84019L5.46692 7.50685C5.27902 7.3045 5.29073 6.98813 5.49309 6.80023Z"
68+
fill="currentColor"
69+
/>
70+
</svg>
71+
);
72+
4473
/**
4574
* A text field, inspired from Material UI, but lightweight
4675
* and faster to render (2 DOM elements, uncontrolled, pure CSS styling).
@@ -103,11 +132,76 @@ export const SimpleTextField: React.ComponentType<{
103132
getCaretPosition,
104133
}));
105134

135+
const incrementRepeatTimeoutId = React.useRef<?TimeoutID>(null);
136+
const incrementRepeatIntervalId = React.useRef<?IntervalID>(null);
137+
138+
const stopIncrementing = React.useCallback(() => {
139+
if (incrementRepeatTimeoutId.current) {
140+
clearTimeout(incrementRepeatTimeoutId.current);
141+
incrementRepeatTimeoutId.current = null;
142+
}
143+
if (incrementRepeatIntervalId.current) {
144+
clearInterval(incrementRepeatIntervalId.current);
145+
incrementRepeatIntervalId.current = null;
146+
}
147+
}, []);
148+
149+
// Ensure the value is not changed forever if the component is unmounted
150+
// while an increment button is pressed.
151+
React.useEffect(() => stopIncrementing, [stopIncrementing]);
152+
153+
const { onChange, additionalContext, disabled } = props;
154+
const incrementValue = React.useCallback(
155+
(direction: 'up' | 'down') => {
156+
const input = inputRef.current;
157+
if (!input || disabled) return;
158+
159+
// Let the browser do the stepping, so that the value stays valid.
160+
if (direction === 'up') input.stepUp();
161+
else input.stepDown();
162+
onChange(input.value, additionalContext, 'change');
163+
},
164+
[onChange, additionalContext, disabled]
165+
);
166+
167+
const startIncrementing = React.useCallback(
168+
(
169+
event: SyntheticPointerEvent<HTMLButtonElement>,
170+
direction: 'up' | 'down'
171+
) => {
172+
// Only the main button of the pointer changes the value.
173+
if (event.button !== 0) return;
174+
175+
// Keep the focus on the input (and give it to it if it was elsewhere),
176+
// like the native increment buttons do.
177+
event.preventDefault();
178+
if (inputRef.current) inputRef.current.focus();
179+
180+
stopIncrementing();
181+
incrementValue(direction);
182+
183+
// Then, keep changing the value while the button is pressed.
184+
incrementRepeatTimeoutId.current = setTimeout(() => {
185+
incrementRepeatIntervalId.current = setInterval(
186+
() => incrementValue(direction),
187+
INCREMENT_REPEAT_DELAY
188+
);
189+
}, INCREMENT_REPEAT_INITIAL_DELAY);
190+
},
191+
[incrementValue, stopIncrementing]
192+
);
193+
194+
// The native increment buttons of number inputs can't be styled (and are
195+
// particularly unpleasant to look at on Firefox): they are hidden by the
196+
// CSS and replaced by these ones.
197+
const hasIncrementButtons = props.type === 'number' && !props.disabled;
198+
106199
return (
107200
<div
108201
className={classNames({
109202
[classes.simpleTextField]: true,
110203
[classes.disabled]: props.disabled,
204+
[classes.withIncrementButtons]: hasIncrementButtons,
111205
})}
112206
>
113207
<input
@@ -148,6 +242,36 @@ export const SimpleTextField: React.ComponentType<{
148242
}}
149243
style={props.italic ? styles.italic : undefined}
150244
/>
245+
{hasIncrementButtons && (
246+
<div className={classes.incrementButtons}>
247+
<button
248+
type="button"
249+
// Not focusable: the value can be changed with the arrow keys
250+
// when the input is focused.
251+
tabIndex={-1}
252+
aria-hidden="true"
253+
className={classes.incrementButton}
254+
onPointerDown={event => startIncrementing(event, 'up')}
255+
onPointerUp={stopIncrementing}
256+
onPointerLeave={stopIncrementing}
257+
onPointerCancel={stopIncrementing}
258+
>
259+
{ChevronArrowTopSvg}
260+
</button>
261+
<button
262+
type="button"
263+
tabIndex={-1}
264+
aria-hidden="true"
265+
className={classes.incrementButton}
266+
onPointerDown={event => startIncrementing(event, 'down')}
267+
onPointerUp={stopIncrementing}
268+
onPointerLeave={stopIncrementing}
269+
onPointerCancel={stopIncrementing}
270+
>
271+
{ChevronArrowBottomSvg}
272+
</button>
273+
</div>
274+
)}
151275
</div>
152276
);
153277
}

newIDE/app/src/UI/SimpleTextField.module.css

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,75 @@
6060
.disabled:focus-within::before {
6161
border-bottom-color: transparent;
6262
}
63+
64+
/**
65+
* The native increment/decrement buttons of number fields can't be styled
66+
* (there is no standard pseudo-element for them, and Firefox displays large
67+
* buttons that don't fit a dense list): they are hidden and replaced by the
68+
* `incrementButtons`.
69+
*/
70+
/* Chrome, Safari, Edge, Opera */
71+
.simpleTextField input::-webkit-outer-spin-button,
72+
.simpleTextField input::-webkit-inner-spin-button {
73+
-webkit-appearance: none;
74+
margin: 0;
75+
}
76+
77+
/* Firefox */
78+
.simpleTextField input[type='number'] {
79+
-moz-appearance: textfield;
80+
appearance: textfield;
81+
}
82+
83+
/* Leave room for the increment buttons, so that they never hide the value. */
84+
.withIncrementButtons input {
85+
box-sizing: border-box;
86+
padding-right: 22px;
87+
}
88+
89+
.incrementButtons {
90+
position: absolute;
91+
right: 0;
92+
top: 50%;
93+
transform: translateY(-50%);
94+
display: flex;
95+
flex-direction: column;
96+
width: 20px;
97+
height: 22px;
98+
border-radius: 4px;
99+
color: var(--theme-text-secondary-color);
100+
}
101+
102+
/* Show that the value can be changed with the buttons, like the other
103+
clickable parts of a row of a list are highlighted when hovered. */
104+
.simpleTextField:hover .incrementButtons,
105+
.simpleTextField:focus-within .incrementButtons {
106+
background-color: var(--theme-text-field-default-background-color);
107+
outline: 1px solid var(--theme-text-field-hover-border-color);
108+
color: var(--theme-text-default-color);
109+
}
110+
111+
.incrementButton {
112+
display: flex;
113+
align-items: center;
114+
justify-content: center;
115+
flex: 1 1 0;
116+
/* The chevrons are drawn larger than the buttons and cropped by them. */
117+
overflow: hidden;
118+
padding: 0;
119+
border: none;
120+
border-radius: 4px;
121+
background-color: transparent;
122+
color: inherit;
123+
cursor: pointer;
124+
}
125+
126+
.incrementButton:hover {
127+
background-color: var(--theme-hover-background-color);
128+
}
129+
130+
.incrementButton svg {
131+
flex: 0 0 auto;
132+
width: 24px;
133+
height: 24px;
134+
}

newIDE/app/src/VariablesList/VariablesList.module.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
/* Every row is positioned at `index * VARIABLE_ROW_HEIGHT` of the container. */
1212
.rowContainer {
1313
position: absolute;
14-
left: 0;
15-
right: 0;
14+
/* Leave a thin gap on the sides, so that the rounded corners of the rows are
15+
not stuck to the borders of the panel/dialog containing the list. */
16+
left: 2px;
17+
right: 2px;
1618
/* Leave a thin gap between two rows. */
1719
padding-bottom: 1px;
1820
box-sizing: border-box;

newIDE/app/src/stories/componentStories/SimpleTextField.stories.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,25 @@ export const Default = (): React.Node => (
2828
value={'456.123'}
2929
onChange={action('onChange')}
3030
/>
31+
<SimpleTextField
32+
disabled={true}
33+
type="number"
34+
id="some-id-3"
35+
value={'456.123'}
36+
onChange={action('onChange')}
37+
/>
3138
<SimpleTextField
3239
disabled={false}
3340
type="text"
34-
id="some-id-3"
41+
id="some-id-4"
3542
italic
3643
value={'Test 456'}
3744
onChange={action('onChange')}
3845
/>
3946
<SimpleTextField
4047
disabled={true}
4148
type="text"
42-
id="some-id-3"
49+
id="some-id-5"
4350
italic
4451
value={'Test 456'}
4552
onChange={action('onChange')}

0 commit comments

Comments
 (0)