Skip to content

Commit 685f06a

Browse files
committed
refactor: spacer component styling
drop prop, prefer tailwind
1 parent f7e74d9 commit 685f06a

2 files changed

Lines changed: 63 additions & 67 deletions

File tree

src/components/Spacer.vue

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
2+
import { computed, h, useAttrs } from 'vue'
33
import { normalizeToPixels, outlookFallbackProp } from './utils.ts'
44
import { useOutlookFallback } from '../composables/useOutlookFallback'
55
6+
defineOptions({ inheritAttrs: false })
7+
68
const props = defineProps({
79
/** The type of spacer. */
810
type: {
911
type: String as () => 'vertical' | 'horizontal',
1012
default: 'vertical'
1113
},
12-
/** The height of the spacer (vertical). */
13-
height: {
14-
type: [String, Number],
15-
default: null
16-
},
1714
/** The width of the spacer (horizontal). */
1815
width: {
1916
type: [String, Number],
2017
default: 16
2118
},
22-
/** The alternative height to use in Outlook. */
23-
msoHeight: {
24-
type: [String, Number],
25-
default: null
26-
},
2719
outlookFallback: outlookFallbackProp,
2820
})
2921
22+
const attrs = useAttrs()
3023
const outlookFallback = useOutlookFallback(props.outlookFallback)
3124
32-
function parsePixelValue(value: string | number): number {
33-
if (typeof value === 'number') return value
34-
return Number.parseFloat(value) || 0
35-
}
25+
const HEIGHT_RE = /(?:^|\s)h-([\w./\-[\]%]+)/g
26+
const LEADING_RE = /(?:^|\s)leading-/
3627
37-
const verticalStyles = computed(() => {
38-
const s = []
28+
const verticalClass = computed(() => {
29+
const userClass = (attrs.class as string) || ''
30+
if (!userClass) return ''
3931
40-
if (props.height) {
41-
s.push(`line-height: ${normalizeToPixels(props.height)};`)
42-
}
32+
const heights = [...userClass.matchAll(HEIGHT_RE)]
33+
const stripped = userClass.replace(HEIGHT_RE, ' ').replace(/\s+/g, ' ').trim()
4334
44-
if (outlookFallback && props.msoHeight) {
45-
s.push(`mso-line-height-alt: ${normalizeToPixels(props.msoHeight)};`)
46-
}
35+
if (!heights.length) return stripped
36+
if (LEADING_RE.test(stripped)) return stripped
4737
48-
return s.join('')
38+
return `${stripped} leading-${heights[heights.length - 1][1]}`.trim()
4939
})
5040
41+
function parsePixelValue(value: string | number): number {
42+
if (typeof value === 'number') return value
43+
return Number.parseFloat(value) || 0
44+
}
45+
5146
const horizontalStyles = computed(() => {
5247
const mso = outlookFallback ? msoFontWidth.value : ''
5348
return `display:inline-block; width: ${normalizeToPixels(props.width)}; font-size: 16px;${mso}`
@@ -71,14 +66,20 @@ const emspCount = computed(() => {
7166
})
7267
7368
const emsps = computed(() => '\u2003'.repeat(emspCount.value))
69+
70+
const HorizontalSpacer = () =>
71+
h('i', { ...attrs, style: horizontalStyles.value }, emsps.value)
7472
</script>
7573

7674
<template>
7775
<template v-if="type === 'horizontal'">
78-
<i :style="horizontalStyles">{{ emsps }}</i>
76+
<HorizontalSpacer />
7977
</template>
8078
<template v-else>
81-
<div v-if="height" role="separator" :style="verticalStyles">&zwj;</div>
82-
<div v-else role="separator">&zwj;</div>
79+
<div
80+
role="separator"
81+
v-bind="{ ...$attrs, class: undefined }"
82+
:class="verticalClass"
83+
>&zwj;</div>
8384
</template>
8485
</template>

src/tests/components/Spacer.test.ts

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,64 @@ import { mount } from '@vue/test-utils'
33
import Spacer from '../../components/Spacer.vue'
44

55
describe('Spacer', () => {
6-
describe('defaults', () => {
6+
describe('vertical', () => {
77
it('renders a div with role="separator"', () => {
88
const wrapper = mount(Spacer)
99
expect(wrapper.html()).toContain('role="separator"')
1010
})
1111

12-
it('renders without style attribute when no height is set', () => {
12+
it('renders without inline style by default', () => {
1313
const wrapper = mount(Spacer)
14-
expect(wrapper.html()).not.toContain('line-height:')
14+
expect(wrapper.html()).not.toContain('style=')
1515
})
1616

1717
it('contains zero-width joiner', () => {
1818
const wrapper = mount(Spacer)
1919
expect(wrapper.text()).toContain('\u200D')
2020
})
21-
})
2221

23-
describe('height prop', () => {
24-
it('sets line-height when provided as string', () => {
25-
const wrapper = mount(Spacer, { props: { height: '32px' } })
26-
expect(wrapper.html()).toContain('line-height: 32px')
22+
it('passes through user leading-* class', () => {
23+
const wrapper = mount(Spacer, { attrs: { class: 'leading-8' } })
24+
expect(wrapper.html()).toContain('leading-8')
2725
})
2826

29-
it('accepts a number and adds px suffix', () => {
30-
const wrapper = mount(Spacer, { props: { height: 24 } })
31-
expect(wrapper.html()).toContain('line-height: 24px')
27+
it('passes through arbitrary mso-line-height-alt class', () => {
28+
const wrapper = mount(Spacer, {
29+
attrs: { class: 'leading-8 [mso-line-height-alt:40px]' },
30+
})
31+
const html = wrapper.html()
32+
expect(html).toContain('leading-8')
33+
expect(html).toContain('[mso-line-height-alt:40px]')
3234
})
3335

34-
it('preserves non-numeric string values', () => {
35-
const wrapper = mount(Spacer, { props: { height: '2rem' } })
36-
expect(wrapper.html()).toContain('line-height: 2rem')
37-
})
38-
})
39-
40-
describe('msoHeight prop', () => {
41-
it('sets mso-line-height-alt', () => {
42-
const wrapper = mount(Spacer, { props: { height: '32px', msoHeight: '40px' } })
43-
expect(wrapper.html()).toContain('mso-line-height-alt: 40px')
36+
it('rewrites h-* to leading-*', () => {
37+
const wrapper = mount(Spacer, { attrs: { class: 'h-4' } })
38+
const html = wrapper.html()
39+
expect(html).toContain('leading-4')
40+
expect(html).not.toContain('h-4')
4441
})
4542

46-
it('accepts a number and adds px suffix', () => {
47-
const wrapper = mount(Spacer, { props: { height: '32px', msoHeight: 48 } })
48-
expect(wrapper.html()).toContain('mso-line-height-alt: 48px')
43+
it('rewrites arbitrary h-[3px] to leading-[3px]', () => {
44+
const wrapper = mount(Spacer, { attrs: { class: 'h-[3px]' } })
45+
const html = wrapper.html()
46+
expect(html).toContain('leading-[3px]')
47+
expect(html).not.toContain('h-[3px]')
4948
})
50-
})
5149

52-
describe('conditional rendering', () => {
53-
it('renders with style when height is provided', () => {
54-
const wrapper = mount(Spacer, { props: { height: '16px' } })
55-
expect(wrapper.html()).toContain('style=')
50+
it('drops h-* when leading-* also passed', () => {
51+
const wrapper = mount(Spacer, { attrs: { class: 'h-4 leading-none' } })
52+
const html = wrapper.html()
53+
expect(html).toContain('leading-none')
54+
expect(html).not.toContain('h-4')
55+
expect(html).not.toContain('leading-4')
5656
})
5757

58-
it('renders without style when no height is provided', () => {
59-
const wrapper = mount(Spacer)
60-
expect(wrapper.html()).not.toContain('style=')
58+
it('preserves other classes alongside h-* rewrite', () => {
59+
const wrapper = mount(Spacer, { attrs: { class: 'h-4 my-2' } })
60+
const html = wrapper.html()
61+
expect(html).toContain('my-2')
62+
expect(html).toContain('leading-4')
63+
expect(html).not.toContain('h-4')
6164
})
6265
})
6366

@@ -126,14 +129,6 @@ describe('Spacer', () => {
126129
})
127130

128131
describe('outlookFallback=false', () => {
129-
it('omits mso-line-height-alt on vertical', () => {
130-
const html = mount(Spacer, {
131-
props: { outlookFallback: false, height: 32, msoHeight: 24 },
132-
}).html()
133-
expect(html).toContain('line-height: 32px')
134-
expect(html).not.toContain('mso-line-height-alt')
135-
})
136-
137132
it('omits mso-font-width on horizontal', () => {
138133
const html = mount(Spacer, {
139134
props: { outlookFallback: false, type: 'horizontal', width: 32 },

0 commit comments

Comments
 (0)