diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f97e89af6..7ef38401e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add new `Tabs` component ([#674](https://github.com/tailwindlabs/headlessui/pull/674))
+- Make `Disclosure.Button` close the disclosure inside a `Disclosure.Panel` ([#682](https://github.com/tailwindlabs/headlessui/pull/682))
## [Unreleased - Vue]
### Added
- Add new `Tabs` component ([#674](https://github.com/tailwindlabs/headlessui/pull/674))
+- Make `DisclosureButton` close the disclosure inside a `DisclosurePanel` ([#682](https://github.com/tailwindlabs/headlessui/pull/682))
## [@headlessui/react@v1.3.0] - 2021-06-21
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
index 06f432bf1b..ccbaa1e812 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx
@@ -9,6 +9,8 @@ import {
assertDisclosureButton,
getDisclosureButton,
getDisclosurePanel,
+ assertActiveElement,
+ getByText,
} from '../../test-utils/accessibility-assertions'
import { click, press, Keys, MouseButton } from '../../test-utils/interactions'
import { Transition } from '../transitions/transition'
@@ -619,4 +621,36 @@ describe('Mouse interactions', () => {
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
})
)
+
+ it(
+ 'should be possible to close the Disclosure by clicking on a Disclosure.Button inside a Disclosure.Panel',
+ suppressConsoleLogs(async () => {
+ render(
+
+ Open
+
+ Close
+
+
+ )
+
+ // Open the disclosure
+ await click(getDisclosureButton())
+
+ let closeBtn = getByText('Close')
+
+ expect(closeBtn).not.toHaveAttribute('id')
+ expect(closeBtn).not.toHaveAttribute('aria-controls')
+ expect(closeBtn).not.toHaveAttribute('aria-expanded')
+
+ // The close button should close the disclosure
+ await click(closeBtn)
+
+ // Verify it is closed
+ assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
+
+ // Verify we restored the Open button
+ assertActiveElement(getDisclosureButton())
+ })
+ )
})
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index 0f2f28785b..3de8a85bb0 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -100,6 +100,13 @@ function useDisclosureContext(component: string) {
return context
}
+let DisclosurePanelContext = createContext(null)
+DisclosurePanelContext.displayName = 'DisclosurePanelContext'
+
+function useDisclosurePanelContext() {
+ return useContext(DisclosurePanelContext)
+}
+
function stateReducer(state: StateDefinition, action: Actions) {
return match(action.type, reducers, state, action)
}
@@ -176,18 +183,35 @@ let Button = forwardRefWithAs(function Button) => {
- switch (event.key) {
- case Keys.Space:
- case Keys.Enter:
- event.preventDefault()
- event.stopPropagation()
- dispatch({ type: ActionTypes.ToggleDisclosure })
- break
+ if (isWithinPanel) {
+ if (state.disclosureState === DisclosureStates.Closed) return
+
+ switch (event.key) {
+ case Keys.Space:
+ case Keys.Enter:
+ event.preventDefault()
+ event.stopPropagation()
+ dispatch({ type: ActionTypes.ToggleDisclosure })
+ document.getElementById(state.buttonId)?.focus()
+ break
+ }
+ } else {
+ switch (event.key) {
+ case Keys.Space:
+ case Keys.Enter:
+ event.preventDefault()
+ event.stopPropagation()
+ dispatch({ type: ActionTypes.ToggleDisclosure })
+ break
+ }
}
},
- [dispatch]
+ [dispatch, isWithinPanel, state.disclosureState]
)
let handleKeyUp = useCallback((event: ReactKeyboardEvent) => {
@@ -205,9 +229,15 @@ let Button = forwardRefWithAs(function Button {
if (isDisabledReactIssue7711(event.currentTarget)) return
if (props.disabled) return
- dispatch({ type: ActionTypes.ToggleDisclosure })
+
+ if (isWithinPanel) {
+ dispatch({ type: ActionTypes.ToggleDisclosure })
+ document.getElementById(state.buttonId)?.focus()
+ } else {
+ dispatch({ type: ActionTypes.ToggleDisclosure })
+ }
},
- [dispatch, props.disabled]
+ [dispatch, props.disabled, state.buttonId, isWithinPanel]
)
let slot = useMemo(
@@ -216,16 +246,20 @@ let Button = forwardRefWithAs(function Button
+ {render({
+ props: { ...passthroughProps, ...propsWeControl },
+ slot,
+ defaultTag: DEFAULT_PANEL_TAG,
+ features: PanelRenderFeatures,
+ visible,
+ name: 'Disclosure.Panel',
+ })}
+
+ )
})
// ---
diff --git a/packages/@headlessui-vue/src/components/disclosure/disclosure.test.ts b/packages/@headlessui-vue/src/components/disclosure/disclosure.test.ts
index e0fd49d1b4..29f5952a42 100644
--- a/packages/@headlessui-vue/src/components/disclosure/disclosure.test.ts
+++ b/packages/@headlessui-vue/src/components/disclosure/disclosure.test.ts
@@ -8,6 +8,8 @@ import {
assertDisclosureButton,
getDisclosureButton,
getDisclosurePanel,
+ getByText,
+ assertActiveElement,
} from '../../test-utils/accessibility-assertions'
import { click, press, Keys, MouseButton } from '../../test-utils/interactions'
import { html } from '../../test-utils/html'
@@ -715,4 +717,38 @@ describe('Mouse interactions', () => {
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
})
)
+
+ it(
+ 'should be possible to close the Disclosure by clicking on a DisclosureButton inside a DisclosurePanel',
+ suppressConsoleLogs(async () => {
+ renderTemplate(
+ html`
+
+ Open
+
+ Close
+
+
+ `
+ )
+
+ // Open the disclosure
+ await click(getDisclosureButton())
+
+ let closeBtn = getByText('Close')
+
+ expect(closeBtn).not.toHaveAttribute('id')
+ expect(closeBtn).not.toHaveAttribute('aria-controls')
+ expect(closeBtn).not.toHaveAttribute('aria-expanded')
+
+ // The close button should close the disclosure
+ await click(closeBtn)
+
+ // Verify it is closed
+ assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
+
+ // Verify we restored the Open button
+ assertActiveElement(getDisclosureButton())
+ })
+ )
})
diff --git a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts
index 7d67deccb4..5b5a5ab702 100644
--- a/packages/@headlessui-vue/src/components/disclosure/disclosure.ts
+++ b/packages/@headlessui-vue/src/components/disclosure/disclosure.ts
@@ -16,7 +16,10 @@ enum DisclosureStates {
interface StateDefinition {
// State
disclosureState: Ref
- panelRef: Ref
+ panel: Ref
+ panelId: string
+ button: Ref
+ buttonId: string
// State mutators
toggleDisclosure(): void
@@ -36,6 +39,11 @@ function useDisclosureContext(component: string) {
return context
}
+let DisclosurePanelContext = Symbol('DisclosurePanelContext') as InjectionKey
+function useDisclosurePanelContext() {
+ return inject(DisclosurePanelContext, null)
+}
+
// ---
export let Disclosure = defineComponent({
@@ -45,14 +53,21 @@ export let Disclosure = defineComponent({
defaultOpen: { type: [Boolean], default: false },
},
setup(props, { slots, attrs }) {
+ let buttonId = `headlessui-disclosure-button-${useId()}`
+ let panelId = `headlessui-disclosure-panel-${useId()}`
+
let disclosureState = ref(
props.defaultOpen ? DisclosureStates.Open : DisclosureStates.Closed
)
- let panelRef = ref(null)
+ let panelRef = ref(null)
+ let buttonRef = ref(null)
let api = {
+ buttonId,
+ panelId,
disclosureState,
- panelRef,
+ panel: panelRef,
+ button: buttonRef,
toggleDisclosure() {
disclosureState.value = match(disclosureState.value, {
[DisclosureStates.Open]: DisclosureStates.Closed,
@@ -91,18 +106,25 @@ export let DisclosureButton = defineComponent({
let api = useDisclosureContext('DisclosureButton')
let slot = { open: api.disclosureState.value === DisclosureStates.Open }
- let propsWeControl = {
- id: this.id,
- type: 'button',
- 'aria-expanded': this.$props.disabled
- ? undefined
- : api.disclosureState.value === DisclosureStates.Open,
- 'aria-controls': this.ariaControls,
- disabled: this.$props.disabled ? true : undefined,
- onClick: this.handleClick,
- onKeydown: this.handleKeyDown,
- onKeyup: this.handleKeyUp,
- }
+ let propsWeControl = this.isWithinPanel
+ ? {
+ type: 'button',
+ onClick: this.handleClick,
+ onKeydown: this.handleKeyDown,
+ }
+ : {
+ id: this.id,
+ ref: 'el',
+ type: 'button',
+ 'aria-expanded': this.$props.disabled
+ ? undefined
+ : api.disclosureState.value === DisclosureStates.Open,
+ 'aria-controls': dom(api.panel) ? api.panelId : undefined,
+ disabled: this.$props.disabled ? true : undefined,
+ onClick: this.handleClick,
+ onKeydown: this.handleKeyDown,
+ onKeyup: this.handleKeyUp,
+ }
return render({
props: { ...this.$props, ...propsWeControl },
@@ -114,26 +136,46 @@ export let DisclosureButton = defineComponent({
},
setup(props) {
let api = useDisclosureContext('DisclosureButton')
- let buttonId = `headlessui-disclosure-button-${useId()}`
- let ariaControls = computed(() => dom(api.panelRef)?.id ?? undefined)
+
+ let panelContext = useDisclosurePanelContext()
+ let isWithinPanel = panelContext === null ? false : panelContext === api.panelId
return {
- id: buttonId,
- ariaControls,
+ isWithinPanel,
+ id: api.buttonId,
+ el: isWithinPanel ? undefined : api.button,
handleClick() {
if (props.disabled) return
- api.toggleDisclosure()
+
+ if (isWithinPanel) {
+ api.toggleDisclosure()
+ dom(api.button)?.focus()
+ } else {
+ api.toggleDisclosure()
+ }
},
handleKeyDown(event: KeyboardEvent) {
if (props.disabled) return
- switch (event.key) {
- case Keys.Space:
- case Keys.Enter:
- event.preventDefault()
- event.stopPropagation()
- api.toggleDisclosure()
- break
+ if (isWithinPanel) {
+ switch (event.key) {
+ case Keys.Space:
+ case Keys.Enter:
+ event.preventDefault()
+ event.stopPropagation()
+ api.toggleDisclosure()
+ dom(api.button)?.focus()
+ break
+ }
+ } else {
+ switch (event.key) {
+ case Keys.Space:
+ case Keys.Enter:
+ event.preventDefault()
+ event.stopPropagation()
+ api.toggleDisclosure()
+ break
+ }
}
},
handleKeyUp(event: KeyboardEvent) {
@@ -177,7 +219,8 @@ export let DisclosurePanel = defineComponent({
},
setup() {
let api = useDisclosureContext('DisclosurePanel')
- let panelId = `headlessui-disclosure-panel-${useId()}`
+
+ provide(DisclosurePanelContext, api.panelId)
let usesOpenClosedState = useOpenClosed()
let visible = computed(() => {
@@ -188,6 +231,6 @@ export let DisclosurePanel = defineComponent({
return api.disclosureState.value === DisclosureStates.Open
})
- return { id: panelId, el: api.panelRef, visible }
+ return { id: api.panelId, el: api.panel, visible }
},
})