Skip to content

Commit ce23ede

Browse files
RobinMalfaitshuvroroyalexnaultEugene Kopichshoeyn
authored
Next release (#431)
* Fixed typos (#350) * chore: Fix typo in render.ts (#347) * Better vue link (#353) * Better vue link * add better React link Co-authored-by: Robin Malfait <[email protected]> * Enable NoScroll feature for the initial useFocusTrap hook (#356) * enable NoScroll feature for the initial useFocusTrap hook Once you are using Tab and Shift+Tab it does the scrolling. Fixes: #345 * update changelog * Revert "Enable NoScroll feature for the initial useFocusTrap hook (#356)" This reverts commit 19590b0. Solution is not 100% correct, so will revert for now! * Improve search (#385) * make search case insensitive for the listbox * make search case insensitive for the menu * update changelog * add `disabled` prop to RadioGroup and RadioGroup Option (#401) * add `disabled` prop to RadioGroup and RadioGroup Option Also did some general cleanup which in turn fixed an issue where the RadioGroup is unreachable when a value is used that doesn't exist in the list of options. Fixes: #378 * update changelog * Fix type of `RadioGroupOption` (#400) Match RadioGroupOption value types to match modelValue allowed types for RadioGroup * update changelog * fix typo's * chore(CI): update main workflow (#395) * chore(CI): update main workflow * Update main.yml * fix dialog event propagation (#422) * re-export the `screen` utility for quick debugging purposes * stop event propagation when clicking inside a Dialog Fixes: #414 * improve dialog escape (#430) * Make sure that `Escape` only closes the top most Dialog * update changelog * add defaultOpen prop to Disclosure component (#447) * add defaultOpen prop to Disclosure component * update changelog Co-authored-by: Shuvro Roy <[email protected]> Co-authored-by: Alex Nault <[email protected]> Co-authored-by: Eugene Kopich <[email protected]> Co-authored-by: Nathan Shoemark <[email protected]> Co-authored-by: Michaël De Boey <[email protected]>
1 parent 6a01c54 commit ce23ede

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1067
-139
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ jobs:
99
uses: actions/checkout@v2
1010

1111
- name: Use Node 12
12-
uses: actions/setup-node@v1
12+
uses: actions/setup-node@v2
1313
with:
14-
node-version: 12.x
14+
node-version: 12
1515

1616
# - name: Use cached node_modules
1717
# id: cache
18-
# uses: actions/cache@v1
18+
# uses: actions/cache@v2
1919
# with:
2020
# path: node_modules
2121
# key: nodeModules-${{ hashFiles('**/yarn.lock') }}

CHANGELOG.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased - React]
99

10-
- Nothing yet!
10+
### Fixes
11+
12+
- Improve search, make searching case insensitive ([#385](https://github.com/tailwindlabs/headlessui/pull/385))
13+
- Fix unreachable `RadioGroup` ([#401](https://github.com/tailwindlabs/headlessui/pull/401))
14+
- Fix closing nested `Dialog` components when pressing `Escape` ([#430](https://github.com/tailwindlabs/headlessui/pull/430))
15+
16+
### Added
17+
18+
- Add `disabled` prop to `RadioGroup` and `RadioGroup.Option` ([#401](https://github.com/tailwindlabs/headlessui/pull/401))
19+
- Add `defaultOpen` prop to the `Disclosure` component ([#447](https://github.com/tailwindlabs/headlessui/pull/447))
1120

1221
## [Unreleased - Vue]
1322

14-
- Nothing yet!
23+
### Fixes
24+
25+
- Improve search, make searching case insensitive ([#385](https://github.com/tailwindlabs/headlessui/pull/385))
26+
- Fix unreachable `RadioGroup` ([#401](https://github.com/tailwindlabs/headlessui/pull/401))
27+
- Fix `RadioGroupOption` value type ([#400](https://github.com/tailwindlabs/headlessui/pull/400))
28+
- Fix closing nested `Dialog` components when pressing `Escape` ([#430](https://github.com/tailwindlabs/headlessui/pull/430))
29+
30+
### Added
31+
32+
- Add `disabled` prop to `RadioGroup` and `RadioGroupOption` ([#401](https://github.com/tailwindlabs/headlessui/pull/401))
33+
- Add `defaultOpen` prop to the `Disclosure` component ([#447](https://github.com/tailwindlabs/headlessui/pull/447))
1534

1635
## [@headlessui/react@v1.0.0] - 2021-04-14
1736

packages/@headlessui-react/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ yarn add @headlessui/react
2525

2626
## Documentation
2727

28-
For full documentation, visit [headlessui.dev](https://headlessui.dev/react).
28+
For full documentation, visit [headlessui.dev](https://headlessui.dev/react/menu).
2929

3030
## Community
3131

packages/@headlessui-react/pages/dialog/dialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ function Nested({ onClose, level = 0 }) {
1919

2020
return (
2121
<>
22-
<Dialog open={true} onClose={onClose} className="fixed z-10 inset-0 pointer-events-none">
22+
<Dialog open={true} onClose={onClose} className="fixed z-10 inset-0">
2323
{true && <Dialog.Overlay className="fixed inset-0 bg-gray-500 opacity-25" />}
2424
<div
25-
className="z-10 fixed left-12 top-24 bg-white w-96 p-4 pointer-events-auto"
25+
className="z-10 fixed left-12 top-24 bg-white w-96 p-4"
2626
style={{
2727
transform: `translate(calc(50px * ${level}), calc(50px * ${level}))`,
2828
}}

packages/@headlessui-react/src/components/description/description.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ it('should be possible to use a DescriptionProvider and a single Description, an
6464
`)
6565
})
6666

67-
it('should be possible to use a DescriptionProvider and multiple Description ocmponents, and have them linked', async () => {
67+
it('should be possible to use a DescriptionProvider and multiple Description components, and have them linked', async () => {
6868
function Component(props: { children: ReactNode }) {
6969
let [describedby, DescriptionProvider] = useDescriptions()
7070

packages/@headlessui-react/src/components/dialog/dialog.test.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getDialogOverlay,
1414
getByText,
1515
assertActiveElement,
16+
getDialogs,
1617
} from '../../test-utils/accessibility-assertions'
1718
import { click, press, Keys } from '../../test-utils/interactions'
1819
import { PropsOf } from '../../types'
@@ -496,4 +497,152 @@ describe('Mouse interactions', () => {
496497
assertActiveElement(getByText('Hello'))
497498
})
498499
)
500+
501+
it(
502+
'should stop propagating click events when clicking on the Dialog.Overlay',
503+
suppressConsoleLogs(async () => {
504+
let wrapperFn = jest.fn()
505+
function Example() {
506+
let [isOpen, setIsOpen] = useState(true)
507+
return (
508+
<div onClick={wrapperFn}>
509+
<Dialog open={isOpen} onClose={setIsOpen}>
510+
Contents
511+
<Dialog.Overlay />
512+
<TabSentinel />
513+
</Dialog>
514+
</div>
515+
)
516+
}
517+
render(<Example />)
518+
519+
// Verify it is open
520+
assertDialog({ state: DialogState.Visible })
521+
522+
// Verify that the wrapper function has not been called yet
523+
expect(wrapperFn).toHaveBeenCalledTimes(0)
524+
525+
// Click the Dialog.Overlay to close the Dialog
526+
await click(getDialogOverlay())
527+
528+
// Verify it is closed
529+
assertDialog({ state: DialogState.InvisibleUnmounted })
530+
531+
// Verify that the wrapper function has not been called yet
532+
expect(wrapperFn).toHaveBeenCalledTimes(0)
533+
})
534+
)
535+
536+
it(
537+
'should stop propagating click events when clicking on an element inside the Dialog',
538+
suppressConsoleLogs(async () => {
539+
let wrapperFn = jest.fn()
540+
function Example() {
541+
let [isOpen, setIsOpen] = useState(true)
542+
return (
543+
<div onClick={wrapperFn}>
544+
<Dialog open={isOpen} onClose={setIsOpen}>
545+
Contents
546+
<button onClick={() => setIsOpen(false)}>Inside</button>
547+
<TabSentinel />
548+
</Dialog>
549+
</div>
550+
)
551+
}
552+
render(<Example />)
553+
554+
// Verify it is open
555+
assertDialog({ state: DialogState.Visible })
556+
557+
// Verify that the wrapper function has not been called yet
558+
expect(wrapperFn).toHaveBeenCalledTimes(0)
559+
560+
// Click the button inside the the Dialog
561+
await click(getByText('Inside'))
562+
563+
// Verify it is closed
564+
assertDialog({ state: DialogState.InvisibleUnmounted })
565+
566+
// Verify that the wrapper function has not been called yet
567+
expect(wrapperFn).toHaveBeenCalledTimes(0)
568+
})
569+
)
570+
})
571+
572+
describe('Nesting', () => {
573+
it('should be possible to open nested Dialog components and close them with `Escape`', async () => {
574+
function Nested({ onClose, level = 1 }: { onClose: (value: boolean) => void; level?: number }) {
575+
let [showChild, setShowChild] = useState(false)
576+
577+
return (
578+
<>
579+
<Dialog open={true} onClose={onClose}>
580+
<div>
581+
<p>Level: {level}</p>
582+
<button onClick={() => setShowChild(true)}>Open {level + 1}</button>
583+
</div>
584+
{showChild && <Nested onClose={setShowChild} level={level + 1} />}
585+
</Dialog>
586+
</>
587+
)
588+
}
589+
590+
function Example() {
591+
let [open, setOpen] = useState(false)
592+
593+
return (
594+
<>
595+
<button onClick={() => setOpen(true)}>Open 1</button>
596+
{open && <Nested onClose={setOpen} />}
597+
</>
598+
)
599+
}
600+
601+
render(<Example />)
602+
603+
// Verify we have no open dialogs
604+
expect(getDialogs()).toHaveLength(0)
605+
606+
// Open Dialog 1
607+
await click(getByText('Open 1'))
608+
609+
// Verify that we have 1 open dialog
610+
expect(getDialogs()).toHaveLength(1)
611+
612+
// Open Dialog 2
613+
await click(getByText('Open 2'))
614+
615+
// Verify that we have 2 open dialogs
616+
expect(getDialogs()).toHaveLength(2)
617+
618+
// Press escape to close the top most Dialog
619+
await press(Keys.Escape)
620+
621+
// Verify that we have 1 open dialog
622+
expect(getDialogs()).toHaveLength(1)
623+
624+
// Open Dialog 2
625+
await click(getByText('Open 2'))
626+
627+
// Verify that we have 2 open dialogs
628+
expect(getDialogs()).toHaveLength(2)
629+
630+
// Open Dialog 3
631+
await click(getByText('Open 3'))
632+
633+
// Verify that we have 3 open dialogs
634+
expect(getDialogs()).toHaveLength(3)
635+
636+
// Press escape to close the top most Dialog
637+
await press(Keys.Escape)
638+
639+
// Verify that we have 2 open dialogs
640+
expect(getDialogs()).toHaveLength(2)
641+
642+
// Press escape to close the top most Dialog
643+
await press(Keys.Escape)
644+
645+
// Verify that we have 1 open dialog
646+
expect(getDialogs()).toHaveLength(1)
647+
})
499648
})

packages/@headlessui-react/src/components/dialog/dialog.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import React, {
1212
ContextType,
1313
ElementType,
1414
MouseEvent as ReactMouseEvent,
15+
KeyboardEvent as ReactKeyboardEvent,
1516
MutableRefObject,
1617
Ref,
1718
} from 'react'
@@ -92,7 +93,13 @@ let DEFAULT_DIALOG_TAG = 'div' as const
9293
interface DialogRenderPropArg {
9394
open: boolean
9495
}
95-
type DialogPropsWeControl = 'id' | 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby'
96+
type DialogPropsWeControl =
97+
| 'id'
98+
| 'role'
99+
| 'aria-modal'
100+
| 'aria-describedby'
101+
| 'aria-labelledby'
102+
| 'onClick'
96103

97104
let DialogRenderFeatures = Features.RenderStrategy | Features.Static
98105

@@ -171,14 +178,6 @@ let DialogRoot = forwardRefWithAs(function Dialog<
171178
close()
172179
})
173180

174-
// Handle `Escape` to close
175-
useWindowEvent('keydown', event => {
176-
if (event.key !== Keys.Escape) return
177-
if (dialogState !== DialogStates.Open) return
178-
if (containers.current.size > 1) return // 1 is myself, otherwise other elements in the Stack
179-
close()
180-
})
181-
182181
// Scroll lock
183182
useEffect(() => {
184183
if (dialogState !== DialogStates.Open) return
@@ -190,6 +189,7 @@ let DialogRoot = forwardRefWithAs(function Dialog<
190189

191190
document.documentElement.style.overflow = 'hidden'
192191
document.documentElement.style.paddingRight = `${scrollbarWidth}px`
192+
193193
return () => {
194194
document.documentElement.style.overflow = overflow
195195
document.documentElement.style.paddingRight = paddingRight
@@ -243,6 +243,20 @@ let DialogRoot = forwardRefWithAs(function Dialog<
243243
'aria-modal': dialogState === DialogStates.Open ? true : undefined,
244244
'aria-labelledby': state.titleId,
245245
'aria-describedby': describedby,
246+
onClick(event: ReactMouseEvent) {
247+
event.preventDefault()
248+
event.stopPropagation()
249+
},
250+
251+
// Handle `Escape` to close
252+
onKeyDown(event: ReactKeyboardEvent) {
253+
if (event.key !== Keys.Escape) return
254+
if (dialogState !== DialogStates.Open) return
255+
if (containers.current.size > 1) return // 1 is myself, otherwise other elements in the Stack
256+
event.preventDefault()
257+
event.stopPropagation()
258+
close()
259+
},
246260
}
247261
let passthroughProps = rest
248262

@@ -302,6 +316,8 @@ let Overlay = forwardRefWithAs(function Overlay<
302316
let handleClick = useCallback(
303317
(event: ReactMouseEvent) => {
304318
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
319+
event.preventDefault()
320+
event.stopPropagation()
305321
close()
306322
},
307323
[close]

packages/@headlessui-react/src/components/disclosure/disclosure.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,29 @@ describe('Rendering', () => {
7979
assertDisclosurePanel({ state: DisclosureState.Visible, textContent: 'Panel is: open' })
8080
})
8181
)
82+
83+
it('should be possible to render a Disclosure in an open state by default', async () => {
84+
render(
85+
<Disclosure defaultOpen>
86+
{({ open }) => (
87+
<>
88+
<Disclosure.Button>Trigger</Disclosure.Button>
89+
<Disclosure.Panel>Panel is: {open ? 'open' : 'closed'}</Disclosure.Panel>
90+
</>
91+
)}
92+
</Disclosure>
93+
)
94+
95+
assertDisclosureButton({
96+
state: DisclosureState.Visible,
97+
attributes: { id: 'headlessui-disclosure-button-1' },
98+
})
99+
assertDisclosurePanel({ state: DisclosureState.Visible, textContent: 'Panel is: open' })
100+
101+
await click(getDisclosureButton())
102+
103+
assertDisclosureButton({ state: DisclosureState.InvisibleUnmounted })
104+
})
82105
})
83106

84107
describe('Disclosure.Button', () => {

packages/@headlessui-react/src/components/disclosure/disclosure.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,16 @@ interface DisclosureRenderPropArg {
111111
}
112112

113113
export function Disclosure<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
114-
props: Props<TTag, DisclosureRenderPropArg>
114+
props: Props<TTag, DisclosureRenderPropArg> & {
115+
defaultOpen?: boolean
116+
}
115117
) {
118+
let { defaultOpen = false, ...passthroughProps } = props
116119
let buttonId = `headlessui-disclosure-button-${useId()}`
117120
let panelId = `headlessui-disclosure-panel-${useId()}`
118121

119122
let reducerBag = useReducer(stateReducer, {
120-
disclosureState: DisclosureStates.Closed,
123+
disclosureState: defaultOpen ? DisclosureStates.Open : DisclosureStates.Closed,
121124
linkedPanel: false,
122125
buttonId,
123126
panelId,
@@ -135,7 +138,7 @@ export function Disclosure<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_
135138
return (
136139
<DisclosureContext.Provider value={reducerBag}>
137140
{render({
138-
props,
141+
props: passthroughProps,
139142
slot,
140143
defaultTag: DEFAULT_DISCLOSURE_TAG,
141144
name: 'Disclosure',

packages/@headlessui-react/src/components/focus-trap/focus-trap.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ it(
9898

9999
let [a, b, c, d] = Array.from(document.querySelectorAll('input'))
100100

101-
// Ensure that input-b is the active elememt
101+
// Ensure that input-b is the active element
102102
assertActiveElement(b)
103103

104104
// Tab to the next item
105105
await press(Keys.Tab)
106106

107-
// Ensure that input-c is the active elememt
107+
// Ensure that input-c is the active element
108108
assertActiveElement(c)
109109

110110
// Try to move focus

0 commit comments

Comments
 (0)