Skip to content
Merged
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
35 changes: 4 additions & 31 deletions src/components/button/button-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { cancelDeferredFocus, deferFocus } from '#internals/deferred-focus.js';
import { EventEmitterMixin } from '#internals/mixins/event-emitter.js';
import { HostAriaMixin } from '#internals/mixins/host-aria.js';
import { partMap } from '#internals/part-map.js';
import { getElementByIdFromRoot } from '#internals/utils/dom.js';
import { runDefaultAction } from '#internals/utils/events.js';
import { bindIf } from '#internals/utils/lit.js';

Expand Down Expand Up @@ -67,7 +66,10 @@ export abstract class IgcButtonBaseComponent extends EventEmitterMixin<IgcButton
private readonly _internals = addInternalsController(this);
private readonly _resolver = addIdRefResolver(
this,
this._resolveCommandForElement
() => this._commandfor,
(element) => {
this.commandForElement = element;
}
);

private _disabled = false;
Expand Down Expand Up @@ -171,7 +173,6 @@ export abstract class IgcButtonBaseComponent extends EventEmitterMixin<IgcButton
this._commandfor = value;
if (value) {
this._resolver.observe();
this._commandForElement = getElementByIdFromRoot(this, value);
} else {
this._commandForElement = null;
this._resolver.unobserve();
Expand Down Expand Up @@ -220,22 +221,6 @@ export abstract class IgcButtonBaseComponent extends EventEmitterMixin<IgcButton
trackLabels(this);
}

public override connectedCallback(): void {
super.connectedCallback();
// resolve `commandfor` in case it was set or changed while disconnected:
if (this._commandfor) {
this._resolveCommandFor();
}
}

protected override firstUpdated(): void {
this.updateComplete.then(() => {
if (this._commandfor) {
this._resolveCommandFor();
}
});
}

//#endregion

//#region Event handlers
Expand Down Expand Up @@ -269,18 +254,6 @@ export abstract class IgcButtonBaseComponent extends EventEmitterMixin<IgcButton

//#endregion

private _resolveCommandForElement(ids: Set<string>): void {
if (this._commandfor && ids.has(this._commandfor)) {
this._resolveCommandFor();
}
}

/** Looks up the `commandfor` target again and renders with it. */
private _resolveCommandFor(): void {
this._commandForElement = getElementByIdFromRoot(this, this._commandfor!);
this.requestUpdate();
}

//#region Public API

/* alternateName: focusComponent */
Expand Down
63 changes: 54 additions & 9 deletions src/internals/controllers/async-consumer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ const testContext = createContext<string>(Symbol('async-consumer-test'));

type Consumer = LitElement & { received: string[] };

function defineProvider(tag: string): void {
customElements.define(
tag,
class extends LitElement {
public provider = new ContextProvider(this, {
context: testContext,
initialValue: 'value',
});
}
);
}

describe('Async context consumer', () => {
const consumerTag = defineCE(
class extends LitElement {
Expand All @@ -34,15 +46,7 @@ describe('Async context consumer', () => {
const consumers = [...container.querySelectorAll<Consumer>(consumerTag)];
await Promise.all(consumers.map(elementUpdated));

customElements.define(
providerTag,
class extends LitElement {
public provider = new ContextProvider(this, {
context: testContext,
initialValue: 'value',
});
}
);
defineProvider(providerTag);

for (const each of consumers) {
expect(each.received).to.eql(['value']);
Expand All @@ -51,4 +55,45 @@ describe('Async context consumer', () => {
// An earlier spec can add the shared listener first.
expect(listen.withArgs('context-provider').callCount).to.be.at.most(1);
});

it('asks again only for the waiting consumers inside the provider', async () => {
const providerTag = 'igc-async-consumer-test-scoped-provider';
const container = await fixture<HTMLElement>(
`<div><${providerTag}><${consumerTag}></${consumerTag}></${providerTag}><${consumerTag}></${consumerTag}></div>`
);
const [inside, outside] = container.querySelectorAll<Consumer>(consumerTag);
await Promise.all([elementUpdated(inside), elementUpdated(outside)]);

const requests = sinon.spy();
outside.addEventListener('context-request', requests);

defineProvider(providerTag);

expect(inside.received).to.eql(['value']);
expect(outside.received).to.be.empty;
expect(requests.called).to.be.false;
});

it('answers a waiting consumer slotted into a provider in a closed shadow root', async () => {
const providerTag = 'igc-async-consumer-test-closed-provider';
const wrapperTag = defineCE(
class extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'closed',
}).innerHTML = `<${providerTag}><slot></slot></${providerTag}>`;
}
}
);
const wrapper = await fixture<HTMLElement>(
`<${wrapperTag}><${consumerTag}></${consumerTag}></${wrapperTag}>`
);
const consumer = wrapper.querySelector<Consumer>(consumerTag)!;
await elementUpdated(consumer);

defineProvider(providerTag);

expect(consumer.received).to.eql(['value']);
});
});
29 changes: 22 additions & 7 deletions src/internals/controllers/async-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ type AsyncContextOptions<T extends Context<unknown, unknown>> = {
callback?: (value: ContextType<T>, dispose?: () => void) => void;
};

type Retry = (context: unknown) => void;
type ProviderEvent = HTMLElementEventMap['context-provider'];
type Retry = (event: ProviderEvent) => void;

/** The unanswered consumers of each document. */
const waiting = new WeakMap<Document, Set<Retry>>();
Expand All @@ -22,13 +23,23 @@ const waiting = new WeakMap<Document, Set<Retry>>();
* remove.
*/
function retryWaiting(event: Event): void {
const { context } = event as Event & { context?: unknown };

for (const retry of waiting.get(event.currentTarget as Document) ?? []) {
retry(context);
retry(event as ProviderEvent);
}
}

/**
* Whether a request of `consumer` can reach `provider`. In one tree, it
* reaches only the ancestors of the consumer. Across trees, slots decide, and
* a closed shadow root hides them.
*/
function mayReach(consumer: Element, provider: Element): boolean {
return (
consumer.getRootNode() !== provider.getRootNode() ||
provider.contains(consumer)
);
}

function createWaiting(document: Document): Set<Retry> {
document.addEventListener('context-provider', retryWaiting);
return new Set();
Expand Down Expand Up @@ -59,9 +70,13 @@ class AsyncContextConsumer<
return this._consumer?.value;
}

private readonly _retry: Retry = (context) => {
if (context === this._options.context) {
this._consumer?.hostConnected();
private readonly _retry: Retry = ({ context, contextTarget }) => {
if (
this._consumer &&
context === this._options.context &&
mayReach(this._host, contextTarget)
) {
this._consumer.hostConnected();
}
};

Expand Down
Loading
Loading