Skip to content

Commit cd2f8bb

Browse files
feat: Collection
1 parent 5ce5339 commit cd2f8bb

File tree

14 files changed

+157
-26
lines changed

14 files changed

+157
-26
lines changed

.vscode/settings.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@
1919
},
2020

2121
// Silent the stylistic rules in you IDE, but still auto fix them
22-
"eslint.rules.customizations": [
23-
{ "rule": "style/*", "severity": "off" },
24-
{ "rule": "format/*", "severity": "off" },
25-
{ "rule": "*-indent", "severity": "off" },
26-
{ "rule": "*-spacing", "severity": "off" },
27-
{ "rule": "*-spaces", "severity": "off" },
28-
{ "rule": "*-order", "severity": "off" },
29-
{ "rule": "*-dangle", "severity": "off" },
30-
{ "rule": "*-newline", "severity": "off" },
31-
{ "rule": "*quotes", "severity": "off" },
32-
{ "rule": "*semi", "severity": "off" }
33-
],
22+
// "eslint.rules.customizations": [
23+
// { "rule": "style/*", "severity": "off" },
24+
// { "rule": "format/*", "severity": "off" },
25+
// { "rule": "*-indent", "severity": "off" },
26+
// { "rule": "*-spacing", "severity": "off" },
27+
// { "rule": "*-spaces", "severity": "off" },
28+
// { "rule": "*-order", "severity": "off" },
29+
// { "rule": "*-dangle", "severity": "off" },
30+
// { "rule": "*-newline", "severity": "off" },
31+
// { "rule": "*quotes", "severity": "off" },
32+
// { "rule": "*semi", "severity": "off" }
33+
// ],
3434

