Skip to content

Commit

Permalink
feat: implement custom style id and selector prefix
Browse files Browse the repository at this point in the history
  • Loading branch information
DerYeger committed Dec 18, 2024
1 parent 8cb5dab commit 80ffa18
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 11 deletions.
8 changes: 7 additions & 1 deletion packages/docs/src/pages/en/features/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,10 @@ export const vuetify = createVuetify({

## Implementation

Vuetify generates theme styles at runtime according to the given configuration. The generated styles are injected into the `<head>` section of the DOM in a `<style>` tag with an **id** of `vuetify-theme-stylesheet`.
Vuetify generates theme styles at runtime according to the given configuration. The generated styles are injected into the `<head>` section of the DOM in a `<style>` tag with a default **id** of `vuetify-theme-stylesheet`.

### Microfrontends

An application using microfrontends with multiple instances of Vuetify may need to define unique **theme.stylesheetId** values for each microfrontend in order to prevent conflicts between their generated stylesheets.
Further, such a scenario might require styles to be scoped to a specific microfrontend, which can be achieved by setting the **theme.styleSelectorPrefix**.
For example, a microfrontend mounted in an element `#my-app` can define a **theme.styleSelectorPrefix** of `:where(#my-app)` to scope its styles to that element and its children instead of `:root` and global classes.
27 changes: 27 additions & 0 deletions packages/vuetify/src/composables/__tests__/theme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,33 @@ describe('createTheme', () => {
expect(theme.computedThemes.value.light.colors).toHaveProperty('color2-lighten-1')
})

it('should allow for customization of the stylesheet id', () => {
const customStylesheetId = 'custom-vuetify-stylesheet-id'
const theme = createTheme({
stylesheetId: customStylesheetId,
})

theme.install(app)

expect(document.getElementById(customStylesheetId)).toBeDefined()
})

it('should allow for a custom style selector prefix', () => {
const customPrefix = '#my-app'
const theme = createTheme({
styleSelectorPrefix: customPrefix,
})

theme.install(app)

const scopedStyles = document.getElementById('vuetify-theme-stylesheet')!.innerHTML
const selectors = scopedStyles.split('\n').filter(line => line.includes('{')).map(line => line.trim())
selectors.forEach(selector => {
expect(selector.startsWith(customPrefix)).toBe(true)
expect(selector).not.toContain(':root')
})
})

// it('should use [email protected] functionality', () => {
// const theme = createTheme()
// const set = jest.fn()
Expand Down
35 changes: 25 additions & 10 deletions packages/vuetify/src/composables/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type ThemeOptions = false | {
defaultTheme?: string
variations?: false | VariationsOptions
themes?: Record<string, ThemeDefinition>
stylesheetId?: string
styleSelectorPrefix?: string
}
export type ThemeDefinition = DeepPartial<InternalThemeDefinition>

Expand All @@ -42,6 +44,8 @@ interface InternalThemeOptions {
defaultTheme: string
variations: false | VariationsOptions
themes: Record<string, InternalThemeDefinition>
stylesheetId: string
styleSelectorPrefix?: string
}

interface VariationsOptions {
Expand Down Expand Up @@ -185,6 +189,7 @@ function genDefaults () {
},
},
},
stylesheetId: 'vuetify-theme-stylesheet',
}
}

Expand Down Expand Up @@ -252,17 +257,27 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
})
const current = computed(() => computedThemes.value[name.value])

function getSelectorWithScope (selector: string) {
if (!parsedOptions.styleSelectorPrefix) {
return selector
}
if (selector === ':root') {
return parsedOptions.styleSelectorPrefix
}
return `${parsedOptions.styleSelectorPrefix} ${selector}`
}

const styles = computed(() => {
const lines: string[] = []

if (current.value?.dark) {
createCssClass(lines, ':root', ['color-scheme: dark'])
createCssClass(lines, getSelectorWithScope(':root'), ['color-scheme: dark'])
}

createCssClass(lines, ':root', genCssVariables(current.value))
createCssClass(lines, getSelectorWithScope(':root'), genCssVariables(current.value))

for (const [themeName, theme] of Object.entries(computedThemes.value)) {
createCssClass(lines, `.v-theme--${themeName}`, [
createCssClass(lines, getSelectorWithScope(`.v-theme--${themeName}`), [
`color-scheme: ${theme.dark ? 'dark' : 'normal'}`,
...genCssVariables(theme),
])
Expand All @@ -274,15 +289,15 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors)))
for (const key of colors) {
if (/^on-[a-z]/.test(key)) {
createCssClass(fgLines, `.${key}`, [`color: rgb(var(--v-theme-${key})) !important`])
createCssClass(fgLines, getSelectorWithScope(`.${key}`), [`color: rgb(var(--v-theme-${key})) !important`])
} else {
createCssClass(bgLines, `.bg-${key}`, [
createCssClass(bgLines, getSelectorWithScope(`.bg-${key}`), [
`--v-theme-overlay-multiplier: var(--v-theme-${key}-overlay-multiplier)`,
`background-color: rgb(var(--v-theme-${key})) !important`,
`color: rgb(var(--v-theme-on-${key})) !important`,
])
createCssClass(fgLines, `.text-${key}`, [`color: rgb(var(--v-theme-${key})) !important`])
createCssClass(fgLines, `.border-${key}`, [`--v-border-color: var(--v-theme-${key})`])
createCssClass(fgLines, getSelectorWithScope(`.text-${key}`), [`color: rgb(var(--v-theme-${key})) !important`])
createCssClass(fgLines, getSelectorWithScope(`.border-${key}`), [`--v-border-color: var(--v-theme-${key})`])
}
}

Expand All @@ -295,7 +310,7 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
return {
style: [{
children: styles.value,
id: 'vuetify-theme-stylesheet',
id: parsedOptions.stylesheetId,
nonce: parsedOptions.cspNonce || false as never,
}],
}
Expand All @@ -321,7 +336,7 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
}
} else {
let styleEl = IN_BROWSER
? document.getElementById('vuetify-theme-stylesheet')
? document.getElementById(parsedOptions.stylesheetId)
: null

if (IN_BROWSER) {
Expand All @@ -334,7 +349,7 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
if (typeof document !== 'undefined' && !styleEl) {
const el = document.createElement('style')
el.type = 'text/css'
el.id = 'vuetify-theme-stylesheet'
el.id = parsedOptions.stylesheetId
if (parsedOptions.cspNonce) el.setAttribute('nonce', parsedOptions.cspNonce)

styleEl = el
Expand Down

0 comments on commit 80ffa18

Please sign in to comment.