Skip to content

Commit acbc4d7

Browse files
authored
skip the Provider component and simplify context (#315)
I was on a walk, and I realised that in Vue you can just call provide(Symbol, context), which means that a hook like `useLabels` can just provide context... This simplifies a lot!
1 parent 2aa95f2 commit acbc4d7

File tree

7 files changed

+249
-225
lines changed

7 files changed

+249
-225
lines changed

packages/@headlessui-vue/src/components/description/description.test.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { defineComponent, h, nextTick } from 'vue'
1+
import { defineComponent, h, nextTick, ref } from 'vue'
22
import prettier from 'prettier'
33

44
import { render } from '../../test-utils/vue-testing-library'
55
import { Description, useDescriptions } from './description'
66

77
import { html } from '../../test-utils/html'
8+
import { click } from '../../test-utils/interactions'
9+
import { getByText } from '../../test-utils/accessibility-assertions'
810

911
function format(input: Element | string) {
1012
let contents = (typeof input === 'string' ? input : (input as HTMLElement).outerHTML).trim()
@@ -36,18 +38,14 @@ function renderTemplate(input: string | Partial<Parameters<typeof defineComponen
3638
)
3739
}
3840

39-
it('should be possible to use a DescriptionProvider without using a Description', async () => {
41+
it('should be possible to use useDescriptions without using a Description', async () => {
4042
let { container } = renderTemplate({
4143
render() {
42-
return h('div', [
43-
h(this.DescriptionProvider, () => [
44-
h('div', { 'aria-describedby': this.describedby }, ['No description']),
45-
]),
46-
])
44+
return h('div', [h('div', { 'aria-describedby': this.describedby }, ['No description'])])
4745
},
4846
setup() {
49-
let [describedby, DescriptionProvider] = useDescriptions()
50-
return { describedby, DescriptionProvider }
47+
let describedby = useDescriptions()
48+
return { describedby }
5149
},
5250
})
5351

@@ -60,21 +58,19 @@ it('should be possible to use a DescriptionProvider without using a Description'
6058
)
6159
})
6260

63-
it('should be possible to use a DescriptionProvider and a single Description, and have them linked', async () => {
61+
it('should be possible to use useDescriptions and a single Description, and have them linked', async () => {
6462
let { container } = renderTemplate({
6563
render() {
6664
return h('div', [
67-
h(this.DescriptionProvider, () => [
68-
h('div', { 'aria-describedby': this.describedby }, [
69-
h(Description, () => 'I am a description'),
70-
h('span', 'Contents'),
71-
]),
65+
h('div', { 'aria-describedby': this.describedby }, [
66+
h(Description, () => 'I am a description'),
67+
h('span', 'Contents'),
7268
]),
7369
])
7470
},
7571
setup() {
76-
let [describedby, DescriptionProvider] = useDescriptions()
77-
return { describedby, DescriptionProvider }
72+
let describedby = useDescriptions()
73+
return { describedby }
7874
},
7975
})
8076

@@ -94,22 +90,20 @@ it('should be possible to use a DescriptionProvider and a single Description, an
9490
)
9591
})
9692

97-
it('should be possible to use a DescriptionProvider and multiple Description components, and have them linked', async () => {
93+
it('should be possible to use useDescriptions and multiple Description components, and have them linked', async () => {
9894
let { container } = renderTemplate({
9995
render() {
10096
return h('div', [
101-
h(this.DescriptionProvider, () => [
102-
h('div', { 'aria-describedby': this.describedby }, [
103-
h(Description, () => 'I am a description'),
104-
h('span', 'Contents'),
105-
h(Description, () => 'I am also a description'),
106-
]),
97+
h('div', { 'aria-describedby': this.describedby }, [
98+
h(Description, () => 'I am a description'),
99+
h('span', 'Contents'),
100+
h(Description, () => 'I am also a description'),
107101
]),
108102
])
109103
},
110104
setup() {
111-
let [describedby, DescriptionProvider] = useDescriptions()
112-
return { describedby, DescriptionProvider }
105+
let describedby = useDescriptions()
106+
return { describedby }
113107
},
114108
})
115109

@@ -131,3 +125,47 @@ it('should be possible to use a DescriptionProvider and multiple Description com
131125
`)
132126
)
133127
})
128+
129+
it('should be possible to update a prop from the parent and it should reflect in the Description component', async () => {
130+
let { container } = renderTemplate({
131+
render() {
132+
return h('div', [
133+
h('div', { 'aria-describedby': this.describedby }, [
134+
h(Description, () => 'I am a description'),
135+
h('button', { onClick: () => this.count++ }, '+1'),
136+
]),
137+
])
138+
},
139+
setup() {
140+
let count = ref(0)
141+
let describedby = useDescriptions({ props: { 'data-count': count } })
142+
return { count, describedby }
143+
},
144+
})
145+
146+
await new Promise<void>(nextTick)
147+
148+
expect(format(container.firstElementChild)).toEqual(
149+
format(html`
150+
<div aria-describedby="headlessui-description-1">
151+
<p data-count="0" id="headlessui-description-1">
152+
I am a description
153+
</p>
154+
<button>+1</button>
155+
</div>
156+
`)
157+
)
158+
159+
await click(getByText('+1'))
160+
161+
expect(format(container.firstElementChild)).toEqual(
162+
format(html`
163+
<div aria-describedby="headlessui-description-1">
164+
<p data-count="1" id="headlessui-description-1">
165+
I am a description
166+
</p>
167+
<button>+1</button>
168+
</div>
169+
`)
170+
)
171+
})

packages/@headlessui-vue/src/components/description/description.ts

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
onUnmounted,
77
provide,
88
ref,
9+
unref,
910

1011
// Types
1112
ComputedRef,
1213
InjectionKey,
13-
Ref,
1414
} from 'vue'
1515

1616
import { useId } from '../../hooks/use-id'
@@ -20,9 +20,9 @@ import { render } from '../../utils/render'
2020

2121
let DescriptionContext = Symbol('DescriptionContext') as InjectionKey<{
2222
register(value: string): () => void
23-
slot: Ref<Record<string, any>>
24-
name: Ref<string>
25-
props: Ref<Record<string, any>>
23+
slot: Record<string, any>
24+
name: string
25+
props: Record<string, any>
2626
}>
2727

2828
function useDescriptionContext() {
@@ -33,42 +33,33 @@ function useDescriptionContext() {
3333
return context
3434
}
3535

36-
export function useDescriptions(): [
37-
ComputedRef<string | undefined>,
38-
ReturnType<typeof defineComponent>
39-
] {
36+
export function useDescriptions({
37+
slot = {},
38+
name = 'Description',
39+
props = {},
40+
}: {
41+
slot?: Record<string, unknown>
42+
name?: string
43+
props?: Record<string, unknown>
44+
} = {}): ComputedRef<string | undefined> {
4045
let descriptionIds = ref<string[]>([])
4146

42-
return [
43-
// The actual id's as string or undefined.
44-
computed(() => (descriptionIds.value.length > 0 ? descriptionIds.value.join(' ') : undefined)),
47+
function register(value: string) {
48+
descriptionIds.value.push(value)
4549

46-
// The provider component
47-
defineComponent({
48-
name: 'DescriptionProvider',
49-
props: ['slot', 'name', 'props'],
50-
setup(props, { slots }) {
51-
function register(value: string) {
52-
descriptionIds.value.push(value)
53-
54-
return () => {
55-
let idx = descriptionIds.value.indexOf(value)
56-
if (idx === -1) return
57-
descriptionIds.value.splice(idx, 1)
58-
}
59-
}
50+
return () => {
51+
let idx = descriptionIds.value.indexOf(value)
52+
if (idx === -1) return
53+
descriptionIds.value.splice(idx, 1)
54+
}
55+
}
6056

61-
provide(DescriptionContext, {
62-
register,
63-
slot: computed(() => props.slot),
64-
name: computed(() => props.name),
65-
props: computed(() => props.props),
66-
})
57+
provide(DescriptionContext, { register, slot, name, props })
6758

68-
return () => slots.default!()
69-
},
70-
}),
71-
]
59+
// The actual id's as string or undefined.
60+
return computed(() =>
61+
descriptionIds.value.length > 0 ? descriptionIds.value.join(' ') : undefined
62+
)
7263
}
7364

7465
// ---
@@ -79,23 +70,30 @@ export let Description = defineComponent({
7970
as: { type: [Object, String], default: 'p' },
8071
},
8172
render() {
73+
let { name = 'Description', slot = {}, props = {} } = this.context
8274
let passThroughProps = this.$props
83-
let propsWeControl = { ...this.props, id: this.id }
75+
let propsWeControl = {
76+
...Object.entries(props).reduce(
77+
(acc, [key, value]) => Object.assign(acc, { [key]: unref(value) }),
78+
{}
79+
),
80+
id: this.id,
81+
}
8482

8583
return render({
86-
props: { ...this.props, ...passThroughProps, ...propsWeControl },
87-
slot: this.slot || {},
84+
props: { ...passThroughProps, ...propsWeControl },
85+
slot,
8886
attrs: this.$attrs,
8987
slots: this.$slots,
90-
name: this.name || 'Description',
88+
name,
9189
})
9290
},
9391
setup() {
94-
let { register, slot, name, props } = useDescriptionContext()
92+
let context = useDescriptionContext()
9593
let id = `headlessui-description-${useId()}`
9694

97-
onMounted(() => onUnmounted(register(id)))
95+
onMounted(() => onUnmounted(context.register(id)))
9896

99-
return { id, slot, name, props }
97+
return { id, context }
10098
},
10199
})

packages/@headlessui-vue/src/components/dialog/dialog.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,15 @@ export let Dialog = defineComponent({
109109
h(Portal, {}, () => [
110110
h(PortalGroup, { target: this.dialogRef }, () => [
111111
h(ForcePortalRoot, { force: false }, () => [
112-
h(this.DescriptionProvider, { slot }, () => [
113-
render({
114-
props: { ...passThroughProps, ...propsWeControl },
115-
slot,
116-
attrs: this.$attrs,
117-
slots: this.$slots,
118-
visible: open,
119-
features: Features.RenderStrategy | Features.Static,
120-
name: 'Dialog',
121-
}),
122-
]),
112+
render({
113+
props: { ...passThroughProps, ...propsWeControl },
114+
slot,
115+
attrs: this.$attrs,
116+
slots: this.$slots,
117+
visible: open,
118+
features: Features.RenderStrategy | Features.Static,
119+
name: 'Dialog',
120+
}),
123121
]),
124122
]),
125123
]),
@@ -182,7 +180,10 @@ export let Dialog = defineComponent({
182180

183181
useFocusTrap(containers, enabled, focusTrapOptions)
184182
useInertOthers(internalDialogRef, enabled)
185-
let [describedby, DescriptionProvider] = useDescriptions()
183+
let describedby = useDescriptions({
184+
name: 'DialogDescription',
185+
slot: { open: props.open },
186+
})
186187

187188
let titleId = ref<StateDefinition['titleId']['value']>(null)
188189

@@ -271,7 +272,6 @@ export let Dialog = defineComponent({
271272
dialogState,
272273
titleId,
273274
describedby,
274-
DescriptionProvider,
275275
}
276276
},
277277
})

0 commit comments

Comments
 (0)