3535
// Enable eslint for all supported languages
3636
"eslint.validate": [
File renamed without changes.

packages/typescript-config/tsconfig.options.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
// "types": [], /* Указать имена пакетов типов, которые должны быть включены без ссылки на их исходный файл. */
3535
// "allowUmdGlobalAccess": true, /* Разрешить доступ к глобальным объектам UMD из модулей. */
3636
// "moduleSuffixes": [], /* Список суффиксов имен файлов для поиска при разрешении модуля. */
37-
// "allowImportingTsExtensions": true, //-1 /* Разрешить импорт файлов TypeScript с расширениями. Требуется установить '--moduleResolution bundler' и либо '--noEmit', либо '--emitDeclarationOnly'. */
37+
"allowImportingTsExtensions": true, /* Разрешить импорт файлов TypeScript с расширениями. Требуется установить '--moduleResolution bundler' и либо '--noEmit', либо '--emitDeclarationOnly'. */
3838
// "resolvePackageJsonExports": true, /* Использовать поле 'exports' в package.json при разрешении импортов пакетов. */
3939
// "resolvePackageJsonImports": true, /* Использовать поле 'imports' в package.json при разрешении импортов. */
4040
// "customConditions": [], /* Условия, устанавливаемые в дополнение к специфичным для разрешателя значениям по умолчанию при разрешении импортов. */
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import antfu from '@antfu/eslint-config'
22

33
export default antfu({
44
rules: {
5-
'import/extensions': ['error', 'always'],
5+
'import/extensions': ['error', 'ignorePackages'],
66
},
77
ignores: [
88
'packages',

packages/vue-primitives/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"build-only": "vite build",
1616
"type-check": "vue-tsc --build --force",
1717
"storybook": "storybook dev -p 6006",
18-
"build-storybook": "storybook build"
18+
"build-storybook": "storybook build",
19+
"eslint": "eslint .",
20+
"eslint:fix": "eslint . --fix"
1921
},
2022
"devDependencies": {
2123
"@chromatic-com/storybook": "^1.5.0",

packages/vue-primitives/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, shallowRef, watchEffect } from 'vue'
2+
import { computed, shallowRef } from 'vue'
33
// import Foo from '../src/Foo.vue'
44
// import Primitive from './primitive/Primitive.vue'
55
import Toggle from './toggle/Toggle.vue'
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { type ShallowReactive, type ShallowRef, shallowReactive, watchEffect } from 'vue'
2+
import { createContext } from '../hooks/createContext.ts'
3+
4+
export const ITEM_DATA_ATTR = 'data-radix-collection-item'
5+
6+
interface ContextValue<ItemElement extends HTMLElement, ItemData = object> {
7+
collectionRef: ShallowRef<ItemElement | undefined>
8+
itemMap: ShallowReactive<Map<ItemElement, { ref: ItemElement, attrs: ItemData }>>
9+
}
10+
11+
export function createCollection<ItemElement extends HTMLElement, ItemData = object>(name: string) {
12+
const [_provideCollectionContext, useCollectionContext] = createContext<ContextValue<ItemElement, ItemData>>(`${name}CollectionProvider`)
13+
14+
function provideCollectionContext(collectionRef: ContextValue<ItemElement, ItemData>['collectionRef']) {
15+
const itemMap = shallowReactive(new Map<ItemElement, { ref: ItemElement, attrs: ItemData }>())
16+
17+
_provideCollectionContext({
18+
collectionRef,
19+
itemMap,
20+
})
21+
}
22+
23+
function useCollectionItem(currentElement: ShallowRef<ItemElement | undefined>, attrs: Record<string, unknown> = {}) {
24+
const { itemMap } = useCollectionContext()
25+
26+
watchEffect((onClean) => {
27+
const unrefElement = currentElement.value
28+
if (!unrefElement)
29+
return
30+
31+
itemMap.set(unrefElement, {
32+
ref: unrefElement,
33+
attrs: attrs as ItemData,
34+
})
35+
36+
onClean(() => {
37+
itemMap.delete(unrefElement)
38+
})
39+
})
40+
41+
return {
42+
itemMap,
43+
}
44+
}
45+
46+
function useCollection() {
47+
const context = useCollectionContext(`${name}CollectionConsumer`)
48+
49+
const getItems = () => {
50+
const collectionNode = context.collectionRef.value
51+
if (!collectionNode)
52+
return []
53+
54+
const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`))
55+
const items = Array.from(context.itemMap.values())
56+
const orderedItems = items.sort(
57+
(a, b) => orderedNodes.indexOf(a.ref) - orderedNodes.indexOf(b.ref),
58+
)
59+
60+
return orderedItems
61+
}
62+
63+
return getItems
64+
}
65+
66+
return [{ provideCollectionContext, useCollectionContext, useCollectionItem }, useCollection] as const
67+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ITEM_DATA_ATTR, createCollection } from './Collection.ts'
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { defineComponent, shallowRef, watchEffect } from 'vue'
2+
import Toggle from '../../toggle/Toggle.vue'
3+
import { ITEM_DATA_ATTR, createCollection } from '../index.ts'
4+
5+
interface ItemData { disabled: boolean }
6+
7+
const [Collection, useCollection] = createCollection<HTMLElement, ItemData>('List')
8+
9+
const List = defineComponent({
10+
setup(_, { slots }) {
11+
const collectionRef = shallowRef<HTMLElement>()
12+
Collection.provideCollectionContext(collectionRef)
13+
14+
return () => (
15+
<ul ref={collectionRef} class="list">
16+
{slots.default?.()}
17+
</ul>
18+
)
19+
},
20+
})
21+
22+
const Item = defineComponent({
23+
setup(_, { slots, attrs }) {
24+
const currentElement = shallowRef<HTMLElement>()
25+
Collection.useCollectionItem(currentElement, attrs)
26+
27+
return () => (
28+
<li ref={currentElement} class="item" style={{ opacity: attrs.disabled ? 0.3 : undefined }} {...{ [ITEM_DATA_ATTR]: '' }}>
29+
{slots.default?.()}
30+
</li>
31+
)
32+
},
33+
})
34+
35+
const LogItems = defineComponent({
36+
setup() {
37+
const getItems = useCollection()
38+
39+
watchEffect(() => {
40+
console.warn('Items:', getItems())
41+
})
42+
43+
return () => null
44+
},
45+
})
46+
47+
export default { title: 'Utilities/Collection' }
48+
49+
export const Styled = () => <Toggle class="root">Toggle</Toggle>
50+
51+
export function Basic() {
52+
return (
53+
<List>
54+
<Item>Red</Item>
55+
<Item {...{ disabled: true }}>
56+
Green
57+
</Item>
58+
<Item>Blue</Item>
59+
<LogItems />
60+
</List>
61+
)
62+
}

packages/vue-primitives/src/utils/createContext.ts renamed to packages/vue-primitives/src/hooks/createContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { type InjectionKey, inject, provide } from 'vue'
99
export function createContext<T>(
1010
contextName: string,
1111
defaultValue?: T,
12-
): readonly [useProvidingState: (state: T) => void, useContext: () => T] {
12+
): readonly [useProvidingState: (state: T) => void, useContext: (consumerName?: string) => T] {
1313
const key: string | InjectionKey<T> = Symbol(contextName)
1414

1515
const useProvideContext = (state: T) => {

0 commit comments

Comments
 (0)