Skip to content

Commit 219901c

Browse files
RobinMalfaitmgcrea
andauthored
Allow passing in your own id prop (#2060)
* accept `id` as a prop where it is currently hardcoded (React) Continuation of #2020 Co-authored-by: Olivier Louvignes <[email protected]> * accept `id` as a prop where it is currently hardcoded (Vue) * update changelog * apply React's hook rules Co-authored-by: Olivier Louvignes <[email protected]>
1 parent 7509124 commit 219901c

File tree

26 files changed

+353
-272
lines changed

26 files changed

+353
-272
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Improve syncing of the `Combobox.Input` value ([#2042](https://github.com/tailwindlabs/headlessui/pull/2042))
1717
- Fix crash when using `multiple` mode without `value` prop (uncontrolled) for `Listbox` and `Combobox` components ([#2058](https://github.com/tailwindlabs/headlessui/pull/2058))
1818
- Apply `enter` and `enterFrom` classes in SSR for `Transition` component ([#2059](https://github.com/tailwindlabs/headlessui/pull/2059))
19+
- Allow passing in your own `id` prop ([#2060](https://github.com/tailwindlabs/headlessui/pull/2060))
1920

2021
## [1.7.4] - 2022-11-03
2122

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

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,6 @@ interface InputRenderPropArg {
657657
disabled: boolean
658658
}
659659
type InputPropsWeControl =
660-
| 'id'
661660
| 'role'
662661
| 'aria-labelledby'
663662
| 'aria-expanded'
@@ -678,15 +677,21 @@ let Input = forwardRefWithAs(function Input<
678677
},
679678
ref: Ref<HTMLInputElement>
680679
) {
681-
let { value, onChange, displayValue, type = 'text', ...theirProps } = props
680+
let internalId = useId()
681+
let {
682+
id = `headlessui-combobox-input-${internalId}`,
683+
onChange,
684+
displayValue,
685+
type = 'text',
686+
...theirProps
687+
} = props
682688
let data = useData('Combobox.Input')
683689
let actions = useActions('Combobox.Input')
684690

685691
let inputRef = useSyncRefs(data.inputRef, ref)
686692

687693
let isTyping = useRef(false)
688694

689-
let id = `headlessui-combobox-input-${useId()}`
690695
let d = useDisposables()
691696

692697
// When a `displayValue` prop is given, we should use it to transform the current selected
@@ -931,7 +936,6 @@ interface ButtonRenderPropArg {
931936
value: any
932937
}
933938
type ButtonPropsWeControl =
934-
| 'id'
935939
| 'type'
936940
| 'tabIndex'
937941
| 'aria-haspopup'
@@ -949,8 +953,8 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
949953
let data = useData('Combobox.Button')
950954
let actions = useActions('Combobox.Button')
951955
let buttonRef = useSyncRefs(data.buttonRef, ref)
952-
953-
let id = `headlessui-combobox-button-${useId()}`
956+
let internalId = useId()
957+
let { id = `headlessui-combobox-button-${internalId}`, ...theirProps } = props
954958
let d = useDisposables()
955959

956960
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLUListElement>) => {
@@ -1017,7 +1021,6 @@ let Button = forwardRefWithAs(function Button<TTag extends ElementType = typeof
10171021
}),
10181022
[data]
10191023
)
1020-
let theirProps = props
10211024
let ourProps = {
10221025
ref: buttonRef,
10231026
id,
@@ -1048,14 +1051,15 @@ interface LabelRenderPropArg {
10481051
open: boolean
10491052
disabled: boolean
10501053
}
1051-
type LabelPropsWeControl = 'id' | 'ref' | 'onClick'
1054+
type LabelPropsWeControl = 'ref' | 'onClick'
10521055

10531056
let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
10541057
props: Props<TTag, LabelRenderPropArg, LabelPropsWeControl>,
10551058
ref: Ref<HTMLLabelElement>
10561059
) {
1060+
let internalId = useId()
1061+
let { id = `headlessui-combobox-label-${internalId}`, ...theirProps } = props
10571062
let data = useData('Combobox.Label')
1058-
let id = `headlessui-combobox-label-${useId()}`
10591063
let actions = useActions('Combobox.Label')
10601064
let labelRef = useSyncRefs(data.labelRef, ref)
10611065

@@ -1068,7 +1072,6 @@ let Label = forwardRefWithAs(function Label<TTag extends ElementType = typeof DE
10681072
[data]
10691073
)
10701074

1071-
let theirProps = props
10721075
let ourProps = { ref: labelRef, id, onClick: handleClick }
10731076

10741077
return render({
@@ -1090,7 +1093,6 @@ type OptionsPropsWeControl =
10901093
| 'aria-activedescendant'
10911094
| 'aria-labelledby'
10921095
| 'hold'
1093-
| 'id'
10941096
| 'onKeyDown'
10951097
| 'role'
10961098
| 'tabIndex'
@@ -1106,13 +1108,12 @@ let Options = forwardRefWithAs(function Options<
11061108
},
11071109
ref: Ref<HTMLUListElement>
11081110
) {
1109-
let { hold = false, ...theirProps } = props
1111+
let internalId = useId()
1112+
let { id = `headlessui-combobox-options-${internalId}`, hold = false, ...theirProps } = props
11101113
let data = useData('Combobox.Options')
11111114

11121115
let optionsRef = useSyncRefs(data.optionsRef, ref)
11131116

1114-
let id = `headlessui-combobox-options-${useId()}`
1115-
11161117
let usesOpenClosedState = useOpenClosed()
11171118
let visible = (() => {
11181119
if (usesOpenClosedState !== null) {
@@ -1179,7 +1180,7 @@ interface OptionRenderPropArg {
11791180
selected: boolean
11801181
disabled: boolean
11811182
}
1182-
type ComboboxOptionPropsWeControl = 'id' | 'role' | 'tabIndex' | 'aria-disabled' | 'aria-selected'
1183+
type ComboboxOptionPropsWeControl = 'role' | 'tabIndex' | 'aria-disabled' | 'aria-selected'
11831184

11841185
let Option = forwardRefWithAs(function Option<
11851186
TTag extends ElementType = typeof DEFAULT_OPTION_TAG,
@@ -1193,11 +1194,16 @@ let Option = forwardRefWithAs(function Option<
11931194
},
11941195
ref: Ref<HTMLLIElement>
11951196
) {
1196-
let { disabled = false, value, ...theirProps } = props
1197+
let internalId = useId()
1198+
let {
1199+
id = `headlessui-combobox-option-${internalId}`,
1200+
disabled = false,
1201+
value,
1202+
...theirProps
1203+
} = props
11971204
let data = useData('Combobox.Option')
11981205
let actions = useActions('Combobox.Option')
11991206

1200-
let id = `headlessui-combobox-option-${useId()}`
12011207
let active =
12021208
data.activeOptionIndex !== null ? data.options[data.activeOptionIndex].id === id : false
12031209

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ let DEFAULT_DESCRIPTION_TAG = 'p' as const
9191

9292
export let Description = forwardRefWithAs(function Description<
9393
TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG
94-
>(props: Props<TTag, {}, 'id'>, ref: Ref<HTMLParagraphElement>) {
94+
>(props: Props<TTag>, ref: Ref<HTMLParagraphElement>) {
95+
let internalId = useId()
96+
let { id = `headlessui-description-${internalId}`, ...theirProps } = props
9597
let context = useDescriptionContext()
96-
let id = `headlessui-description-${useId()}`
9798
let descriptionRef = useSyncRefs(ref)
9899

99100
useIsoMorphicEffect(() => context.register(id), [id, context.register])
100101

101-
let theirProps = props
102102
let ourProps = { ref: descriptionRef, ...context.props, id }
103103

104104
return render({

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

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ let DEFAULT_DIALOG_TAG = 'div' as const
140140
interface DialogRenderPropArg {
141141
open: boolean
142142
}
143-
type DialogPropsWeControl = 'id' | 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby'
143+
type DialogPropsWeControl = 'role' | 'aria-modal' | 'aria-describedby' | 'aria-labelledby'
144144

145145
let DialogRenderFeatures = Features.RenderStrategy | Features.Static
146146

@@ -156,7 +156,15 @@ let DialogRoot = forwardRefWithAs(function Dialog<
156156
},
157157
ref: Ref<HTMLDivElement>
158158
) {
159-
let { open, onClose, initialFocus, __demoMode = false, ...theirProps } = props
159+
let internalId = useId()
160+
let {
161+
id = `headlessui-dialog-${internalId}`,
162+
open,
163+
onClose,
164+
initialFocus,
165+
__demoMode = false,
166+
...theirProps
167+
} = props
160168
let [nestedDialogCount, setNestedDialogCount] = useState(0)
161169

162170
let usesOpenClosedState = useOpenClosed()
@@ -295,8 +303,6 @@ let DialogRoot = forwardRefWithAs(function Dialog<
295303

296304
let [describedby, DescriptionProvider] = useDescriptions()
297305

298-
let id = `headlessui-dialog-${useId()}`
299-
300306
let contextBag = useMemo<ContextType<typeof DialogContext>>(
301307
() => [{ dialogState, close, setTitleId }, state],
302308
[dialogState, state, close, setTitleId]
@@ -381,16 +387,16 @@ let DEFAULT_OVERLAY_TAG = 'div' as const
381387
interface OverlayRenderPropArg {
382388
open: boolean
383389
}
384-
type OverlayPropsWeControl = 'id' | 'aria-hidden' | 'onClick'
390+
type OverlayPropsWeControl = 'aria-hidden' | 'onClick'
385391

386392
let Overlay = forwardRefWithAs(function Overlay<
387393
TTag extends ElementType = typeof DEFAULT_OVERLAY_TAG
388394
>(props: Props<TTag, OverlayRenderPropArg, OverlayPropsWeControl>, ref: Ref<HTMLDivElement>) {
395+
let internalId = useId()
396+
let { id = `headlessui-dialog-overlay-${internalId}`, ...theirProps } = props
389397
let [{ dialogState, close }] = useDialogContext('Dialog.Overlay')
390398
let overlayRef = useSyncRefs(ref)
391399

392-
let id = `headlessui-dialog-overlay-${useId()}`
393-
394400
let handleClick = useEvent((event: ReactMouseEvent) => {
395401
if (event.target !== event.currentTarget) return
396402
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
@@ -404,7 +410,6 @@ let Overlay = forwardRefWithAs(function Overlay<
404410
[dialogState]
405411
)
406412

407-
let theirProps = props
408413
let ourProps = {
409414
ref: overlayRef,
410415
id,
@@ -427,16 +432,16 @@ let DEFAULT_BACKDROP_TAG = 'div' as const
427432
interface BackdropRenderPropArg {
428433
open: boolean
429434
}
430-
type BackdropPropsWeControl = 'id' | 'aria-hidden' | 'onClick'
435+
type BackdropPropsWeControl = 'aria-hidden' | 'onClick'
431436

432437
let Backdrop = forwardRefWithAs(function Backdrop<
433438
TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG
434439
>(props: Props<TTag, BackdropRenderPropArg, BackdropPropsWeControl>, ref: Ref<HTMLDivElement>) {
440+
let internalId = useId()
441+
let { id = `headlessui-dialog-backdrop-${internalId}`, ...theirProps } = props
435442
let [{ dialogState }, state] = useDialogContext('Dialog.Backdrop')
436443
let backdropRef = useSyncRefs(ref)
437444

438-
let id = `headlessui-dialog-backdrop-${useId()}`
439-
440445
useEffect(() => {
441446
if (state.panelRef.current === null) {
442447
throw new Error(
@@ -450,7 +455,6 @@ let Backdrop = forwardRefWithAs(function Backdrop<
450455
[dialogState]
451456
)
452457

453-
let theirProps = props
454458
let ourProps = {
455459
ref: backdropRef,
456460
id,
@@ -483,11 +487,11 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
483487
props: Props<TTag, PanelRenderPropArg>,
484488
ref: Ref<HTMLDivElement>
485489
) {
490+
let internalId = useId()
491+
let { id = `headlessui-dialog-panel-${internalId}`, ...theirProps } = props
486492
let [{ dialogState }, state] = useDialogContext('Dialog.Panel')
487493
let panelRef = useSyncRefs(ref, state.panelRef)
488494

489-
let id = `headlessui-dialog-panel-${useId()}`
490-
491495
let slot = useMemo<PanelRenderPropArg>(
492496
() => ({ open: dialogState === DialogStates.Open }),
493497
[dialogState]
@@ -499,7 +503,6 @@ let Panel = forwardRefWithAs(function Panel<TTag extends ElementType = typeof DE
499503
event.stopPropagation()
500504
})
501505

502-
let theirProps = props
503506
let ourProps = {
504507
ref: panelRef,
505508
id,
@@ -521,15 +524,15 @@ let DEFAULT_TITLE_TAG = 'h2' as const
521524
interface TitleRenderPropArg {
522525
open: boolean
523526
}
524-
type TitlePropsWeControl = 'id'
525527

526528
let Title = forwardRefWithAs(function Title<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
527-
props: Props<TTag, TitleRenderPropArg, TitlePropsWeControl>,
529+
props: Props<TTag, TitleRenderPropArg>,
528530
ref: Ref<HTMLHeadingElement>
529531
) {
532+
let internalId = useId()
533+
let { id = `headlessui-dialog-title-${internalId}`, ...theirProps } = props
530534
let [{ dialogState, setTitleId }] = useDialogContext('Dialog.Title')
531535

532-
let id = `headlessui-dialog-title-${useId()}`
533536
let titleRef = useSyncRefs(ref)
534537

535538
useEffect(() => {
@@ -542,7 +545,6 @@ let Title = forwardRefWithAs(function Title<TTag extends ElementType = typeof DE
542545
[dialogState]
543546
)
544547

545-
let theirProps = props
546548
let ourProps = { ref: titleRef, id }
547549

548550
return render({

0 commit comments

Comments
 (0)