@opencode/ui 2.0.27 → 2.0.29
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/dist/context/dialog.d.ts +7 -0
- package/dist/data-display/badge/badge.d.ts +1 -1
- package/dist/i18n/en.d.ts +3 -0
- package/package.json +1 -1
- package/src/actions/button/button.css +260 -243
- package/src/actions/icon-button/icon-button.css +125 -117
- package/src/actions/submit.css +58 -53
- package/src/context/dialog.tsx +70 -4
- package/src/data-display/badge/badge.css +5 -0
- package/src/data-display/badge/badge.tsx +1 -1
- package/src/forms/text-input/text-input.css +185 -180
- package/src/i18n/en.ts +3 -0
- package/src/overlays/dialog/dialog.tsx +5 -3
|
@@ -1,120 +1,128 @@
|
|
|
1
|
+
/* Vite can link this before the app entry CSS, so name every layer here or `components` ranks below Tailwind's base reset. */
|
|
2
|
+
@layer properties, theme, base, components, utilities;
|
|
1
3
|
@import "../submit.css";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
[data-component="icon-button-v2"][data-variant="ghost"]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[data-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[data-component="icon-button-v2"][data-variant="ghost-muted"]
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
[data-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
[data-component="icon-button-v2"][data-variant="ghost-muted"]:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
[data-component="icon-button-v2"][data-variant="ghost-muted"]:
|
|
118
|
-
|
|
119
|
-
|
|
5
|
+
@layer components {
|
|
6
|
+
[data-component="icon-button-v2"] {
|
|
7
|
+
-webkit-font-smoothing: antialiased;
|
|
8
|
+
-moz-osx-font-smoothing: grayscale;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
border-radius: 6px;
|
|
13
|
+
color: var(--v2-text-text-base);
|
|
14
|
+
user-select: none;
|
|
15
|
+
outline: none;
|
|
16
|
+
padding: 0;
|
|
17
|
+
cursor: default;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
[data-component="icon-button-v2"]:focus {
|
|
21
|
+
outline: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[data-component="icon-button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
|
|
25
|
+
outline: 2px solid var(--v2-border-border-focus);
|
|
26
|
+
outline-offset: 2.5px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-component="icon-button-v2"][data-size="small"] {
|
|
30
|
+
width: 20px;
|
|
31
|
+
height: 20px;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[data-component="icon-button-v2"][data-size="normal"] {
|
|
36
|
+
width: 24px;
|
|
37
|
+
height: 24px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[data-component="icon-button-v2"][data-size="large"] {
|
|
41
|
+
width: 28px;
|
|
42
|
+
height: 28px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-component="icon-button-v2"] [data-slot="icon-svg"] {
|
|
46
|
+
color: currentColor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Neutral */
|
|
50
|
+
[data-component="icon-button-v2"][data-variant="neutral"] {
|
|
51
|
+
background-color: var(--v2-background-bg-button-neutral);
|
|
52
|
+
color: var(--v2-text-text-base);
|
|
53
|
+
box-shadow: var(--v2-elevation-button-neutral);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[data-component="icon-button-v2"][data-variant="neutral"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
|
57
|
+
background-image:
|
|
58
|
+
linear-gradient(90deg, var(--v2-overlay-simple-overlay-hover) 0%, var(--v2-overlay-simple-overlay-hover) 100%),
|
|
59
|
+
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[data-component="icon-button-v2"][data-variant="neutral"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
|
63
|
+
background-image:
|
|
64
|
+
linear-gradient(
|
|
65
|
+
90deg,
|
|
66
|
+
var(--v2-overlay-simple-overlay-pressed) 0%,
|
|
67
|
+
var(--v2-overlay-simple-overlay-pressed) 100%
|
|
68
|
+
),
|
|
69
|
+
linear-gradient(90deg, var(--v2-background-bg-button-neutral) 0%, var(--v2-background-bg-button-neutral) 100%);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[data-component="icon-button-v2"][data-variant="neutral"]:is(:disabled, [data-state="disabled"]) {
|
|
73
|
+
opacity: 0.5;
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Ghost */
|
|
78
|
+
[data-component="icon-button-v2"][data-variant="ghost"] {
|
|
79
|
+
background-color: transparent;
|
|
80
|
+
color: var(--v2-text-text-base);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not(
|
|
84
|
+
[data-expanded]
|
|
85
|
+
) {
|
|
86
|
+
background-color: var(--v2-overlay-simple-overlay-hover);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[data-component="icon-button-v2"][data-variant="ghost"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
|
90
|
+
background-color: var(--v2-overlay-simple-overlay-pressed);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
[data-component="icon-button-v2"][data-variant="ghost"]:where([data-expanded]):not(:disabled) {
|
|
94
|
+
background-color: var(--v2-overlay-simple-overlay-pressed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[data-component="icon-button-v2"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) {
|
|
98
|
+
opacity: 0.5;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Ghost */
|
|
103
|
+
[data-component="icon-button-v2"][data-variant="ghost-muted"] {
|
|
104
|
+
background-color: transparent;
|
|
105
|
+
color: var(--v2-icon-icon-muted);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not(
|
|
109
|
+
[data-expanded]
|
|
110
|
+
) {
|
|
111
|
+
background-color: var(--v2-overlay-simple-overlay-hover);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
|
115
|
+
background-color: var(--v2-overlay-simple-overlay-pressed);
|
|
116
|
+
color: var(--v2-icon-icon-base);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-component="icon-button-v2"][data-variant="ghost-muted"]:where([data-expanded]):not(:disabled) {
|
|
120
|
+
background-color: var(--v2-overlay-simple-overlay-pressed);
|
|
121
|
+
color: var(--v2-icon-icon-base);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:disabled, [data-state="disabled"]) {
|
|
125
|
+
opacity: 0.5;
|
|
126
|
+
cursor: not-allowed;
|
|
127
|
+
}
|
|
120
128
|
}
|
package/src/actions/submit.css
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
[data-component="button-v2"][data-variant="submit"],
|
|
4
|
-
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
5
|
-
) {
|
|
6
|
-
--submit-highlight: var(--v2-alpha-light-20);
|
|
7
|
-
--submit-hover: var(--v2-overlay-simple-overlay-contrast-hover);
|
|
1
|
+
/* Vite can link this before the app entry CSS, so name every layer here or `components` ranks below Tailwind's base reset. */
|
|
2
|
+
@layer properties, theme, base, components, utilities;
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
linear-gradient(
|
|
12
|
-
90deg,
|
|
13
|
-
var(--v2-background-bg-icon-button-contrast) 0%,
|
|
14
|
-
var(--v2-background-bg-icon-button-contrast) 100%
|
|
15
|
-
);
|
|
16
|
-
color: var(--v2-text-text-contrast);
|
|
17
|
-
text-shadow: 0 0.5px 0.5px rgba(0, 0, 0, 0.3);
|
|
18
|
-
box-shadow: var(--v2-elevation-button-contrast);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
[data-color-scheme="dark"]
|
|
4
|
+
@layer components {
|
|
5
|
+
/* Shared by Composer submit actions and contrast icon buttons. */
|
|
22
6
|
:is(
|
|
23
7
|
[data-component="button-v2"][data-variant="submit"],
|
|
24
8
|
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
25
9
|
) {
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
--submit-highlight: var(--v2-alpha-light-20);
|
|
11
|
+
--submit-hover: var(--v2-overlay-simple-overlay-contrast-hover);
|
|
28
12
|
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
background-image:
|
|
14
|
+
linear-gradient(180deg, var(--submit-highlight) 0%, var(--v2-alpha-light-0) 100%),
|
|
15
|
+
linear-gradient(
|
|
16
|
+
90deg,
|
|
17
|
+
var(--v2-background-bg-icon-button-contrast) 0%,
|
|
18
|
+
var(--v2-background-bg-icon-button-contrast) 100%
|
|
19
|
+
);
|
|
20
|
+
color: var(--v2-text-text-contrast);
|
|
21
|
+
text-shadow: 0 0.5px 0.5px rgba(0, 0, 0, 0.3);
|
|
22
|
+
box-shadow: var(--v2-elevation-button-contrast);
|
|
23
|
+
}
|
|
31
24
|
|
|
32
|
-
[data-color-scheme="dark"]
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
[data-color-scheme="dark"]
|
|
26
|
+
:is(
|
|
27
|
+
[data-component="button-v2"][data-variant="submit"],
|
|
28
|
+
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
29
|
+
) {
|
|
30
|
+
--submit-highlight: var(--v2-alpha-light-60);
|
|
31
|
+
--submit-hover: var(--v2-alpha-dark-8);
|
|
32
|
+
|
|
33
|
+
color: var(--v2-text-text-inverse);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-color-scheme="dark"] [data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"]) {
|
|
37
|
+
color: var(--v2-icon-icon-inverse);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@media (hover: hover) {
|
|
41
|
+
:is(
|
|
42
|
+
[data-component="button-v2"][data-variant="submit"],
|
|
43
|
+
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
44
|
+
):is(:hover, [data-state="hover"]):not(:disabled) {
|
|
45
|
+
background-image:
|
|
46
|
+
linear-gradient(90deg, var(--submit-hover) 0%, var(--submit-hover) 100%),
|
|
47
|
+
linear-gradient(180deg, var(--submit-highlight) 0%, var(--v2-alpha-light-0) 100%),
|
|
48
|
+
linear-gradient(
|
|
49
|
+
90deg,
|
|
50
|
+
var(--v2-background-bg-icon-button-contrast) 0%,
|
|
51
|
+
var(--v2-background-bg-icon-button-contrast) 100%
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
35
55
|
|
|
36
|
-
@media (hover: hover) {
|
|
37
56
|
:is(
|
|
38
57
|
[data-component="button-v2"][data-variant="submit"],
|
|
39
58
|
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
40
|
-
):is(:
|
|
59
|
+
):is(:active, [data-state="pressed"]):not(:disabled) {
|
|
41
60
|
background-image:
|
|
42
|
-
linear-gradient(
|
|
61
|
+
linear-gradient(
|
|
62
|
+
90deg,
|
|
63
|
+
var(--v2-overlay-simple-overlay-contrast-pressed) 0%,
|
|
64
|
+
var(--v2-overlay-simple-overlay-contrast-pressed) 100%
|
|
65
|
+
),
|
|
43
66
|
linear-gradient(180deg, var(--submit-highlight) 0%, var(--v2-alpha-light-0) 100%),
|
|
44
67
|
linear-gradient(
|
|
45
68
|
90deg,
|
|
@@ -47,30 +70,12 @@
|
|
|
47
70
|
var(--v2-background-bg-icon-button-contrast) 100%
|
|
48
71
|
);
|
|
49
72
|
}
|
|
50
|
-
}
|
|
51
73
|
|
|
52
|
-
:is(
|
|
74
|
+
:is(
|
|
53
75
|
[data-component="button-v2"][data-variant="submit"],
|
|
54
76
|
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
55
|
-
):is(:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
var(--v2-overlay-simple-overlay-contrast-pressed) 0%,
|
|
60
|
-
var(--v2-overlay-simple-overlay-contrast-pressed) 100%
|
|
61
|
-
),
|
|
62
|
-
linear-gradient(180deg, var(--submit-highlight) 0%, var(--v2-alpha-light-0) 100%),
|
|
63
|
-
linear-gradient(
|
|
64
|
-
90deg,
|
|
65
|
-
var(--v2-background-bg-icon-button-contrast) 0%,
|
|
66
|
-
var(--v2-background-bg-icon-button-contrast) 100%
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
:is(
|
|
71
|
-
[data-component="button-v2"][data-variant="submit"],
|
|
72
|
-
[data-component="icon-button-v2"]:is([data-variant="submit"], [data-variant="contrast"])
|
|
73
|
-
):is(:disabled, [data-state="disabled"]) {
|
|
74
|
-
opacity: 0.4;
|
|
75
|
-
cursor: not-allowed;
|
|
77
|
+
):is(:disabled, [data-state="disabled"]) {
|
|
78
|
+
opacity: 0.4;
|
|
79
|
+
cursor: not-allowed;
|
|
80
|
+
}
|
|
76
81
|
}
|
package/src/context/dialog.tsx
CHANGED
|
@@ -18,6 +18,8 @@ import { makeEventListener } from "@solid-primitives/event-listener"
|
|
|
18
18
|
|
|
19
19
|
type DialogElement = () => JSX.Element
|
|
20
20
|
|
|
21
|
+
type FocusReturn = { element: HTMLElement; range?: Range }
|
|
22
|
+
|
|
21
23
|
type Active = {
|
|
22
24
|
id: string
|
|
23
25
|
node: JSX.Element
|
|
@@ -25,6 +27,9 @@ type Active = {
|
|
|
25
27
|
owner: Owner
|
|
26
28
|
onClose?: () => void
|
|
27
29
|
setClosing: (closing: boolean) => void
|
|
30
|
+
/** Where focus was before this dialog opened, or before the dialogs it replaced opened. */
|
|
31
|
+
focus?: FocusReturn
|
|
32
|
+
returnFocus: () => void
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
const Context = createContext<ReturnType<typeof init>>()
|
|
@@ -81,6 +86,10 @@ function init() {
|
|
|
81
86
|
setExiting((ids) => new Set([...ids, current.id]))
|
|
82
87
|
isolate(() => current.onClose?.())
|
|
83
88
|
current.setClosing(true)
|
|
89
|
+
|
|
90
|
+
// Only the top open dialog holds focus. It returns before close() does, as a native dialog's does, so an
|
|
91
|
+
// action that runs after closing, such as a palette command that opens the terminal, can move it on.
|
|
92
|
+
if (stack().findLast((item) => item === current || !closing.has(item.id)) === current) current.returnFocus()
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/** Programmatic close. Without an id it closes the top dialog, one at a time; with an id it never waits. */
|
|
@@ -108,6 +117,9 @@ function init() {
|
|
|
108
117
|
|
|
109
118
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
110
119
|
if (event.key !== "Escape") return
|
|
120
|
+
|
|
121
|
+
// A control that cancels its own edit on Escape, such as an inline rename, keeps the dialog open.
|
|
122
|
+
if (event.target instanceof Element && event.target.closest("[data-owns-escape]")) return
|
|
111
123
|
dismiss()
|
|
112
124
|
event.preventDefault()
|
|
113
125
|
event.stopPropagation()
|
|
@@ -116,7 +128,13 @@ function init() {
|
|
|
116
128
|
makeEventListener(window, "keydown", onKeyDown, { capture: true })
|
|
117
129
|
})
|
|
118
130
|
|
|
119
|
-
const mount = (
|
|
131
|
+
const mount = (
|
|
132
|
+
element: DialogElement,
|
|
133
|
+
owner: Owner,
|
|
134
|
+
onClose: (() => void) | undefined,
|
|
135
|
+
key: string | undefined,
|
|
136
|
+
focus: FocusReturn | undefined,
|
|
137
|
+
) => {
|
|
120
138
|
// A deferred open (e.g. from a focus callback) must not mount after the provider is gone.
|
|
121
139
|
if (state.disposed) return
|
|
122
140
|
const id = key ?? Math.random().toString(36).slice(2)
|
|
@@ -131,6 +149,7 @@ function init() {
|
|
|
131
149
|
const zIndex = () => String(50 + layer() * 10)
|
|
132
150
|
let dispose: (() => void) | undefined
|
|
133
151
|
let setClosing: ((closing: boolean) => void) | undefined
|
|
152
|
+
let layerRef: HTMLDivElement | undefined
|
|
134
153
|
|
|
135
154
|
// Stacked dialogs render as sibling portals, so only the top layer may own the focus trap.
|
|
136
155
|
const node = runWithOwner(owner, () =>
|
|
@@ -157,6 +176,7 @@ function init() {
|
|
|
157
176
|
}}
|
|
158
177
|
/>
|
|
159
178
|
<div
|
|
179
|
+
ref={layerRef}
|
|
160
180
|
data-dialog-layer={layer()}
|
|
161
181
|
style={{
|
|
162
182
|
position: "fixed",
|
|
@@ -178,19 +198,35 @@ function init() {
|
|
|
178
198
|
|
|
179
199
|
if (!dispose || !setClosing) return
|
|
180
200
|
|
|
181
|
-
const active: Active = {
|
|
201
|
+
const active: Active = {
|
|
202
|
+
id,
|
|
203
|
+
node,
|
|
204
|
+
dispose,
|
|
205
|
+
owner,
|
|
206
|
+
onClose,
|
|
207
|
+
setClosing,
|
|
208
|
+
focus,
|
|
209
|
+
returnFocus: () => {
|
|
210
|
+
// A close inside a reactive update unmounts the dialog, and its focus trap, only when the update ends.
|
|
211
|
+
if (layerRef?.contains(document.activeElement)) return queueMicrotask(() => returnFocus(focus, layerRef))
|
|
212
|
+
returnFocus(focus, layerRef)
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
182
216
|
setStack((items) => [...items, active])
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
const push = (element: DialogElement, owner: Owner, onClose?: () => void, id?: string) => {
|
|
186
220
|
lock.value = false
|
|
187
|
-
mount(element, owner, onClose, id)
|
|
221
|
+
mount(element, owner, onClose, id, captureFocus())
|
|
188
222
|
}
|
|
189
223
|
|
|
190
224
|
const show = (element: DialogElement, owner: Owner, onClose?: () => void, id?: string) => {
|
|
225
|
+
// A replacement returns focus to where the dialogs it replaces would have, not to their now-removed content.
|
|
226
|
+
const focus = stack().length > 0 ? stack()[0].focus : captureFocus()
|
|
191
227
|
disposeAll(true)
|
|
192
228
|
lock.value = false
|
|
193
|
-
mount(element, owner, onClose, id)
|
|
229
|
+
mount(element, owner, onClose, id, focus)
|
|
194
230
|
}
|
|
195
231
|
|
|
196
232
|
return {
|
|
@@ -210,6 +246,36 @@ function isolate(fn: () => void) {
|
|
|
210
246
|
}
|
|
211
247
|
}
|
|
212
248
|
|
|
249
|
+
/** Remembers the focused element and, in editable content, the caret, which focusing alone does not restore. */
|
|
250
|
+
function captureFocus(): FocusReturn | undefined {
|
|
251
|
+
const element = document.activeElement
|
|
252
|
+
|
|
253
|
+
if (!(element instanceof HTMLElement) || element === document.body) return
|
|
254
|
+
const selection = document.getSelection()
|
|
255
|
+
|
|
256
|
+
if (!element.isContentEditable || !selection?.rangeCount || !element.contains(selection.anchorNode)) {
|
|
257
|
+
return { element }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { element, range: selection.getRangeAt(0).cloneRange() }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Kobalte only returns focus to a dialog trigger, and these dialogs open without one. */
|
|
264
|
+
function returnFocus(focus: FocusReturn | undefined, layer: HTMLElement | undefined) {
|
|
265
|
+
const active = document.activeElement
|
|
266
|
+
|
|
267
|
+
if (!focus?.element.isConnected) return
|
|
268
|
+
|
|
269
|
+
// Something outside the closed dialog already has focus, for example because the dialog's onClose moved it.
|
|
270
|
+
if (active && active !== document.body && !layer?.contains(active)) return
|
|
271
|
+
focus.element.focus({ preventScroll: true })
|
|
272
|
+
|
|
273
|
+
if (!focus.range || !focus.element.contains(focus.range.commonAncestorContainer)) return
|
|
274
|
+
const selection = document.getSelection()
|
|
275
|
+
selection?.removeAllRanges()
|
|
276
|
+
selection?.addRange(focus.range)
|
|
277
|
+
}
|
|
278
|
+
|
|
213
279
|
export function DialogProvider(props: ParentProps) {
|
|
214
280
|
const ctx = init()
|
|
215
281
|
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
color: var(--v2-text-text-contrast);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
[data-component="tag"][data-variant="subtle"] {
|
|
36
|
+
border: 0;
|
|
37
|
+
background: var(--v2-background-bg-layer-03);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
[data-component="tag"][data-appearance="standard"] {
|
|
36
41
|
height: 18px;
|
|
37
42
|
padding: 0 6px;
|