Skip to content

Commit 799c1dc

Browse files
authored
feat(@sanity/presets): adds image preset with image, alt text and caption fields (#824)
* feat(@sanity/presets): adds image preset with image, alt text and caption fields * fix(@sanity/presets): dynamic preview title based on enabled fields * refactor(@sanity/presets): use destructuring defaults for image preset config
1 parent 0c34e2f commit 799c1dc

6 files changed

Lines changed: 230 additions & 0 deletions

File tree

dev/test-studio/src/presets/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
CTA_TYPE_NAME,
33
ctaType,
4+
IMAGE_TYPE_NAME,
5+
imageType,
46
LINK_TYPE_NAME,
57
linkType,
68
PAGE_TYPE_NAME,
@@ -35,6 +37,11 @@ const corePresetsTest = defineType({
3537
title: 'CTA',
3638
type: CTA_TYPE_NAME,
3739
},
40+
{
41+
name: 'featuredImage',
42+
title: 'Featured image',
43+
type: IMAGE_TYPE_NAME,
44+
},
3845
],
3946
})
4047

@@ -45,6 +52,16 @@ export const presetsWorkspace = definePlugin(() => ({
4552
internalTypes: [PAGE_TYPE_NAME],
4653
}),
4754
ctaType(),
55+
imageType({
56+
map: {
57+
fields: (fields = []) =>
58+
fields.map((field) =>
59+
field.name === 'caption'
60+
? {...field, name: 'description', title: 'Description'}
61+
: field,
62+
),
63+
},
64+
}),
4865
pageType({
4966
pageBuilderBlocks: ['blockquote'],
5067
}),

plugins/@sanity/presets/src/__snapshots__/index.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ exports[`package exports 1`] = `
44
{
55
".": {
66
"CTA_TYPE_NAME": "string",
7+
"IMAGE_TYPE_NAME": "string",
78
"LINK_TYPE_NAME": "string",
89
"PAGE_TYPE_NAME": "string",
910
"SEO_TYPE_NAME": "string",
1011
"ctaType": "function",
12+
"imageType": "function",
1113
"linkType": "function",
1214
"pageType": "function",
1315
"presets": "function",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export {presets} from './composer'
22
export {linkType, LINK_TYPE_NAME} from './presets/link-type'
33
export {ctaType, CTA_TYPE_NAME} from './presets/cta-type'
4+
export {imageType, IMAGE_TYPE_NAME} from './presets/image-type'
45
export {pageType, PAGE_TYPE_NAME} from './presets/page-type'
56
export {seoType, SEO_TYPE_NAME} from './presets/seo-type'
7+
export type {ImageTypeConfig} from './presets/image-type'
68
export type {LinkTypeConfig} from './presets/link-type'
79
export type {PresetResult} from './types'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const IMAGE_TYPE_NAME = 'core.presets.image'
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type {FieldDefinition} from 'sanity'
2+
import {describe, expect, test} from 'vitest'
3+
4+
import {IMAGE_TYPE_NAME} from './constants'
5+
import {imageType} from './index'
6+
7+
function getFields(result: ReturnType<typeof imageType>): FieldDefinition[] {
8+
const typeDef = result[0]?.type
9+
if (!typeDef || !('fields' in typeDef) || !typeDef.fields) {
10+
throw new Error('Expected an object type definition with fields')
11+
}
12+
return typeDef.fields
13+
}
14+
15+
function getField(fields: FieldDefinition[], name: string): FieldDefinition {
16+
const field = fields.find((entry) => entry.name === name)
17+
if (!field) {
18+
throw new Error(`Field "${name}" not found`)
19+
}
20+
return field
21+
}
22+
23+
describe('imageType', () => {
24+
test('returns one type named core.presets.image', () => {
25+
const result = imageType()
26+
27+
expect(result).toHaveLength(1)
28+
expect(result[0]?.type?.name).toBe(IMAGE_TYPE_NAME)
29+
})
30+
31+
test('default config includes image, altText, and caption fields', () => {
32+
const fields = getFields(imageType())
33+
34+
expect(fields).toHaveLength(3)
35+
36+
const fieldNames = fields.map((field) => field.name)
37+
expect(fieldNames).toEqual(['image', 'altText', 'caption'])
38+
})
39+
40+
test('altText: false excludes the altText field', () => {
41+
const fields = getFields(imageType({altText: false}))
42+
43+
expect(fields).toHaveLength(2)
44+
45+
const fieldNames = fields.map((field) => field.name)
46+
expect(fieldNames).toEqual(['image', 'caption'])
47+
})
48+
49+
test('caption: false excludes the caption field', () => {
50+
const fields = getFields(imageType({caption: false}))
51+
52+
expect(fields).toHaveLength(2)
53+
54+
const fieldNames = fields.map((field) => field.name)
55+
expect(fieldNames).toEqual(['image', 'altText'])
56+
})
57+
58+
test('both altText and caption disabled leaves only the image field', () => {
59+
const fields = getFields(imageType({altText: false, caption: false}))
60+
61+
expect(fields).toHaveLength(1)
62+
63+
const fieldNames = fields.map((field) => field.name)
64+
expect(fieldNames).toEqual(['image'])
65+
})
66+
67+
test('hotspot is enabled by default', () => {
68+
const fields = getFields(imageType())
69+
const imageField = getField(fields, 'image')
70+
71+
expect(imageField).toHaveProperty('options.hotspot', true)
72+
})
73+
74+
test('hotspot: false disables hotspot on the image field', () => {
75+
const fields = getFields(imageType({hotspot: false}))
76+
const imageField = getField(fields, 'image')
77+
78+
expect(imageField).toHaveProperty('options.hotspot', false)
79+
})
80+
81+
test('user-provided fields are appended', () => {
82+
const result = imageType({
83+
fields: [{name: 'credit', type: 'string', title: 'Credit'}],
84+
})
85+
const fields = getFields(result)
86+
87+
expect(fields).toHaveLength(4)
88+
89+
const fieldNames = fields.map((field) => field.name)
90+
expect(fieldNames).toEqual(['image', 'altText', 'caption', 'credit'])
91+
})
92+
})
93+
94+
describe('imageType map hooks', () => {
95+
test('map.fields can rename the caption field', () => {
96+
const result = imageType({
97+
map: {
98+
fields: (fields = []) =>
99+
fields.map((field) =>
100+
field.name === 'caption'
101+
? {...field, name: 'description', title: 'Description'}
102+
: field,
103+
),
104+
},
105+
})
106+
const fields = getFields(result)
107+
108+
const fieldNames = fields.map((field) => field.name)
109+
expect(fieldNames).toEqual(['image', 'altText', 'description'])
110+
expect(getField(fields, 'image').type).toBe('image')
111+
expect(getField(fields, 'altText').type).toBe('string')
112+
expect(getField(fields, 'description').title).toBe('Description')
113+
})
114+
})
115+
116+
describe('imageType preview.select', () => {
117+
test('selects altText as title by default', () => {
118+
const typeDef = imageType()[0]?.type
119+
const select = typeDef && 'preview' in typeDef ? typeDef.preview?.select : undefined
120+
121+
expect(select).toEqual({
122+
title: 'altText',
123+
media: 'image',
124+
})
125+
})
126+
127+
test('selects caption as title when altText is disabled', () => {
128+
const typeDef = imageType({altText: false})[0]?.type
129+
const select = typeDef && 'preview' in typeDef ? typeDef.preview?.select : undefined
130+
131+
expect(select).toEqual({
132+
title: 'caption',
133+
media: 'image',
134+
})
135+
})
136+
137+
test('selects filename as title when altText and caption are disabled', () => {
138+
const typeDef = imageType({altText: false, caption: false})[0]?.type
139+
const select = typeDef && 'preview' in typeDef ? typeDef.preview?.select : undefined
140+
141+
expect(select).toEqual({
142+
title: 'image.asset.originalFilename',
143+
media: 'image',
144+
})
145+
})
146+
})
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {defineField, defineType} from 'sanity'
2+
3+
import {definePresetType} from '../../definePresetType'
4+
import {IMAGE_TYPE_NAME} from './constants'
5+
6+
export {IMAGE_TYPE_NAME} from './constants'
7+
8+
export interface ImageTypeConfig {
9+
altText?: boolean
10+
caption?: boolean
11+
hotspot?: boolean
12+
}
13+
14+
export const imageType = definePresetType<ImageTypeConfig, 'object', 'preview'>((context) => {
15+
const {altText = true, caption = true, hotspot = true, fields, ...objectConfig} = context ?? {}
16+
17+
return {
18+
name: IMAGE_TYPE_NAME,
19+
schemaType: defineType({
20+
name: IMAGE_TYPE_NAME,
21+
title: 'Image',
22+
...objectConfig,
23+
type: 'object',
24+
fields: [
25+
defineField({
26+
name: 'image',
27+
title: 'Image',
28+
type: 'image',
29+
options: {
30+
hotspot,
31+
},
32+
}),
33+
...(altText
34+
? [
35+
defineField({
36+
name: 'altText',
37+
title: 'Alt text',
38+
type: 'string',
39+
validation: (rule) => rule.warning('Alt text improves accessibility.'),
40+
}),
41+
]
42+
: []),
43+
...(caption
44+
? [
45+
defineField({
46+
name: 'caption',
47+
title: 'Caption',
48+
type: 'text',
49+
}),
50+
]
51+
: []),
52+
...(fields ?? []),
53+
],
54+
preview: {
55+
select: {
56+
title: altText ? 'altText' : caption ? 'caption' : 'image.asset.originalFilename',
57+
media: 'image',
58+
},
59+
},
60+
}),
61+
}
62+
})

0 commit comments

Comments
 (0)