@threadlabs/looma 0.25.0 → 0.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui-input-group/ui-input-group.html +55 -5
- package/components/ui-input-group/ui-input-group.js +19 -2
- package/components/ui-textarea/ui-textarea.html +49 -14
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-input-group.css +56 -5
- package/styles/ui-textarea.css +49 -14
- package/vanilla/UiInputGroup.d.ts +2 -0
- package/vanilla/UiInputGroup.js +1 -1
- package/vue/UiInputGroup.d.ts +18 -0
- package/vue/UiInputGroup.vue +103 -7
- package/vue/UiTextarea.vue +92 -15
- package/vue/chunks/UiInputGroup.js +54 -3
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/components.css +175 -52
package/package.json
CHANGED
|
@@ -35,6 +35,17 @@
|
|
|
35
35
|
--_ui-default-input-focus-outline: none;
|
|
36
36
|
--_ui-default-input-radius: 0;
|
|
37
37
|
--_ui-default-input-surface: transparent;
|
|
38
|
+
--_ui-default-textarea-border-width: 0;
|
|
39
|
+
--_ui-default-textarea-border: transparent;
|
|
40
|
+
--_ui-default-textarea-border-hover: transparent;
|
|
41
|
+
--_ui-default-textarea-focus-border: transparent;
|
|
42
|
+
--_ui-default-textarea-invalid-border: transparent;
|
|
43
|
+
--_ui-default-textarea-invalid-focus-shadow: none;
|
|
44
|
+
--_ui-default-textarea-shadow: none;
|
|
45
|
+
--_ui-default-textarea-focus-shadow: none;
|
|
46
|
+
--_ui-default-textarea-focus-outline: none;
|
|
47
|
+
--_ui-default-textarea-radius: 0;
|
|
48
|
+
--_ui-default-textarea-surface: transparent;
|
|
38
49
|
|
|
39
50
|
display: flex;
|
|
40
51
|
align-items: stretch;
|
|
@@ -68,13 +79,13 @@
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
:scope:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
|
|
82
|
+
:scope:hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
|
|
72
83
|
border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/* The ring shows when the field's input would show its own: when it is focus-visible, which a
|
|
76
87
|
text input is for a pointer as well as a keyboard. A focused action button draws its own. */
|
|
77
|
-
:scope:has(.field input:focus-visible) {
|
|
88
|
+
:scope:has(.field :is(input, textarea):focus-visible) {
|
|
78
89
|
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
79
90
|
box-shadow:
|
|
80
91
|
var(--ui-control-inset),
|
|
@@ -85,7 +96,7 @@
|
|
|
85
96
|
border-color: var(--ui-input-group-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
:scope:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
|
|
99
|
+
:scope:has(:is(input, textarea):is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
|
|
89
100
|
box-shadow: var(
|
|
90
101
|
--ui-input-group-invalid-focus-shadow,
|
|
91
102
|
var(--ui-control-inset),
|
|
@@ -99,14 +110,14 @@
|
|
|
99
110
|
|
|
100
111
|
/* Forced colors drop box shadows, so the focus ring becomes an outline. */
|
|
101
112
|
@media (forced-colors: active) {
|
|
102
|
-
:scope:has(input:focus-visible) {
|
|
113
|
+
:scope:has(:is(input, textarea):focus-visible) {
|
|
103
114
|
outline: var(--ui-focus-width) solid Highlight;
|
|
104
115
|
outline-offset: var(--ui-border-width);
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
:scope[data-ui-input-group-state~="disabled"],
|
|
109
|
-
:scope:has(input:disabled) {
|
|
120
|
+
:scope:has(:is(input, textarea):disabled) {
|
|
110
121
|
background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
|
|
111
122
|
box-shadow: none;
|
|
112
123
|
cursor: not-allowed;
|
|
@@ -161,6 +172,41 @@
|
|
|
161
172
|
padding-inline: var(--ui-space-1);
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
/* Multiline text owns the first row; its action remains inside the same frame below it. */
|
|
176
|
+
:scope[data-ui-input-group-state~="multiline"] {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
:scope[data-ui-input-group-state~="multiline"] .field {
|
|
182
|
+
grid-column: 2;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
:scope[data-ui-input-group-state~="multiline"] .action {
|
|
186
|
+
grid-column: 1 / -1;
|
|
187
|
+
justify-content: end;
|
|
188
|
+
padding: var(--ui-space-2);
|
|
189
|
+
padding-block-start: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
|
|
193
|
+
so a translated label or pending indicator cannot cover what the person types. */
|
|
194
|
+
:scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] {
|
|
195
|
+
--_ui-default-textarea-padding-inline-end: calc(var(--_ui-input-group-action-inline-size, 7rem) + var(--ui-space-3));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] .field {
|
|
199
|
+
grid-row: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
:scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] .action {
|
|
203
|
+
grid-column: 2;
|
|
204
|
+
grid-row: 1;
|
|
205
|
+
align-self: start;
|
|
206
|
+
justify-self: end;
|
|
207
|
+
padding: var(--ui-space-2);
|
|
208
|
+
}
|
|
209
|
+
|
|
164
210
|
.affix:empty,
|
|
165
211
|
.action:empty {
|
|
166
212
|
display: none;
|
|
@@ -198,5 +244,10 @@
|
|
|
198
244
|
/* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
|
|
199
245
|
small button sits inside it with the difference split evenly, and the field keeps its height. */
|
|
200
246
|
|
|
247
|
+
/* Multiline text owns the first row; its action remains inside the same frame below it. */
|
|
248
|
+
|
|
249
|
+
/* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
|
|
250
|
+
so a translated label or pending indicator cannot cover what the person types. */
|
|
251
|
+
|
|
201
252
|
/* A component that sets its display still honours the hidden attribute on its root. */
|
|
202
253
|
}
|
package/styles/ui-textarea.css
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
/* Generated by HTML Next 1.0.0-alpha.29. Do not edit. */
|
|
2
|
+
@property --ui-textarea-border-width {
|
|
3
|
+
syntax: "*";
|
|
4
|
+
inherits: false;
|
|
5
|
+
}
|
|
6
|
+
@property --ui-textarea-border {
|
|
7
|
+
syntax: "*";
|
|
8
|
+
inherits: false;
|
|
9
|
+
}
|
|
10
|
+
@property --ui-textarea-border-hover {
|
|
11
|
+
syntax: "*";
|
|
12
|
+
inherits: false;
|
|
13
|
+
}
|
|
14
|
+
@property --ui-textarea-focus-border {
|
|
15
|
+
syntax: "*";
|
|
16
|
+
inherits: false;
|
|
17
|
+
}
|
|
18
|
+
@property --ui-textarea-focus-outline {
|
|
19
|
+
syntax: "*";
|
|
20
|
+
inherits: false;
|
|
21
|
+
}
|
|
22
|
+
@property --ui-textarea-surface {
|
|
23
|
+
syntax: "*";
|
|
24
|
+
inherits: false;
|
|
25
|
+
}
|
|
2
26
|
@property --ui-textarea-focus-shadow {
|
|
3
27
|
syntax: "*";
|
|
4
28
|
inherits: false;
|
|
@@ -19,6 +43,10 @@
|
|
|
19
43
|
syntax: "*";
|
|
20
44
|
inherits: false;
|
|
21
45
|
}
|
|
46
|
+
@property --ui-textarea-padding-inline-end {
|
|
47
|
+
syntax: "*";
|
|
48
|
+
inherits: false;
|
|
49
|
+
}
|
|
22
50
|
@scope ([data-component~="ui-textarea"]) to ([data-component], [data-slotted]) {
|
|
23
51
|
/* This component's hooks style the element they are set on, and never a nested one. */
|
|
24
52
|
|
|
@@ -31,15 +59,17 @@
|
|
|
31
59
|
margin: 0;
|
|
32
60
|
--_padding-block: var(--ui-space-2);
|
|
33
61
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
62
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-3)));
|
|
34
63
|
appearance: none;
|
|
35
64
|
resize: vertical;
|
|
36
|
-
border: var(--ui-border-width
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
border: var(--ui-textarea-border-width, var(--_ui-default-textarea-border-width, var(--ui-border-width))) solid
|
|
66
|
+
var(--ui-textarea-border, var(--_ui-default-textarea-border, var(--ui-control-border, var(--ui-border-strong))));
|
|
67
|
+
border-radius: var(--ui-textarea-radius, var(--_ui-default-textarea-radius, var(--ui-field-radius, var(--ui-radius-md))));
|
|
68
|
+
background: var(--ui-textarea-surface, var(--_ui-default-textarea-surface, var(--ui-control-surface, var(--ui-surface-elevated))));
|
|
39
69
|
color: var(--ui-text-primary);
|
|
40
70
|
font: inherit;
|
|
41
71
|
line-height: var(--ui-line-height-md);
|
|
42
|
-
box-shadow: var(--ui-textarea-shadow, var(--ui-control-inset));
|
|
72
|
+
box-shadow: var(--ui-textarea-shadow, var(--_ui-default-textarea-shadow, var(--ui-control-inset)));
|
|
43
73
|
transition:
|
|
44
74
|
border-color var(--ui-motion-fast) var(--ui-motion-ease),
|
|
45
75
|
box-shadow var(--ui-motion-fast) var(--ui-motion-ease),
|
|
@@ -58,29 +88,32 @@
|
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
:scope:hover:not(:focus):not(:disabled):not(:read-only):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
|
|
61
|
-
border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
|
|
91
|
+
border-color: var(--ui-textarea-border-hover, var(--_ui-default-textarea-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary))));
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
:scope:focus-visible {
|
|
65
|
-
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
66
|
-
outline: none;
|
|
67
|
-
box-shadow: var(--ui-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow));
|
|
95
|
+
border-color: var(--ui-textarea-focus-border, var(--_ui-default-textarea-focus-border, var(--ui-control-focus, var(--ui-accent-solid))));
|
|
96
|
+
outline: var(--ui-textarea-focus-outline, var(--_ui-default-textarea-focus-outline, none));
|
|
97
|
+
box-shadow: var(--ui-textarea-focus-shadow, var(--_ui-default-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow)));
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
/* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
|
|
71
101
|
:scope[aria-invalid="true"],
|
|
72
102
|
:scope:is(:user-invalid, [data-user-invalid]) {
|
|
73
|
-
border-color: var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
103
|
+
border-color: var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))));
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
:scope:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible {
|
|
77
107
|
box-shadow: var(
|
|
78
108
|
--ui-textarea-invalid-focus-shadow,
|
|
79
|
-
var(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
var(
|
|
110
|
+
--_ui-default-textarea-invalid-focus-shadow,
|
|
111
|
+
var(--ui-control-inset),
|
|
112
|
+
0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(
|
|
113
|
+
in srgb,
|
|
114
|
+
var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)))) 22%,
|
|
115
|
+
transparent
|
|
116
|
+
)
|
|
84
117
|
)
|
|
85
118
|
);
|
|
86
119
|
}
|
|
@@ -95,12 +128,14 @@
|
|
|
95
128
|
:scope[data-ui-textarea-state~="size=sm"] {
|
|
96
129
|
--_padding-block: var(--ui-space-1);
|
|
97
130
|
padding: var(--ui-space-1) var(--ui-space-2);
|
|
131
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-2)));
|
|
98
132
|
font-size: var(--ui-font-size-sm);
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
:scope[data-ui-textarea-state~="size=lg"] {
|
|
102
136
|
--_padding-block: var(--ui-space-3);
|
|
103
137
|
padding: var(--ui-space-3) var(--ui-space-4);
|
|
138
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-4)));
|
|
104
139
|
font-size: var(--ui-font-size-md);
|
|
105
140
|
}
|
|
106
141
|
|
|
@@ -6,7 +6,9 @@ export interface UiInputGroupElement extends HTMLDivElement {
|
|
|
6
6
|
removeEventListener<K extends keyof UiInputGroupEventMap>(type: K, listener: (this: UiInputGroupElement, event: UiInputGroupEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
7
7
|
}
|
|
8
8
|
export interface UiInputGroupProps {
|
|
9
|
+
actionPosition?: "below" | "top-end" | null;
|
|
9
10
|
disabled?: boolean | null;
|
|
11
|
+
multiline?: boolean | null;
|
|
10
12
|
prefixKind?: "text" | "icon" | null;
|
|
11
13
|
attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
|
|
12
14
|
children?: readonly (string | Node)[];
|
package/vanilla/UiInputGroup.js
CHANGED
|
@@ -3,7 +3,7 @@ import { manageComponentLifecycle } from "@nextwebwg/html-next/runtime";
|
|
|
3
3
|
import * as controller from "../components/ui-input-group/ui-input-group.js";
|
|
4
4
|
import "../styles/ui-input-group.css";
|
|
5
5
|
|
|
6
|
-
const definition = {...{"contract":{"tag":"ui-input-group","props":{"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"prefixKind":{"type":{"kind":"terminal","name":"keyword"},"values":["text","icon"],"required":false,"target":{"attribute":"prefixkind"},"default":"text"}}},"template":{"kind":"element","name":"div","attributes":[],"children":[{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"prefix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"field"}],"children":[{"kind":"slot"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"suffix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"action"}],"children":[{"kind":"slot","fallback":[],"name":"action"}]}]},"declarations":[],"root":{"kind":"native","element":"div","choices":["div"]}},source:{file:import.meta.url},css:""};
|
|
6
|
+
const definition = {...{"contract":{"tag":"ui-input-group","props":{"actionPosition":{"type":{"kind":"terminal","name":"keyword"},"values":["below","top-end"],"required":false,"target":{"attribute":"actionposition"},"default":"below"},"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"multiline":{"type":"boolean","required":false,"target":{"attribute":"multiline"},"default":false},"prefixKind":{"type":{"kind":"terminal","name":"keyword"},"values":["text","icon"],"required":false,"target":{"attribute":"prefixkind"},"default":"text"}}},"template":{"kind":"element","name":"div","attributes":[],"children":[{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"prefix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"field"}],"children":[{"kind":"slot"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"suffix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"action"}],"children":[{"kind":"slot","fallback":[],"name":"action"}]}]},"declarations":[],"root":{"kind":"native","element":"div","choices":["div"]}},source:{file:import.meta.url},css:""};
|
|
7
7
|
|
|
8
8
|
export function createUiInputGroup(options = {}) {
|
|
9
9
|
const { attributes = {}, children = [], slots = {}, ...componentProps } = options;
|
package/vue/UiInputGroup.d.ts
CHANGED
|
@@ -10,25 +10,43 @@ type __VLS_Slots = {} & {
|
|
|
10
10
|
action?: (props: typeof __VLS_7) => any;
|
|
11
11
|
};
|
|
12
12
|
declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
13
|
+
actionPosition: {
|
|
14
|
+
type: PropType<"below" | "top-end" | null>;
|
|
15
|
+
default: string;
|
|
16
|
+
};
|
|
13
17
|
disabled: {
|
|
14
18
|
type: PropType<boolean | null>;
|
|
15
19
|
default: boolean;
|
|
16
20
|
};
|
|
21
|
+
multiline: {
|
|
22
|
+
type: PropType<boolean | null>;
|
|
23
|
+
default: boolean;
|
|
24
|
+
};
|
|
17
25
|
prefixKind: {
|
|
18
26
|
type: PropType<"text" | "icon" | null>;
|
|
19
27
|
default: string;
|
|
20
28
|
};
|
|
21
29
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
30
|
+
actionPosition: {
|
|
31
|
+
type: PropType<"below" | "top-end" | null>;
|
|
32
|
+
default: string;
|
|
33
|
+
};
|
|
22
34
|
disabled: {
|
|
23
35
|
type: PropType<boolean | null>;
|
|
24
36
|
default: boolean;
|
|
25
37
|
};
|
|
38
|
+
multiline: {
|
|
39
|
+
type: PropType<boolean | null>;
|
|
40
|
+
default: boolean;
|
|
41
|
+
};
|
|
26
42
|
prefixKind: {
|
|
27
43
|
type: PropType<"text" | "icon" | null>;
|
|
28
44
|
default: string;
|
|
29
45
|
};
|
|
30
46
|
}>> & Readonly<{}>, {
|
|
31
47
|
disabled: boolean | null;
|
|
48
|
+
multiline: boolean | null;
|
|
49
|
+
actionPosition: "top-end" | "below" | null;
|
|
32
50
|
prefixKind: "text" | "icon" | null;
|
|
33
51
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
34
52
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
package/vue/UiInputGroup.vue
CHANGED
|
@@ -9,6 +9,15 @@ defineOptions({ inheritAttrs: false })
|
|
|
9
9
|
|
|
10
10
|
const propValidityContract = {
|
|
11
11
|
props: {
|
|
12
|
+
actionPosition: {
|
|
13
|
+
type: { kind: 'terminal', name: 'keyword' },
|
|
14
|
+
required: false,
|
|
15
|
+
target: { attribute: 'actionposition' },
|
|
16
|
+
description:
|
|
17
|
+
'In a multiline group, below keeps the action under the text. top-end places it inside the textarea at its upper end and reserves text padding for its\n measured width.',
|
|
18
|
+
values: ['below', 'top-end'],
|
|
19
|
+
default: 'below',
|
|
20
|
+
},
|
|
12
21
|
disabled: {
|
|
13
22
|
type: { kind: 'terminal', name: 'boolean' },
|
|
14
23
|
required: false,
|
|
@@ -17,6 +26,13 @@ const propValidityContract = {
|
|
|
17
26
|
'Fades the group\'s framing to match a disabled input inside it. Disable the input itself too.',
|
|
18
27
|
default: false,
|
|
19
28
|
},
|
|
29
|
+
multiline: {
|
|
30
|
+
type: { kind: 'terminal', name: 'boolean' },
|
|
31
|
+
required: false,
|
|
32
|
+
target: { attribute: 'multiline' },
|
|
33
|
+
description: 'Groups a Textarea with an action below its text inside the shared frame.',
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
20
36
|
prefixKind: {
|
|
21
37
|
type: { kind: 'terminal', name: 'keyword' },
|
|
22
38
|
required: false,
|
|
@@ -29,11 +45,28 @@ const propValidityContract = {
|
|
|
29
45
|
},
|
|
30
46
|
} as const
|
|
31
47
|
const props = defineProps({
|
|
48
|
+
actionPosition: {
|
|
49
|
+
type: null as unknown as PropType<'below' | 'top-end' | null>,
|
|
50
|
+
default: 'below',
|
|
51
|
+
},
|
|
32
52
|
disabled: { type: null as unknown as PropType<boolean | null>, default: false },
|
|
53
|
+
multiline: { type: null as unknown as PropType<boolean | null>, default: false },
|
|
33
54
|
prefixKind: { type: null as unknown as PropType<'text' | 'icon' | null>, default: 'text' },
|
|
34
55
|
})
|
|
35
|
-
const acceptedProps: Record<string, unknown> = {
|
|
56
|
+
const acceptedProps: Record<string, unknown> = {
|
|
57
|
+
actionPosition: 'below',
|
|
58
|
+
disabled: false,
|
|
59
|
+
multiline: false,
|
|
60
|
+
prefixKind: 'text',
|
|
61
|
+
}
|
|
36
62
|
const checkedProps = computed(() => ({
|
|
63
|
+
actionPosition: checkedProp<'below' | 'top-end'>(
|
|
64
|
+
props['actionPosition'],
|
|
65
|
+
{ kind: 'terminal', name: 'keyword' },
|
|
66
|
+
false,
|
|
67
|
+
'actionPosition',
|
|
68
|
+
acceptedProps,
|
|
69
|
+
) as 'below' | 'top-end',
|
|
37
70
|
disabled: checkedProp<boolean>(
|
|
38
71
|
props['disabled'],
|
|
39
72
|
{ kind: 'terminal', name: 'boolean' },
|
|
@@ -41,6 +74,13 @@ const checkedProps = computed(() => ({
|
|
|
41
74
|
'disabled',
|
|
42
75
|
acceptedProps,
|
|
43
76
|
) as boolean,
|
|
77
|
+
multiline: checkedProp<boolean>(
|
|
78
|
+
props['multiline'],
|
|
79
|
+
{ kind: 'terminal', name: 'boolean' },
|
|
80
|
+
false,
|
|
81
|
+
'multiline',
|
|
82
|
+
acceptedProps,
|
|
83
|
+
) as boolean,
|
|
44
84
|
prefixKind: checkedProp<'text' | 'icon'>(
|
|
45
85
|
props['prefixKind'],
|
|
46
86
|
{ kind: 'terminal', name: 'keyword' },
|
|
@@ -112,6 +152,10 @@ const hostState = computed(() =>
|
|
|
112
152
|
checkedProps.value.prefixKind && 'prefixKind',
|
|
113
153
|
checkedProps.value.prefixKind != null
|
|
114
154
|
&& `prefixKind=${encodeURIComponent(checkedProps.value.prefixKind)}`,
|
|
155
|
+
checkedProps.value.multiline && 'multiline',
|
|
156
|
+
checkedProps.value.actionPosition && 'actionPosition',
|
|
157
|
+
checkedProps.value.actionPosition != null
|
|
158
|
+
&& `actionPosition=${encodeURIComponent(checkedProps.value.actionPosition)}`,
|
|
115
159
|
].filter(Boolean).join(' ')
|
|
116
160
|
)
|
|
117
161
|
|
|
@@ -132,7 +176,7 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
132
176
|
? (incoming[name] ?? incoming[kebab] ?? null)
|
|
133
177
|
: null
|
|
134
178
|
},
|
|
135
|
-
propNames: ['disabled', 'prefixKind'],
|
|
179
|
+
propNames: ['actionPosition', 'disabled', 'multiline', 'prefixKind'],
|
|
136
180
|
propValidity: (name: string) =>
|
|
137
181
|
propValidityState({ contract: propValidityContract, values: { ...props } }, name),
|
|
138
182
|
})
|
|
@@ -143,7 +187,9 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
143
187
|
data-component="ui-input-group"
|
|
144
188
|
v-bind="nativeAttrs($attrs)"
|
|
145
189
|
v-prop-validity="{ contract: propValidityContract, values: { ...props } }"
|
|
190
|
+
:data-action-position="reflectedProp('actionPosition', 'action-position', checkedProps.actionPosition, undefined, false)"
|
|
146
191
|
:data-disabled="reflectedProp('disabled', 'disabled', checkedProps.disabled, undefined, false)"
|
|
192
|
+
:data-multiline="reflectedProp('multiline', 'multiline', checkedProps.multiline, undefined, false)"
|
|
147
193
|
:data-prefix-kind="reflectedProp('prefixKind', 'prefix-kind', checkedProps.prefixKind, undefined, false)"
|
|
148
194
|
:data-ui-input-group-state="hostState || undefined"
|
|
149
195
|
ref="root"
|
|
@@ -191,6 +237,17 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
191
237
|
--_ui-default-input-focus-outline: none;
|
|
192
238
|
--_ui-default-input-radius: 0;
|
|
193
239
|
--_ui-default-input-surface: transparent;
|
|
240
|
+
--_ui-default-textarea-border-width: 0;
|
|
241
|
+
--_ui-default-textarea-border: transparent;
|
|
242
|
+
--_ui-default-textarea-border-hover: transparent;
|
|
243
|
+
--_ui-default-textarea-focus-border: transparent;
|
|
244
|
+
--_ui-default-textarea-invalid-border: transparent;
|
|
245
|
+
--_ui-default-textarea-invalid-focus-shadow: none;
|
|
246
|
+
--_ui-default-textarea-shadow: none;
|
|
247
|
+
--_ui-default-textarea-focus-shadow: none;
|
|
248
|
+
--_ui-default-textarea-focus-outline: none;
|
|
249
|
+
--_ui-default-textarea-radius: 0;
|
|
250
|
+
--_ui-default-textarea-surface: transparent;
|
|
194
251
|
|
|
195
252
|
display: flex;
|
|
196
253
|
align-items: stretch;
|
|
@@ -226,13 +283,13 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
226
283
|
}
|
|
227
284
|
}
|
|
228
285
|
|
|
229
|
-
[data-component~="ui-input-group"]:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
|
|
286
|
+
[data-component~="ui-input-group"]:hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
|
|
230
287
|
border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
|
|
231
288
|
}
|
|
232
289
|
|
|
233
290
|
/* The ring shows when the field's input would show its own: when it is focus-visible, which a
|
|
234
291
|
text input is for a pointer as well as a keyboard. A focused action button draws its own. */
|
|
235
|
-
[data-component~="ui-input-group"]:has(.field input:focus-visible) {
|
|
292
|
+
[data-component~="ui-input-group"]:has(.field :is(input, textarea):focus-visible) {
|
|
236
293
|
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
237
294
|
box-shadow:
|
|
238
295
|
var(--ui-control-inset),
|
|
@@ -246,7 +303,7 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
246
303
|
);
|
|
247
304
|
}
|
|
248
305
|
|
|
249
|
-
[data-component~="ui-input-group"]:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
|
|
306
|
+
[data-component~="ui-input-group"]:has(:is(input, textarea):is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
|
|
250
307
|
box-shadow: var(
|
|
251
308
|
--ui-input-group-invalid-focus-shadow,
|
|
252
309
|
var(--ui-control-inset),
|
|
@@ -260,14 +317,14 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
260
317
|
|
|
261
318
|
/* Forced colors drop box shadows, so the focus ring becomes an outline. */
|
|
262
319
|
@media (forced-colors: active) {
|
|
263
|
-
[data-component~="ui-input-group"]:has(input:focus-visible) {
|
|
320
|
+
[data-component~="ui-input-group"]:has(:is(input, textarea):focus-visible) {
|
|
264
321
|
outline: var(--ui-focus-width) solid Highlight;
|
|
265
322
|
outline-offset: var(--ui-border-width);
|
|
266
323
|
}
|
|
267
324
|
}
|
|
268
325
|
|
|
269
326
|
[data-component~="ui-input-group"][data-ui-input-group-state~="disabled"],
|
|
270
|
-
[data-component~="ui-input-group"]:has(input:disabled) {
|
|
327
|
+
[data-component~="ui-input-group"]:has(:is(input, textarea):disabled) {
|
|
271
328
|
background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
|
|
272
329
|
box-shadow: none;
|
|
273
330
|
cursor: not-allowed;
|
|
@@ -323,6 +380,45 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
|
|
|
323
380
|
padding-inline: var(--ui-space-1);
|
|
324
381
|
}
|
|
325
382
|
|
|
383
|
+
/* Multiline text owns the first row; its action remains inside the same frame below it. */
|
|
384
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] {
|
|
385
|
+
display: grid;
|
|
386
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] .field {
|
|
390
|
+
grid-column: 2;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] .action {
|
|
394
|
+
grid-column: 1 / -1;
|
|
395
|
+
justify-content: end;
|
|
396
|
+
padding: var(--ui-space-2);
|
|
397
|
+
padding-block-start: 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
|
|
401
|
+
so a translated label or pending indicator cannot cover what the person types. */
|
|
402
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] {
|
|
403
|
+
--_ui-default-textarea-padding-inline-end: calc(
|
|
404
|
+
var(--_ui-input-group-action-inline-size, 7rem) + var(--ui-space-3)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"]
|
|
409
|
+
.field {
|
|
410
|
+
grid-row: 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
[data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"]
|
|
414
|
+
.action {
|
|
415
|
+
grid-column: 2;
|
|
416
|
+
grid-row: 1;
|
|
417
|
+
align-self: start;
|
|
418
|
+
justify-self: end;
|
|
419
|
+
padding: var(--ui-space-2);
|
|
420
|
+
}
|
|
421
|
+
|
|
326
422
|
.affix:empty,
|
|
327
423
|
.action:empty {
|
|
328
424
|
display: none;
|