vuetify icon indicating copy to clipboard operation
vuetify copied to clipboard

[Feature Request] Shadow DOM improvements for handling focus/active elements

Open Maxim-Mazurok opened this issue 2 years ago • 4 comments
trafficstars

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();
       }

Maxim-Mazurok avatar May 31 '23 06:05 Maxim-Mazurok

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-textarea have 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!

davidstackio avatar Dec 17 '23 22:12 davidstackio

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-package on npm

Hope this helps, cheers!

Maxim-Mazurok avatar Dec 18 '23 00:12 Maxim-Mazurok

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-package on npm

Hope this helps, cheers!

Thank you!!

haydenbbickerton avatar Jun 03 '24 02:06 haydenbbickerton

I can confirm @Maxim-Mazurok 's patch fixed the problem with input type="date/time/datetime-local" for me.

PierrickBrun avatar Jun 19 '24 14:06 PierrickBrun

@KaelWD this wasn't solved, I believe, please reopen, thanks!

Maxim-Mazurok avatar Jan 05 '25 00:01 Maxim-Mazurok

@johnleider please reopen, cheers!

Maxim-Mazurok avatar Jan 08 '25 13:01 Maxim-Mazurok