vuetify
vuetify copied to clipboard
[Feature Request] Shadow DOM improvements for handling focus/active elements
Problem to solve
So far symptoms that we observed were:
- Input label jumps and is being crossed by outline when clicking on it in dialogs
- When clicking on dialog body - close dialog button becomes focused
- Clicking on input type=date doesn't bring up the native date picker
The main idea behind this issue is to share fixes that worked for us.
Related to https://github.com/vuetifyjs/vuetify/issues/17074
Proposed solution
The gist of the fix/issue:
- document.activeElement points to
instead of the particular element inside of Shadow DOM, which doesn't play nicely with Vuetify logic. Replaced with recursive search for activeElement in shadow roots - event.target for "focusin" events also points to
instead of the particular element, replaced with the above activeElement
vuetify+3.1.2.patch:
Have to replace `document.activeElement` with this:
(() => {
const getActiveElement = (document) => {
if (document.activeElement.shadowRoot) {
return getActiveElement(document.activeElement.shadowRoot);
}
return document.activeElement;
};
return getActiveElement(document);
})()
in order to make it work with shadow DOM.
Also need to replace `event.target` for `focusin` events with it, becase https://medium.com/dev-channel/focus-inside-shadow-dom-78e8a575b73#989d
diff --git a/node_modules/vuetify/lib/components/VDialog/VDialog.mjs b/node_modules/vuetify/lib/components/VDialog/VDialog.mjs
index e244e04..9b91843 100644
--- a/node_modules/vuetify/lib/components/VDialog/VDialog.mjs
+++ b/node_modules/vuetify/lib/components/VDialog/VDialog.mjs
@@ -45,7 +45,15 @@ export const VDialog = genericComponent()({
function onFocusin(e) {
var _overlay$value, _overlay$value2;
const before = e.relatedTarget;
- const after = e.target;
+ const after = (() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+ })();
if (before !== after && (_overlay$value = overlay.value) != null && _overlay$value.contentEl && // We're the topmost dialog
(_overlay$value2 = overlay.value) != null && _overlay$value2.globalTop &&
// It isn't the document or the dialog body
diff --git a/node_modules/vuetify/lib/components/VField/VField.mjs b/node_modules/vuetify/lib/components/VField/VField.mjs
index 0d7f70f..33b2d0e 100644
--- a/node_modules/vuetify/lib/components/VField/VField.mjs
+++ b/node_modules/vuetify/lib/components/VField/VField.mjs
@@ -138,7 +138,15 @@ export const VField = genericComponent()({
focus
}));
function onClick(e) {
- if (e.target !== document.activeElement) {
+ if (e.target !== (() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})()) {
e.preventDefault();
}
emit('click:control', e);
diff --git a/node_modules/vuetify/lib/components/VFileInput/VFileInput.mjs b/node_modules/vuetify/lib/components/VFileInput/VFileInput.mjs
index aa53c94..a9ed42a 100644
--- a/node_modules/vuetify/lib/components/VFileInput/VFileInput.mjs
+++ b/node_modules/vuetify/lib/components/VFileInput/VFileInput.mjs
@@ -94,7 +94,15 @@ export const VFileInput = defineComponent({
return props.messages.length ? props.messages : props.persistentHint ? props.hint : '';
});
function onFocus() {
- if (inputRef.value !== document.activeElement) {
+ if (inputRef.value !== (() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})()) {
var _inputRef$value;
(_inputRef$value = inputRef.value) == null ? void 0 : _inputRef$value.focus();
}
diff --git a/node_modules/vuetify/lib/components/VList/VList.mjs b/node_modules/vuetify/lib/components/VList/VList.mjs
index 94f72e8..36e7864 100644
--- a/node_modules/vuetify/lib/components/VList/VList.mjs
+++ b/node_modules/vuetify/lib/components/VList/VList.mjs
@@ -173,9 +173,25 @@ export const VList = genericComponent()({
function focus(location) {
if (!contentRef.value) return;
const focusable = [...contentRef.value.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')].filter(el => !el.hasAttribute('disabled'));
- const idx = focusable.indexOf(document.activeElement);
+ const idx = focusable.indexOf((() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})());
if (!location) {
- if (!contentRef.value.contains(document.activeElement)) {
+ if (!contentRef.value.contains((() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})())) {
var _focusable$;
(_focusable$ = focusable[0]) == null ? void 0 : _focusable$.focus();
}
diff --git a/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs b/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
index 872e0b7..c04a03d 100644
--- a/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
+++ b/node_modules/vuetify/lib/components/VOtpInput/VOtpInput.mjs
@@ -174,7 +174,15 @@ export default baseMixins.extend().extend({
const elements = this.$refs.input;
const ref = this.$refs.input && elements[otpIdx || 0];
if (!ref) return;
- if (document.activeElement !== ref) {
+ if ((() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})() !== ref) {
ref.focus();
return ref.select();
}
diff --git a/node_modules/vuetify/lib/components/VTextField/VTextField.mjs b/node_modules/vuetify/lib/components/VTextField/VTextField.mjs
index b06f0e3..1115ef4 100644
--- a/node_modules/vuetify/lib/components/VTextField/VTextField.mjs
+++ b/node_modules/vuetify/lib/components/VTextField/VTextField.mjs
@@ -78,7 +78,15 @@ export const VTextField = genericComponent()({
return props.messages.length ? props.messages : isFocused.value || props.persistentHint ? props.hint : '';
});
function onFocus() {
- if (inputRef.value !== document.activeElement) {
+ if (inputRef.value !== (() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})()) {
var _inputRef$value;
(_inputRef$value = inputRef.value) == null ? void 0 : _inputRef$value.focus();
}
diff --git a/node_modules/vuetify/lib/components/VTextarea/VTextarea.mjs b/node_modules/vuetify/lib/components/VTextarea/VTextarea.mjs
index 7257e09..2cd5945 100644
--- a/node_modules/vuetify/lib/components/VTextarea/VTextarea.mjs
+++ b/node_modules/vuetify/lib/components/VTextarea/VTextarea.mjs
@@ -84,7 +84,15 @@ export const VTextarea = defineComponent({
return props.messages.length ? props.messages : isActive.value || props.persistentHint ? props.hint : '';
});
function onFocus() {
- if (textareaRef.value !== document.activeElement) {
+ if (textareaRef.value !== (() => {
+ const getActiveElement = (document) => {
+ if (document.activeElement.shadowRoot) {
+ return getActiveElement(document.activeElement.shadowRoot);
+ }
+ return document.activeElement;
+ };
+ return getActiveElement(document);
+})()) {
var _textareaRef$value;
(_textareaRef$value = textareaRef.value) == null ? void 0 : _textareaRef$value.focus();
}
Do you think this issue would prevent a v-textarea from not being able to be focused programmatically? I already have the autofocus prop set on the textarea, but it doesn't work when the page is refreshed, so I tried the code below. That still doesn't work. I also tried to call my focusInput() at the end of the form submit function (not shown) to refocus the field, but that didn't work either.
Script
const promptInput = ref<HTMLElement | null>(null);
const userInput = ref("");
// Focus input
const focusInput = () => {
if (promptInput.value) {
promptInput.value.focus();
}
};
onMounted(focusInput);
Template
<v-textarea
ref="promptInput"
v-model.trim="userInput"
variant="outlined"
label="Type a message"
rows="1"
max-rows="8"
color="primary"
rounded
autofocus
auto-grow
></v-textarea>
Related
- https://github.com/vuetifyjs/vuetify/issues/18827 - does
v-textareahave similar functionality? - https://michaelnthiessen.com/set-focus-on-input-vue
Any insights on how to get a v-textarea field to programmatically autofocus would be much appreciated!
Not sure... I recommend to try:
- Regular html textarea element instead of Vuetify, just to see if autofocus will work there, shadow DOM is pretty bad with focus stuff, see https://github.com/whatwg/html/issues/833 for example
- Try my patch, it might help. Make sure you're using vuetify 3.1.2 for it to work, check out
patch-packageon npm
Hope this helps, cheers!
Not sure... I recommend to try:
- Regular html textarea element instead of Vuetify, just to see if autofocus will work there, shadow DOM is pretty bad with focus stuff, see Shadow DOM and autofocus="" whatwg/html#833 for example
- Try my patch, it might help. Make sure you're using vuetify 3.1.2 for it to work, check out
patch-packageon npmHope this helps, cheers!
Thank you!!
I can confirm @Maxim-Mazurok 's patch fixed the problem with input type="date/time/datetime-local" for me.
@KaelWD this wasn't solved, I believe, please reopen, thanks!
@johnleider please reopen, cheers!