mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-12 17:45:49 +08:00
This CL fixes two issues. 1. The angle popover would hide when: (1). a new CSS property line is created by either pressing Tab in the Styles section or clicking at an empty space; (2). angle popover is opened when angle swatch is clicked; (3). click anywhere on the popover would hide the popover, which is not expected because clicking on the popover should only update the angle but not closing it. Upon investigation, the culprit is when clicking on the angle swatch, the mousedown event is consumed with `preventDefault`, which also delays the blur event of the newly created CSS property line. There is an internal `_editingStyle` property that determines if the angle component will be re-created and thus lose previous popover state. This property is set to false, which recreates <angle>, by the blur listener, and then set to true, which prevents the re-creation (thus preserving the popover). Since the blur event is delayed, the sequence of events is off and causes this early dismissal of angle popover. The fix is to not `preventDefault()` in angle swatch's mousedown event, but this creates another problem where holding shift will select text around <angle>. This is then fixed with setting `user-select: none` on it. 2. This CL also fixes a previous TODO: when angle popover is open, shift-clicking it should close the popover and not toggle the unit, so that the behavior is consistent with color swatch for now. Fixed: chromium:1142523 Change-Id: I1e1f4d8500d5dd9308bf5d225be3aff958fea971 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2611131 Commit-Queue: Changhao Han <changhaohan@chromium.org> Commit-Queue: Mathias Bynens <mathias@chromium.org> Auto-Submit: Changhao Han <changhaohan@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org>
285 lines
8.2 KiB
TypeScript
285 lines
8.2 KiB
TypeScript
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import './CSSAngleEditor.js';
|
|
import './CSSAngleSwatch.js';
|
|
|
|
import * as LitHtml from '../third_party/lit-html/lit-html.js';
|
|
|
|
import {Angle, AngleUnit, convertAngleUnit, getNewAngleFromEvent, getNextUnit, parseText, roundAngleByUnit} from './CSSAngleUtils.js';
|
|
|
|
import type {CSSAngleEditorData} from './CSSAngleEditor.js';
|
|
import type {CSSAngleSwatchData} from './CSSAngleSwatch.js';
|
|
|
|
const {render, html} = LitHtml;
|
|
const styleMap = LitHtml.Directives.styleMap;
|
|
|
|
const ContextAwareProperties = new Set(['color', 'background', 'background-color']);
|
|
|
|
export class PopoverToggledEvent extends Event {
|
|
data: {open: boolean};
|
|
|
|
constructor(open: boolean) {
|
|
super('popover-toggled', {});
|
|
this.data = {open};
|
|
}
|
|
}
|
|
|
|
export class ValueChangedEvent extends Event {
|
|
data: {value: string};
|
|
|
|
constructor(value: string) {
|
|
super('value-changed', {});
|
|
this.data = {value};
|
|
}
|
|
}
|
|
|
|
export class UnitChangedEvent extends Event {
|
|
data: {value: string};
|
|
|
|
constructor(value: string) {
|
|
super('unit-changed', {});
|
|
this.data = {value};
|
|
}
|
|
}
|
|
|
|
export interface CSSAngleData {
|
|
propertyName: string;
|
|
propertyValue: string;
|
|
angleText: string;
|
|
containingPane: HTMLElement;
|
|
}
|
|
|
|
const DefaultAngle = {
|
|
value: 0,
|
|
unit: AngleUnit.Rad,
|
|
};
|
|
|
|
export class CSSAngle extends HTMLElement {
|
|
private readonly shadow = this.attachShadow({mode: 'open'});
|
|
private angle: Angle = DefaultAngle;
|
|
private displayedAngle: Angle = DefaultAngle;
|
|
private propertyName = '';
|
|
private propertyValue = '';
|
|
private containingPane?: HTMLElement;
|
|
private angleElement: HTMLElement|null = null;
|
|
private swatchElement: HTMLElement|null = null;
|
|
private popoverOpen = false;
|
|
private popoverStyleTop = '';
|
|
private onMinifyingAction = this.minify.bind(this);
|
|
private onAngleUpdate = this.updateAngle.bind(this);
|
|
|
|
set data(data: CSSAngleData) {
|
|
const parsedResult = parseText(data.angleText);
|
|
if (!parsedResult) {
|
|
return;
|
|
}
|
|
this.angle = parsedResult;
|
|
this.displayedAngle = {...parsedResult};
|
|
this.propertyName = data.propertyName;
|
|
this.propertyValue = data.propertyValue;
|
|
this.containingPane = data.containingPane;
|
|
this.render();
|
|
}
|
|
|
|
disconnectedCallback(): void {
|
|
this.unbindMinifyingAction();
|
|
}
|
|
|
|
// We bind and unbind mouse event listeners upon popping over and minifying,
|
|
// because we anticipate most of the time this widget is minified even when
|
|
// it's attached to the DOM tree.
|
|
popover(): void {
|
|
if (!this.containingPane) {
|
|
return;
|
|
}
|
|
|
|
if (!this.angleElement) {
|
|
this.angleElement = this.shadow.querySelector<HTMLElement>('.css-angle');
|
|
}
|
|
if (!this.swatchElement) {
|
|
this.swatchElement = this.shadow.querySelector<HTMLElement>('devtools-css-angle-swatch');
|
|
}
|
|
if (!this.angleElement || !this.swatchElement) {
|
|
return;
|
|
}
|
|
|
|
this.dispatchEvent(new PopoverToggledEvent(true));
|
|
this.bindMinifyingAction();
|
|
|
|
const miniIconBottom = this.swatchElement.getBoundingClientRect().bottom;
|
|
if (miniIconBottom) {
|
|
// We offset mini icon's Y position with the containing styles pane's Y position
|
|
// because DevTools' root SplitWidget's insertion-point-sidebar slot,
|
|
// where most of the DevTools content lives, has an offset of Y position,
|
|
// which makes all of its children's DOMRect Y positions to have this offset.
|
|
const topElementOffset = this.containingPane.getBoundingClientRect().top;
|
|
this.popoverStyleTop = `${miniIconBottom - topElementOffset}px`;
|
|
}
|
|
|
|
this.popoverOpen = true;
|
|
this.render();
|
|
this.angleElement.focus();
|
|
}
|
|
|
|
minify(): void {
|
|
if (this.popoverOpen === false) {
|
|
return;
|
|
}
|
|
|
|
this.popoverOpen = false;
|
|
this.dispatchEvent(new PopoverToggledEvent(false));
|
|
this.unbindMinifyingAction();
|
|
this.render();
|
|
}
|
|
|
|
updateProperty(name: string, value: string): void {
|
|
this.propertyName = name;
|
|
this.propertyValue = value;
|
|
this.render();
|
|
}
|
|
|
|
private updateAngle(angle: Angle): void {
|
|
this.displayedAngle = roundAngleByUnit(convertAngleUnit(angle, this.displayedAngle.unit));
|
|
this.angle = this.displayedAngle;
|
|
this.dispatchEvent(new ValueChangedEvent(`${this.angle.value}${this.angle.unit}`));
|
|
}
|
|
|
|
private displayNextUnit(): void {
|
|
const nextUnit = getNextUnit(this.displayedAngle.unit);
|
|
this.displayedAngle = roundAngleByUnit(convertAngleUnit(this.angle, nextUnit));
|
|
this.dispatchEvent(new UnitChangedEvent(`${this.displayedAngle.value}${this.displayedAngle.unit}`));
|
|
}
|
|
|
|
private bindMinifyingAction(): void {
|
|
document.addEventListener('mousedown', this.onMinifyingAction);
|
|
if (this.containingPane) {
|
|
this.containingPane.addEventListener('scroll', this.onMinifyingAction);
|
|
}
|
|
}
|
|
|
|
private unbindMinifyingAction(): void {
|
|
document.removeEventListener('mousedown', this.onMinifyingAction);
|
|
if (this.containingPane) {
|
|
this.containingPane.removeEventListener('scroll', this.onMinifyingAction);
|
|
}
|
|
}
|
|
|
|
private onMiniIconClick(event: MouseEvent): void {
|
|
event.stopPropagation();
|
|
if (event.shiftKey && !this.popoverOpen) {
|
|
this.displayNextUnit();
|
|
return;
|
|
}
|
|
this.popoverOpen ? this.minify() : this.popover();
|
|
}
|
|
|
|
// Fix that the previous text will be selected when double-clicking the angle icon
|
|
private consume(event: MouseEvent): void {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
private onKeydown(event: KeyboardEvent): void {
|
|
if (!this.popoverOpen) {
|
|
return;
|
|
}
|
|
|
|
switch (event.key) {
|
|
case 'Escape':
|
|
event.stopPropagation();
|
|
this.minify();
|
|
this.blur();
|
|
break;
|
|
case 'ArrowUp':
|
|
case 'ArrowDown': {
|
|
const newAngle = getNewAngleFromEvent(this.angle, event);
|
|
if (newAngle) {
|
|
this.updateAngle(newAngle);
|
|
}
|
|
event.preventDefault();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private render(): void {
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
render(html`
|
|
<style>
|
|
.css-angle {
|
|
display: inline-block;
|
|
position: relative;
|
|
outline: none;
|
|
}
|
|
|
|
devtools-css-angle-swatch {
|
|
display: inline-block;
|
|
margin-right: 2px;
|
|
user-select: none;
|
|
}
|
|
|
|
devtools-css-angle-editor {
|
|
--dial-color: #a3a3a3;
|
|
--border-color: var(--toolbar-bg-color);
|
|
position: fixed;
|
|
z-index: 2;
|
|
}
|
|
</style>
|
|
|
|
<div class="css-angle" @keydown=${this.onKeydown} tabindex="-1">
|
|
<div class="preview">
|
|
<devtools-css-angle-swatch
|
|
@click=${this.onMiniIconClick}
|
|
@mousedown=${this.consume}
|
|
@dblclick=${this.consume}
|
|
.data=${{
|
|
angle: this.angle,
|
|
} as CSSAngleSwatchData}>
|
|
</devtools-css-angle-swatch><slot></slot>
|
|
</div>
|
|
${this.popoverOpen ? this.renderPopover() : null}
|
|
</div>
|
|
`, this.shadow, {
|
|
eventContext: this,
|
|
});
|
|
// clang-format on
|
|
}
|
|
|
|
private renderPopover(): LitHtml.TemplateResult {
|
|
let contextualBackground = '';
|
|
// TODO(crbug.com/1143010): for now we ignore values with "url"; when we refactor
|
|
// CSS value parsing we should properly apply atomic contextual background.
|
|
if (ContextAwareProperties.has(this.propertyName) && !this.propertyValue.match(/url\(.*\)/i)) {
|
|
contextualBackground = this.propertyValue;
|
|
}
|
|
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
return html`
|
|
<devtools-css-angle-editor
|
|
class="popover popover-css-angle"
|
|
style=${styleMap({top: this.popoverStyleTop})}
|
|
.data=${{
|
|
angle: this.angle,
|
|
onAngleUpdate: this.onAngleUpdate,
|
|
background: contextualBackground,
|
|
} as CSSAngleEditorData}
|
|
></devtools-css-angle-editor>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
}
|
|
|
|
if (!customElements.get('devtools-css-angle')) {
|
|
customElements.define('devtools-css-angle', CSSAngle);
|
|
}
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
interface HTMLElementTagNameMap {
|
|
'devtools-css-angle': CSSAngle;
|
|
}
|
|
}
|