Ensure clicking on interactive elements inside <Label> works (#3709)
This PR fixes an issue where clicking on an interactive element _inside_ of a `<Label>` component should work as expected. For example, if you have this situation: ```html <label for="tac"> <input id="tac" type="checkbox" name="terms-and-conditions" /> I agree to the <a href="terms-and-conditions.html">Terms and Conditions</a> </label> ``` Clicking on the `<a href="#">` inside the label should _not_ check the checkbox, but should open the link instead. Fixes: #3658
This commit is contained in:
@@ -17,6 +17,7 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
|
||||
import { useDisabled } from '../../internal/disabled'
|
||||
import { useProvidedId } from '../../internal/id'
|
||||
import type { Props } from '../../types'
|
||||
import * as DOM from '../../utils/dom'
|
||||
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
|
||||
|
||||
// ---
|
||||
@@ -131,6 +132,26 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
|
||||
let handleClick = useEvent((e: ReactMouseEvent) => {
|
||||
let current = e.currentTarget
|
||||
|
||||
// If a click happens on an interactive element inside of the label, then we
|
||||
// don't want to trigger the label behavior and let the browser handle the
|
||||
// click event.
|
||||
//
|
||||
// In a situation like:
|
||||
//
|
||||
// ```html
|
||||
// <label>
|
||||
// I accept the
|
||||
// <a href="#">terms and agreement</a>
|
||||
// <input type="checkbox" />
|
||||
// </label>
|
||||
// ```
|
||||
//
|
||||
// Clicking on the link, should not check the checkbox, but open the link
|
||||
// instead.
|
||||
if (e.target !== e.currentTarget && DOM.isInteractiveElement(e.target)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Labels connected to 'real' controls will already click the element. But we don't know that
|
||||
// ahead of time. This will prevent the default click, such that only a single click happens
|
||||
// instead of two. Otherwise this results in a visual no-op.
|
||||
|
||||
@@ -19,3 +19,24 @@ export function isHTMLElement(element: unknown): element is HTMLElement {
|
||||
if (element === null) return false
|
||||
return 'nodeName' in element
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#interactive-content-2
|
||||
// - a (if the href attribute is present)
|
||||
// - audio (if the controls attribute is present)
|
||||
// - button
|
||||
// - details
|
||||
// - embed
|
||||
// - iframe
|
||||
// - img (if the usemap attribute is present)
|
||||
// - input (if the type attribute is not in the Hidden state)
|
||||
// - label
|
||||
// - select
|
||||
// - textarea
|
||||
// - video (if the controls attribute is present)
|
||||
export function isInteractiveElement(element: unknown): element is Element {
|
||||
if (!isHTMLElement(element)) return false
|
||||
|
||||
return element.matches(
|
||||
'a[href],audio[controls],button,details,embed,iframe,img[usemap],input:not([type="hidden"]),label,select,textarea,video[controls]'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ export default function App() {
|
||||
</Section>
|
||||
<Section title="Listbox">
|
||||
<div className="w-full space-y-1">
|
||||
<Field>
|
||||
<Label>Assigned to:</Label>
|
||||
<Listbox name="person" defaultValue={people[1]}>
|
||||
{({ value }) => (
|
||||
<>
|
||||
@@ -243,10 +245,13 @@ export default function App() {
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</Field>
|
||||
</div>
|
||||
</Section>
|
||||
<Section title="Combobox">
|
||||
<div className="w-full space-y-1">
|
||||
<Field>
|
||||
<Label>Location:</Label>
|
||||
<Combobox
|
||||
name="location"
|
||||
defaultValue={'New York'}
|
||||
@@ -330,6 +335,7 @@ export default function App() {
|
||||
)
|
||||
}}
|
||||
</Combobox>
|
||||
</Field>
|
||||
</div>
|
||||
</Section>
|
||||
<Section title="Default form controls">
|
||||
@@ -338,7 +344,12 @@ export default function App() {
|
||||
<Input type="text" />
|
||||
</Field>
|
||||
<Field className="flex flex-col p-1">
|
||||
<Label>Label for {'<Input type="checkbox">'}</Label>
|
||||
<Label>
|
||||
I agree to the{' '}
|
||||
<a href="https://google.com" target="_blank" className="underline">
|
||||
terms and conditions
|
||||
</a>
|
||||
</Label>
|
||||
<Input type="checkbox" />
|
||||
</Field>
|
||||
<Field className="flex flex-col p-1">
|
||||
|
||||
Reference in New Issue
Block a user