Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement custom stylesheet id and theme scope #20789

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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