Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Maxim-Mazurok opened this issue May 31, 2023 · 6 comments
Assignees
Labels
S: stale This issue is untriaged and hasn't seen any activity in at least six months. S: triage

Comments

@Maxim-Mazurok
Copy link
Contributor

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 #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();
       }
@davidstackio
Copy link

davidstackio commented Dec 17, 2023

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

Any insights on how to get a v-textarea field to programmatically autofocus would be much appreciated!

@Maxim-Mazurok
Copy link
Contributor Author

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!

@haydenbbickerton
Copy link

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

@PierrickBrun
Copy link

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

@github-actions github-actions bot added the S: stale This issue is untriaged and hasn't seen any activity in at least six months. label Dec 20, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 4, 2025
@Maxim-Mazurok
Copy link
Contributor Author

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

@Maxim-Mazurok
Copy link
Contributor Author

@johnleider please reopen, cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S: stale This issue is untriaged and hasn't seen any activity in at least six months. S: triage
Projects
None yet
Development

No branches or pull requests

5 participants