From 47c12a36247ef5d44295f98cad07e44d7b4da376 Mon Sep 17 00:00:00 2001 From: nn-morishita Date: Mon, 6 Apr 2026 01:15:27 +0900 Subject: [PATCH 1/4] fix: handle summary element correctly --- src/utils/isElementVisible.ts | 8 +++++++- tests/isVisible.spec.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utils/isElementVisible.ts b/src/utils/isElementVisible.ts index 27aa0cfd4..5c4e1ea5d 100644 --- a/src/utils/isElementVisible.ts +++ b/src/utils/isElementVisible.ts @@ -20,6 +20,12 @@ function isStyleVisible(element: T) { } function isAttributeVisible(element: T) { + if ( + element.nodeName === 'SUMMARY' && + element.closest('details') + ) { + return true + } return ( !element.hasAttribute('hidden') && (element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true) @@ -31,6 +37,6 @@ export function isElementVisible(element: T): boolean { element.nodeName !== '#comment' && isStyleVisible(element) && isAttributeVisible(element) && - (!element.parentElement || isElementVisible(element.parentElement)) + (element.nodeName === 'SUMMARY' || !element.parentElement || isElementVisible(element.parentElement)) ) } diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts index 2de407a61..280e85d78 100644 --- a/tests/isVisible.spec.ts +++ b/tests/isVisible.spec.ts @@ -228,6 +228,16 @@ describe('isVisible', () => { }) expect(wrapper.isVisible()).toBe(false) }) + it ('DetailContent should be visible when summary is visible', () => { + const DetailContent = defineComponent({ + template: `
Summary
Content
` + }) + + const wrapper = mount(DetailContent) + expect(wrapper.find('summary').isVisible()).toBe(true) + expect(wrapper.find('div').isVisible()).toBe(false) + + }) }) }) }) From d23d0a8f132e2f7467b685eab1d6710a520704ae Mon Sep 17 00:00:00 2001 From: nn-morishita Date: Tue, 5 May 2026 00:34:38 +0900 Subject: [PATCH 2/4] fix: preserve visible details/summary behavior --- src/utils/isElementVisible.ts | 36 +++++++++++++++++++++++++---------- tests/isVisible.spec.ts | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/utils/isElementVisible.ts b/src/utils/isElementVisible.ts index 5c4e1ea5d..1b5421551 100644 --- a/src/utils/isElementVisible.ts +++ b/src/utils/isElementVisible.ts @@ -20,16 +20,31 @@ function isStyleVisible(element: T) { } function isAttributeVisible(element: T) { - if ( - element.nodeName === 'SUMMARY' && - element.closest('details') - ) { - return true + if (element instanceof HTMLInputElement && element.type === 'hidden') { + return false } - return ( - !element.hasAttribute('hidden') && - (element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true) - ) + + if (element instanceof HTMLElement && element.hidden) { + return false + } + + return true +} + +function isHiddenByClosedDetails(element: T) { + let parent = element.parentElement + + while (parent) { + if (parent.nodeName === 'DETAILS' && !(parent as HTMLDetailsElement).open) { + if (!(element.nodeName === 'SUMMARY' && element.parentElement === parent)) { + return true + } + } + + parent = parent.parentElement + } + + return false } export function isElementVisible(element: T): boolean { @@ -37,6 +52,7 @@ export function isElementVisible(element: T): boolean { element.nodeName !== '#comment' && isStyleVisible(element) && isAttributeVisible(element) && - (element.nodeName === 'SUMMARY' || !element.parentElement || isElementVisible(element.parentElement)) + !isHiddenByClosedDetails(element) && + (!element.parentElement || isElementVisible(element.parentElement)) ) } diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts index 280e85d78..56edb1a1e 100644 --- a/tests/isVisible.spec.ts +++ b/tests/isVisible.spec.ts @@ -234,10 +234,45 @@ describe('isVisible', () => { }) const wrapper = mount(DetailContent) + expect(wrapper.find('details').isVisible()).toBe(true) expect(wrapper.find('summary').isVisible()).toBe(true) expect(wrapper.find('div').isVisible()).toBe(false) }) + it('should consider a summary as hidden when an ancestor is hidden', () => { + const HiddenAncestorSummary = defineComponent({ + template: ` +
+
+ Summary +
+
+ ` + }) + + const wrapper = mount(HiddenAncestorSummary) + expect(wrapper.find('summary').isVisible()).toBe(false) + }) + it('should consider a summary as hidden when nested inside closed details content', () => { + const NestedSummaryInClosedDetails = defineComponent({ + template: ` +
+ Main summary +
+
+ Nested summary +
+
+
+ ` + }) + + const wrapper = mount(NestedSummaryInClosedDetails) + const summaries = wrapper.findAll('summary') + + expect(summaries[0].isVisible()).toBe(true) + expect(summaries[1].isVisible()).toBe(false) + }) }) }) }) From edab927e5d37245ed6762df61db1aa4897184d33 Mon Sep 17 00:00:00 2001 From: nn-morishita Date: Sun, 10 May 2026 23:15:20 +0900 Subject: [PATCH 3/4] fix: format --- src/utils/isElementVisible.ts | 4 +++- tests/isVisible.spec.ts | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/isElementVisible.ts b/src/utils/isElementVisible.ts index 1b5421551..a30e606c9 100644 --- a/src/utils/isElementVisible.ts +++ b/src/utils/isElementVisible.ts @@ -36,7 +36,9 @@ function isHiddenByClosedDetails(element: T) { while (parent) { if (parent.nodeName === 'DETAILS' && !(parent as HTMLDetailsElement).open) { - if (!(element.nodeName === 'SUMMARY' && element.parentElement === parent)) { + if ( + !(element.nodeName === 'SUMMARY' && element.parentElement === parent) + ) { return true } } diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts index 56edb1a1e..412c65a4a 100644 --- a/tests/isVisible.spec.ts +++ b/tests/isVisible.spec.ts @@ -228,7 +228,7 @@ describe('isVisible', () => { }) expect(wrapper.isVisible()).toBe(false) }) - it ('DetailContent should be visible when summary is visible', () => { + it('DetailContent should be visible when summary is visible', () => { const DetailContent = defineComponent({ template: `
Summary
Content
` }) @@ -237,7 +237,6 @@ describe('isVisible', () => { expect(wrapper.find('details').isVisible()).toBe(true) expect(wrapper.find('summary').isVisible()).toBe(true) expect(wrapper.find('div').isVisible()).toBe(false) - }) it('should consider a summary as hidden when an ancestor is hidden', () => { const HiddenAncestorSummary = defineComponent({ From 70ffcbab014a310540294aed69000d89154e9c51 Mon Sep 17 00:00:00 2001 From: nn-morishita Date: Fri, 15 May 2026 00:43:51 +0900 Subject: [PATCH 4/4] refactor: enhance isElementVisible with JSDoc and fix test cases --- src/utils/isElementVisible.ts | 29 +++++++------ tests/isVisible.spec.ts | 81 +++++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/utils/isElementVisible.ts b/src/utils/isElementVisible.ts index a30e606c9..ba7d00e22 100644 --- a/src/utils/isElementVisible.ts +++ b/src/utils/isElementVisible.ts @@ -19,30 +19,35 @@ function isStyleVisible(element: T) { ) } -function isAttributeVisible(element: T) { - if (element instanceof HTMLInputElement && element.type === 'hidden') { - return false - } - - if (element instanceof HTMLElement && element.hidden) { +/** + * checks if an element is visible by checking its attributes and its parents' attributes. + * @param element + * @returns boolean + */ +function isElementVisibleByAttribute(element: T) { + if (element instanceof HTMLInputElement && (element.type === 'hidden' || element.hidden)) { return false } - return true } +/** + * checks if an element is hidden by a closed details element. + * @param element + * @returns boolean + */ function isHiddenByClosedDetails(element: T) { + const summary = element.closest('summary') let parent = element.parentElement while (parent) { if (parent.nodeName === 'DETAILS' && !(parent as HTMLDetailsElement).open) { - if ( - !(element.nodeName === 'SUMMARY' && element.parentElement === parent) - ) { + // If no ancestor exists, or the is not a direct child of this
, + // then the content is hidden + if (!summary || summary.parentElement !== parent) { return true } } - parent = parent.parentElement } @@ -53,7 +58,7 @@ export function isElementVisible(element: T): boolean { return ( element.nodeName !== '#comment' && isStyleVisible(element) && - isAttributeVisible(element) && + isElementVisibleByAttribute(element) && !isHiddenByClosedDetails(element) && (!element.parentElement || isElementVisible(element.parentElement)) ) diff --git a/tests/isVisible.spec.ts b/tests/isVisible.spec.ts index 412c65a4a..61bbaaadc 100644 --- a/tests/isVisible.spec.ts +++ b/tests/isVisible.spec.ts @@ -228,6 +228,31 @@ describe('isVisible', () => { }) expect(wrapper.isVisible()).toBe(false) }) + }) + + describe('details and summary elements', () => { + it('DetailContent should be invisible when display:none is applied by class', () => { + const style = document.createElement('style') + document.head.appendChild(style) + style.sheet!.insertRule('.hidden { display: none; }') + + const wrapper = mount({ + template: '' + }) + + expect(wrapper.get('#my-div').isVisible()).toBe(false) + expect(wrapper.find('summary').isVisible()).toBe(false) + expect(wrapper.find('details').isVisible()).toBe(false) + }) + it('DetailContent should be invisible when display:none is applied by style attribute', () => { + const wrapper = mount({ + template: '' + }) + + expect(wrapper.get('#my-div').isVisible()).toBe(false) + expect(wrapper.find('summary').isVisible()).toBe(false) + expect(wrapper.find('details').isVisible()).toBe(false) + }) it('DetailContent should be visible when summary is visible', () => { const DetailContent = defineComponent({ template: `
Summary
Content
` @@ -238,19 +263,12 @@ describe('isVisible', () => { expect(wrapper.find('summary').isVisible()).toBe(true) expect(wrapper.find('div').isVisible()).toBe(false) }) - it('should consider a summary as hidden when an ancestor is hidden', () => { - const HiddenAncestorSummary = defineComponent({ - template: ` -
-
- Summary -
-
- ` + it ('DetailContent shouild be visible when summarys child is visible', () => { + const childContent = defineComponent({ + template: `
Summary
` }) - - const wrapper = mount(HiddenAncestorSummary) - expect(wrapper.find('summary').isVisible()).toBe(false) + const wrapper = mount(childContent) + expect(wrapper.find('summary span').isVisible()).toBe(true); }) it('should consider a summary as hidden when nested inside closed details content', () => { const NestedSummaryInClosedDetails = defineComponent({ @@ -265,10 +283,47 @@ describe('isVisible', () => {
` }) - const wrapper = mount(NestedSummaryInClosedDetails) const summaries = wrapper.findAll('summary') + expect(summaries[0].isVisible()).toBe(true) + expect(summaries[1].isVisible()).toBe(false) + }) + it('should consider a summary as visible when nested inside open details content', () => { + const NestedSummaryInOpenDetails = defineComponent({ + template: ` +
+ Main summary +
+
+ Nested summary +
+
+
+ ` + }) + const wrapper = mount(NestedSummaryInOpenDetails) + const summaries = wrapper.findAll('summary') + + expect(summaries[0].isVisible()).toBe(true) + expect(summaries[1].isVisible()).toBe(true) + }) + it('should consider a 1st summary as visible and 2nd as hidden when nested inside closed details content which is applied display: none', () => { + const NestedSummaryInOpenDetails = defineComponent({ + template: ` +
+ Main summary +
+
+ Nested summary +
+
+
+ ` + }) + const wrapper = mount(NestedSummaryInOpenDetails) + const summaries = wrapper.findAll('summary') + expect(summaries[0].isVisible()).toBe(true) expect(summaries[1].isVisible()).toBe(false) })