Skip to content

Commit

Permalink
Add ability to use Disclosure.Button inside a Disclosure.Panel (#682
Browse files Browse the repository at this point in the history
)

* add ability to use `Disclosure.Button` inside a `Disclosure.Panel`

If you do it this way, then the `Disclosure.Button` will function as a
`close` button.

This will make it consistent with the `Popover.Button` inside the
`Popover.Panel` funcitonality.

* update changelog
  • Loading branch information
RobinMalfait authored Jul 13, 2021
1 parent 9af04a0 commit 10110a9
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 57 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(
<Disclosure>
<Disclosure.Button>Open</Disclosure.Button>
<Disclosure.Panel>
<Disclosure.Button>Close</Disclosure.Button>
</Disclosure.Panel>
</Disclosure>
)

// 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())
})
)
})
94 changes: 66 additions & 28 deletions packages/@headlessui-react/src/components/disclosure/disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ function useDisclosureContext(component: string) {
return context
}

let DisclosurePanelContext = createContext<string | null>(null)
DisclosurePanelContext.displayName = 'DisclosurePanelContext'

function useDisclosurePanelContext() {
return useContext(DisclosurePanelContext)
}

function stateReducer(state: StateDefinition, action: Actions) {
return match(action.type, reducers, state, action)
}
Expand Down Expand Up @@ -176,18 +183,35 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
let [state, dispatch] = useDisclosureContext([Disclosure.name, Button.name].join('.'))
let buttonRef = useSyncRefs(ref)

let panelContext = useDisclosurePanelContext()
let isWithinPanel = panelContext === null ? false : panelContext === state.panelId

let handleKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
Expand All @@ -205,9 +229,15 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
(event: ReactMouseEvent) => {
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<ButtonRenderPropArg>(
Expand All @@ -216,16 +246,20 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
)

let passthroughProps = props
let propsWeControl = {
ref: buttonRef,
id: state.buttonId,
type: 'button',
'aria-expanded': props.disabled ? undefined : state.disclosureState === DisclosureStates.Open,
'aria-controls': state.linkedPanel ? state.panelId : undefined,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
onClick: handleClick,
}
let propsWeControl = isWithinPanel
? { type: 'button', onKeyDown: handleKeyDown, onClick: handleClick }
: {
ref: buttonRef,
id: state.buttonId,
type: 'button',
'aria-expanded': props.disabled
? undefined
: state.disclosureState === DisclosureStates.Open,
'aria-controls': state.linkedPanel ? state.panelId : undefined,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
onClick: handleClick,
}

return render({
props: { ...passthroughProps, ...propsWeControl },
Expand Down Expand Up @@ -285,14 +319,18 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
}
let passthroughProps = props

return render({
props: { ...passthroughProps, ...propsWeControl },
slot,
defaultTag: DEFAULT_PANEL_TAG,
features: PanelRenderFeatures,
visible,
name: 'Disclosure.Panel',
})
return (
<DisclosurePanelContext.Provider value={state.panelId}>
{render({
props: { ...passthroughProps, ...propsWeControl },
slot,
defaultTag: DEFAULT_PANEL_TAG,
features: PanelRenderFeatures,
visible,
name: 'Disclosure.Panel',
})}
</DisclosurePanelContext.Provider>
)
})

// ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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`
<Disclosure>
<DisclosureButton>Open</DisclosureButton>
<DisclosurePanel>
<DisclosureButton>Close</DisclosureButton>
</DisclosurePanel>
</Disclosure>
`
)

// 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())
})
)
})
Loading

0 comments on commit 10110a9

Please sign in to comment.