Skip to content

Commit 87b592a

Browse files
feat: ContextMenu
1 parent 8adfe20 commit 87b592a

File tree

81 files changed

+1973
-385
lines changed

Some content is hidden

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

81 files changed

+1973
-385
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Enter the component you want most in the components, leave the emojis and follow
4545
| [Avatar](https://vue-primitives.netlify.app/?path=/story/components-avatar--styled) ||
4646
| [Checkbox](https://vue-primitives.netlify.app/?path=/story/components-checkbox--styled) ||
4747
| [Collapsible](https://vue-primitives.netlify.app/?path=/story/components-collapsible--styled) ||
48+
| [Context Menu](https://vue-primitives.netlify.app/?path=/story/components-contextmenu--styled) ||
4849
| [Dialog](https://vue-primitives.netlify.app/?path=/story/components-dialog--styled) ||
4950
| DropdownMenu | 🚧 |
5051
| Form | ✖️ |

packages/vue-primitives/src/avatar/AvatarFallback.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { AvatarFallbackProps } from './AvatarFallback.ts'
33
import { isClient } from '@vueuse/core'
4-
import { shallowRef, watchEffect } from 'vue'
4+
import { onWatcherCleanup, shallowRef, watchEffect } from 'vue'
55
import { Primitive } from '../primitive/index.ts'
66
import { useAvatarContext } from './AvatarRoot.ts'
77
@@ -17,10 +17,10 @@ const context = useAvatarContext('AvatarFallback')
1717
const canRender = shallowRef(props.delayMs === undefined)
1818
1919
if (isClient) {
20-
watchEffect((onCleanup) => {
20+
watchEffect(() => {
2121
if (props.delayMs !== undefined) {
2222
const timerId = window.setTimeout(() => canRender.value = true, props.delayMs)
23-
onCleanup(() => {
23+
onWatcherCleanup(() => {
2424
window.clearTimeout(timerId)
2525
})
2626
}

packages/vue-primitives/src/avatar/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type { AvatarImageProps } from './AvatarImage.ts'
22
import type { ImageLoadingStatus } from './AvatarRoot.ts'
33
import { isClient } from '@vueuse/core'
4-
import { type Ref, shallowRef, toValue, watchEffect } from 'vue'
4+
import { onWatcherCleanup, type Ref, shallowRef, toValue, watchEffect } from 'vue'
55

66
export function useImageLoadingStatus(src: Ref<AvatarImageProps['src']> | (() => AvatarImageProps['src'])) {
77
const loadingStatus = shallowRef<ImageLoadingStatus>('idle')
88

99
if (!isClient)
1010
return loadingStatus
1111

12-
watchEffect((onCleanup) => {
12+
watchEffect(() => {
1313
const value = toValue(src)
1414

1515
if (!value) {
@@ -31,7 +31,7 @@ export function useImageLoadingStatus(src: Ref<AvatarImageProps['src']> | (() =>
3131
image.onerror = updateStatus('error')
3232
image.src = value
3333

34-
onCleanup(() => {
34+
onWatcherCleanup(() => {
3535
isMounted = false
3636
})
3737
})

packages/vue-primitives/src/checkbox/BubbleInput.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { MutableRefObject } from '../hooks/index.ts'
2+
import type { CheckedState } from './CheckboxRoot.ts'
3+
4+
export interface CheckboxBubbleInputProps {
5+
checked: CheckedState
6+
control: HTMLElement | undefined
7+
bubbles: MutableRefObject<boolean>
8+
}

packages/vue-primitives/src/checkbox/BubbleInput.vue renamed to packages/vue-primitives/src/checkbox/CheckboxBubbleInput.vue

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
<script setup lang="ts">
2-
import type { BubbleInputProps } from './BubbleInput.ts'
2+
import type { CheckboxBubbleInputProps } from './CheckboxBubbleInput.ts'
33
import { watch } from 'vue'
44
import { useSize } from '../hooks/index.ts'
55
import { isIndeterminate } from './utils.ts'
66
77
defineOptions({
8-
name: 'BubbleInput',
8+
name: 'CheckboxBubbleInput',
99
})
1010
11-
const props = withDefaults(defineProps<BubbleInputProps>(), {
11+
const props = withDefaults(defineProps<CheckboxBubbleInputProps>(), {
1212
checked: undefined,
1313
control: undefined,
14-
bubbles: true,
1514
})
1615
1716
let input: HTMLInputElement | undefined
@@ -34,7 +33,7 @@ watch(() => props.checked, (checked, prevChecked) => {
3433
3534
if (prevChecked !== checked && setChecked) {
3635
// TODO: Check if this is the correct way to create a change event
37-
const event = new Event('change', { bubbles: props.bubbles })
36+
const event = new Event('change', { bubbles: props.bubbles.current })
3837
input.indeterminate = isIndeterminate(checked)
3938
setChecked.call(input, isIndeterminate(checked) ? false : checked)
4039
input.dispatchEvent(event)

packages/vue-primitives/src/checkbox/CheckboxRoot.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Ref } from 'vue'
22
import type { PrimitiveProps } from '../primitive/index.ts'
3-
import { createContext } from '../hooks/index.ts'
3+
import { createContext, type MutableRefObject } from '../hooks/index.ts'
44

55
export interface CheckboxRootProps {
66
as?: PrimitiveProps['as']
@@ -19,6 +19,22 @@ export type CheckboxRootEmits = {
1919
'click': [event: MouseEvent]
2020
}
2121

22+
// eslint-disable-next-line ts/consistent-type-definitions
23+
export type CheckboxRootSlots = {
24+
default: (props: {
25+
isFormControl: boolean
26+
input: {
27+
control: HTMLButtonElement | undefined
28+
bubbles: MutableRefObject<boolean>
29+
name?: string
30+
value: string
31+
checked: CheckedState
32+
required?: boolean
33+
disabled?: boolean
34+
}
35+
}) => any
36+
}
37+
2238
export type CheckedState = boolean | 'indeterminate'
2339

2440
export interface CheckboxContext {
Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
<script setup lang="ts">
2-
import { computed, shallowRef, watchEffect } from 'vue'
3-
import { useControllableState, useForwardElement } from '../hooks/index.ts'
2+
import { computed, onWatcherCleanup, shallowRef, watchEffect } from 'vue'
3+
import { useControllableState, useForwardElement, useRef } from '../hooks/index.ts'
44
import { Primitive } from '../primitive/index.ts'
55
import { composeEventHandlers } from '../utils/vue.ts'
6-
import BubbleInput from './BubbleInput.vue'
7-
import { type CheckboxRootEmits, type CheckboxRootProps, provideCheckboxContext } from './CheckboxRoot.ts'
6+
import { type CheckboxRootEmits, type CheckboxRootProps, type CheckboxRootSlots, provideCheckboxContext } from './CheckboxRoot.ts'
87
import { getState, isIndeterminate } from './utils.ts'
98
109
defineOptions({
1110
name: 'Checkbox',
12-
inheritAttrs: false,
1311
})
1412
1513
const props = withDefaults(defineProps<CheckboxRootProps>(), {
@@ -18,27 +16,34 @@ const props = withDefaults(defineProps<CheckboxRootProps>(), {
1816
as: 'button',
1917
})
2018
const emit = defineEmits<CheckboxRootEmits>()
21-
const $el = shallowRef<HTMLButtonElement>()
22-
const forwardElement = useForwardElement($el)
2319
24-
const hasConsumerStoppedPropagation = shallowRef(false)
20+
defineSlots<CheckboxRootSlots>()
21+
22+
const control = shallowRef<HTMLButtonElement>()
23+
// const elRef = useRef<HTMLButtonElement>()
24+
const forwardElement = useForwardElement<HTMLButtonElement>(control)
25+
26+
const bubbles = useRef(true)
2527
// We set this to true by default so that events bubble to forms without JS (SSR)
26-
const isFormControl = computed(() => $el.value ? Boolean($el.value.closest('form')) : true)
28+
const isFormControl = computed(() => control.value ? Boolean(control.value.closest('form')) : true)
2729
const checked = useControllableState(props, v => emit('update:checked', v), 'checked', props.defaultChecked)
2830
2931
const initialCheckedStateRef = checked.value
3032
31-
watchEffect((onCleanup) => {
32-
const form = $el.value?.form
33-
if (form) {
34-
const reset = () => {
35-
checked.value = initialCheckedStateRef
36-
}
37-
38-
form.addEventListener('reset', reset)
33+
watchEffect(() => {
34+
const form = control.value?.form
35+
if (!form)
36+
return
3937
40-
onCleanup(() => form.removeEventListener('reset', reset))
38+
const reset = () => {
39+
checked.value = initialCheckedStateRef
4140
}
41+
42+
form.addEventListener('reset', reset)
43+
44+
onWatcherCleanup(() => {
45+
form.removeEventListener('reset', reset)
46+
})
4247
})
4348
4449
const onKeydown = composeEventHandlers<KeyboardEvent>((event) => {
@@ -54,11 +59,11 @@ const onClick = composeEventHandlers<MouseEvent>((event) => {
5459
}, (event) => {
5560
checked.value = isIndeterminate(checked.value) ? true : !checked.value
5661
if (isFormControl.value) {
57-
hasConsumerStoppedPropagation.value = event.cancelBubble
62+
bubbles.current = !event.cancelBubble
5863
// if checkbox is in a form, stop propagation from the button so that we only propagate
5964
// one click event (from the input). We propagate changes from an input so that native
6065
// form validation works and form events reflect checkbox updates.
61-
if (!hasConsumerStoppedPropagation.value)
66+
if (bubbles.current)
6267
event.stopPropagation()
6368
}
6469
})
@@ -69,10 +74,6 @@ provideCheckboxContext({
6974
},
7075
state: checked,
7176
})
72-
73-
defineExpose({
74-
$el,
75-
})
7677
</script>
7778

7879
<template>
@@ -82,26 +83,25 @@ defineExpose({
8283
type="button"
8384
role="checkbox"
8485
:aria-checked="isIndeterminate(checked) ? 'mixed' : checked"
85-
:aria-required="$attrs.required"
86+
:aria-required="required"
8687
:data-state="getState(checked)"
8788
:data-disabled="disabled ? '' : undefined"
8889
:disabled="disabled"
8990
:value="value"
90-
v-bind="$attrs"
9191
@keydown="onKeydown"
9292
@click="onClick"
9393
>
94-
<slot />
94+
<slot
95+
:is-form-control="isFormControl"
96+
:input="{
97+
control,
98+
bubbles,
99+
name,
100+
value,
101+
checked,
102+
required,
103+
disabled,
104+
}"
105+
/>
95106
</Primitive>
96-
97-
<BubbleInput
98-
v-if="isFormControl"
99-
:control="$el"
100-
:bubbles="!hasConsumerStoppedPropagation"
101-
:name="name"
102-
:value="value"
103-
:checked="checked"
104-
:required="required"
105-
:disabled="disabled"
106-
/>
107107
</template>

packages/vue-primitives/src/checkbox/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
export { default as CheckboxBubbleInput } from './CheckboxBubbleInput.vue'
12
export { type CheckboxIndicatorProps } from './CheckboxIndicator.ts'
2-
export { default as CheckboxIndicator } from './CheckboxIndicator.vue'
33

4+
export { default as CheckboxIndicator } from './CheckboxIndicator.vue'
45
export {
56
type CheckboxContext,
67
type CheckboxRootEmits,

packages/vue-primitives/src/checkbox/stories/WithinForm.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { shallowRef } from 'vue'
3-
import { CheckboxIndicator, CheckboxRoot } from '../index.ts'
3+
import { CheckboxBubbleInput, CheckboxIndicator, CheckboxRoot } from '../index.ts'
44
import './styles.css'
55
66
const data = shallowRef({
@@ -23,7 +23,10 @@ function onChange(event: Event) {
2323
<legend>optional checked: {{ String(data.optional) }}</legend>
2424
<label>
2525
<CheckboxRoot v-model:checked="checked" class="checkbox_rootClass" name="optional">
26-
<CheckboxIndicator class="checkbox_indicatorClass" />
26+
<template #default="scope">
27+
<CheckboxBubbleInput v-if="scope.isFormControl" v-bind="scope.input" />
28+
<CheckboxIndicator class="checkbox_indicatorClass" />
29+
</template>
2730
</CheckboxRoot>{{ ' ' }}
2831
with label
2932
</label>
@@ -46,7 +49,10 @@ function onChange(event: Event) {
4649
<fieldset>
4750
<legend>required checked: {{ String(data.required) }}</legend>
4851
<CheckboxRoot class="checkbox_rootClass" name="required" required>
49-
<CheckboxIndicator class="checkbox_indicatorClass" />
52+
<template #default="scope">
53+
<CheckboxBubbleInput v-if="scope.isFormControl" v-bind="scope.input" />
54+
<CheckboxIndicator class="checkbox_indicatorClass" />
55+
</template>
5056
</CheckboxRoot>
5157
</fieldset>
5258

@@ -56,7 +62,10 @@ function onChange(event: Event) {
5662
<fieldset>
5763
<legend>stop propagation checked: {{ String(data.stopprop) }}</legend>
5864
<CheckboxRoot class="checkbox_rootClass" name="stopprop" @click="(event: Event) => event.stopPropagation()">
59-
<CheckboxIndicator class="checkbox_indicatorClass" />
65+
<template #default="scope">
66+
<CheckboxBubbleInput v-if="scope.isFormControl" v-bind="scope.input" />
67+
<CheckboxIndicator class="checkbox_indicatorClass" />
68+
</template>
6069
</CheckboxRoot>
6170
</fieldset>
6271

0 commit comments

Comments
 (0)