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

chore: add 'system' context to sp-theme #4755

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
21 changes: 20 additions & 1 deletion packages/icon/src/IconBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ import {
SpectrumElement,
TemplateResult,
} from '@spectrum-web-components/base';
import { property } from '@spectrum-web-components/base/src/decorators.js';
import {
SystemResolutionController,
systemResolverUpdatedSymbol,
} from '@spectrum-web-components/reactive-controllers/src/SystemContextResolution.js';

import {
property,
state,
} from '@spectrum-web-components/base/src/decorators.js';

import iconStyles from './icon.css.js';

Expand All @@ -32,6 +40,11 @@ export class IconBase extends SpectrumElement {
@property({ reflect: true })
public size?: 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl';

private systemResolver = new SystemResolutionController(this);

@state()
public spectrumVersion = 1;

protected override update(changes: PropertyValues): void {
if (changes.has('label')) {
if (this.label) {
Expand All @@ -40,6 +53,12 @@ export class IconBase extends SpectrumElement {
this.setAttribute('aria-hidden', 'true');
}
}

if (changes.has(systemResolverUpdatedSymbol)) {
this.spectrumVersion =
this.systemResolver.system === 'spectrum-two' ? 2 : 1;
}

super.update(changes);
}

Expand Down
4 changes: 4 additions & 0 deletions tools/reactive-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
"development": "./src/RovingTabindex.dev.js",
"default": "./src/RovingTabindex.js"
},
"./src/SystemContextResolution.js": {
"development": "./src/SystemContextResolution.dev.js",
"default": "./src/SystemContextResolution.js"
},
"./src/index.js": {
"development": "./src/index.dev.js",
"default": "./src/index.js"
Expand Down
64 changes: 64 additions & 0 deletions tools/reactive-controllers/src/SystemContextResolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import type { ReactiveController, ReactiveElement } from 'lit';
import type { SystemVariant } from '@spectrum-web-components/theme';

export const systemResolverUpdatedSymbol = Symbol('system resolver updated');

export type ProvideSystem = {
callback: (system: SystemVariant, unsubscribe: () => void) => void;
};

export class SystemResolutionController implements ReactiveController {
private host: ReactiveElement;
public system: SystemVariant = 'spectrum';
private unsubscribe?: () => void;

constructor(host: ReactiveElement) {
this.host = host;
this.host.addController(this);
}

public hostConnected(): void {
this.resolveSystem();
}

public hostDisconnected(): void {
this.unsubscribe?.();
}

private resolveSystem(): void {
const querySystemEvent = new CustomEvent<ProvideSystem>(
'sp-system-context',
{
bubbles: true,
composed: true,
detail: {
callback: (
system: SystemVariant,
unsubscribe: () => void
) => {
const previous = this.system;
this.system = system;
this.unsubscribe = unsubscribe;
this.host.requestUpdate(
systemResolverUpdatedSymbol,
previous
);
},
},
cancelable: true,
}
);
this.host.dispatchEvent(querySystemEvent);
}
}
72 changes: 72 additions & 0 deletions tools/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,75 @@ previewing or editing content that will be displayed in a light theme with a rig
## Language Context

The `<sp-theme>` element provides a language context for its descendents in the DOM. Descendents can resolve this context by dispatching an `sp-language-context` DOM event and supplying a `callback(lang: string) => void` method in the `detail` entry of the Custom Event. These callbacks will be reactively envoked when the `lang` attribute on the `<sp-theme>` element is updated. This way, you can control the resolved language in [`<sp-number-field>`](../components/number-field), [`<sp-slider>`](./components/slider), and other elements in one centralized place.

## System Context (private Beta API - subject to changes)

The <sp-theme> element provides a "system" context to its descendants in the DOM. This context indicates the Spectrum design system variant currently in use (e.g., 'spectrum', 'express', or 'spectrum-two').

#### Consuming the System Context in Components

Components can consume the system context by using the `SystemResolutionController`. This controller encapsulates the logic for resolving the system context, allowing it to be integrated into any component in few steps.

#### Steps to Consume the System Context:

1. Import the `SystemResolutionController` and the necessary types:

```ts
import {
SystemResolutionController,
systemResolverUpdatedSymbol,
} from './SystemResolutionController.js';
import type { SystemVariant } from '@spectrum-web-components/theme';
```

2. Instantiate the `SystemResolutionController`:

In your component class, create an instance of SystemResolutionController, passing `this` as the host element.

```ts
export class MyComponent extends LitElement {
private systemResolver = new SystemResolutionController(this);

// Rest of your component code...
}
```

3. Respond to system context changes:

Override the `update` lifecycle method to detect changes in the system context using the `systemResolverUpdatedSymbol`.

```ts
protected update(changes: Map<PropertyKey, unknown>): void {
super.update(changes);
if (changes.has(systemResolverUpdatedSymbol)) {
this.handleSystemChange();
}
}
```

4. Implement the handler for system changes:

Create a method that will be called whenever the system context changes. Use `this.systemResolver.system` to access the current system variant.

```ts
private handleSystemChange(): void {
const currentSystem: SystemVariant = this.systemResolver.system;
// Implement logic based on the current system variant.
// For example, update styles, states or re-render parts of the component.
}
```

5. Use the system context in other parts of your component logic and/or template:

You can now use `this.systemResolver.system` anywhere in your component to adjust behavior or rendering based on the system variant.

```ts
render() {
return html`
<div>
<!-- Use the system context in your rendering logic -->
Current system variant: ${this.systemResolver.system}
</div>
`;
}
```
70 changes: 70 additions & 0 deletions tools/theme/src/Theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
SettableFragmentTypes,
ShadowRootWithAdoptedStyleSheets,
SYSTEM_VARIANT_VALUES,
SystemContextCallback,
SystemVariant,
ThemeFragmentMap,
ThemeKindProvider,
Expand Down Expand Up @@ -94,9 +95,11 @@ export class Theme extends HTMLElement implements ThemeKindProvider {
this._provideContext();
} else if (attrName === 'theme') {
this.theme = value as SystemVariant;
this._provideSystemContext();
warnBetaSystem(this, value as SystemVariant);
} else if (attrName === 'system') {
this.system = value as SystemVariant;
this._provideSystemContext();
warnBetaSystem(this, value as SystemVariant);
} else if (attrName === 'dir') {
this.dir = value as 'ltr' | 'rtl' | '';
Expand Down Expand Up @@ -301,9 +304,63 @@ export class Theme extends HTMLElement implements ThemeKindProvider {
'sp-language-context',
this._handleContextPresence as EventListener
);
this.addEventListener(
'sp-system-context',
this._handleSystemContext as EventListener
);

this.updateComplete = this.__createDeferredPromise();
}

/**
* Stores system context consumers and their associated callbacks.
*
* This Map associates each consumer component (HTMLElement) with a tuple containing:
* - The `SystemContextCallback` function to be invoked with the system context.
* - An `unsubscribe` function to remove the consumer from the Map when it's no longer needed.
*/
private _systemContextConsumers = new Map<
HTMLElement,
[SystemContextCallback, () => void]
>();

/**
* Handles the 'sp-system-context' event dispatched by descendant components requesting the system context.
*
* This method registers the requesting component's callback and provides the current system context to it.
* It also manages the unsubscribe mechanism to clean up when the component is disconnected.
*
* @param event - The custom event containing the callback function to provide the system context.
*/
private _handleSystemContext(
rubencarvalho marked this conversation as resolved.
Show resolved Hide resolved
event: CustomEvent<{ callback: SystemContextCallback }>
): void {
event.stopPropagation();

const target = event.composedPath()[0] as HTMLElement;

// Avoid duplicate registrations
if (this._systemContextConsumers.has(target)) {
return;
}

// Create an unsubscribe function
const unsubscribe: () => void = () =>
this._systemContextConsumers.delete(target);

// Store the callback and unsubscribe function
this._systemContextConsumers.set(target, [
event.detail.callback,
unsubscribe,
]);

// Provide the context data
const [callback] = this._systemContextConsumers.get(target) || [];
if (callback) {
callback(this.system, unsubscribe);
}
}

public updateComplete!: Promise<boolean>;
private __resolve!: (compelted: boolean) => void;

Expand Down Expand Up @@ -402,6 +459,19 @@ export class Theme extends HTMLElement implements ThemeKindProvider {
);
}

/**
* Provides the current system context to all registered consumers.
*
* This method iterates over all registered system context consumers and invokes their callbacks,
* passing the current system variant and the unsubscribe function. This ensures that any component
* consuming the system context receives the updated system variant when the `system` (or `theme`) attribute changes.
*/
private _provideSystemContext(): void {
this._systemContextConsumers.forEach(([callback, unsubscribe]) =>
callback(this.system, unsubscribe)
);
}

private _handleContextPresence(event: CustomEvent<ProvideLang>): void {
event.stopPropagation();
const target = event.composedPath()[0] as HTMLElement;
Expand Down
5 changes: 5 additions & 0 deletions tools/theme/src/theme-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export type SystemVariant = (typeof SYSTEM_VARIANT_VALUES)[number];
export type Scale = (typeof SCALE_VALUES)[number];
export type Color = (typeof COLOR_VALUES)[number];

export type SystemContextCallback = (
system: SystemVariant | '',
unsubscribe: () => void
) => void;

export type FragmentName = Color | Scale | SystemVariant | 'core' | 'app';

export type ThemeKindProvider = {
Expand Down
Loading