Skip to content

Commit 7873fd9

Browse files
authored
Merge pull request #7501 from nextcloud-libraries/fix/input-styles
fix: adjust input border styles for dark theme and NcSelect
2 parents b2a187b + 8cd47b5 commit 7873fd9

11 files changed

+201
-179
lines changed

src/assets/input-border.scss

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Similar as inputBorder but without active styles.
8+
*/
9+
@mixin inputLikeBorder($legacySelector, $borderColor: var(--color-border-maxcontrast)) {
10+
--input-border-box-shadow-light: 0 -1px #{$borderColor},
11+
0 0 0 1px color-mix(in srgb, #{$borderColor}, 65% transparent);
12+
--input-border-box-shadow-dark: 0 1px #{$borderColor},
13+
0 0 0 1px color-mix(in srgb, #{$borderColor}, 65% transparent);
14+
--input-border-box-shadow: var(--input-border-box-shadow-light);
15+
border: none;
16+
border-radius: var(--border-radius-element);
17+
box-shadow: var(--input-border-box-shadow);
18+
19+
&:hover:not([disabled]) {
20+
box-shadow: 0 0 0 1px $borderColor;
21+
}
22+
23+
// override with system theme
24+
@media (prefers-color-scheme: dark) {
25+
--input-border-box-shadow: var(--input-border-box-shadow-dark);
26+
}
27+
28+
// override with nextcloud theme
29+
@at-root [data-theme-dark] #{&} {
30+
--input-border-box-shadow: var(--input-border-box-shadow-dark);
31+
}
32+
@at-root [data-theme-light] #{&} {
33+
--input-border-box-shadow: var(--input-border-box-shadow-light);
34+
}
35+
36+
@at-root #{$legacySelector} #{&} {
37+
box-shadow: 0 0 0 1px $borderColor;
38+
39+
&:hover:not([disabled]) {
40+
box-shadow: 0 0 0 2px $borderColor;
41+
}
42+
}
43+
}
44+
45+
/**
46+
* Create a consistent border for an input element.
47+
* With Nextcloud 32+ there is no real border anymore but we use a box-shadow.
48+
*/
49+
@mixin inputBorder($legacySelector, $borderColor: var(--color-border-maxcontrast)) {
50+
@include inputLikeBorder($legacySelector, $borderColor);
51+
52+
&:focus-within:not([disabled]),
53+
&:active:not([disabled]) {
54+
box-shadow: 0 0 0 2px $borderColor,
55+
0 0 0 4px var(--color-main-background) !important;
56+
}
57+
}

