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
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { computed } from "nanostores";
import { nanoid } from "nanoid";
import { useState } from "react";
import { computed } from "nanostores";
import { useStore } from "@nanostores/react";
import { matchSorter } from "match-sorter";
import { type Instance, Props, descendantComponent } from "@webstudio-is/sdk";
import { ariaAttributes } from "@webstudio-is/html-data";
import {
type Instance,
type Props,
descendantComponent,
} from "@webstudio-is/sdk";
import {
theme,
Combobox,
Expand All @@ -20,15 +26,18 @@ import {
$isContentMode,
$memoryProps,
$selectedBreakpoint,
$registeredComponentPropsMetas,
} from "~/shared/nano-states";
import { CollapsibleSectionWithAddButton } from "~/builder/shared/collapsible-section";
import { serverSyncStore } from "~/shared/sync";
import { $selectedInstance, $selectedInstanceKey } from "~/shared/awareness";
import { renderControl } from "../controls/combined";
import { usePropsLogic, type PropAndMeta } from "./use-props-logic";
import { serverSyncStore } from "~/shared/sync";
import { $selectedInstanceKey } from "~/shared/awareness";
import { AnimationSection } from "./animation/animation-section";
import { nanoid } from "nanoid";
import { $matchingBreakpoints } from "../../style-panel/shared/model";
import {
$instanceTags,
$matchingBreakpoints,
} from "../../style-panel/shared/model";
import { matchMediaBreakpoints } from "./match-media-breakpoints";

type Item = {
Expand All @@ -44,14 +53,15 @@ const matchOrSuggestToCreate = (
items: Array<Item>,
itemToString: (item: Item) => string
): Array<Item> => {
if (search.trim() === "") {
return items;
}
const matched = matchSorter(items, search, {
keys: [itemToString],
});

if (
search.trim() !== "" &&
itemToString(matched[0]).toLocaleLowerCase() !==
search.toLocaleLowerCase().trim()
search.toLocaleLowerCase().trim()
) {
matched.unshift({
name: search.trim(),
Expand Down Expand Up @@ -88,11 +98,43 @@ const renderProperty = (

const forbiddenProperties = new Set(["style", "class", "className"]);

const $availableProps = computed(
[$selectedInstance, $props, $registeredComponentPropsMetas, $instanceTags],
(instance, props, propsMetas, instanceTags) => {
const availableProps = new Map<Item["name"], Item>();
if (instance === undefined) {
return [];
}
// add component props
const metas = propsMetas.get(instance.component);
for (const [name, propMeta] of Object.entries(metas?.props ?? {})) {
const { label, description } = propMeta;
availableProps.set(name, { name, label, description });
}
// add aria attributes only for components with tags
const tag = instanceTags.get(instance.id);
if (tag) {
for (const { name, description } of ariaAttributes) {
availableProps.set(name, { name, description });
}
}
// remove initial props
for (const name of metas?.initialProps ?? []) {
availableProps.delete(name);
}
// remove defined props
for (const prop of props.values()) {
if (prop.instanceId === instance.id) {
availableProps.delete(prop.name);
}
}
return Array.from(availableProps.values());
}
);

const AddPropertyOrAttribute = ({
availableProps,
onPropSelected,
}: {
availableProps: Item[];
onPropSelected: (propName: string) => void;
}) => {
const [value, setValue] = useState("");
Expand All @@ -108,7 +150,8 @@ const AddPropertyOrAttribute = ({
autoFocus
color={isValid ? undefined : "error"}
placeholder="Select or create"
getItems={() => availableProps}
// lazily load available props to not bloat component renders
getItems={() => $availableProps.get()}
itemToString={itemToString}
onItemSelect={(item) => {
if (
Expand Down Expand Up @@ -241,7 +284,6 @@ export const PropsSection = (props: PropsSectionProps) => {
<Flex gap="1" direction="column">
{addingProp && (
<AddPropertyOrAttribute
availableProps={logic.availableProps}
onPropSelected={(propName) => {
setAddingProp(false);
logic.handleAdd(propName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { isRichText } from "~/shared/content-model";
import { $selectedInstancePath } from "~/shared/awareness";
import { showAttributeMeta, type PropValue } from "../shared";
import { ariaAttributes } from "@webstudio-is/html-data";

type PropOrName = { prop?: Prop; propName: string };

Expand Down Expand Up @@ -153,6 +154,42 @@ const $canHaveTextContent = computed(
});
}
);
type Attribute = (typeof ariaAttributes)[number];

const attributeToMeta = (attribute: Attribute): PropMeta => {
if (attribute.type === "string") {
return {
type: "string",
control: "text",
required: false,
};
}
if (attribute.type === "select") {
const options = attribute.options ?? [];
return {
type: "string",
control: options.length > 3 ? "select" : "radio",
required: false,
options,
};
}
if (attribute.type === "number") {
return {
type: "number",
control: "number",
required: false,
};
}
if (attribute.type === "boolean") {
return {
type: "boolean",
control: "boolean",
required: false,
};
}
attribute.type satisfies never;
throw Error("impossible case");
};

/** usePropsLogic expects that key={instanceId} is used on the ancestor component */
export const usePropsLogic = ({
Expand Down Expand Up @@ -193,9 +230,14 @@ export const usePropsLogic = ({

// we will delete items from these maps as we categorize the props
const unprocessedSaved = new Map(savedProps.map((prop) => [prop.name, prop]));
const unprocessedKnown = new Map<Prop["name"], PropMeta>(
Object.entries(meta.props)
);

const metas = new Map<Prop["name"], PropMeta>();
for (const attribute of ariaAttributes) {
metas.set(attribute.name, attributeToMeta(attribute));
}
for (const [name, propMeta] of Object.entries(meta.props)) {
metas.set(name, propMeta);
}

const initialPropsNames = new Set(meta.initialProps ?? []);

Expand Down Expand Up @@ -238,9 +280,9 @@ export const usePropsLogic = ({
const initialProps: PropAndMeta[] = [];
for (const name of initialPropsNames) {
const saved = getAndDelete<Prop>(unprocessedSaved, name);
const known = getAndDelete(unprocessedKnown, name);
const propMeta = metas.get(name);

if (known === undefined) {
if (propMeta === undefined) {
console.error(
`The prop "${name}" is defined in meta.initialProps but not in meta.props`
);
Expand All @@ -258,14 +300,14 @@ export const usePropsLogic = ({
// - where 0 is a fallback when no default is available
// - they think that width is set to 0, but it's actually not set at all
//
if (prop === undefined && known.defaultValue !== undefined) {
prop = getStartingProp(instance.id, known, name);
if (prop === undefined && propMeta.defaultValue !== undefined) {
prop = getStartingProp(instance.id, propMeta, name);
}

initialProps.push({
prop,
propName: name,
meta: known,
meta: propMeta,
});
}

Expand All @@ -276,22 +318,18 @@ export const usePropsLogic = ({
continue;
}

const meta =
getAndDelete(unprocessedKnown, prop.name) ??
getDefaultMetaForType("string");
const propMeta = metas.get(prop.name) ?? getDefaultMetaForType("string");

addedProps.push({
prop,
propName: prop.name,
meta,
meta: propMeta,
});
}

const handleAdd = (propName: string) => {
const propMeta =
unprocessedKnown.get(propName) ??
// In case of custom property/attribute we get a string.
getDefaultMetaForType("string");
// In case of custom property/attribute we get a string.
const propMeta = metas.get(propName) ?? getDefaultMetaForType("string");
const prop = getStartingProp(instance.id, propMeta, propName);
if (prop) {
updateProp(prop);
Expand Down Expand Up @@ -330,10 +368,5 @@ export const usePropsLogic = ({
),
/** Optional props that were added by user */
addedProps: addedProps.filter(({ propName }) => isPropVisible(propName)),
/** List of remaining props still available to add */
availableProps: Array.from(
unprocessedKnown.entries(),
([name, { label, description }]) => ({ name, label, description })
),
};
};
9 changes: 9 additions & 0 deletions packages/generate-arg-types/src/arg-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const propsToArgTypes = (
.filter(([propName]) => propName.startsWith("$") === false)
// Exclude props that are in the exclude list
.filter(([propName]) => exclude.includes(propName) === false)
.filter(([_propName, propItem]) => {
for (const { fileName, name } of propItem.declarations ?? []) {
// ignore aria attributes
if (fileName.endsWith("/@types/react/index.d.ts")) {
return name !== "AriaAttributes";
}
}
return true;
})
.map(([propName, propItem]) => {
// Remove @see and @deprecated from description also {@link ...} is removed as it always go after @see
propItem.description = propItem.description
Expand Down
3 changes: 2 additions & 1 deletion packages/generate-arg-types/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ type CustomDescriptionsType = {
}
}

const generatedFile = `${path.basename(filePath, ".tsx")}.props.ts`;
const basename = path.basename(filePath, ".tsx");
const generatedFile = `${basename}.props.ts`;
const generatedPath = path.join(generatedDir, generatedFile);

const componentDocs = tsConfigParser.parse(filePath);
Expand Down
8 changes: 8 additions & 0 deletions packages/html-data/bin/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ type Attribute = {
options?: string[];
};

const overrides: Record<string, Partial<Attribute>> = {
"aria-label": {
description:
Comment thread
TrySound marked this conversation as resolved.
"Provides the accessible name that describes an interactive element if no other accessible name exists, for example in a button that contains an image with no text.",
},
};

const html = await loadPage("aria1.3", "https://www.w3.org/TR/wai-aria-1.3");
const document = parseHtml(html);
const list = findTags(document, "dl").find(
Expand All @@ -44,6 +51,7 @@ for (const [name, meta] of aria.entries()) {
name,
description: descriptions.get(name) ?? "",
type: "string",
...overrides[name],
};
if (meta.type === "string" || meta.type === "boolean") {
attribute.type = meta.type;
Expand Down
2 changes: 1 addition & 1 deletion packages/html-data/src/__generated__/aria.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/html-data/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./__generated__/content-model";
export * from "./descriptions";
export * from "./dom-attributes-react-mappings";
export * from "./__generated__/attributes";
export * from "./__generated__/aria";

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading