127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
// Copyright (c) 2024 The Brave Authors. All rights reserved.
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Scoped CSS for Front-end Components
|
|
// -----------------------------------------------------------------------------
|
|
//
|
|
// Overview:
|
|
//
|
|
// This module provides CSS scoping using the native CSS `@scope` at-rule.
|
|
// Styles defined with `scoped.css` are automatically scoped to the component
|
|
// and won't leak into child components that define their own scope.
|
|
//
|
|
// Basic usage:
|
|
//
|
|
// 1. Create a style file for your component:
|
|
//
|
|
// // my_component.style.ts
|
|
// import { scoped } from '$web-common/scoped_css'
|
|
//
|
|
// export const style = scoped.css`
|
|
// & {
|
|
// display: flex;
|
|
// padding: 16px;
|
|
// }
|
|
//
|
|
// .title {
|
|
// font-weight: bold;
|
|
// }
|
|
// `
|
|
//
|
|
// 2. Apply the scope to your front-end component:
|
|
//
|
|
// // my_component.tsx
|
|
// import { style } from './my_component.style'
|
|
//
|
|
// export function MyComponent() {
|
|
// return (
|
|
// <div data-css-scope={style.scope}>
|
|
// <h1 className="title">Hello</h1>
|
|
// </div>
|
|
// )
|
|
// }
|
|
//
|
|
// Passthrough styles:
|
|
//
|
|
// Use `style.passthrough.css` when you need styles to apply to nested scoped
|
|
// components (e.g., for flex container properties):
|
|
//
|
|
// style.passthrough.css`
|
|
// .child-container {
|
|
// flex: 1;
|
|
// }
|
|
// `
|
|
|
|
const stylesheetMap = new Map<string, CSSStyleSheet>()
|
|
|
|
// Adds CSS to the document. If a stylesheet with the specified `id` has already
|
|
// been added to the document, then it will be replaced with the provided CSS.
|
|
export async function addStyles(id: string, cssText: unknown) {
|
|
let stylesheet = stylesheetMap.get(id)
|
|
if (!stylesheet) {
|
|
stylesheet = new CSSStyleSheet()
|
|
stylesheetMap.set(id, stylesheet)
|
|
document.adoptedStyleSheets.push(stylesheet)
|
|
}
|
|
await stylesheet.replace(String(cssText))
|
|
}
|
|
|
|
const scopeAttributeName = 'data-css-scope'
|
|
|
|
class ScopedCSSAttribute {
|
|
[scopeAttributeName]: string
|
|
|
|
constructor(scopeName: string) {
|
|
this[scopeAttributeName] = scopeName
|
|
}
|
|
|
|
get scope() {
|
|
return this[scopeAttributeName]
|
|
}
|
|
|
|
get selector() {
|
|
return `[${scopeAttributeName}=${CSS.escape(this[scopeAttributeName])}]`
|
|
}
|
|
|
|
get passthrough() {
|
|
const { selector } = this
|
|
return {
|
|
// Adds styles that "pass-through" descendants with a "data-css-scope"
|
|
// attribute. Pass-through styles can be useful for containers that need
|
|
// to supply styling to children (e.g. flex properties) that might
|
|
// themselves define a style scope.
|
|
css(callsite: TemplateStringsArray, ...values: any[]) {
|
|
addStyles(
|
|
`${selector}-passthrough`,
|
|
`@scope (${selector}) { ${String.raw(callsite, ...values)} }`
|
|
)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
let nextScopeID = 0x5c09ed
|
|
|
|
// A template tag that adds scoped CSS to the document. The provided CSS text
|
|
// is wrapped with a "@scope" at-rule and only applies to elements with a
|
|
// "data-css-scope" attribute whose value matches `scopeName`. The CSS rules do
|
|
// not apply to any descendant elements that have a "data-css-scope" attribute.
|
|
// Returns an object representing the CSS scope data attribute, which can be
|
|
// object-spread into a collection of HTML attributes.
|
|
export const scoped = {
|
|
css(callsite: TemplateStringsArray, ...values: any[]) {
|
|
const id = (nextScopeID++).toString(36)
|
|
const attr = new ScopedCSSAttribute(id)
|
|
addStyles(
|
|
attr.selector,
|
|
`@scope (${attr.selector}) to ([${scopeAttributeName}]) {
|
|
${String.raw(callsite, ...values)}
|
|
}`
|
|
)
|
|
return attr
|
|
},
|
|
}
|