src/components/NcInputField/NcInputField.vue

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ import type { VueClassType } from '../../utils/VueTypes.ts'
2222
2323
import { mdiAlertCircleOutline, mdiCheck } from '@mdi/js'
2424
import { computed, useAttrs, useTemplateRef, warn } from 'vue'
25+
import NcButton from '../NcButton/NcButton.vue'
26+
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
2527
import { createElementId } from '../../utils/createElementId.ts'
2628
import { isLegacy } from '../../utils/legacy.ts'
27-
import NcButton from '../NcButton/index.ts'
28-
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.ts'
2929
3030
export interface NcInputFieldProps {
3131
/**
@@ -308,12 +308,11 @@ function handleInput(event: Event) {
308308
</template>
309309

310310
<style lang="scss" scoped>
311+
@use '../../assets/input-border.scss' as border;
311312
312313
.input-field {
313314
--input-border-color: var(--color-border-maxcontrast);
314315
--input-border-radius: var(--border-radius-element);
315-
// Used e.g. if border width differs between focused and unfocused we need to compensate to prevent jumping
316-
--input-border-width-offset: calc(var(--border-width-input-focused, 2px) - var(--border-width-input, 2px));
317316
// The padding before the input can start (leading button or border)
318317
--input-padding-start: var(--border-radius-element);
319318
// The padding where the input has to end (trailing button or border)
@@ -347,23 +346,15 @@ function handleInput(event: Event) {
347346
348347
&__main-wrapper {
349348
height: var(--default-clickable-area);
350-
padding: var(--border-width-input, 2px);
349+
padding: var(--border-width-input-focused, 2px);
351350
position: relative;
352-
353-
&:not(:has([disabled])):has(input:focus),
354-
&:not(:has([disabled])):has(input:active) {
355-
padding: 0;
356-
}
357351
}
358352
359353
&__input {
354+
@include border.inputBorder('.input-field--legacy', var(--input-border-color));
360355
background-color: var(--color-main-background);
361356
color: var(--color-main-text);
362-
border: none;
363357
border-radius: var(--input-border-radius);
364-
box-shadow:
365-
0 -1px var(--input-border-color),
366-
0 0 0 1px color-mix(in srgb, var(--input-border-color), 65% transparent);
367358
368359
cursor: pointer;
369360
-webkit-appearance: textfield !important;
@@ -373,11 +364,11 @@ function handleInput(event: Event) {
373364
font-size: var(--default-font-size);
374365
text-overflow: ellipsis;
375366
367+
padding-block: 0;
368+
padding-inline: var(--input-padding-start) var(--input-padding-end);
376369
height: 100% !important;
377370
min-height: unset;
378371
width: 100%;
379-
padding-block: var(--input-border-width-offset);
380-
padding-inline: calc(var(--input-padding-start) + var(--input-border-width-offset)) calc(var(--input-padding-end) + var(--input-border-width-offset));
381372
382373
&::placeholder {
383374
color: var(--color-text-maxcontrast);
@@ -397,17 +388,9 @@ function handleInput(event: Event) {
397388
display: none;
398389
}
399390
400-
&:hover:not([disabled]) {
401-
box-shadow: 0 0 0 1px var(--input-border-color);;
402-
}
403-
404391
&:active:not([disabled]),
405392
&:focus:not([disabled]) {
406393
--input-border-color: var(--color-main-text);
407-
// Reset padding offset when focused
408-
--input-border-width-offset: 0px;
409-
border: var(--border-width-input-focused, 2px) solid var(--input-border-color);
410-
box-shadow: 0 0 0 2px var(--color-main-background) !important;
411394
}
412395
413396
&:focus + .input-field__label,
@@ -543,23 +526,5 @@ function handleInput(event: Event) {
543526
color: var(--color-border-success, var(--color-success));
544527
}
545528
}
546-
547-
&--legacy {
548-
.input-field__input {
549-
box-shadow: 0 0 0 1px var(--input-border-color) inset;
550-
}
551-
552-
.input-field__main-wrapper:hover:not(:has([disabled])) {
553-
padding: 0;
554-
555-
.input-field__input {
556-
--input-border-color: var(--color-main-text);
557-
// Reset padding offset when focused
558-
--input-border-width-offset: 0px;
559-
border: var(--border-width-input-focused, 2px) solid var(--input-border-color);
560-
box-shadow: 0 0 0 2px var(--color-main-background) !important;
561-
}
562-
}
563-
}
564529
}
565530
</style>

src/components/NcSelect/NcSelect.vue

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ export default {
325325
<VueSelect
326326
class="select"
327327
:class="{
328+
'select--legacy': isLegacy,
328329
'select--no-wrap': noWrap,
329330
}"
330331
v-bind="propsToForward"
@@ -398,10 +399,11 @@ import { h, warn } from 'vue'
398399
import VueSelect from 'vue-select'
399400
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
400401
import Close from 'vue-material-design-icons/Close.vue'
402+
import NcEllipsisedOption from '../NcEllipsisedOption/NcEllipsisedOption.vue'
403+
import NcLoadingIcon from '../NcLoadingIcon/NcLoadingIcon.vue'
401404
import { t } from '../../l10n.ts'
402405
import { createElementId } from '../../utils/createElementId.ts'
403-
import NcEllipsisedOption from '../NcEllipsisedOption/index.js'
404-
import NcLoadingIcon from '../NcLoadingIcon/index.ts'
406+
import { isLegacy } from '../../utils/legacy.ts'
405407
406408
// TODO: Use @nextcloud/vue-select once a vue 3 version is available.
407409
// Until then, all @nextcloud/vue-select specific improvements won't be available.
@@ -779,6 +781,7 @@ export default {
779781
780782
return {
781783
avatarSize,
784+
isLegacy,
782785
}
783786
},
784787
@@ -902,6 +905,8 @@ export default {
902905
</script>
903906
904907
<style lang="scss">
908+
@use '../../assets/input-border.scss' as border;
909+
905910
body {
906911
/**
907912
* Set custom vue-select CSS variables.
@@ -971,7 +976,7 @@ body {
971976
972977
.v-select.select {
973978
/* Override default vue-select styles */
974-
min-height: var(--default-clickable-area);
979+
min-height: calc(var(--default-clickable-area) - 2 * var(--border-width-input));
975980
min-width: 260px;
976981
margin: 0 0 var(--default-grid-baseline);
977982
@@ -1016,7 +1021,7 @@ body {
10161021
.vs__dropdown-toggle {
10171022
position: relative;
10181023
max-height: 100px;
1019-
padding: 0;
1024+
padding: var(--border-width-input);
10201025
overflow-y: auto;
10211026
}
10221027
@@ -1030,15 +1035,22 @@ body {
10301035
}
10311036
10321037
&.vs--open .vs__dropdown-toggle {
1033-
border-width: var(--border-width-input-focused);
1034-
outline: 2px solid var(--color-main-background);
10351038
border-color: var(--color-main-text);
10361039
border-bottom-color: transparent;
1040+
border-bottom-left-radius: 0;
1041+
border-bottom-right-radius: 0;
1042+
border-style: solid;
1043+
border-width: var(--border-width-input-focused);
1044+
outline: 2px solid var(--color-main-background);
1045+
padding: 0;
10371046
}
10381047
1039-
&:not(.vs--disabled, .vs--open) .vs__dropdown-toggle:hover {
1040-
outline: 2px solid var(--color-main-background);
1041-
border-color: var(--color-main-text);
1048+
&:not(.vs--disabled, .vs--open) {
1049+
.vs__dropdown-toggle:active,
1050+
.vs__dropdown-toggle:focus-within {
1051+
outline: 2px solid var(--color-main-background);
1052+
border-color: var(--color-main-text);
1053+
}
10421054
}
10431055
10441056
&.vs--disabled {
@@ -1105,6 +1117,10 @@ body {
11051117
}
11061118
}
11071119
1120+
.vs__dropdown-toggle {
1121+
@include border.inputLikeBorder('.select--legacy', var(--vs-border-color));
1122+
}
1123+
11081124
.vs__dropdown-menu {
11091125
border-width: var(--border-width-input-focused) !important;
11101126
border-color: var(--color-main-text) !important;

src/components/NcTextArea/NcTextArea.vue

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,13 @@ defineExpose({
163163
select,
164164
})
165165
166+
const attrs = useAttrs()
167+
166168
/**
167169
* The native text area component instance
168170
*/
169171
const textAreaElement = useTemplateRef('input')
170172
171-
// needs to be a getter as attrs are not reactive
172-
const attrs = useAttrs()
173-
174173
const internalPlaceholder = computed(() => props.placeholder || (isLegacy ? props.label : undefined))
175174
176175
// warn about invalid labels (missing label and no label outside)
@@ -285,6 +284,8 @@ function select() {
285284
</template>
286285

287286
<style lang="scss" scoped>
287+
@use '../../assets/input-border.scss' as border;
288+
288289
.textarea {
289290
--input-border-color: var(--color-border-maxcontrast);
290291
--input-border-width-offset: calc(var(--border-width-input-focused, 2px) - var(--border-width-input, 2px));
@@ -301,42 +302,27 @@ function select() {
301302
302303
&__main-wrapper {
303304
height: calc(var(--default-clickable-area) * 2);
304-
padding: var(--border-width-input, 2px);
305+
padding: var(--border-width-input-focused, 2px);
305306
position: relative;
306-
307-
&:not(:has([disabled])):has(textarea:focus),
308-
&:not(:has([disabled])):has(textarea:active) {
309-
padding: 0;
310-
}
311307
}
312308
313309
&__input {
314310
margin: 0;
315-
padding-block: calc(10px + var(--input-border-width-offset));
316-
padding-inline: calc(12px - var(--border-width-input, 2px) + var(--input-border-width-offset)); // align with label 8px margin label + 4px padding label - 2px border input
311+
padding-block: var(--border-radius-element);
312+
padding-inline: 10px; // align with label 8px margin label + 4px padding label - 2px border input
317313
width: 100%;
318314
font-size: var(--default-font-size);
319315
text-overflow: ellipsis;
320316
cursor: pointer;
321317
322318
background-color: var(--color-main-background);
323319
color: var(--color-main-text);
324-
// we use box shadow to create a border as this allows use to have a nice gradient
325-
border: none;
326-
border-radius: var(--border-radius-element);
327-
box-shadow:
328-
0 -1px var(--input-border-color),
329-
0 0 0 1px color-mix(in srgb, var(--input-border-color), 65% transparent);
330-
331-
&:hover:not([disabled]) {
332-
box-shadow: 0 0 0 1px var(--input-border-color);
333-
}
320+
@include border.inputBorder('.textarea--legacy', var(--input-border-color));
321+
334322
&:active:not([disabled]),
335323
&:focus:not([disabled]) {
336324
--input-border-width-offset: 0px;
337325
--input-border-color: var(--color-main-text);
338-
border: var(--border-width-input-focused, 2px) solid var(--input-border-color);
339-
box-shadow: 0 0 0 2px var(--color-main-background) !important;
340326
}
341327
342328
// Hide placeholder while not focussed -> show label instead (only if internal label is used)
@@ -420,24 +406,5 @@ function select() {
420406
color: var(--color-success-text);
421407
}
422408
}
423-
424-
// for Nextcloud 31 and older we need the old design with only one color
425-
&--legacy {
426-
.textarea__input {
427-
box-shadow: 0 0 0 1px var(--input-border-color);
428-
}
429-
430-
.textarea__main-wrapper:hover:not(:has([disabled])) {
431-
padding: 0;
432-
433-
.textarea__input {
434-
--input-border-color: var(--color-main-text);
435-
// Reset padding offset when focused
436-
--input-border-width-offset: 0px;
437-
border: var(--border-width-input-focused, 2px) solid var(--input-border-color);
438-
box-shadow: 0 0 0 2px var(--color-main-background) !important;
439-
}
440-
}
441-
}
442409
}
443410
</style>

0 commit comments

Comments
 (0)