Skip to content

Commit

Permalink
feat: implement custom stylesheet id and theme scope
Browse files Browse the repository at this point in the history
Closes #4065
  • Loading branch information
DerYeger committed Dec 19, 2024
1 parent 8cb5dab commit d2fb876
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 12 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.scope** property.
For example, a microfrontend mounted in an element `#my-app` can define a **theme.scope** of `#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 themes to be scoped', () => {
const scope = '#my-app'
const theme = createTheme({
scope,
})

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(`:where(${scope})`)).toBe(true)
expect(selector).not.toContain(':root')
})
})

// it('should use [email protected] functionality', () => {
// const theme = createTheme()
// const set = jest.fn()
Expand Down
38 changes: 27 additions & 11 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
scope?: 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
scope?: string
}

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

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

function createCssClass (lines: string[], selector: string, content: string[]) {
lines.push(
`${getScopedSelector(selector)} {\n`,
...content.map(line => ` ${line};\n`),
'}\n',
)
}

function getScopedSelector (selector: string) {
if (!parsedOptions.scope) {
return selector
}
const scopeSelector = `:where(${parsedOptions.scope})`
if (selector === ':root') {
return scopeSelector
}
return `${scopeSelector} ${selector}`
}

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

Expand Down Expand Up @@ -295,7 +319,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 +345,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 +358,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 Expand Up @@ -400,14 +424,6 @@ export function useTheme () {
return theme
}

function createCssClass (lines: string[], selector: string, content: string[]) {
lines.push(
`${selector} {\n`,
...content.map(line => ` ${line};\n`),
'}\n',
)
}

function genCssVariables (theme: InternalThemeDefinition) {
const lightOverlay = theme.dark ? 2 : 1
const darkOverlay = theme.dark ? 1 : 2
Expand Down

0 comments on commit d2fb876

Please sign in to comment.