Files
react-native-devtools-frontend/front_end/panels/elements/CSSRuleValidator.ts
T
Alex Rudenko 354fb0fd3f [CSSHints] Fix validation for the order property
Previously, the property was incorrectly validated on grid or flex containers. The correct place is to validate it on grid or flex items.

Drive-by: improved the readability of the test because expectedResult was ambigious.

Fixed: 1402019
Change-Id: I30c66f17b88b1618d9da8e66645fccf657b5992c
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4116525
Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org>
Commit-Queue: Ergün Erdoğmuş <ergunsh@chromium.org>
2022-12-22 11:00:41 +00:00

659 lines
23 KiB
TypeScript

// Copyright 2022 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 * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import {
buildPropertyDefinitionText,
buildPropertyName,
buildPropertyValue,
isFlexContainer,
isGridContainer,
isInlineElement,
isMulticolContainer,
isPossiblyReplacedElement,
} from './CSSRuleValidatorHelper.js';
const UIStrings = {
/**
*@description The message shown in the Style pane when the user hovers over a property that has no effect due to some other property.
*@example {flex-wrap: nowrap} REASON_PROPERTY_DECLARATION_CODE
*@example {align-content} AFFECTED_PROPERTY_DECLARATION_CODE
*/
ruleViolatedBySameElementRuleReason:
'The {REASON_PROPERTY_DECLARATION_CODE} property prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.',
/**
*@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to some other property.
*@example {flex-wrap} PROPERTY_NAME
@example {nowrap} PROPERTY_VALUE
*/
ruleViolatedBySameElementRuleFix: 'Try setting {PROPERTY_NAME} to something other than {PROPERTY_VALUE}.',
/**
*@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the current property value.
*@example {display: block} EXISTING_PROPERTY_DECLARATION
*@example {display: flex} TARGET_PROPERTY_DECLARATION
*/
ruleViolatedBySameElementRuleChangeSuggestion:
'Try setting the {EXISTING_PROPERTY_DECLARATION} property to {TARGET_PROPERTY_DECLARATION}.',
/**
*@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to properties of the parent element.
*@example {display: block} REASON_PROPERTY_DECLARATION_CODE
*@example {flex} AFFECTED_PROPERTY_DECLARATION_CODE
*/
ruleViolatedByParentElementRuleReason:
'The {REASON_PROPERTY_DECLARATION_CODE} property on the parent element prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.',
/**
*@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the properties of the parent element.
*@example {display: block} EXISTING_PARENT_ELEMENT_RULE
*@example {display: flex} TARGET_PARENT_ELEMENT_RULE
*/
ruleViolatedByParentElementRuleFix:
'Try setting the {EXISTING_PARENT_ELEMENT_RULE} property on the parent to {TARGET_PARENT_ELEMENT_RULE}.',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/CSSRuleValidator.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const enum HintType {
INACTIVE_PROPERTY = 'ruleValidation',
DEPRECATED_PROPERTY = 'deprecatedProperty',
}
export class Hint {
readonly #hintMessage: string;
readonly #possibleFixMessage: string|null;
readonly #learnMoreLink: string|undefined;
constructor(hintMessage: string, possibleFixMessage: string|null, learnMoreLink?: string) {
this.#hintMessage = hintMessage;
this.#possibleFixMessage = possibleFixMessage;
this.#learnMoreLink = learnMoreLink;
}
getMessage(): string {
return this.#hintMessage;
}
getPossibleFixMessage(): string|null {
return this.#possibleFixMessage;
}
getLearnMoreLink(): string|undefined {
return this.#learnMoreLink;
}
}
export abstract class CSSRuleValidator {
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.Other;
}
readonly #affectedProperties: string[];
constructor(affectedProperties: string[]) {
this.#affectedProperties = affectedProperties;
}
getApplicableProperties(): string[] {
return this.#affectedProperties;
}
abstract getHint(
propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>,
nodeName?: string): Hint|undefined;
}
export class AlignContentValidator extends CSSRuleValidator {
constructor() {
super(['align-content']);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.AlignContent;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
if (computedStyles === null || computedStyles === undefined) {
return true;
}
if (!isFlexContainer(computedStyles)) {
return true;
}
return computedStyles.get('flex-wrap') !== 'nowrap';
}
getHint(_propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('flex-wrap', 'nowrap');
const affectedPropertyDeclarationCode = buildPropertyName('align-content');
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('flex-wrap'),
PROPERTY_VALUE: buildPropertyValue('nowrap'),
}),
);
}
}
export class FlexItemValidator extends CSSRuleValidator {
constructor() {
super(['flex', 'flex-basis', 'flex-grow', 'flex-shrink']);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.FlexItem;
}
#isRuleValid(computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): boolean {
if (parentComputedStyles === null) {
return true;
}
return isFlexContainer(parentComputedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
|undefined {
if (this.#isRuleValid(computedStyles, parentComputedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'flex');
return new Hint(
i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration,
'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration,
}),
);
}
}
export class FlexContainerValidator extends CSSRuleValidator {
constructor() {
super(['flex-direction', 'flex-flow', 'flex-wrap']);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.FlexContainer;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
if (computedStyles === null) {
return true;
}
return isFlexContainer(computedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const targetRuleCode = buildPropertyDefinitionText('display', 'flex');
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, {
'EXISTING_PROPERTY_DECLARATION': reasonPropertyDeclaration,
'TARGET_PROPERTY_DECLARATION': targetRuleCode,
}),
);
}
}
export class GridContainerValidator extends CSSRuleValidator {
constructor() {
super([
'grid',
'grid-auto-columns',
'grid-auto-flow',
'grid-auto-rows',
'grid-template',
'grid-template-areas',
'grid-template-columns',
'grid-template-rows',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.GridContainer;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
return isGridContainer(computedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const targetRuleCode = buildPropertyDefinitionText('display', 'grid');
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, {
'EXISTING_PROPERTY_DECLARATION': reasonPropertyDeclaration,
'TARGET_PROPERTY_DECLARATION': targetRuleCode,
}),
);
}
}
export class GridItemValidator extends CSSRuleValidator {
constructor() {
super([
'grid-area',
'grid-column',
'grid-row',
'grid-row-end',
'grid-row-start',
// At the time of writing (November 2022), `justify-self` is only in effect in grid layout.
// There are no other browsers that support `justify-self` in other layouts.
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Alignment/Box_Alignment_In_Block_Abspos_Tables
// TODO: move `justify-self` to other validator or change pop-over text if Chrome supports CSS Align in other layouts.
'justify-self',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.GridItem;
}
#isRuleValid(computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): boolean {
if (!parentComputedStyles) {
return true;
}
return isGridContainer(parentComputedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
|undefined {
if (this.#isRuleValid(computedStyles, parentComputedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'grid');
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration,
'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration,
}),
);
}
}
export class FlexOrGridItemValidator extends CSSRuleValidator {
constructor() {
super([
'place-self',
'align-self',
'order',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.FlexOrGridItem;
}
#isRuleValid(computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): boolean {
if (!parentComputedStyles) {
return true;
}
return isFlexContainer(parentComputedStyles) || isGridContainer(parentComputedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
|undefined {
if (this.#isRuleValid(computedStyles, parentComputedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
const targetParentPropertyDeclaration =
`${buildPropertyDefinitionText('display', 'flex')} or ${buildPropertyDefinitionText('display', 'grid')}`;
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration,
'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration,
}),
);
}
}
export class FlexGridValidator extends CSSRuleValidator {
constructor() {
super([
'justify-content',
'align-content',
'place-content', // Shorthand <'align-content'> <'justify-content'>?
'align-items',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.FlexGrid;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
if (computedStyles === null) {
return true;
}
return isFlexContainer(computedStyles) || isGridContainer(computedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('display'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
}),
);
}
}
export class MulticolFlexGridValidator extends CSSRuleValidator {
constructor() {
super([
'gap',
'column-gap',
'row-gap',
'grid-gap',
'grid-column-gap',
'grid-column-end',
'grid-row-gap',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.MulticolFlexGrid;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
if (computedStyles === null) {
return true;
}
return isMulticolContainer(computedStyles) || isFlexContainer(computedStyles) || isGridContainer(computedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('display'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
}),
);
}
}
export class PaddingValidator extends CSSRuleValidator {
constructor() {
super([
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.Padding;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
const display = computedStyles?.get('display');
if (!display) {
return true;
}
return !['table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-column-group',
'table-column']
.includes(display);
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('display'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
}),
);
}
}
export class PositionValidator extends CSSRuleValidator {
constructor() {
super([
'top',
'right',
'bottom',
'left',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.Position;
}
#isRuleValid(computedStyles?: Map<string, string>): boolean {
const position = computedStyles?.get('position');
if (position === null || position === undefined) {
return true;
}
return position !== 'static';
}
getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
if (this.#isRuleValid(computedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('position'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string),
}),
);
}
}
export class ZIndexValidator extends CSSRuleValidator {
constructor() {
super([
'z-index',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.ZIndex;
}
#isRuleValid(computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): boolean {
const position = computedStyles?.get('position');
if (!position) {
return true;
}
return ['absolute', 'relative', 'fixed', 'sticky'].includes(position) || isFlexContainer(parentComputedStyles);
}
getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
|undefined {
if (this.#isRuleValid(computedStyles, parentComputedStyles)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('position'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string),
}),
);
}
}
/**
* Validates if CSS width/height are having an effect on an element.
* See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-width.
* See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-height.
*/
export class SizingValidator extends CSSRuleValidator {
constructor() {
super([
'width',
'height',
]);
}
getMetricType(): Host.UserMetrics.CSSHintType {
return Host.UserMetrics.CSSHintType.Sizing;
}
#isRuleValid(computedStyles?: Map<string, string>, nodeName?: string): boolean {
if (!computedStyles || !nodeName) {
return true;
}
if (!isInlineElement(computedStyles)) {
return true;
}
// See https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements.
return isPossiblyReplacedElement(nodeName);
}
getHint(
propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>,
nodeName?: string): Hint|undefined {
if (this.#isRuleValid(computedStyles, nodeName)) {
return;
}
const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
return new Hint(
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
PROPERTY_NAME: buildPropertyName('display'),
PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
}),
);
}
}
const CSS_RULE_VALIDATORS = [
AlignContentValidator,
FlexContainerValidator,
FlexGridValidator,
FlexItemValidator,
GridContainerValidator,
GridItemValidator,
FlexOrGridItemValidator,
SizingValidator,
MulticolFlexGridValidator,
PaddingValidator,
PositionValidator,
ZIndexValidator,
];
const setupCSSRulesValidators = (): Map<string, CSSRuleValidator[]> => {
const validatorsMap = new Map<string, CSSRuleValidator[]>();
for (const validatorClass of CSS_RULE_VALIDATORS) {
const validator = new validatorClass();
const affectedProperties = validator.getApplicableProperties();
for (const affectedProperty of affectedProperties) {
let propertyValidators = validatorsMap.get(affectedProperty);
if (propertyValidators === undefined) {
propertyValidators = [];
}
propertyValidators.push(validator);
validatorsMap.set(affectedProperty, propertyValidators);
}
}
return validatorsMap;
};
export const cssRuleValidatorsMap: Map<string, CSSRuleValidator[]> = setupCSSRulesValidators();