Skip to content

Commit d3289ca

Browse files
authored
Merge branch 'material-components:main' into cg-fix-validator-custom-error
2 parents d19e809 + c9360e2 commit d3289ca

File tree

28 files changed

+797
-219
lines changed

28 files changed

+797
-219
lines changed

.github/workflows/update-size-on-main.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ jobs:
3737
committer: lit-robot <lit-robot@google.com>
3838
title: 'chore: update sizes'
3939
body: This PR was auto generated by the update-size-on-main GitHub action.
40-
reviewers: e111077,asyncliz
41-
branch: auto-update-size
42-
# Don't automatically add Ready for Google label until we're ready
40+
branch: auto-update-sizes
41+
# Don't automatically add reviewers or Ready for Google label until we're ready
4342
# since this will be noisy.

CONTRIBUTING.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Contributing
2+
3+
Thank you for your interest in contributing! The following sections describe
4+
ways to get involved.
5+
6+
## Code of conduct
7+
8+
Please review and follow our [code of conduct](CODE_OF_CONDUCT.md).
9+
10+
## Feedback
11+
12+
User feedback is the most valuable to us. It's a great way to start
13+
contributing!
14+
15+
- [File new issues](https://github.com/material-components/material-web/issues/new/choose)
16+
for bugs you run into or feature requests you have.
17+
18+
- [Create a discussion](https://github.com/material-components/material-web/discussions/new/choose)
19+
for help, feedback on changes, or feature proposals.
20+
21+
## Discord
22+
23+
Join the `#material` channel on [Lit's Discord](https://lit.dev/discord) to chat
24+
directly with the team and other users.
25+
26+
## Pull requests
27+
28+
Pull requests are welcome! Keep a few things in mind:
29+
30+
- Create an
31+
[issue](https://github.com/material-components/material-web/issues/new/choose)
32+
or
33+
[discussion](https://github.com/material-components/material-web/discussions/new/choose)
34+
before opening a pull request.
35+
- Trivial changes, such as documentation, don't need an issue.
36+
- Create draft PRs in the `@material/web/labs` folder for new features.
37+
- Please be patient! It may take a while for the team to review. Keep changes
38+
small and scoped to speed things up.
39+
40+
### New components
41+
42+
Please understand that new component implementations are difficult for us to
43+
directly accept. They need to complete several engineer, design, and
44+
accessibility reviews that are not easy with our externally available workflows.
45+
46+
If you want to help build a new component, create a
47+
[new discussion](https://github.com/material-components/material-web/discussions/new/choose).
48+
Add any design docs, code samples, reference implementations in draft PRs, and
49+
get community feedback.
50+
51+
### Contributor License Agreement
52+
53+
Code contributions must
54+
[sign Google's CLA](https://cla.developers.google.com/clas). When you open a
55+
pull request, our friendly bot will check and provide help if you haven't
56+
signed.
57+
58+
[Set your email in git](https://help.github.com/articles/setting-your-email-in-git/)
59+
to the same email used to sign the CLA.

button/internal/button.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import {property, query, queryAssignedElements} from 'lit/decorators.js';
1212

1313
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1414
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
15-
import {
16-
dispatchActivationClick,
17-
isActivationClick,
18-
} from '../../internal/controller/events.js';
1915
import {
2016
FormSubmitter,
2117
FormSubmitterType,
2218
setupFormSubmitter,
2319
} from '../../internal/controller/form-submitter.js';
20+
import {
21+
dispatchActivationClick,
22+
isActivationClick,
23+
} from '../../internal/events/form-label-activation.js';
2424
import {
2525
internals,
2626
mixinElementInternals,

checkbox/internal/checkbox.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
1616
import {
1717
dispatchActivationClick,
1818
isActivationClick,
19-
redispatchEvent,
20-
} from '../../internal/controller/events.js';
19+
} from '../../internal/events/form-label-activation.js';
20+
import {redispatchEvent} from '../../internal/events/redispatch-event.js';
2121
import {
2222
createValidator,
2323
getValidityAnchor,

chips/internal/filter-chip.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {html, nothing} from 'lit';
1010
import {property, query} from 'lit/decorators.js';
1111

1212
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
13-
import {redispatchEvent} from '../../internal/controller/events.js';
13+
import {redispatchEvent} from '../../internal/events/redispatch-event.js';
1414

1515
import {MultiActionChip} from './multi-action-chip.js';
1616
import {renderRemoveButton} from './trailing-icons.js';

dialog/internal/dialog.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {classMap} from 'lit/directives/class-map.js';
1212

1313
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1414
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
15-
import {redispatchEvent} from '../../internal/controller/events.js';
15+
import {redispatchEvent} from '../../internal/events/redispatch-event.js';
1616

1717
import {
1818
DIALOG_DEFAULT_CLOSE_ANIMATION,
@@ -114,6 +114,21 @@ export class Dialog extends LitElement {
114114
@state() private hasActions = false;
115115
@state() private hasIcon = false;
116116

117+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1512224
118+
// Chrome v120 has a bug where escape keys do not trigger cancels. If we get
119+
// a dialog "close" event that is triggered without a "cancel" after an escape
120+
// keydown, then we need to manually trigger our closing logic.
121+
//
122+
// This bug occurs when pressing escape to close a dialog without first
123+
// interacting with the dialog's content.
124+
//
125+
// Cleanup tracking:
126+
// https://github.com/material-components/material-web/issues/5330
127+
// This can be removed when full CloseWatcher support added and the above bug
128+
// in Chromium is fixed to fire 'cancel' with one escape press and close with
129+
// multiple.
130+
private escapePressedWithoutCancel = false;
131+
117132
constructor() {
118133
super();
119134
if (!isServer) {
@@ -243,6 +258,8 @@ export class Dialog extends LitElement {
243258
role=${this.type === 'alert' ? 'alertdialog' : nothing}
244259
@cancel=${this.handleCancel}
245260
@click=${this.handleDialogClick}
261+
@close=${this.handleClose}
262+
@keydown=${this.handleKeydown}
246263
.returnValue=${this.returnValue || nothing}>
247264
<div class="container" @click=${this.handleContentClick}>
248265
<div class="headline">
@@ -328,6 +345,7 @@ export class Dialog extends LitElement {
328345
return;
329346
}
330347

348+
this.escapePressedWithoutCancel = false;
331349
const preventDefault = !redispatchEvent(this, event);
332350
// We always prevent default on the original dialog event since we'll
333351
// animate closing it before it actually closes.
@@ -339,6 +357,30 @@ export class Dialog extends LitElement {
339357
this.close();
340358
}
341359

360+
private handleClose() {
361+
if (!this.escapePressedWithoutCancel) {
362+
return;
363+
}
364+
365+
this.escapePressedWithoutCancel = false;
366+
this.dialog?.dispatchEvent(new Event('cancel', {cancelable: true}));
367+
}
368+
369+
private handleKeydown(event: KeyboardEvent) {
370+
if (event.key !== 'Escape') {
371+
return;
372+
}
373+
374+
// An escape key was pressed. If a "close" event fires next without a
375+
// "cancel" event first, then we know we're in the Chrome v120 bug.
376+
this.escapePressedWithoutCancel = true;
377+
// Wait a full task for the cancel/close event listeners to fire, then
378+
// reset the flag.
379+
setTimeout(() => {
380+
this.escapePressedWithoutCancel = false;
381+
});
382+
}
383+
342384
private async animateDialog(animation: DialogAnimation) {
343385
const {dialog, scrim, container, headline, content, actions} = this;
344386
if (!dialog || !scrim || !container || !headline || !content || !actions) {

docs/components/progress.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ssrOnly: true
1313

1414
<!--*
1515
# Document freshness: For more information, see go/fresh-source.
16-
freshness: { owner: 'lizmitchell' reviewed: '2023-08-23' }
16+
freshness: { owner: 'lizmitchell' reviewed: '2023-12-18' }
1717
tag: 'docType:reference'
1818
*-->
1919

@@ -261,7 +261,7 @@ Token | Default value
261261
> Note: the active indicator width must be specified as a unit-less percentage
262262
> of the size.
263263
264-
* [All tokens](https://github.com/material-components/material-web/blob/main/tokens/_md-comp-circular-progress-indicator.scss)
264+
* [All tokens](https://github.com/material-components/material-web/blob/main/tokens/_md-comp-circular-progress.scss)
265265
<!-- {.external} -->
266266

267267
### Circular progress example
@@ -320,7 +320,7 @@ Token | Default value
320320
`--md-linear-progress-active-indicator-color` | `--md-sys-color-primary`
321321
`--md-linear-progress-active-indicator-height` | `4px`
322322

323-
* [All tokens](https://github.com/material-components/material-web/blob/main/tokens/_md-comp-linear-progress-indicator.scss)
323+
* [All tokens](https://github.com/material-components/material-web/blob/main/tokens/_md-comp-linear-progress.scss)
324324
<!-- {.external} -->
325325

326326
### Linear progress example

internal/events/dispatch-hooks.ts

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* A symbol used to access dispatch hooks on an event.
9+
*/
10+
const dispatchHooks = Symbol('dispatchHooks');
11+
12+
/**
13+
* An `Event` with additional symbols for dispatch hooks.
14+
*/
15+
interface EventWithDispatchHooks extends Event {
16+
[dispatchHooks]: EventTarget;
17+
}
18+
19+
/**
20+
* Add a hook for an event that is called after the event is dispatched and
21+
* propagates to other event listeners.
22+
*
23+
* This is useful for behaviors that need to check if an event is canceled.
24+
*
25+
* The callback is invoked synchronously, which allows for better integration
26+
* with synchronous platform APIs (like `<form>` or `<label>` clicking).
27+
*
28+
* Note: `setupDispatchHooks()` must be called on the element before adding any
29+
* other event listeners. Call it in the constructor of an element or
30+
* controller.
31+
*
32+
* @example
33+
* ```ts
34+
* class MyControl extends LitElement {
35+
* constructor() {
36+
* super();
37+
* setupDispatchHooks(this, 'click');
38+
* this.addEventListener('click', event => {
39+
* afterDispatch(event, () => {
40+
* if (event.defaultPrevented) {
41+
* return
42+
* }
43+
*
44+
* // ... perform logic
45+
* });
46+
* });
47+
* }
48+
* }
49+
* ```
50+
*
51+
* @example
52+
* ```ts
53+
* class MyController implements ReactiveController {
54+
* constructor(host: ReactiveElement) {
55+
* // setupDispatchHooks() may be called multiple times for the same
56+
* // element and events, making it safe for multiple controllers to use it.
57+
* setupDispatchHooks(host, 'click');
58+
* host.addEventListener('click', event => {
59+
* afterDispatch(event, () => {
60+
* if (event.defaultPrevented) {
61+
* return;
62+
* }
63+
*
64+
* // ... perform logic
65+
* });
66+
* });
67+
* }
68+
* }
69+
* ```
70+
*
71+
* @param event The event to add a hook to.
72+
* @param callback A hook that is called after the event finishes dispatching.
73+
*/
74+
export function afterDispatch(event: Event, callback: () => void) {
75+
const hooks = (event as EventWithDispatchHooks)[dispatchHooks];
76+
if (!hooks) {
77+
throw new Error(`'${event.type}' event needs setupDispatchHooks().`);
78+
}
79+
80+
hooks.addEventListener('after', callback);
81+
}
82+
83+
/**
84+
* A lookup map of elements and event types that have a dispatch hook listener
85+
* set up. Used to ensure we don't set up multiple hook listeners on the same
86+
* element for the same event.
87+
*/
88+
const ELEMENT_DISPATCH_HOOK_TYPES = new WeakMap<Element, Set<string>>();
89+
90+
/**
91+
* Sets up an element to add dispatch hooks to given event types. This must be
92+
* called before adding any event listeners that need to use dispatch hooks
93+
* like `afterDispatch()`.
94+
*
95+
* This function is safe to call multiple times with the same element or event
96+
* types. Call it in the constructor of elements, mixins, and controllers to
97+
* ensure it is set up before external listeners.
98+
*
99+
* @example
100+
* ```ts
101+
* class MyControl extends LitElement {
102+
* constructor() {
103+
* super();
104+
* setupDispatchHooks(this, 'click');
105+
* this.addEventListener('click', this.listenerUsingAfterDispatch);
106+
* }
107+
* }
108+
* ```
109+
*
110+
* @param element The element to set up event dispatch hooks for.
111+
* @param eventTypes The event types to add dispatch hooks to.
112+
*/
113+
export function setupDispatchHooks(
114+
element: Element,
115+
...eventTypes: [string, ...string[]]
116+
) {
117+
let typesAlreadySetUp = ELEMENT_DISPATCH_HOOK_TYPES.get(element);
118+
if (!typesAlreadySetUp) {
119+
typesAlreadySetUp = new Set();
120+
ELEMENT_DISPATCH_HOOK_TYPES.set(element, typesAlreadySetUp);
121+
}
122+
123+
for (const eventType of eventTypes) {
124+
// Don't register multiple dispatch hook listeners. A second registration
125+
// would lead to the second listener re-dispatching a re-dispatched event,
126+
// which can cause an infinite loop inside the other one.
127+
if (typesAlreadySetUp.has(eventType)) {
128+
continue;
129+
}
130+
131+
// When we re-dispatch the event, it's going to immediately trigger this
132+
// listener again. Use a flag to ignore it.
133+
let isRedispatching = false;
134+
element.addEventListener(
135+
eventType,
136+
(event: Event) => {
137+
if (isRedispatching) {
138+
return;
139+
}
140+
141+
// Do not let the event propagate to any other listener (not just
142+
// bubbling listeners with `stopPropagation()`).
143+
event.stopImmediatePropagation();
144+
// Make a copy.
145+
const eventCopy = Reflect.construct(event.constructor, [
146+
event.type,
147+
event,
148+
]);
149+
150+
// Add hooks onto the event.
151+
const hooks = new EventTarget();
152+
(eventCopy as EventWithDispatchHooks)[dispatchHooks] = hooks;
153+
154+
// Re-dispatch the event. We can't reuse `redispatchEvent()` since we
155+
// need to add the hooks to the copy before it's dispatched.
156+
isRedispatching = true;
157+
const dispatched = element.dispatchEvent(eventCopy);
158+
isRedispatching = false;
159+
if (!dispatched) {
160+
event.preventDefault();
161+
}
162+
163+
// Synchronously call afterDispatch() hooks.
164+
hooks.dispatchEvent(new Event('after'));
165+
},
166+
{
167+
// Ensure this listener runs before other listeners.
168+
// `setupDispatchHooks()` should be called in constructors to also
169+
// ensure they run before any other externally-added capture listeners.
170+
capture: true,
171+
},
172+
);
173+
174+
typesAlreadySetUp.add(eventType);
175+
}
176+
}

0 commit comments

Comments
 (0)