Skip to content

Commit

Permalink
feat(ix): mark unsupported CSS choice
Browse files Browse the repository at this point in the history
  • Loading branch information
caugner committed Feb 21, 2025
1 parent 6ce7382 commit 485faa6
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 10 deletions.
28 changes: 20 additions & 8 deletions client/src/lit/interactive-example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import exampleStyle from "./example.css?raw";
import choiceJs from "./choice.js?raw";
import choiceStyle from "./choice.css?raw";
import { PlayEditor } from "../play/editor.js";
import { isCSSSupported } from "./utils.js";

/**
* @import { Ref } from 'lit/directives/ref.js';
Expand All @@ -30,6 +31,7 @@ export class InteractiveExample extends GleanMixin(LitElement) {
static properties = {
name: { type: String },
choiceSelected: { type: Number, state: true },
choiceUnsupportedMask: { type: Number, state: true },
};

static styles = styles;
Expand All @@ -43,6 +45,8 @@ export class InteractiveExample extends GleanMixin(LitElement) {
this._code = {};
/** @type {number} */
this.choiceSelected = 0;
/** @type {number} */
this.choiceUnsupportedMask = 0;
}

/** @type {Ref<PlayController>} */
Expand All @@ -56,6 +60,7 @@ export class InteractiveExample extends GleanMixin(LitElement) {

_reset() {
this.choiceSelected = 0;
this.choiceUnsupportedMask = 0;
this._controller.value?.reset();
}

Expand Down Expand Up @@ -119,18 +124,21 @@ export class InteractiveExample extends GleanMixin(LitElement) {
// TODO: deal with update race conditions (editor updates after user clicks on different editor)
if (target instanceof PlayEditor) {
const choice = target.closest(".choice");
this.choiceSelected = Array.prototype.indexOf.call(
const choiceIndex = Array.prototype.indexOf.call(
choice?.parentNode?.children,
choice
);
this.choiceSelected = choiceIndex;

const code = target.value;
const unsupported = !isCSSSupported(code);
this.choiceUnsupportedMask &= ~(1 << choiceIndex);
this.choiceUnsupportedMask |= Number(unsupported) << choiceIndex;

// TODO: nicer interface for posting messages than this:
const iframe = this._runner.value?.shadowRoot?.querySelector("iframe");

iframe?.contentWindow?.postMessage(
{ typ: "choice", code: target.value },
"*"
);
iframe?.contentWindow?.postMessage({ typ: "choice", code }, "*");
}
}

Expand Down Expand Up @@ -220,9 +228,13 @@ export class InteractiveExample extends GleanMixin(LitElement) {
${this._choices?.map(
(code, index) => html`
<div
class=${index === this.choiceSelected
? "choice selected"
: "choice"}
class=${[
"choice",
...(index === this.choiceSelected ? ["selected"] : []),
...((1 << index) & this.choiceUnsupportedMask
? ["unsupported"]
: []),
].join(" ")}
>
<play-editor
language="css"
Expand Down
19 changes: 17 additions & 2 deletions client/src/lit/interactive-example/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ tab-wrapper {
gap: 0.4rem;
grid-area: choice;
padding: 1rem;
padding-right: 0rem;
padding-right: 0;

.choice {
--field-focus-border: #0085f2;
Expand All @@ -224,10 +224,11 @@ tab-wrapper {
color: var(--field-focus-border);
content: "";
font-size: 0.6875rem;
opacity: 0;
padding: 0;
padding-left: 0.25rem;
padding-right: 1.25rem;
opacity: 0;
width: 1rem;
}

&.selected {
Expand All @@ -245,6 +246,20 @@ tab-wrapper {
}
}

&.unsupported {
&::after {
background-image: url("../../assets/icons/warning.svg");
background-position: center center;
background-repeat: no-repeat;
background-size: 1rem;
content: "";
height: 1rem;
opacity: 1;
transition: none;
width: 1rem;
}
}

play-editor {
background: var(--background-secondary);
border: 1px solid var(--border-secondary);
Expand Down
83 changes: 83 additions & 0 deletions client/src/lit/interactive-example/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Checks if the CSS code is supported by the current browser.
*
* @param {string} code
*/
export function isCSSSupported(code) {
// http://regexr.com/3fvik
const cssCommentsMatch = /(\/\*)[\s\S]+(\*\/)/g;
const element = document.createElement("div");

// strip out any CSS comments before applying the code
code = code.replace(cssCommentsMatch, "");

const vendorPrefixMatch = /^-(?:webkit|moz|ms|o)-/;
const style = element.style;
// Expecting declarations to be separated by ";"
// Declarations with just white space are ignored
const declarationsArray = code
.split(";")
.map((d) => d.trim())
.filter((d) => d.length > 0);

/**
* @param {string} declaration
* @returns {boolean} - true if declaration starts with -webkit-, -moz-, -ms- or -o-
*/
function hasVendorPrefix(declaration) {
return vendorPrefixMatch.test(declaration);
}

/**
* Looks for property name by cutting off optional vendor prefix at the beginning
* and then cutting off rest of the declaration, starting from any whitespace or ":" in property name.
* @param {string} declaration - single css declaration, with not white space at the beginning
* @returns {string} - property name without vendor prefix.
*/
function getPropertyNameNoPrefix(declaration) {
const prefixMatch = vendorPrefixMatch.exec(declaration);
const prefix = prefixMatch === null ? "" : prefixMatch[0];
const declarationNoPrefix =
prefix === null ? declaration : declaration.slice(prefix.length);
// Expecting property name to be over, when any whitespace or ":" is found
const propertyNameSeparator = /[\s:]/;
return declarationNoPrefix.split(propertyNameSeparator)[0] ?? "";
}

// Clearing previous state
style.cssText = "";

// List of found and applied properties with vendor prefix
const appliedPropertiesWithPrefix = new Set();
// List of not applied properties - because of lack of support for its name or value
const notAppliedProperties = new Set();

for (const declaration of declarationsArray) {
const previousCSSText = style.cssText;
// Declarations are added one by one, because browsers sometimes combine multiple declarations into one
// For example Chrome changes "column-count: auto;column-width: 8rem;" into "columns: 8rem auto;"
style.cssText += declaration + ";"; // ";" was previous removed while using split method
// In case property name or value is not supported, browsers skip single declaration, while leaving rest of them intact
const correctlyApplied = style.cssText !== previousCSSText;

const vendorPrefixFound = hasVendorPrefix(declaration);
const propertyName = getPropertyNameNoPrefix(declaration);

if (correctlyApplied && vendorPrefixFound) {
// We are saving applied properties with prefix, so equivalent property with no prefix doesn't need to be supported
appliedPropertiesWithPrefix.add(propertyName);
} else if (!correctlyApplied && !vendorPrefixFound) {
notAppliedProperties.add(propertyName);
}
}

if (notAppliedProperties.size !== 0) {
// If property with vendor prefix is supported, we can ignore the fact that browser doesn't support property with no prefix
for (const substitute of appliedPropertiesWithPrefix) {
notAppliedProperties.delete(substitute);
}
// If any other declaration is not supported, whole block should be marked as invalid
if (notAppliedProperties.size !== 0) return false;
}
return true;
}

0 comments on commit 485faa6

Please sign in to comment.