diff --git a/.changeset/two-jeans-share.md b/.changeset/two-jeans-share.md new file mode 100644 index 000000000..615c68da4 --- /dev/null +++ b/.changeset/two-jeans-share.md @@ -0,0 +1,5 @@ +--- +'@qwik-ui/headless': minor +--- + +We are removing the existing popover animations shimming and instead wil now only support native popover animations. This is considered a breaking change but will be more reliable overall. diff --git a/.eslintignore b/.eslintignore index 925698059..7f91a7ca5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ node_modules dist coverage .eslintrc.* -vite.config.ts \ No newline at end of file +vite.config.ts +packages/kit-headless/browsers/** \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4806d6a8..6c80f20d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: jobs: test: runs-on: ubuntu-latest + name: Test NodeJS ${{ matrix.node_version }} strategy: matrix: diff --git a/.vscode/settings.json b/.vscode/settings.json index cf055a154..1a269d81d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,6 @@ ], "editor.codeActionsOnSave": { "source.removeUnusedImports": "explicit" - } + }, + "vitest.disableWorkspaceWarning": true } diff --git a/apps/website/auto-api.ts b/apps/website/auto-api.ts new file mode 100644 index 000000000..0d9442c8d --- /dev/null +++ b/apps/website/auto-api.ts @@ -0,0 +1,117 @@ +import * as fs from 'fs'; +import { resolve } from 'path'; +import { ViteDevServer } from 'vite'; +export default function autoAPI() { + return { + name: 'watch-monorepo-changes', + configureServer(server: ViteDevServer) { + const watchPath = resolve(__dirname, '../../packages/kit-headless'); + server.watcher.on('change', (file: string) => { + if (file.startsWith(watchPath)) { + loopOnAllChildFiles(file); + } + }); + }, + }; +} +// the object should have this general structure, arranged from parent to child +// componentName:[subComponent,subcomponent,...] +// subComponentName:[publicType,publicType,...] +// publicType:[{ comment,prop,type },{ comment,prop,type },...] +// THEY UPPER-MOST KEY IS ALWAYS USED AS A HEADING +export type ComponentParts = Record; +type SubComponents = SubComponent[]; +export type SubComponent = Record; +export type PublicType = Record; +type ParsedProps = { + comment: string; + prop: string; + type: string; +}; +function parseSingleComponentFromDir(path: string, ref: SubComponents) { + const component_name = /\/([\w-]*).tsx/.exec(path); + if (component_name === null || component_name[1] === null) { + // may need better behavior + return; + } + const sourceCode = fs.readFileSync(path, 'utf-8'); + const comments = extractPublicTypes(sourceCode); + const parsed: PublicType[] = []; + for (const comment of comments) { + const api = extractComments(comment.string); + const pair: PublicType = { [comment.label]: api }; + parsed.push(pair); + } + const completeSubComponent: SubComponent = { [component_name[1]]: parsed }; + ref.push(completeSubComponent); + return ref; +} + +function extractPublicTypes(strg: string) { + const getPublicTypes = /type Public([A-Z][\w]*)*[\w\W]*?{([\w|\W]*?)}(;| &)/gm; + const cms = []; + let groups; + while ((groups = getPublicTypes.exec(strg)) !== null) { + const string = groups[2]; + cms.push({ label: groups[1], string }); + } + return cms; +} +function extractComments(strg: string): ParsedProps[] { + const magical_regex = + /^\s*?\/[*]{2}\n?([\w|\W|]*?)\s*[*]{1,2}[/]\n[ ]*([\w|\W]*?): ([\w|\W]*?);?$/gm; + + const cms = []; + let groups; + + while ((groups = magical_regex.exec(strg)) !== null) { + const trimStart = /^ *|(\* *)/g; + const comment = groups[1].replaceAll(trimStart, ''); + const prop = groups[2]; + const type = groups[3]; + cms.push({ comment, prop, type }); + } + return cms; +} +function writeToDocs(fullPath: string, componentName: string, api: ComponentParts) { + if (fullPath.includes('kit-headless')) { + const relDocPath = `../website/src/routes//docs/headless/${componentName}`; + const fullDocPath = resolve(__dirname, relDocPath); + const dirPath = fullDocPath.concat('/auto-api'); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + const json = JSON.stringify(api, null, 2); + const hacky = `export const api=${json}`; + + try { + fs.writeFileSync(dirPath.concat('/api.ts'), hacky); + console.log('auto-api: succesfully genereated new json!!! :)'); + } catch (err) { + return; + } + } +} +function loopOnAllChildFiles(filePath: string) { + const childComponentRegex = /\/([\w-]*).tsx$/.exec(filePath); + if (childComponentRegex === null) { + return; + } + const parentDir = filePath.replace(childComponentRegex[0], ''); + const componentRegex = /\/(\w*)$/.exec(parentDir); + if (!fs.existsSync(parentDir) || componentRegex == null) { + return; + } + const componentName = componentRegex[1]; + const allParts: SubComponents = []; + const store: ComponentParts = { [componentName]: allParts }; + fs.readdirSync(parentDir).forEach((fileName) => { + if (/tsx$/.test(fileName)) { + const fullPath = parentDir + '/' + fileName; + parseSingleComponentFromDir(fullPath, store[componentName]); + } + }); + + writeToDocs(filePath, componentName, store); +} diff --git a/apps/website/src/_state/component-statuses.ts b/apps/website/src/_state/component-statuses.ts index d1fdf99d7..5894906dd 100644 --- a/apps/website/src/_state/component-statuses.ts +++ b/apps/website/src/_state/component-statuses.ts @@ -37,22 +37,17 @@ export const statusByComponent: ComponentKitsStatuses = { Textarea: ComponentStatus.Draft, }, headless: { - Accordion: ComponentStatus.Beta, Carousel: ComponentStatus.Beta, - Collapsible: ComponentStatus.Beta, Combobox: ComponentStatus.Beta, Checkbox: ComponentStatus.Draft, Dropdown: ComponentStatus.Draft, - Label: ComponentStatus.Draft, - Modal: ComponentStatus.Beta, + Label: ComponentStatus.Beta, Pagination: ComponentStatus.Draft, - Popover: ComponentStatus.Beta, Progress: ComponentStatus.Beta, Select: ComponentStatus.Beta, - Separator: ComponentStatus.Beta, Tabs: ComponentStatus.Beta, - Toggle: ComponentStatus.Draft, - ToggleGroup: ComponentStatus.Draft, + Toggle: ComponentStatus.Beta, + 'Toggle Group': ComponentStatus.Beta, Tooltip: ComponentStatus.Beta, }, }; diff --git a/apps/website/src/components/animations/caveats.tsx b/apps/website/src/components/animations/caveats.tsx new file mode 100644 index 000000000..e58c54c42 --- /dev/null +++ b/apps/website/src/components/animations/caveats.tsx @@ -0,0 +1,49 @@ +import { component$ } from '@builder.io/qwik'; +import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure + +export const TopLayerAnimationsCaveats = component$(() => { + return ( + + Important Caveats for Animating Discrete Properties + +
    +
  • + + Animating display and overlay: + +

    + The display property must be included in the transitions list to + ensure the element remains visible throughout the animation. The value flips + from none to block at 0% of the animation, ensuring + visibility for the entire duration. The  + overlay ensures the element stays in the top layer until the + animation completes. +

    +
  • +
  • + + Using transition-behavior: allow-discrete: + +

    + This property is essential when animating discrete properties like{' '} + display and overlay, which are not typically + animatable. It ensures smooth transitions for these discrete properties. +

    +
  • +
  • + + Setting Starting Styles with @starting-style: + +

    + CSS transitions are only triggered when a property changes on a visible + element. The  + @starting-style at-rule allows you to set initial styles (e.g.,{' '} + opacity and + transform) when the element first appears, ensuring that the + animation behaves predictably. +

    +
  • +
+
+ ); +}); diff --git a/apps/website/src/components/animations/compatability.tsx b/apps/website/src/components/animations/compatability.tsx new file mode 100644 index 000000000..ff15fa2a4 --- /dev/null +++ b/apps/website/src/components/animations/compatability.tsx @@ -0,0 +1,22 @@ +import { component$ } from '@builder.io/qwik'; +import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure + +export const BrowserAnimationsCompatability = component$(() => { + return ( + +
+

+ Browser Compatability +

+

+ + Browser versions that do not support the popover API natively + {' '} + have known issues when trying to use animations or transitions. If you need to + support legacy versions of browsers, please be sure to test this functionality + independently. +

+
+
+ ); +}); diff --git a/apps/website/src/components/api-table/api-table.tsx b/apps/website/src/components/api-table/api-table.tsx index d5dbfdd06..65f1fe672 100644 --- a/apps/website/src/components/api-table/api-table.tsx +++ b/apps/website/src/components/api-table/api-table.tsx @@ -1,9 +1,9 @@ import { component$ } from '@builder.io/qwik'; import { InfoPopup } from '../info-popup/info-popup'; -type APITableProps = { +export type APITableProps = { propDescriptors: { name: string; - info: string; + info?: string; type: string; description: string; }[]; diff --git a/apps/website/src/components/api-table/auto-api.tsx b/apps/website/src/components/api-table/auto-api.tsx new file mode 100644 index 000000000..b216be717 --- /dev/null +++ b/apps/website/src/components/api-table/auto-api.tsx @@ -0,0 +1,138 @@ +import { JSXOutput, component$, $, QRL, useTask$, useSignal } from '@builder.io/qwik'; +import { APITable, type APITableProps } from './api-table'; + +//This is a workaround for not being able to export across packages due to nx rule: +// https://nx.dev/features/enforce-module-boundaries#enforce-module-boundaries +type ComponentParts = Record; +type SubComponents = SubComponent[]; +type SubComponent = Record; +type PublicType = Record; +type ParsedProps = { + comment: string; + prop: string; + type: string; +}; +type AutoAPIConfig = { + topHeader?: QRL<(text: string) => JSXOutput>; + subHeader?: QRL<(text: string) => JSXOutput>; + props?: QRL<(text: string) => string>; +}; + +type AnatomyTableProps = { + api?: ComponentParts; + config: AutoAPIConfig; +}; + +type SubComponentProps = { + subComponent: SubComponent; + config: AutoAPIConfig; +}; +type ParsedCommentsProps = { + parsedProps: PublicType; + config: AutoAPIConfig; +}; +const currentHeader = $(() => { + //cannot send h2 from here because current TOC can only read md + return null; +}); + +const currentSubHeader = $((text: string) => { + let subHeader = text.replace(/(p|P)rops/, ''); + const hasCapital = /[a-z][A-Z]/.exec(subHeader)?.index; + if (hasCapital != undefined) { + subHeader = + subHeader.slice(0, hasCapital + 1) + '.' + subHeader.slice(hasCapital + 1); + } + return ( + <> +

{subHeader}

+ + ); +}); + +const removeQuestionMarkFromProp = $((text: string) => { + return text.replace('?', ''); +}); +const defaultConfig: AutoAPIConfig = { + topHeader: currentHeader, + subHeader: currentSubHeader, + props: removeQuestionMarkFromProp, +}; +export const AutoAPI = component$( + ({ api, config = defaultConfig }) => { + if (api === undefined) { + return null; + } + const key = Object.keys(api)[0]; + const topHeaderSig = useSignal(key); + const subComponents = api[key].filter((e) => e[Object.keys(e)[0]].length > 0); + useTask$(async () => { + if (config.topHeader) { + topHeaderSig.value = await config.topHeader(key as string); + } + }); + return ( + <> + {topHeaderSig.value} + {subComponents.map((e, index) => ( + + ))} + + ); + }, +); + +const SubComponent = component$(({ subComponent, config }) => { + const subComponentKey = Object.keys(subComponent)[0]; + const comments = subComponent[subComponentKey]; + return ( + <> + {comments.map((e) => ( + <> + + + ))} + + ); +}); + +const ParsedComments = component$(({ parsedProps, config }) => { + const key = Object.keys(parsedProps)[0]; + const subHeaderSig = useSignal(key); + useTask$(async () => { + if (config.subHeader) { + subHeaderSig.value = await config.subHeader(key as string); + } + }); + const appliedPropsSig = useSignal(null); + useTask$(async () => { + const translation: APITableProps = { + propDescriptors: parsedProps[key].map((e) => { + const isObject = e.type.includes('{'); + const isUnion = e.type.includes('|'); + const isPopup = isObject || isUnion; + + return { + name: e.prop, + type: isObject ? 'object' : isUnion ? 'union' : e.type, + description: e.comment, + info: (isPopup && e.type) || undefined, + }; + }), + }; + if (config.props) { + for (const props of translation.propDescriptors) { + props.name = await config.props(props.name); + } + } + appliedPropsSig.value = translation; + }); + return ( + <> + {subHeaderSig.value} + {appliedPropsSig.value?.propDescriptors && ( + + )} + + ); +}); diff --git a/apps/website/src/components/mdx-components/index.tsx b/apps/website/src/components/mdx-components/index.tsx index 3a1d9e7ee..0e983b5ab 100644 --- a/apps/website/src/components/mdx-components/index.tsx +++ b/apps/website/src/components/mdx-components/index.tsx @@ -2,6 +2,7 @@ import { Component, PropsOf, Slot, component$ } from '@builder.io/qwik'; import { cn } from '@qwik-ui/utils'; import { AnatomyTable } from '../anatomy-table/anatomy-table'; import { APITable } from '../api-table/api-table'; +import { AutoAPI } from '../api-table/auto-api'; import { CodeCopy } from '../code-copy/code-copy'; import { CodeSnippet } from '../code-snippet/code-snippet'; import { FeatureList } from '../feature-list/feature-list'; @@ -10,6 +11,8 @@ import { KeyboardInteractionTable } from '../keyboard-interaction-table/keyboard import { Note } from '../note/note'; import { Showcase } from '../showcase/showcase'; import { StatusBanner } from '../status-banner/status-banner'; +import { TopLayerAnimationsCaveats } from '../animations/caveats'; +import { BrowserAnimationsCompatability } from '../animations/compatability'; export const components: Record = { p: component$>(({ ...props }) => { @@ -132,4 +135,7 @@ export const components: Record = { Note, StatusBanner, Showcase, + AutoAPI, + TopLayerAnimationsCaveats, + BrowserAnimationsCompatability, }; diff --git a/apps/website/src/components/status-banner/status-banner.tsx b/apps/website/src/components/status-banner/status-banner.tsx index bcbb67824..e45c5b744 100644 --- a/apps/website/src/components/status-banner/status-banner.tsx +++ b/apps/website/src/components/status-banner/status-banner.tsx @@ -77,7 +77,7 @@ export const StatusBanner = component$(({ status }: StatusBannerProps) => { <>