diff --git a/.changeset/curly-jobs-eat.md b/.changeset/curly-jobs-eat.md new file mode 100644 index 0000000000..66186ca786 --- /dev/null +++ b/.changeset/curly-jobs-eat.md @@ -0,0 +1,5 @@ +--- +"@rhds/elements": patch +--- + +``: added support for rtl language overflow scroll buttons diff --git a/.changeset/curly-ways-march.md b/.changeset/curly-ways-march.md new file mode 100644 index 0000000000..c81ecdc195 --- /dev/null +++ b/.changeset/curly-ways-march.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +React: add generated react wrappers to NPM package diff --git a/.changeset/eager-dolls-wear.md b/.changeset/eager-dolls-wear.md new file mode 100644 index 0000000000..cec9f5da7b --- /dev/null +++ b/.changeset/eager-dolls-wear.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +``: automatically fetch status for the current domain diff --git a/.changeset/plenty-rabbits-scream.md b/.changeset/plenty-rabbits-scream.md new file mode 100644 index 0000000000..c75425db2a --- /dev/null +++ b/.changeset/plenty-rabbits-scream.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +``: hide header, body, or footer regions when they have no content diff --git a/.changeset/shy-houses-arrive.md b/.changeset/shy-houses-arrive.md new file mode 100644 index 0000000000..3cf7b3bed1 --- /dev/null +++ b/.changeset/shy-houses-arrive.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +``: applied heading font to card headings diff --git a/.changeset/spicy-planes-lead.md b/.changeset/spicy-planes-lead.md new file mode 100644 index 0000000000..9c47cff405 --- /dev/null +++ b/.changeset/spicy-planes-lead.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +``: corrected 'show more' button styles diff --git a/.changeset/tough-pens-brake.md b/.changeset/tough-pens-brake.md new file mode 100644 index 0000000000..5838b51fd5 --- /dev/null +++ b/.changeset/tough-pens-brake.md @@ -0,0 +1,5 @@ +--- +"@rhds/elements": patch +--- + +``: ensure correct font-family is used diff --git a/.changeset/wet-feet-pull.md b/.changeset/wet-feet-pull.md new file mode 100644 index 0000000000..1c351913b7 --- /dev/null +++ b/.changeset/wet-feet-pull.md @@ -0,0 +1,4 @@ +--- +"@rhds/elements": patch +--- +``: removes landmark semantics from card, simplifying page navigation for screen reader users diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 34c582799b..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,55 +0,0 @@ -!.eleventy.cjs - -*.mp3 -*.vtt -*.css -*.woff -*.d.ts -*.ico -*.jpeg -*.jpg -*.map -*.md -*.njk -*.patch -*.png -*.scss -*.sh -*.spec.js -*.svg -*.toml -*.tsbuildinfo -*.txt -*.yml -*.yaml -*.min.js -*.tgz -*.csv -CNAME - -custom-elements.json -package-lock.json - -_site -docs/_data/todos.json -docs/demo.js -docs/pfe.min.js -docs/bundle.js -docs/core -docs/components -docs/assets/playgrounds -node_modules - -core/pfe-sass/docs/index.html -core/**/*.js -elements/**/*.js -lib/**/*.js -tools/**/*.js - -!core/*/demo/*.js -!elements/*/demo/*.js - -tools/create-element/templates/**/* -node_modules -node_modules/**/* - diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index cfe32ac8bb..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@patternfly/elements", - "rules": { - "brace-style": [ - "error", - "1tbs", - { - "allowSingleLine": true - } - ] - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0002ca1850..0dd76658c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,8 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline - + - name: Install Playwright Browsers + run: npx playwright install --with-deps - name: Lint id: lint run: npm run lint diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000000..3bb682f067 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,25 @@ +name: Validate PRs + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm i semantic-release @changesets/read --prefer-offline + - uses: actions/github-script@v7 + with: + script: | + const { validate } = await import('${{ github.workspace }}/scripts/validate-prs.js'); + await validate({ context }); diff --git a/.gitignore b/.gitignore index 12b4b78b54..1216e5e6df 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ docs/assets/playgrounds/ # Build artifacts elements/*/*.js elements/*/test/*.js +react lib/**/*.js !elements/**/demo/*.css *.map diff --git a/.markdownlint-cli2.mjs b/.markdownlint-cli2.mjs index f0c5b6719a..1e459cf2d5 100644 --- a/.markdownlint-cli2.mjs +++ b/.markdownlint-cli2.mjs @@ -5,23 +5,23 @@ export default { description: 'Require changesets to use the correct package name', tags: ['frontmatter'], function({ name, frontMatterLines, ...rest }, onError) { - const yaml = YAML.load(frontMatterLines.filter(Boolean).filter(x => x !== '---')) + const yaml = YAML.load(frontMatterLines.filter(Boolean).filter(x => x !== '---')); for (const [key, value] of Object.entries(yaml)) { - if (['patch','minor','major'].includes(value)) { + if (['patch', 'minor', 'major'].includes(value)) { if (key !== '@rhds/elements') { onError({ lineNumber: 2, detail: `incorrect package name ${key}`, - }) + }); } } } - } + }, }], dot: true, files: './changeset/*.md', config: { - default: false, + 'default': false, 'markdownlint-changeset-packagename': true, - } -} + }, +}; diff --git a/.nvmrc b/.nvmrc index e44a38e080..790e1105f2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.12.1 +v20.10.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee41dadd5..c474c257fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,116 @@ # @rhds/elements +## 1.4.1 + +### Patch Changes + +- 862380b: corrected `@patternfly/elements` dependency to be included with the package + +## 1.4.0 + +### Minor Changes + +- fecdcbf: ``: added line numbers +- fecdcbf: ✨ Added `` + + Website status communicates the operational status of a website or domain using a status icon and link. It is usually located in the Footer component. + + ```html + + ``` + +- fecdcbf: ✨ Added ``. + + Back to top component is a fragment link that allows users to quickly navigate to the top of a lengthy content. + + ```html + Back to top + ``` + +- fecdcbf: ✨ Added ``. + + A skip link is used to skip repetitive content on a page. It is hidden by default and can be activated by hitting the "Tab" key after loading/refreshing a page. + + ```html + + Skip to main content + + ``` + +- fecdcbf: ⚛️ Added React wrapper components + + You can now more easily integrate RHDS elements into your React apps by importing our wrapper components + + First, make sure that you list `@lit/react` as a dependency in your project + + ```sh + npm install --save @lit/react + ``` + + Then import the element components you need and treat them like any other react component + + ```js + import { Tabs } from '@rhds/elements/react/rh-tabs/rh-tabs.js'; + import { Tab } from '@rhds/elements/react/rh-tabs/rh-tab.js'; + import { TabPanel } from '@rhds/elements/react/rh-tabs/rh-tab-panel.js'; + + import { useState } from 'react'; + + const tabs = [ + { heading: 'Hello Red Hat', content: 'Let\'s break down silos' }, + { heading: 'Web components', content: 'They work everywhere' } + ]; + + function App() { + const [index, setExpanded] = useState(-1); + return ( + expanded {expanded} + {tabs.map(({ heading, content }, i) => ( + setExpanded(i)}>{heading} + {content}))} + + ); + } + ``` + +- fecdcbf: ``: added `Show more` toggle +- fecdcbf: ``: added copy and wrap actions, with localizable slots for the button labels + + ```html + + Copy to Clipboard + + Toggle word wrap + + + + ``` + +### Patch Changes + +- fecdcbf: ``: improved focus accessibility for keyboard navigation users on firefox + ``: improved focus accessibility on firefox +- fecdcbf: ``: added a accents slot with placement options as inline and bottom +- fecdcbf: Context: aligned context implementation with updated [protocol defintions](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#definitions) +- fecdcbf: Update dependencies, including Lit version 3 +- fecdcbf: ``: make sure alerts always have to correct (lightest) colour palette +- fecdcbf: ``: allow tabs with long text content to fit into different-sized containers + +## 1.3.2 + +### Patch Changes + +- 1d1640705: ``: corrected icon slot visibility with a slotted icon +- d61b8dc71: ``: ensure that `cancel`, `open`, and `closed` events fire + ## 1.3.1 ### Patch Changes diff --git a/declaration.d.ts b/declaration.d.ts index ce9a0440cf..8fb49f9bc2 100644 --- a/declaration.d.ts +++ b/declaration.d.ts @@ -1,8 +1,5 @@ declare module '*.css' { - import type { CSSResult } from 'lit'; - - // import style from './some-styles.css'; - const style: CSSResult; + const style: CSSStyleSheet export default style; } diff --git a/docs/_data/playgrounds.cjs b/docs/_data/playgrounds.cjs index 5221bd2b19..b4d7b5c9fc 100644 --- a/docs/_data/playgrounds.cjs +++ b/docs/_data/playgrounds.cjs @@ -8,8 +8,8 @@ function groupBy(prop, xs) { function getDemoFilename(x) { return `demo/${(x.url.split('/demo/').pop() || `${x.primaryElementName}.html`).replace(/\/$/, '.html')}` - .replace('.html', '/index.html') - .replace(`${x.primaryElementName}/index.html`, 'index.html'); + .replace('.html', '/index.html') + .replace(`${x.primaryElementName}/index.html`, 'index.html'); } /** @@ -26,7 +26,8 @@ function getDemoFilename(x) { * > One of `.`, or **WORD** (_>= 1x_) * `"` */ -const DEMO_SUBRESOURCE_RE = /(?href|src)="\/elements\/rh-(?.*)\/(?.*)\.(?[.\w]+)"/g; +const DEMO_SUBRESOURCE_RE = + /(?href|src)="\/elements\/rh-(?.*)\/(?.*)\.(?[.\w]+)"/g; /** * `/elements/` @@ -63,16 +64,16 @@ function demoPaths(content, pathname) { function isModuleScript(node) { return ( - node.tagName === 'script' && - node.attrs.some(x => x.name === 'type' && x.value === 'module') + node.tagName === 'script' + && node.attrs.some(x => x.name === 'type' && x.value === 'module') ); } function isStyleLink(node) { return ( - node.tagName === 'link' && - node.attrs.some(x => x.name === 'rel' && x.value === 'stylesheet') && - node.attrs.some(x => x.name === 'href') + node.tagName === 'link' + && node.attrs.some(x => x.name === 'rel' && x.value === 'stylesheet') + && node.attrs.some(x => x.name === 'href') ); } @@ -130,11 +131,11 @@ module.exports = async function(data) { Tools.createCommentNode('playground-fold'), Tools.createElement('link', { rel: 'stylesheet', - href: 'https://static.redhat.com/libs/redhat/redhat-font/4/webfonts/red-hat-font.min.css' + href: 'https://static.redhat.com/libs/redhat/redhat-font/4/webfonts/red-hat-font.min.css', }), Tools.createElement('link', { rel: 'stylesheet', - href: 'https://static.redhat.com/libs/redhat/redhat-theme/6/advanced-theme.css' + href: 'https://static.redhat.com/libs/redhat/redhat-theme/6/advanced-theme.css', }), Tools.createElement('link', { rel: 'stylesheet', @@ -147,17 +148,19 @@ module.exports = async function(data) { const filename = getDemoFilename(demo); /** @see docs/_plugins/rhds.cjs demoPaths transform */ - const base = url.pathToFileURL(path.join(process.cwd(), 'elements', primaryElementName, 'demo/')); + const base = url.pathToFileURL(path.join(process.cwd(), + 'elements', + primaryElementName, + 'demo/')); const docsDir = url.pathToFileURL(path.join(process.cwd(), 'docs/')); const isMainDemo = filename === 'demo/index.html'; const demoSlug = filename.split('/').at(1); const addSubresourceURL = async subresourceURL => { if (subresourceURL && !subresourceURL.startsWith('http')) { - const subresourceFileURL = !subresourceURL.startsWith('/') + const subresourceFileURL = !subresourceURL.startsWith('/') ? // non-tabular ternary - // eslint-disable-next-line operator-linebreak - ? new URL(subresourceURL, base) + new URL(subresourceURL, base) : new URL(subresourceURL.replace('/', './'), docsDir); try { const resourceName = @@ -171,7 +174,11 @@ module.exports = async function(data) { fileMap.set(resourceName, { content, hidden: true }); } } catch (e) { - throw new SubresourceError(`Error generating playground for ${demo.slug}.\nCould not find subresource ${subresourceURL} at ${subresourceFileURL?.href ?? 'unknown'}`, e, subresourceFileURL); + throw new SubresourceError( + `Error generating playground for ${demo.slug}.\nCould not find subresource ${subresourceURL} at ${subresourceFileURL?.href ?? 'unknown'}`, + e, + subresourceFileURL, + ); } } }; @@ -183,13 +190,13 @@ module.exports = async function(data) { }); const hrefSubresourceElements = Tools.queryAll(fragment, node => - Tools.isElementNode(node) && - isStyleLink(node)); + Tools.isElementNode(node) + && isStyleLink(node)); const srcSubresourceElements = Tools.queryAll(fragment, node => - Tools.isElementNode(node) && - SRC_SUBRESOURCE_TAGNAMES.has(node.tagName) && - hasLocalSrcAttr(node)); + Tools.isElementNode(node) + && SRC_SUBRESOURCE_TAGNAMES.has(node.tagName) + && hasLocalSrcAttr(node)); // register demo css resources for (const el of hrefSubresourceElements) { @@ -219,9 +226,9 @@ module.exports = async function(data) { // HACK: https://github.com/google/playground-elements/issues/93#issuecomment-1775247123 const inlineModules = Tools.queryAll(fragment, node => - Tools.isElementNode(node) && - isModuleScript(node) && - !node.attrs.some(({ name }) => name === 'src')); + Tools.isElementNode(node) + && isModuleScript(node) + && !node.attrs.some(({ name }) => name === 'src')); Array.from(inlineModules).forEach((el, i) => { const moduleName = `${primaryElementName}-${demoSlug.replace('.html', '')}-inline-script-${i++}.js`; diff --git a/docs/_data/relatedItems.yaml b/docs/_data/relatedItems.yaml index 2e6c24df9d..08ae0dc52d 100644 --- a/docs/_data/relatedItems.yaml +++ b/docs/_data/relatedItems.yaml @@ -40,6 +40,7 @@ rh-footer: - rh-accordion - rh-popover - rh-tooltip + - rh-site-status rh-jump-links: - rh-pagination - rh-progress-steps @@ -64,6 +65,13 @@ rh-progress-steps: - rh-jump-links - rh-pagination - rh-tabs +rh-skip-link: + - rh-navigation + - rh-navigation-secondary + - rh-subnav +rh-site-status: + - rh-card + - rh-footer rh-spinner: - form - search-bar diff --git a/docs/_data/repoStatus.csv b/docs/_data/repoStatus.csv deleted file mode 100644 index 34c2051104..0000000000 --- a/docs/_data/repoStatus.csv +++ /dev/null @@ -1,84 +0,0 @@ -id,Element/Pattern,RHDS,WebRH,WebDMS,Adobe Target, -Accordion,Small size,x,x,,, -Accordion,Large size,x,x,,, -Alert,Inline,x,,,, -Alert,Inline variation,x,,,, -Alert,Toast,x,,,, -Avatar,Avatar,x,,,, -Badge,Badge,x,,,, -Blockquote,Default size,x,,,, -Blockquote,Large size,x,,,, -Button,Danger,x,,,, -Button,Primary,x,,,, -Button,Secondary,x,,,, -Button,Tertiary,x,,,, -Button,Link,x,,,, -Button,Play,x,,,, -Button,Close,x,,,, -Card,Basic,,x,,, -Card,Lists,,x,,, -Card,Data/fast facts,,,x,, -Card,Logo,,x,,, -Card,Bar/with hat,,x,,, -Card,Icon,,x,,, -Card,Image,,x,,, -Card,Asset,,,x,, -Card,Quote,,x,,, -Card,Avatars,,,x,, -Card,Video,,,x,, -Card,Pricing,,,x,, -Card,Logo slider,,,x,, -Card,Name slider,,,x,, -Code Block,Code Block,x,,,, -Cta,Primary,x,,,, -Cta,"Primary, video",x,,,, -Cta,Secondary,x,,,, -Cta,"Secondary, video",,,,, -Cta,Brick,x,,,, -Cta,"Brick, icon",x,,,, -Cta,Default,x,,,, -Cta,"Default, video",x,,,, -Dialog,Dialog,x,,,, -Footer,Footer,x,,,, -Navigation Secondary,Secondary navigation,x,,,, -Pagination,Compact size,x,,,, -Pagination,Full size,x,,,, -Spinner,Small size,x,,,, -Spinner,Medium size,x,,,, -Spinner,Large size,x,,,, -Stat,Default size,x,,,, -Stat,Large size,x,,,, -Tabs,Open,x,,,, -Tabs,Boxed,x,,,, -Tag,Red - filled and unfilled,x,,,, -Tag,Orange - filled and unfilled,x,,,, -Tag,Green - filled and unfilled,x,,,, -Tag,Cyan - filled and unfilled,x,,,, -Tag,Blue - filled and unfilled,x,,,, -Tag,Purple - filled and unfilled,x,,,, -Tag,Gray - filled and unfilled,x,,,, -Tooltip,Tooltip,x,,,, -,12-column grid,,x,x,, -,Audio player,,,x,, -,Announcement bar,,,x,x, -,Cookie banner,,,,, -,Filter box,,x,,, -,Fixed bottom banner,,,x,x, -,Footer group,,,,, -,Form - full,,x,,, -,Form - input group - money,,,,, -,Form - input group - email,,,,, -,Form - input group - username,,,,, -,Form - input group - date,,,,, -,Form - input group - search,,,,, -,Form - input group - help,,,,, -,Jump links - horizontal,,,,, -,Jump links - vertical,,,,, -,Link with icon,,,,, -,Logo bar,,,x,, -,Navigation,,x,,, -,Quote,x,x,x,, -,Search bar,,,,, -,Search combo,,,,, -,Skip to main content,,,,, -,Video with thumbnail,,,x,, diff --git a/docs/_data/repoStatus.yaml b/docs/_data/repoStatus.yaml new file mode 100644 index 0000000000..baa46d1e5c --- /dev/null +++ b/docs/_data/repoStatus.yaml @@ -0,0 +1,406 @@ +- name: "Accordion" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Alert" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Audio Player" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Avatar" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Back To Top" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Planned + - name: Documentation + status: In Progress +- name: "Badge" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Blockquote" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Button" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Cta" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Card" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Code Block" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Dialog" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Footer" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Jumplinks" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: N/A + - name: Responsive + status: N/A + - name: RH Elements + status: N/A + - name: webRH + status: N/A + - name: Documentation + status: Ready +- name: "Navigation Secondary" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Pagination" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Popover" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Skip Link" + type: "Element" + overallStatus: "New" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Planned + - name: Documentation + status: Ready +- name: "Site Status" + type: "Element" + overallStatus: "New" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Planned + - name: Documentation + status: Ready +- name: "Spinner" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Stat" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Subnav" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Surface" + type: "Element" + overallStatus: "New" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Table" + type: "Element" + overallStatus: "New" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Tabs" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Tag" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Tile" + type: "Element" + overallStatus: "New" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Timestamp" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready +- name: "Tooltip" + type: "Element" + overallStatus: "Available" + libraries: + - name: Figma library + status: Ready + - name: Responsive + status: Ready + - name: RH Elements + status: Ready + - name: webRH + status: Ready + - name: Documentation + status: Ready \ No newline at end of file diff --git a/docs/_includes/component/header.njk b/docs/_includes/component/header.njk index cc4ccaa1c3..e0180926e3 100755 --- a/docs/_includes/component/header.njk +++ b/docs/_includes/component/header.njk @@ -48,7 +48,7 @@ - +
  • @@ -61,7 +61,7 @@ {%- for link in collections.getstarted -%}
  • {{ link.data.title }} + href="{{ link.url | url }}">{{ link.data.heading }}
  • {%- endfor -%} @@ -172,7 +172,8 @@ [ 'Content', '/accessibility/content/' ], [ 'Design', '/accessibility/design/' ], [ 'Development', '/accessibility/development/' ], - [ 'Contributors', '/accessibility/contributors/' ] + [ 'Contributors', '/accessibility/contributors/' ], + [ 'Resources', '/accessibility/resources/' ] ] -%} {%- for text, link in a11yLinks -%}
  • @@ -203,14 +204,3 @@ - - - diff --git a/docs/_includes/layout-foundations.njk b/docs/_includes/layout-with-subnav.njk similarity index 64% rename from docs/_includes/layout-foundations.njk rename to docs/_includes/layout-with-subnav.njk index 553658ae12..81f792b66d 100644 --- a/docs/_includes/layout-foundations.njk +++ b/docs/_includes/layout-with-subnav.njk @@ -7,6 +7,7 @@ importElements: - rh-subnav - rh-tag - rh-badge + - rh-code-block --- {% include 'component/header.njk' %} @@ -14,13 +15,16 @@ importElements:
    -
    +
    diff --git a/docs/_plugins/alphabetize-tags.cjs b/docs/_plugins/alphabetize-tags.cjs index 8c2eb9ebf0..87f0ac089e 100644 --- a/docs/_plugins/alphabetize-tags.cjs +++ b/docs/_plugins/alphabetize-tags.cjs @@ -3,7 +3,7 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { for (const tag of tagsToAlphabetize) { eleventyConfig.addCollection(tag, function(collection) { const currentCollection = [...collection.getFilteredByTag(tag)] - .sort((a, b) => (a.data.order ?? Infinity) - (b.data.order ?? Infinity)); + .sort((a, b) => (a.data.order ?? Infinity) - (b.data.order ?? Infinity)); // Final sorted array of collection items const sorted = new Set(); diff --git a/docs/_plugins/cem-shortcodes.cjs b/docs/_plugins/cem-shortcodes.cjs new file mode 100644 index 0000000000..c68483dc59 --- /dev/null +++ b/docs/_plugins/cem-shortcodes.cjs @@ -0,0 +1,456 @@ +/** quick and dirty dedent, also provides in-editor syntax highlighting */ +const html = (...args) => + String.raw(...args) + .split('\n') + .map(x => x.replace(/^ {6}/, '')) + .join('\n'); + +/** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */ +module.exports = function(eleventyConfig) { + eleventyConfig.addPairedShortcode('renderCodeDocs', + function renderCodeDocs(content, kwargs = {}) { + const renderers = new Renderers(this, kwargs); + return renderers.renderAll(content); + } + ); +}; + +function innerMD(content = '') { + const trimmed = content.trim(); + return trimmed && `\n\n\n${trimmed}\n\n\n`; +} + +function mdHeading(content, length = 2) { + const hashes = Array.from({ length }, () => '#').join(''); + return innerMD(`${hashes} ${content}`); +} + +function type(content = '', { lang = 'ts' } = {}) { + return content.trim() && `\n\n\`\`\`${lang}\n${content.trim()}\n\n\`\`\`\n\n`; +} + +function stringifyParams(method) { + return method.parameters?.map?.(p => + `${p.name}: ${p.type?.text ?? 'unknown'}`).join(', ') ?? ''; +} + +function renderBand(content, { level, header = '' } = {}) { + return html` +
    + ${header && mdHeading(header, { level })} + ${innerMD(content)} +
    `; +} + +/** + * docs pages contain a #styling-hooks anchor as back compat for older versions of the page + * to prevent this id from rendering more than once, we track the number of times each page + * renders css custom properties. + */ +const cssStylingHookIdTracker = new WeakSet(); + +/** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ +module.exports = function(eleventyConfig) { + eleventyConfig.addFilter('innerMD', innerMD); + eleventyConfig.addFilter('mdHeading', mdHeading); + eleventyConfig.addFilter('type', type); + eleventyConfig.addFilter('stringifyParams', stringifyParams); + eleventyConfig.addPairedShortcode('band', renderBand); + for (const shortCode of [ + 'renderAttributes', + 'renderCssCustomProperties', + 'renderCssParts', + 'renderEvents', + 'renderMethods', + 'renderOverview', + 'renderProperties', + 'renderInstallation', + 'renderSlots', + ]) { + eleventyConfig.addPairedShortcode(shortCode, function(content, kwargs) { + return Renderers.forPage(this)[shortCode](content, kwargs); + }); + } +}; + +class Renderers { + /** @type{WeakMap} */ + static renderers = new WeakMap(); + static forPage(page) { + return new Renderers(page); + } + + constructor(page) { + if (Renderers.renderers.has(page)) { + return Renderers.renderers.get(page); + } + /** + * NB: since the data for this shortcode is no a POJO, + * but a DocsPage instance, 11ty assigns it to this.ctx._ + * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93 + * @type {DocsPage} + */ + this.docsPage = page.ctx._; + this.manifest = this.docsPage.manifest; + Renderers.renderers.set(page, this); + } + + packageTagName(kwargs) { + if (kwargs.for && !kwargs.for.match(/@/)) { + return kwargs.for; + } else { + const [, tagName = this.tagName] = (kwargs?.for ?? '').match(/@[-\w]+\/(.*)/) ?? []; + return tagName ?? this.docsPage.tagName; + } + } + + /** + * Render the overview of a component page + * @param {string} content - shortcode content + */ + renderOverview(content) { + return html` +
    +

    Overview

    +
    + ${content} +
    +
    + +
    +

    Installation

    + + ~~~shell + npm install ${this.manifest.packageJson.name} + ~~~ + +
    `; + } + + /** + * Render the list of element attributes + * @param {string} content section content + * @param {object} kwargs shortcode keyword args + * @param {string} [kwargs.header] heading text + * @param {number} [kwargs.level] heading level (e.g. `3` for `

    `) + */ + renderAttributes(content, { header = 'Attributes', level = 2, ...kwargs } = {}) { + const _attrs = this.manifest.getAttributes(this.packageTagName(kwargs)) ?? []; + const deprecated = _attrs.filter(x => x.deprecated); + const attributes = _attrs.filter(x => !x.deprecated); + return html` +
    + ${mdHeading(header)}${!content && !attributes.length ? html` + None` : html` + ${innerMD(content)} +
    ${attributes?.map(attribute => html` +
    ${attribute.name}
    +
    + ${innerMD(attribute.description)} +
    ${!attribute.fieldName ? '' : html` +
    DOM Property
    +
    ${attribute.fieldName}
    `} +
    Type
    +
    ${type(attribute.type?.text ?? 'unknown')}
    +
    Default
    +
    ${type(attribute.default ?? 'unknown')}
    +
    +
    `).join('\n') ?? ''} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(attribute => html` +
    ${attribute.name}
    +
    + ${innerMD(attribute.description)} + Note: ${attribute.name} is deprecated. ${innerMD(attribute.deprecated)} +
    ${!attribute.fieldName ? '' : html` +
    DOM Property
    +
    ${attribute.fieldName}
    `} +
    Type
    +
    ${innerMD(attribute.type?.text ?? 'unknown')}
    +
    Default
    +
    ${innerMD(attribute.default ?? 'unknown')}
    +
    +
    `).join('\n')} +
    +
    `} +
    `; + } + + /** Render the list of element DOM properties */ + renderProperties(content, { header = 'DOM Properties', level = 2, ...kwargs } = {}) { + const allProperties = this.manifest.getProperties(this.packageTagName(kwargs)) ?? []; + const deprecated = allProperties.filter(x => x.deprecated); + const properties = allProperties.filter(x => !x.deprecated); + // TODO: inline code highlighting for type and default: render the markdown to html and extract the `` from the `
    `
    +    return html`
    +      
    + ${mdHeading(header)}${!content && !properties.length ? html` + None` : html` + ${innerMD(content)} +
    ${properties.map(property => html` +
    ${property.name}
    +
    + ${innerMD(property.description)} +
    +
    Type
    +
    ${type(property.type?.text ?? 'unknown')}
    +
    Default
    +
    ${type(property.default ?? 'unknown')}
    +
    +
    `).join('\n')} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(property => html` +
    ${property.name}
    +
    + ${innerMD(property.description)} + Note: ${property.name} is deprecated. ${innerMD(property.deprecated)} +
    +
    Type
    +
    ${type(property.type?.text ?? 'unknown')}
    +
    Default
    +
    ${type(property.default ?? 'unknown')}
    +
    +
    `).join('\n')} +
    +
    `} +
    `; + } + + /** + * Render a table of element CSS Custom Properties + * @param {string} content shortcode content + * @param {object} [kwargs] shortcode keyword args + * @param {string} [kwargs.header] heading text + * @param {number} [kwargs.level] heading level (e.g. `3` for `

    `) + */ + renderCssCustomProperties(content, { + header = 'CSS Custom Properties', + level = 2, + ...kwargs + } = {}) { + const allCssProperties = + this.manifest.getCssCustomProperties(this.packageTagName(kwargs)) ?? []; + const cssProperties = allCssProperties.filter(x => !x.deprecated); + const deprecated = allCssProperties.filter(x => x.deprecated); + return html` +
    + ${mdHeading(header)}${!content && !cssProperties.length ? html` + None` : html` + ${innerMD(content)} + + + + + + + + + ${cssProperties.map(prop => html` + + + + + `).join('\n')} + +
    CSS PropertyDescriptionDefault
    ${prop.name}${innerMD(prop.description ?? '')}${!prop.default?.startsWith('#') ? html` + ` : html` + `} + ${prop.default ?? '—'} + +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} + + + + + + + + + ${deprecated.map(prop => html` + + + + + `).join('\n')} + +
    CSS PropertyDescriptionDefault
    ${prop.name}${innerMD(prop.description)}${innerMD(prop.default ?? '—')}
    +
    `} +
    `; + } + + /** Render the list of element CSS Shadow Parts */ + renderCssParts(content, { header = 'CSS Shadow Parts', level = 2, ...kwargs } = {}) { + const allParts = this.manifest.getCssParts(this.packageTagName(kwargs)) ?? []; + const parts = allParts.filter(x => !x.deprecated); + const deprecated = allParts.filter(x => x.deprecated); + return html` +
    + ${mdHeading(header)}${!content && !parts.length ? html` + None` : html` + ${innerMD(content)} +
    ${parts.map(part => html` +
    ${part.name}
    +
    ${innerMD(part.description)}
    `).join('\n')} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(part => html` +
    ${part.name}
    +
    + ${innerMD(part.description)} + Note: ${part.name} is deprecated. ${innerMD(part.deprecated)} +
    `).join('\n')} +
    +
    `} +
    `; + } + + /** Render the list of events for the element */ + renderEvents(content, { header = 'Events', level = 2, ...kwargs } = {}) { + const _events = this.manifest.getEvents(this.packageTagName(kwargs)) ?? []; + const deprecated = _events.filter(x => x.deprecated); + const events = _events.filter(x => !x.deprecated); + return html` +
    + ${mdHeading(header)}${!content && !events.length ? html` + None` : html` + ${innerMD(content)} +
    ${events.map(event => html` +
    ${event.name}
    +
    + ${innerMD(event.description)} + + Event Type: ${type(event.type?.text ?? 'unknown')} + +
    `).join('\n')} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(event => html` +
    ${event.name}
    +
    + ${innerMD(event.description)} + Note: ${event.name} is deprecated. ${innerMD(event.deprecated)} + Event Type: ${type(event.type?.text ?? 'unknown')} +
    `).join('\n')} +
    +
    `} +
    `; + } + + /** + * Render the installation instructions for the element + * @param {string} content shortcode content + * @param {object} [kwargs] shortcode keyword args + * @param {string} [kwargs.header] heading text + * @param {number} [kwargs.level] heading level (e.g. `3` for `

    `) + * @param {string} [kwargs.tagName] tag name to print instructions for + */ + renderInstallation(content, { + header = 'Installation', + level = 2, + tagName = this.docsPage.tagName, + } = {}) { + return html` +
    + ${header} + + We recommend loading elements via a CDN such as [JSPM][inst-jspm] and + using an import map to manage your dependencies. + + For more information on import maps and how to use them, + see the [import map reference on MDN Web Docs][inst-mdn]. + + If you are using node and NPM, you can install this component using npm: + + ~~~shell + npm install ${this.manifest.packageJson.name} + ~~~ + + Then import this component into your project by using a + [bare module specifier][inst-bms]: + + ~~~js + import '@rhds/elements/${tagName}/${tagName}.js'; + ~~~ + + **Please Note** You should either load elements via a CDN or + install them locally through NPM. *Do not do both.* + + ${content} + +
    + + [inst-jspm]: https://jspm.dev + [inst-mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap/ + [inst-bms]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules`; + } + + /** Render the list of element methods */ + renderMethods(content, { header = 'Methods', level = 2, ...kwargs } = {}) { + const allMethods = this.manifest.getMethods(this.packageTagName(kwargs)) ?? []; + const deprecated = allMethods.filter(x => x.deprecated); + const methods = allMethods.filter(x => !x.deprecated); + // TODO: inline code highlighting for type and default: render the markdown to html and extract the `` from the `
    `
    +    return html`
    +      
    + ${mdHeading(header)}${!content && !methods.length ? html` + None` : html` + ${innerMD(content)} +
    ${methods.map(method => html` +
    ${method.name}(${stringifyParams(method)})
    +
    ${innerMD(method.description)}
    `).join('\n')} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(method => html` +
    ${method.name}(${stringifyParams(method)})
    +
    + ${innerMD(method.description)} + Note: ${method.name} is deprecated. ${innerMD(method.deprecated)} +
    `).join('\n')} +
    +
    `} +
    `; + } + + /** Render the list of the element's slots */ + renderSlots(content, { header = 'Slots', level = 2, ...kwargs } = {}) { + const allSlots = this.docsPage.manifest.getSlots(this.packageTagName(kwargs)) ?? []; + const slots = allSlots.filter(x => !x.deprecated); + const deprecated = allSlots.filter(x => x.deprecated); + return html` +
    + ${mdHeading(header)}${!content && !slots.length ? html` + None` : html` + ${innerMD(content)} +
    ${slots.map(slot => html` +
    ${slot.name ? html` + ${slot.name}` : html` + Default Slot`} +
    +
    ${innerMD(slot.description)}
    `).join('\n')} +
    `}${!deprecated.length ? '' : html` +
    + ${mdHeading(`Deprecated ${header}`, { level: level + 1 })} +
    ${deprecated.map(slot => html` +
    ${slot.name ? html` + ${slot.name}` : html` + Default Slot`} +
    +
    + ${innerMD(slot.description)} + Note: ${slot.name} is deprecated. ${innerMD(slot.deprecated)} +
    `).join('\n')} +
    +
    `} +
    `; + } +} diff --git a/docs/_plugins/importMap.cjs b/docs/_plugins/importMap.cjs index 9f27c2aa38..b465d63a82 100644 --- a/docs/_plugins/importMap.cjs +++ b/docs/_plugins/importMap.cjs @@ -8,7 +8,11 @@ function logPerf() { /* eslint-disable no-console */ const chalk = require('chalk'); const TOTAL = performance.measure('importMap-total', 'importMap-start', 'importMap-end'); - const RESOLVE = performance.measure('importMap-resolve', 'importMap-start', 'importMap-afterLocalPackages'); + const RESOLVE = performance.measure( + 'importMap-resolve', + 'importMap-start', + 'importMap-afterLocalPackages' + ); if (TOTAL.duration > 2000) { console.log( `🦥 Import map generator done in ${chalk.red(TOTAL.duration)}ms\n`, @@ -61,7 +65,9 @@ async function getCachedImportMap({ const providers = { '@patternfly': 'nodemodules', ...Object.fromEntries(localPackages?.map(packageName => - packageName.match(/@(rhds|patternfly)/) ? [nothing] : [packageName, 'nodemodules']) ?? []), + packageName.match(/@(rhds|patternfly)/) ? + [nothing] + : [packageName, 'nodemodules']) ?? []), }; delete providers[nothing]; @@ -132,7 +138,7 @@ module.exports = function(eleventyConfig, { inputMap, localPackages, cwd, - assetCache + assetCache, }); }); diff --git a/docs/_plugins/markdown-it.cjs b/docs/_plugins/markdown-it.cjs index e3239e4a12..9d8bf6550a 100644 --- a/docs/_plugins/markdown-it.cjs +++ b/docs/_plugins/markdown-it.cjs @@ -22,21 +22,21 @@ const rhdsPermalink = makePermalink((slug, opts, anchorOpts, state, idx) => { content: /* html */` <${headerOpen.tag} ${headerOpen.attrs.map(([key, value]) => `${key}="${value}"`).join(' ')}> - `.trim() + `.trim(), }), - inline, - Object.assign(new state.Token('html_block', '', 0), { content: /* html */` + inline, + Object.assign(new state.Token('html_block', '', 0), { content: /* html */` `.trim(), - }) + }) ); }); /** @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig */ module.exports = function(eleventyConfig) { eleventyConfig.amendLibrary('md', /** @param {import('markdown-it')} md*/md => md - .set({ html: true, breaks: false }) - .use(markdownItAnchor, { permalink: rhdsPermalink() }) - .use(markdownItAttrs)); + .set({ html: true, breaks: false }) + .use(markdownItAnchor, { permalink: rhdsPermalink() }) + .use(markdownItAttrs)); }; diff --git a/docs/_plugins/rhds.cjs b/docs/_plugins/rhds.cjs index cb72e1f9f9..fc2237eea3 100644 --- a/docs/_plugins/rhds.cjs +++ b/docs/_plugins/rhds.cjs @@ -5,7 +5,6 @@ const path = require('node:path'); const _slugify = require('slugify'); const slugify = typeof _slugify === 'function' ? _slugify : _slugify.default; const capitalize = require('capitalize'); -const { glob } = require('glob'); const exec = require('node:util').promisify(require('node:child_process').exec); const cheerio = require('cheerio'); const RHDSAlphabetizeTagsPlugin = require('./alphabetize-tags.cjs'); @@ -28,7 +27,9 @@ function demoPaths(content) { const el = $(this); const attr = el.attr('href') ? 'href' : 'src'; const val = el.attr(attr); - if (!val) { return; } + if (!val) { + return; + } if (!val.startsWith('http') && !val.startsWith('/') && !val.startsWith('#')) { el.attr(attr, `${isNested ? '../' : ''}${val}`); } else if (val.startsWith('/elements/rh-')) { @@ -50,7 +51,7 @@ const LIGHTDOM_PATH_RE = /href="\.(.*)"/; function prettyDate(dateStr, options = {}) { const { dateStyle = 'medium' } = options; return new Intl.DateTimeFormat('en-US', { dateStyle }) - .format(new Date(dateStr)); + .format(new Date(dateStr)); } function getTagNameSlug(tagName, config) { @@ -127,7 +128,7 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { const filesToCopy = getFilesToCopy(); eleventyConfig.addPassthroughCopy(filesToCopy, { - filter: /** @param {string} path */path => !path.endsWith('.html'), + filter: /** @param {string} path pathname */path => !path.endsWith('.html'), }); eleventyConfig.addTransform('demo-subresources', demoPaths); @@ -143,7 +144,10 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { const { tagName } = tagNameMatch.groups; // slugify the value of each key in aliases creating a new cloned copy - const modifiedAliases = Object.fromEntries(Object.entries(aliases).map(([key, value]) => [slugify(key, { strict: true, lower: true }), value])); + const modifiedAliases = Object.fromEntries(Object.entries(aliases).map(([key, value]) => [ + slugify(key, { strict: true, lower: true }), + value, + ])); // does the tagName exist in the aliases object? const key = Object.keys(modifiedAliases).find(key => modifiedAliases[key] === tagName); @@ -156,8 +160,8 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { const [, path] = match.match(LIGHTDOM_PATH_RE) ?? []; const { pathname } = new URL(path, `file:///${outputPath}`); content = content.replace(`.${path}`, pathname - .replace(`/_site/elements/${redirect.old}/`, `/assets/packages/@rhds/elements/elements/${redirect.new}/`) - .replace('/demo/', '/')); + .replace(`/_site/elements/${redirect.old}/`, `/assets/packages/@rhds/elements/elements/${redirect.new}/`) + .replace('/demo/', '/')); } } } @@ -166,10 +170,10 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { }); eleventyConfig.addFilter('getTitleFromDocs', function(docs) { - return docs.find(x => x.docsPage?.title)?.alias ?? - docs[0]?.alias ?? - docs[0]?.docsPage?.title ?? - eleventyConfig.getFilter('deslugify')(docs[0]?.slug); + return docs.find(x => x.docsPage?.title)?.alias + ?? docs[0]?.alias + ?? docs[0]?.docsPage?.title + ?? eleventyConfig.getFilter('deslugify')(docs[0]?.slug); }); /** get the element overview from the manifest */ @@ -202,7 +206,7 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { return { name: x, url: slug === x ? `/patterns/${slug}` : `/elements/${slug}`, - text: pfeconfig.aliases[x] || deslugify(slug) + text: pfeconfig.aliases[x] || deslugify(slug), }; }).sort((a, b) => a.text < b.text ? -1 : a.text > b.text ? 1 : 0); return related; @@ -215,7 +219,26 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { eleventyConfig.addCollection('sortedColor', async function(collectionApi) { const colorCollection = collectionApi.getFilteredByTags('color'); return colorCollection.sort((a, b) => { - if (a.data.order > b.data.order) { return 1; } else if (a.data.order < b.data.order) { return -1; } else { return 0; } + if (a.data.order > b.data.order) { + return 1; + } else if (a.data.order < b.data.order) { + return -1; + } else { + return 0; + } + }); + }); + + eleventyConfig.addCollection('sortedDevelopers', async function(collectionApi) { + const developersCollection = collectionApi.getFilteredByTags('developers'); + return developersCollection.sort((a, b) => { + if (a.data.order > b.data.order) { + return 1; + } else if (a.data.order < b.data.order) { + return -1; + } else { + return 0; + } }); }); @@ -254,28 +277,31 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) { screenshotPath, permalink, href, - overviewHref + overviewHref, }; } try { + const { glob } = await import('glob'); /** @type {(import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage & { repoStatus?: any[] })[]} */ const elements = await eleventyConfig.globalData?.elements(); const filePaths = (await glob(`elements/*/docs/*.md`, { cwd: process.cwd() })) - .filter(x => x.match(/\d{1,3}-[\w-]+\.md$/)); // only include new style docs - const { repoStatus } = collectionApi.items.find(item => item.data?.repoStatus)?.data || {}; + .filter(x => x.match(/\d{1,3}-[\w-]+\.md$/)); // only include new style docs + const { repoStatus } = collectionApi.items.find(item => item.data?.repoStatus)?.data || []; return filePaths - .map(filePath => { - const props = getProps(filePath); - const docsPage = elements.find(x => x.tagName === props.tagName); - if (docsPage) { docsPage.repoStatus = repoStatus; } - const tabs = filePaths - .filter(x => x.split('/docs/').at(0) === (`elements/${props.tagName}`)) - .sort() - .map(x => getProps(x)); - return { docsPage, tabs, ...props }; - }) - .sort(alphabeticallyBySlug); + .map(filePath => { + const props = getProps(filePath); + const docsPage = elements.find(x => x.tagName === props.tagName); + if (docsPage) { + docsPage.repoStatus = repoStatus; + } + const tabs = filePaths + .filter(x => x.split('/docs/').at(0) === (`elements/${props.tagName}`)) + .sort() + .map(x => getProps(x)); + return { docsPage, tabs, ...props }; + }) + .sort(alphabeticallyBySlug); } catch (e) { // it's important to surface this // eslint-disable-next-line no-console diff --git a/docs/_plugins/shortcodes.cjs b/docs/_plugins/shortcodes.cjs index 9255d44e25..3c26b30bde 100644 --- a/docs/_plugins/shortcodes.cjs +++ b/docs/_plugins/shortcodes.cjs @@ -1,5 +1,9 @@ const Playground = require('./shortcodes/playground.cjs'); -const RepoStatus = require('./shortcodes/repoStatus.cjs'); +const { + RepoStatusList, + RepoStatusChecklist, + RepoStatusTable, +} = require('./shortcodes/repoStatus.cjs'); const RenderInstallation = require('./shortcodes/renderInstallation.cjs'); const ExampleImage = require('./shortcodes/example.cjs'); const Cta = require('./shortcodes/cta.cjs'); @@ -23,7 +27,9 @@ const renderCodeDocs = require('./shortcodes/renderCodeDocs.cjs'); */ module.exports = function(eleventyConfig) { eleventyConfig.addPlugin(Section); - eleventyConfig.addPlugin(RepoStatus); + eleventyConfig.addPlugin(RepoStatusList); + eleventyConfig.addPlugin(RepoStatusChecklist); + eleventyConfig.addPlugin(RepoStatusTable); eleventyConfig.addPlugin(Playground); eleventyConfig.addPlugin(RenderInstallation); eleventyConfig.addPlugin(ExampleImage); diff --git a/docs/_plugins/shortcodes/alert.cjs b/docs/_plugins/shortcodes/alert.cjs index 45ea8758cd..4438abb735 100644 --- a/docs/_plugins/shortcodes/alert.cjs +++ b/docs/_plugins/shortcodes/alert.cjs @@ -7,13 +7,13 @@ module.exports = function(eleventyConfig) { * @param {string} content * @param {Record} attrs */ - function alert(content, { - state = 'info', - title = 'Note:', - style = null, - level = 3, - } = {}) { - return /* html */` + function alert(content, { + state = 'info', + title = 'Note:', + style = null, + level = 3, + } = {}) { + return /* html */` ${title} @@ -23,5 +23,5 @@ module.exports = function(eleventyConfig) { `; - }); + }); }; diff --git a/docs/_plugins/shortcodes/cta.cjs b/docs/_plugins/shortcodes/cta.cjs index 40cdeb1600..a5156ad6aa 100644 --- a/docs/_plugins/shortcodes/cta.cjs +++ b/docs/_plugins/shortcodes/cta.cjs @@ -1,11 +1,14 @@ const { attrMap } = require('./helpers.cjs'); module.exports = function(eleventyConfig) { - eleventyConfig.addPairedShortcode('cta', + eleventyConfig.addPairedShortcode( + 'cta', /** * Render a Call to Action - * @param {string} content - * @param {Record} attrs + * @param {string} content shortcode content + * @param {object} [attrs] cta link attrs + * @param {object} [attrs.href] cta link href + * @param {object} [attrs.target] optional cta link target */ async function cta(content, { href = '#', diff --git a/docs/_plugins/shortcodes/demo.cjs b/docs/_plugins/shortcodes/demo.cjs index 8ecb8fb009..e657f9e0eb 100644 --- a/docs/_plugins/shortcodes/demo.cjs +++ b/docs/_plugins/shortcodes/demo.cjs @@ -9,13 +9,13 @@ module.exports = function(eleventyConfig) { * @param {string} options.palette Palette to apply, e.g. lightest, light see components/_section.scss * @param {string} options.headingLevel The heading level, defaults to 3 */ - function demoShortcode(content, { - headline = null, - palette = 'light', - headingLevel = '3', - } = {}) { - const slugify = eleventyConfig.getFilter('slugify'); - return /* html*/` + function demoShortcode(content, { + headline = null, + palette = 'light', + headingLevel = '3', + } = {}) { + const slugify = eleventyConfig.getFilter('slugify'); + return /* html*/`
    ${!headline ? '' : ` ${headline}`} @@ -33,5 +33,5 @@ ${content.trim()}
    `; - }); + }); }; diff --git a/docs/_plugins/shortcodes/example.cjs b/docs/_plugins/shortcodes/example.cjs index 3c61e4b728..45996268f6 100644 --- a/docs/_plugins/shortcodes/example.cjs +++ b/docs/_plugins/shortcodes/example.cjs @@ -1,24 +1,27 @@ // @ts-check const { attrMap } = require('./helpers.cjs'); - -/** @typedef {import('../shortcodes.cjs').EleventyContext} EleventyContext */ - const { promisify } = require('node:util'); -const Image = require('@11ty/eleventy-img'); -const sizeOf = promisify(/** @type{import('image-size').default}*/(/** @type{unknown}*/(require('image-size') ))); +const EleventyImage = require('@11ty/eleventy-img'); +const sizeOf = promisify( + /** @type{import('image-size').default}*/( + /** @type{unknown}*/( + require('image-size') + ) + ) +); const path = require('path'); /** * generate images and return metadata - * @param {Image.ImageSource} url + * @param {EleventyImage.ImageSource} url * @param {'auto' | number | null} width1x * @param {'auto' | number | null} width2x * @param {string} outputDir * @param {string} urlPath */ async function getImg(url, width1x, width2x, outputDir, urlPath) { - return await Image(url, { + return await EleventyImage(url, { urlPath, outputDir, formats: ['auto'], @@ -50,7 +53,7 @@ async function getImageHTML(opts) { const styles = [`width:${width1x}px`, `height:auto`].join(';'); const img = await getImg(srcHref, width1x, width2x, outputDir, urlPath); const sizes = `(max-width: ${width1x}px) ${width1x}px, ${width2x}px`; - return `${!img ? '' : Image.generateHTML(img, { alt, sizes, style: styles, loading, decoding })}`; + return `${!img ? '' : EleventyImage.generateHTML(img, { alt, sizes, style: styles, loading, decoding })}`; } else { return `${alt}`; } @@ -58,11 +61,11 @@ async function getImageHTML(opts) { /** @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig */ module.exports = function(eleventyConfig) { - eleventyConfig.addShortcode('example', + eleventyConfig.addShortcode( + 'example', /** * Example * An example image or component - * * @param {object} options * @param {string} [options.alt] Image alt text * @param {string} [options.src] Image url @@ -70,10 +73,9 @@ module.exports = function(eleventyConfig) { * @param {string} [options.style] styles for the wrapper * @param {string} [options.wrapperClass] class names for container element * @param {string} [options.headline] Text to go in the heading - * @param {string} [options.palette='light'] Palette to apply, e.g. lightest, light see components/_section.scss - * @param {2|3|4|5|6} [options.headingLevel=3] The heading level - * @param {boolean} [options.srcAbsolute=false] If true, doesn't include the page url in the img src - * @this {EleventyContext} + * @param {string} [options.palette] Palette to apply, e.g. lightest, light see components/_section.scss + * @param {2|3|4|5|6} [options.headingLevel] The heading level + * @param {boolean} [options.srcAbsolute] If true, doesn't include the page url in the img src */ async function example({ alt = '', @@ -95,7 +97,17 @@ module.exports = function(eleventyConfig) { const loading = 'lazy'; const decoding = 'async'; const classes = `example example--palette-${palette} ${wrapperClass ?? ''}`; - const imageHTML = src && await getImageHTML({ alt, decoding, imageFile, loading, outputDir, src, srcHref, style, urlPath }); + const imageHTML = src && await getImageHTML({ + alt, + decoding, + imageFile, + loading, + outputDir, + src, + srcHref, + style, + urlPath, + }); return /* html */`
    ${!headline ? '' : ` diff --git a/docs/_plugins/shortcodes/feedback.cjs b/docs/_plugins/shortcodes/feedback.cjs index 490bfc2807..421f6fc28c 100644 --- a/docs/_plugins/shortcodes/feedback.cjs +++ b/docs/_plugins/shortcodes/feedback.cjs @@ -11,8 +11,8 @@ module.exports = function(eleventyConfig) { * @param {string} options.palette Palette to apply, e.g. lightest, light see components/_section.scss * @param {string} options.headingLevel The heading level, defaults to 3 */ - function demoShortcode(content) { - return /* html*/` + function demoShortcode(content) { + return /* html*/` `; - }); + }); }; diff --git a/docs/_plugins/shortcodes/helpers.cjs b/docs/_plugins/shortcodes/helpers.cjs index 521c691a33..ee50db4eae 100644 --- a/docs/_plugins/shortcodes/helpers.cjs +++ b/docs/_plugins/shortcodes/helpers.cjs @@ -16,7 +16,7 @@ function getAttrMapValue(k, v) { */ exports.attrMap = function attrMap(attrObj) { return Object.entries(attrObj) - .filter(([, v]) => v != null) - .map(([k, v]) => `${k}="${getAttrMapValue(k, v)}"`) - .join(' '); + .filter(([, v]) => v != null) + .map(([k, v]) => `${k}="${getAttrMapValue(k, v)}"`) + .join(' '); }; diff --git a/docs/_plugins/shortcodes/playground.cjs b/docs/_plugins/shortcodes/playground.cjs index 4f7de7a88f..748415cb3d 100644 --- a/docs/_plugins/shortcodes/playground.cjs +++ b/docs/_plugins/shortcodes/playground.cjs @@ -23,8 +23,8 @@ async function playground(_, { const options = getPfeConfig(); const { filePath } = docsPage.manifest - .getDemoMetadata(tagName, options) - ?.find(x => x.url === `https://ux.redhat.com/elements/${x.slug}/demo/`) ?? {}; + .getDemoMetadata(tagName, options) + ?.find(x => x.url === `https://ux.redhat.com/elements/${x.slug}/demo/`) ?? {}; const content = filePath && await readFile(filePath, 'utf8'); return /* html*/` diff --git a/docs/_plugins/shortcodes/renderCodeDocs.cjs b/docs/_plugins/shortcodes/renderCodeDocs.cjs index 4a3d859ea8..6b318d0d2a 100644 --- a/docs/_plugins/shortcodes/renderCodeDocs.cjs +++ b/docs/_plugins/shortcodes/renderCodeDocs.cjs @@ -5,17 +5,17 @@ const { copyCell, getTokenHref } = require('../tokensHelpers.cjs'); /** quick and dirty dedent, also provides in-editor syntax highlighting */ const html = (...args) => String.raw(...args) - .split('\n') - .map(x => x.replace(/^ {6}/, '')) - .join('\n'); + .split('\n') + .map(x => x.replace(/^ {6}/, '')) + .join('\n'); /** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */ module.exports = function(eleventyConfig) { eleventyConfig.addPairedShortcode('renderCodeDocs', - function renderCodeDocs(content, kwargs = {}) { - const renderers = new Renderers(this, kwargs); - return renderers.renderAll(content); - } + function renderCodeDocs(content, kwargs = {}) { + const renderers = new Renderers(this, kwargs); + return renderers.renderAll(content); + } ); }; @@ -214,8 +214,12 @@ class Renderers { } /** Render a table of element Design Tokens */ - renderTokens(content, { header = 'Design Tokens', level = 2, ...kwargs } = {}) { - const allCssProperties = this.manifest.getCssCustomProperties(this.packageTagName(kwargs)) ?? []; + renderTokens(content, { + header = 'Design Tokens', + ...kwargs + } = {}) { + const allCssProperties = + this.manifest.getCssCustomProperties(this.packageTagName(kwargs)) ?? []; const elTokens = allCssProperties.filter(x => tokens.has(x.name)); return html`
    @@ -242,8 +246,12 @@ class Renderers { } /** Render a table of element CSS Custom Properties */ - renderCssCustomProperties(content, { header = 'CSS Custom Properties', level = 2, ...kwargs } = {}) { - const allCssProperties = this.manifest.getCssCustomProperties(this.packageTagName(kwargs)) ?? []; + renderCssCustomProperties(content, { + header = 'CSS Custom Properties', + level = 2, ...kwargs + } = {}) { + const allCssProperties = + this.manifest.getCssCustomProperties(this.packageTagName(kwargs)) ?? []; const cssProperties = allCssProperties.filter(x => !x.deprecated && !tokens.has(x.name)); const deprecated = allCssProperties.filter(x => x.deprecated && !tokens.has(x.name)); return html` diff --git a/docs/_plugins/shortcodes/renderInstallation.cjs b/docs/_plugins/shortcodes/renderInstallation.cjs index b6b13e1c93..f03497b209 100644 --- a/docs/_plugins/shortcodes/renderInstallation.cjs +++ b/docs/_plugins/shortcodes/renderInstallation.cjs @@ -26,13 +26,25 @@ fully initialized. return /* md */` + +
    ## Installation ${!docsPage.manifest?.packageJson ? '' : /* md */` Red Hat CDN - +

    CDN Prerelease

    We are currently working on our CDN, which will be soon moving @@ -81,9 +93,9 @@ ${!lightdomcss ? '' : /* md */`${lightdomcssblock} ~~~ `} - + NPM - + Install RHDS using your team's preferred NPM package manager, e.g. @@ -111,9 +123,9 @@ Replace \`/path/to\` in the \`href\` attribute with the installation path to the \`${docsPage.tagName}\` directory in your project. `} - + JSPM - +

    Public CDNs

    JSPM and other public CDNs should not be used on corporate domains. @@ -153,7 +165,7 @@ ${!lightdomcss ? '' : /* md */ `${lightdomcssblock} ~~~ `} - + ${content ?? ''}`} diff --git a/docs/_plugins/shortcodes/repoStatus.cjs b/docs/_plugins/shortcodes/repoStatus.cjs index 5b511ba84e..746488e5e2 100644 --- a/docs/_plugins/shortcodes/repoStatus.cjs +++ b/docs/_plugins/shortcodes/repoStatus.cjs @@ -1,55 +1,345 @@ -const fs = require('fs'); +const STATUS_LEGEND = { + 'Planned': { + description: 'Ready to be worked on or ready to be released', + color: 'gray', + variant: 'filled', + icon: /* html*/` + + + + + + + + + `, + }, + 'In Progress': { + description: 'In the design or development process', + color: 'green', + variant: 'outline', + icon: /* html*/` + + + + +`, + }, + 'Ready': { + description: 'Ready to use and approved by all team members', + color: 'green', + variant: 'filled', + icon: /* html*/` + + + +`, + }, + 'Deprecated': { + description: 'No longer supported by RHDS', + color: 'orange', + variant: 'filled', + icon: /* html*/` + + + + +`, + }, + 'N/A': { + description: 'Not planned, not available, or does not apply', + color: 'gray', + variant: 'outline', + icon: /* html*/` + + + +`, + }, + 'Beta': { + color: 'purple', + variant: 'outline', + icon: /* html*/` + + + + + `, + }, + 'Experimental': { + color: 'orange', + variant: 'outline', + icon: /* html*/` + + + + +`, + }, + 'New': { + color: 'cyan', + variant: 'outline', + icon: /* html*/` + + + + + + + + `, + }, + +}; + +const STATUS_CHECKLIST = { + 'Figma library': { + 'Ready': 'Component is available in the Figma library', + 'In progress': 'Component will be added to the Figma library when ready', + 'Planned': 'Component is scheduled to be worked on', + 'Deprecated': 'Component has been removed from the current Figma library ', + 'N/A': 'Component not available in the Figma library', + }, + 'Responsive': { + 'Ready': 'Component responds to changing viewport sizes in Figma and the browser', + 'N/A': 'Responsiveness does not apply to this component', + }, + 'RH Elements': { + 'Ready': 'Component is available as a web component', + 'In progress': 'Component will be added to the RH Elements repo when ready', + 'Planned': 'Component is scheduled to become a web component', + 'Deprecated': 'Component is no longer a web component', + 'N/A': 'Component not available as a web component', + }, + 'webRH': { + 'Ready': 'Component is available as a web component', + 'In progress': 'Component will be added to the webRH repo when ready', + 'Planned': 'Component is scheduled to become a web component', + 'Deprecated': 'Component is no longer a web component', + 'N/A': 'Component not available as a web component', + }, +}; /** - * Reads component status data from global data (see above) and outputs a table for each component - * @this {EleventyContext} + * Reads repo status data from global data and outputs an array with component keys */ -function repoStatus({ heading = 'Repo status', type = 'Pattern', level = 2 } = {}) { - return ''; - // https://github.com/RedHat-UX/red-hat-design-system/issues/1174 - // eslint-disable-next-line no-unreachable - const headingLevel = Array.from({ length: level }, () => '#').join(''); - const checkSVG = fs.readFileSync('node_modules/@patternfly/icons/fas/check.svg', 'utf8'); - /** @type {string[][]} */ +function getRepoData() { const docsPage = this.ctx._; const allStatuses = this.ctx.repoStatus ?? docsPage?.repoStatus ?? []; const title = this.ctx.title ?? docsPage?.title; - const [header, ...repoStatus] = allStatuses; - if (Array.isArray(header)) { - header[1] = type; + return allStatuses.find( + component => component.name === title && component.type === 'Element' + )?.libraries; +} + +/** + * Calls getRepoData function and outputs a definition list for each component + */ +function repoStatusList({ heading = 'Status', level = 2 } = {}) { + // Removing Documentation status from the repoStatusList + const statusList = getRepoData.call(this)?.filter(repo => repo.name !== 'Documentation'); + + if (!Array.isArray(statusList) || !statusList.length) { + return ''; + } else { + return /* html */` +

    + + +
    +
    + ${statusList.map(listItem => { + return /* html */` +
    +
    ${listItem.name}
    +
    + +${listItem.status}${STATUS_LEGEND[listItem.status].icon} + +
    +
    `; + }).join('\n').trim()} +
    +
    +
    `; } - const bodyRows = repoStatus.filter(([id]) => id === title); - if (!Array.isArray(bodyRows) || !bodyRows.length) { +} + +/** + * Reads component status data from global data (see above) and outputs a table for Design/Code status page + */ +function repoStatusTable() { + const docsPage = this.ctx._; + const allStatuses = this.ctx.repoStatus ?? docsPage?.repoStatus ?? []; + // Filtering out 'Responsive' status from all the libraries + const elementsList = allStatuses.map(item => ({ + ...item, + libraries: item.libraries.filter(lib => lib.name !== 'Responsive'), + })); + + if (!Array.isArray(elementsList) || !elementsList.length) { return ''; } else { - return /* html*/` + // This table renders the current state of all the components on the Design/Code status page + return /* html */` + + +++++++ + + + + + + + + + + + ${elementsList.map(listItem => { + return /* html */` + + +${listItem.libraries.map(lib => { + return /* html */` + + `; + }).join('\n').trim()} +`; + }).join('\n').trim()} + +
    NameFigma libraryRH ElementsWebRHDocumentation
    + + ${listItem.name} + ${listItem.overallStatus !== 'Available' ? + ` + ${listItem.overallStatus}${STATUS_LEGEND[listItem.overallStatus].icon} + ` : ''} + + + + + ${lib.status}${STATUS_LEGEND[lib.status].icon} + + +
    +
    +
    `; + } +} +/** + * Calls getRepoData function and outputs a status checklist table for each component + */ +function repoStatusChecklist({ heading = 'Status checklist', level = 2 } = {}) { + const headingLevel = Array.from({ length: level }, () => '#').join(''); + const statusList = getRepoData.call(this)?.filter(repo => repo.name !== 'Documentation'); + if (!Array.isArray(statusList) || !statusList.length) { + return ''; + } else { + // This is the checklist table to be used on all the "Overview" tab in docs and is different from the table used in Design/Code Status page + return /* html */`
    ${`${headingLevel} ${heading} {.section-title .pfe-jump-links-panel__section}`} - -

    Learn more about our various code repos by visiting this page.

    -
    - - - ${header.slice(1).map(x => ` - `.trim()).join('\n').trim()} - - - ${bodyRows.map(([, rowHeader, ...columns]) => ` - - - ${columns.map(x => ``.trim()).join('\n').trim()} - `.trim()).join('\n').trim()} - -
    ${x}
    ${rowHeader}${x === 'x' ? `${checkSVG}` : ''}
    +
    + + +++++ + + + + + + + + + ${statusList.map(listItem => { + return /* html */` + + + + +`; + }).join('\n').trim()} + +
    PropertyStatusMeaning
    ${listItem.name} + + +${listItem.status}${STATUS_LEGEND[listItem.status].icon} + + +${STATUS_CHECKLIST[listItem.name][listItem.status]}
    +
    -
    - - `; +
    `; } } -module.exports = function(eleventyConfig) { - eleventyConfig.addShortcode('repoStatus', repoStatus); -}; +function RepoStatusList(eleventyConfig) { + eleventyConfig.addShortcode('repoStatusList', repoStatusList); +} + +function RepoStatusChecklist(eleventyConfig) { + eleventyConfig.addShortcode('repoStatusChecklist', repoStatusChecklist); +} + +function RepoStatusTable(eleventyConfig) { + eleventyConfig.addShortcode('repoStatusTable', repoStatusTable); +} + +module.exports = { RepoStatusList, RepoStatusChecklist, RepoStatusTable }; diff --git a/docs/_plugins/shortcodes/rh-playground.js b/docs/_plugins/shortcodes/rh-playground.js index 6688ee49f1..0b41a74977 100644 --- a/docs/_plugins/shortcodes/rh-playground.js +++ b/docs/_plugins/shortcodes/rh-playground.js @@ -77,8 +77,12 @@ class RhPlayground extends LitElement { const { config } = await import(`/assets/playgrounds/${this.tagName}-playground.js`); this.config = config; this.demos = Object.entries(config.files ?? {}) - .filter(([, { contentType }]) => contentType?.startsWith('text/html')) - .map(([filename, { label }]) => ({ filename, label, active: filename === 'demo/index.html' })); + .filter(([, { contentType }]) => contentType?.startsWith('text/html')) + .map(([filename, { label }]) => ({ + filename, + label, + active: filename === 'demo/index.html', + })); await import('playground-elements'); this.requestUpdate(); this.show(); diff --git a/docs/_plugins/shortcodes/spacerTokensTable.cjs b/docs/_plugins/shortcodes/spacerTokensTable.cjs index 9e78ed6c37..cab4cab9b9 100644 --- a/docs/_plugins/shortcodes/spacerTokensTable.cjs +++ b/docs/_plugins/shortcodes/spacerTokensTable.cjs @@ -2,11 +2,11 @@ const { tokens: metaTokens } = require('@rhds/tokens/meta.js'); /** * Reads token data from @rhds/tokens and outputs a table for specified tokens - * @this {EleventyContext} + * @param {import('@11ty/eleventy').UserConfig} eleventyConfig computed config */ - module.exports = function(eleventyConfig) { - eleventyConfig.addPairedShortcode('spacerTokensTable', + eleventyConfig.addPairedShortcode( + 'spacerTokensTable', function(content, { tokens = '', style, @@ -14,11 +14,11 @@ module.exports = function(eleventyConfig) { headingLevel = '3', caption = '', wrapperClass, - palette = 'light' + palette = 'light', } = {}) { const slugify = eleventyConfig.getFilter('slugify'); const tokenList = (Array.isArray(tokens) ? tokens : tokens.split(',')) - .map(token => token.trim()).filter(Boolean); + .map(token => token.trim()).filter(Boolean); const metaData = []; if (tokenList.length === 0) { diff --git a/docs/_plugins/shortcodes/tokensTable.cjs b/docs/_plugins/shortcodes/tokensTable.cjs index a157d32a39..a8e22436ec 100644 --- a/docs/_plugins/shortcodes/tokensTable.cjs +++ b/docs/_plugins/shortcodes/tokensTable.cjs @@ -4,8 +4,8 @@ module.exports = function(eleventyConfig) { * Tokens Table * Display a table rows with token usage */ - function(content) { - const show = process.env.SHOW_TOKENS_TABLE; - return show !== 'false' ? `${content}` : ``; - }); + function(content) { + const show = process.env.SHOW_TOKENS_TABLE; + return show !== 'false' ? `${content}` : ``; + }); }; diff --git a/docs/_plugins/tokens.cjs b/docs/_plugins/tokens.cjs index 33b89e22c1..bf7e6f1e15 100644 --- a/docs/_plugins/tokens.cjs +++ b/docs/_plugins/tokens.cjs @@ -14,7 +14,7 @@ const { /** * Generate an HTML table of tokens - * @param {object} [opts={}] + * @param {object} [opts] * @param {object} opts.tokens the collection of tokens to render * @param {string} opts.name the name of the collection * @param {object} opts.docs the docs extension for the collection @@ -35,33 +35,33 @@ function table({ tokens, name = '', docs, options } = {}) { - ${tokens.map(token => { /* eslint-disable indent */ - const { r, g, b } = token.attributes?.rgb ?? {}; - const { h, s, l } = token.attributes?.hsl ?? {}; - const isColor = !!token.path.includes('color'); - const isCrayon = isColor && token.name.match(/0$/); - const isDimension = token.$type === 'dimension'; - const isHSLorRGB = isColor && !!token.name.match(/(hsl|rgb)$/); - const isFamily = !!token.path.includes('family'); - const isFont = !!token.path.includes('font'); - const isRadius = !!token.path.includes('radius'); - const isSize = !!token.path.includes('size'); - const isWeight = !!token.path.includes('weight'); - const isWidth = !!token.path.includes('width'); - - return isHSLorRGB ? '' : /* html */` + ${tokens.map(token => { + const { r, g, b } = token.attributes?.rgb ?? {}; + const { h, s, l } = token.attributes?.hsl ?? {}; + const isColor = !!token.path.includes('color'); + const isCrayon = isColor && token.name.match(/0$/); + const isDimension = token.$type === 'dimension'; + const isHSLorRGB = isColor && !!token.name.match(/(hsl|rgb)$/); + const isFamily = !!token.path.includes('family'); + const isFont = !!token.path.includes('font'); + const isRadius = !!token.path.includes('radius'); + const isSize = !!token.path.includes('size'); + const isWeight = !!token.path.includes('weight'); + const isWidth = !!token.path.includes('width'); + + return isHSLorRGB ? '' : /* html */` + '--radius': isRadius ? token.$value : 'initial', + '--width': isWidth ? token.$value : 'initial', + '--color': isColor ? token.$value : 'initial', + '--font-family': isFamily ? token.$value : 'var(--rh-font-family-body-text)', + '--font-size': isSize ? token.$value : 'var(--rh-font-size-heading-md)', + '--font-weight': isWeight ? token.$value : 'var(--rh-font-weight-body-text-regular)', + [`--${token.attributes.type === 'icon' && token.$type === 'dimension' ? `${name}-size` : name}`]: token.$value, + })}"> ${isColor && token.path.includes('text') ? 'Aa' @@ -122,23 +122,25 @@ function table({ tokens, name = '', docs, options } = {}) { `}`; - }).map(dedent).join('\n')} + }).map(dedent).join('\n')} `).trim(); - /* eslint-enable indent */ } /** Returns Markdown from the Tokens source YAML files OR from linked markdown files */ function getTokenDocs(path) { - const { parent, key } = getParentCollection({ path }, require('@rhds/tokens/json/rhds.tokens.json')); + const { parent, key } = + getParentCollection({ path }, require('@rhds/tokens/json/rhds.tokens.json')); const collection = parent[key]; return getDocs(collection, { docsExtension: 'com.redhat.ux' }); } /** * @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig - * @param {PluginOptions} [pluginOptions={}] + * @param {PluginOptions} [pluginOptions] */ -module.exports = function RHDSPlugin(eleventyConfig, pluginOptions = { }) { +module.exports = function RHDSPlugin( + eleventyConfig, + pluginOptions = { }) { eleventyConfig.addGlobalData('tokens', tokensJSON); eleventyConfig.addGlobalData('tokenCategories', require('./tokenCategories.json')); @@ -160,7 +162,8 @@ module.exports = function RHDSPlugin(eleventyConfig, pluginOptions = { }) { [join(__dirname, '11ty', '*')]: assetsPath, }); - eleventyConfig.addShortcode('category', + eleventyConfig.addShortcode( + 'category', async function category(options = {}) { options.tokens ??= await eleventyConfig.globalData.tokens; options.attrs ??= pluginOptions.attrs ?? (() => ''); @@ -170,10 +173,15 @@ module.exports = function RHDSPlugin(eleventyConfig, pluginOptions = { }) { const path = options.path ?? '.'; const level = options.level ?? 2; const exclude = options.exclude ?? []; - const include = Array.isArray(options.include) ? options.include : [options.include].filter(Boolean); + const include = Array.isArray(options.include) ? + options.include + : [options.include].filter(Boolean); const name = options.name ?? path.split('.').pop(); - const { parent, key } = getParentCollection(options, eleventyConfig.globalData.tokens ?? eleventyConfig.globalData?.tokenCategories); + const { parent, key } = getParentCollection( + options, + eleventyConfig.globalData.tokens ?? eleventyConfig.globalData?.tokenCategories + ); const collection = parent[key]; const docs = getDocs(collection, options); const heading = docs?.heading ?? capitalize(name.replace('-', ' ')); @@ -185,17 +193,20 @@ module.exports = function RHDSPlugin(eleventyConfig, pluginOptions = { }) { * @example isChildEntry(['500', tokens.color.blue.500]); // false */ const isChildEntry = ([key, value]) => - !value.$value && typeof value === 'object' && !key.startsWith('$') && !exclude.includes(key); + !value.$value + && typeof value === 'object' + && !key.startsWith('$') + && !exclude.includes(key); const children = Object.entries(collection) - .filter(isChildEntry) - .map(([key], i, a) => ({ - path: key, - parent: collection, - level: level + 1, - parentName: `${parentName} ${name}`.trim(), - isLast: i === a.length - 1, - })); + .filter(isChildEntry) + .map(([key], i, a) => ({ + path: key, + parent: collection, + level: level + 1, + parentName: `${parentName} ${name}`.trim(), + isLast: i === a.length - 1, + })); /** * 0. render the description @@ -210,18 +221,18 @@ module.exports = function RHDSPlugin(eleventyConfig, pluginOptions = { }) { ${(dedent(await getDescription(collection, pluginOptions)))}
    - ${await table({ /* eslint-disable indent */ - tokens: Object.values(collection).filter(x => x.$value), - options, - name, - docs, - })/* eslint-enable indent */} + ${await table({ + tokens: Object.values(collection).filter(x => x.$value), + options, + name, + docs, + })} ${(await Promise.all(children.map(category))).join('\n')} - ${(await Promise.all(include.map((path, i, a) => category({ /* eslint-disable indent */ - path, - level: level + 1, - isLast: !a[i + 1], - })))).join('\n')/* eslint-enable indent*/} + ${(await Promise.all(include.map((path, i, a) => category({ + path, + level: level + 1, + isLast: !a[i + 1], + })))).join('\n')} `); }); }; diff --git a/docs/_plugins/tokensHelpers.cjs b/docs/_plugins/tokensHelpers.cjs index 9a0511eeb9..d5826bea11 100644 --- a/docs/_plugins/tokensHelpers.cjs +++ b/docs/_plugins/tokensHelpers.cjs @@ -53,7 +53,7 @@ function getDescription(collection, options) { const { filePath = getFilePathGuess(collection), description = '', - descriptionFile + descriptionFile, } = getDocs(collection, options) ?? {}; if (description) { diff --git a/docs/accessibility/content.md b/docs/accessibility/content.md index e2675eba8d..2e0ff1aeb8 100644 --- a/docs/accessibility/content.md +++ b/docs/accessibility/content.md @@ -3,9 +3,17 @@ title: Content tags: accessibility importElements: - rh-code-block + - rh-blockquote + - rh-table --- + + + + + + + + + + Ready to be worked on or ready to be released + + + + + + In progress + + + + + + + + + In the design or development process + + + + + + Ready + + + + + + + + Ready to use and approved by all team members + + + + + + Deprecated + + + + + + + + + No longer supported by RHDS + + + + + + N/A + + + + + + + + Not planned, not available, or does not apply + + + + {%- endcall %} -{% call components.section("Design/code status") -%} -

    Last updated on October 5, 2022.

    +{% call components.section("Web component status") -%} -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Foundation or component nameDesign statusCode statusDocumentation
    AccordionRefresh in progress
    PFE
    Studio
    WebRH
    AlertComplete
    Adobe Target
    PFE
    RHDS
    PFE (backlog)
    AnnouncementComplete
    Studio
    Audio player (redesign)CompleteBacklog
    ux.redhat.com (backlog)
    AvatarComplete
    PFE
    Studio
    BlockquoteComplete
    RHDS
    Studio
    WebRH
    ux.redhat.com
    BreadcrumbComplete
    WebRH
    ux.redhat.com (backlog)
    ButtonComplete
    PFE
    Custom
    Button (Stateful)Complete
    PFE
    ux.redhat.com (backlog)
    Call to actionComplete
    PFE
    RHDS
    Custom
    CardComplete
    PFE
    Custom
    Code blockIn progress
    PFE
    ux.redhat.com (backlog)
    PFE (backlog)
    Cookie alertComplete
    WebRH
    ux.redhat.com (backlog)
    DisclosureComplete
    PFE
    WebRH
    Dialog (content)Complete
    PFE
    RHDS
    Dialog (video)Complete
    RHDS
    DropdownComplete
    PFE
    ux.redhat.com (backlog)
    FilterComplete
    WebRH
    ux.redhat.com
    FooterComplete
    RHDS
    -
    FormComplete
    WebRH
    GridComplete
    Studio
    WebRH
    HeroBacklogNot coded yetux.redhat.com (backlog)
    Jump linksComplete
    PFE
    Custom
    LabelCompleteIn progress
    ux.redhat.com (in progress)
    LinkComplete
    Studio
    WebRH
    Link with iconComplete
    Custom
    ux.redhat.com
    Navigation (primary)Complete
    PFE
    WebRH
    Navigation (secondary)Complete
    RHDS
    Navigation (universal)Complete
    Custom
    ux.redhat.com (backlog)
    PaginationCompleteIn progress
    ux.redhat.com (backlog)
    PFE (backlog)
    PopoverComplete
    Studio
    WebRH
    Progress stepsComplete
    PFE
    Search barComplete
    Custom
    ux.redhat.com
    Skip navigationComplete
    PFE
    SpacerComplete
    Custom
    ux.redhat.com
    StatisticComplete
    RHDS
    ux.redhat.com
    Sticky bannerComplete
    Adobe Target
    Studio
    ux.redhat.com
    Sticky cardComplete
    Adobe Target
    Studio
    ux.redhat.com
    SwitchCompleteIn progressux.redhat.com (backlog)
    TableCompleteIn progress
    ux.redhat.com (backlog)
    PFE (backlog)
    TabsComplete
    PFE
    Studio
    WebRH
    TooltipComplete
    RHDS
    Custom
    ux.redhat.com
    TypographyComplete
    PFE
    Studio
    WebRH
    Video thumbnailComplete
    Studio
    ux.redhat.com
    -
    -
    + {% repoStatusTable %} {%- endcall %} diff --git a/docs/elements/elements.html b/docs/elements/elements.html index 137068d878..21f14c1d72 100644 --- a/docs/elements/elements.html +++ b/docs/elements/elements.html @@ -16,6 +16,7 @@ title: "{{ doc.pageTitle }} | {{ doc.slug | deslugify }}" importElements: - rh-alert + - rh-tag - rh-cta - rh-footer - rh-subnav diff --git a/docs/foundations/color/accessibility.md b/docs/foundations/color/accessibility.md index ad1dd89f86..0254f64758 100644 --- a/docs/foundations/color/accessibility.md +++ b/docs/foundations/color/accessibility.md @@ -1,10 +1,11 @@ --- -layout: layout-foundations.njk +layout: layout-with-subnav.njk title: Accessibility heading: Color tags: - color permalink: /foundations/color/accessibility/index.html +subNavCollection: sortedColor order: 20 bodyClasses: element-docs --- diff --git a/docs/foundations/color/index.md b/docs/foundations/color/index.md index be16e80aab..786ce2515c 100644 --- a/docs/foundations/color/index.md +++ b/docs/foundations/color/index.md @@ -1,11 +1,12 @@ --- -layout: layout-foundations.njk +layout: layout-with-subnav.njk title: Overview heading: Color tags: - foundations - color permalink: /foundations/color/index.html +subNavCollection: sortedColor order: 00 bodyClasses: element-docs --- diff --git a/docs/foundations/color/usage.md b/docs/foundations/color/usage.md index 7735d2b969..4a3dd66f29 100644 --- a/docs/foundations/color/usage.md +++ b/docs/foundations/color/usage.md @@ -1,10 +1,11 @@ --- -layout: layout-foundations.njk +layout: layout-with-subnav.njk title: Usage heading: Color tags: - color permalink: /foundations/color/usage/index.html +subNavCollection: sortedColor order: 10 bodyClasses: element-docs --- diff --git a/docs/get-started.md b/docs/get-started.md index 36ebc2c841..9eb0f0f817 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -37,13 +37,15 @@ The Red Hat Design System for digital experiences gives designers and developers
    Designers
    -
    - {% example - palette="descriptive", - alt="Card overlapping code editor user interface", - src="/assets/get-started/developers.png" %} -
    Developers (Coming soon)
    -
    + +
    + {% example + palette="descriptive", + alt="Card overlapping code editor user interface", + src="/assets/get-started/developers.png" %} +
    Developers
    +
    +
    {% feedback %} diff --git a/docs/get-started/designers.md b/docs/get-started/designers.md index 3d3bad2cf8..be53c9d084 100644 --- a/docs/get-started/designers.md +++ b/docs/get-started/designers.md @@ -1,6 +1,7 @@ --- layout: layout-basic.njk title: Designers +heading: Designers order: 2 tags: - getstarted @@ -39,6 +40,10 @@ Our design system libraries and the documentation website offer assets and guida

    Elements and patterns

    Our libraries include elements and patterns you can use to create digital experiences.

    +
    +

    Accessibility

    +

    Designer-specific guidelines equip you with the information to create inclusive digital experiences.

    +
    ## Access Figma diff --git a/docs/get-started/developers/index.md b/docs/get-started/developers/index.md new file mode 100644 index 0000000000..d25fdc1ec7 --- /dev/null +++ b/docs/get-started/developers/index.md @@ -0,0 +1,52 @@ +--- +layout: layout-with-subnav.njk +title: Overview +heading: Developers +tags: + - getstarted + - developers +permalink: /get-started/developers/index.html +subNavCollection: sortedDevelopers +order: 00 +bodyClasses: element-docs +--- + +## Introduction + +Welcome to the **Red Hat Design System** (RHDS) for digital experiences. If you need to develop something using our design system, you have come to the right place. + +Read this section to get started and e-mail [design-system@redhat.com](mailto:design-system@redhat.com) or connect with us on Slack if you have any questions along the way. + +## Learn about our design system + +Our design system libraries and the documentation website offer assets and guidance needed to create digital experiences. Please use these resources to have a better understanding of how to use our design system. + +
    +
    +

    Foundations

    +

    Foundations are how we express our brand through color, space, typography, etc.

    +
    +
    +

    Design tokens

    +

    Design tokens are how we translate our design language decisions into code.

    +
    +
    +

    Documentation

    +

    This website offers guidance about how to use our elements and patterns. Learn how to apply them accessibily with developer-specific guidelines.

    +
    +
    +

    GitHub repositories

    +

    Explore our code, roadmaps, and discussions in the Red Hat Design System repo and the Red Hat Design Tokens repo.

    +
    +
    + +## About web components + +Web components are based on a set of web platform APIs that help to create reusable and encapsulated UI elements. Those standards consist of custom elements, shadow DOM, and HTML Templates. + +Because they're able to work with any framework that supports HTML, web components can be used without having to rework all of your code and are less likely to be affected by changes in preferred web stacks. Encapsulation within the shadow DOM prevents a page's code from breaking a component's style and allows for a scalable design system. + +{% feedback %} +

    Designers

    +

    To get started using our design system as a designer, go to the Designers page.

    +{% endfeedback %} \ No newline at end of file diff --git a/docs/get-started/developers/installation.md b/docs/get-started/developers/installation.md new file mode 100644 index 0000000000..e4d6db4240 --- /dev/null +++ b/docs/get-started/developers/installation.md @@ -0,0 +1,107 @@ +--- +layout: layout-with-subnav.njk +title: Installation +heading: Developers +tags: + - developers +permalink: /get-started/developers/installation/index.html +subNavCollection: sortedDevelopers +order: 10 +bodyClasses: element-docs +--- + +## How to install + +There are three ways you can install the Red Hat Design System's web components: CDN, NPM, or JSPM. Each element's "Code" page includes the same installation information with code snippets that are specific to that element. + +### Red Hat CDN + +{% alert title="CDN Prerelease", + state="warning" %} +

    We are currently working on our CDN, which will be soon moving into beta. This will be the preferred method of installation in the near future. If you are a Red Hat associate and have questions or comments about the CDN or installation process please connect with us on Slack.

    +{% endalert %} + +The recommended way to load RHDS is via the Red Hat Digital Experience CDN, and using an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap). + +If you have full control over the page you are using, add an import map to the ``, pointing to the CDN, or update any existing import map. If you are not responsible for the page's ``, request that the page owner makes the change on your behalf. + + + + + +Once the import map is established, you can load the element with the following module, containing a [bare module specifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). The example below shows how you'd load in <`rh-button>`. + + + + + +Note that modules may be placed in the ``. Since they are deferred by default, they will not block rendering. + +### NPM + +Install RHDS using your team's preferred NPM package manager. + + + + + +Once that's been accomplished, you will need to use a bundler to resolve the bare module specifiers and optionally optimize the package for your site's particular use case and needs. Comprehensive guides to bundling are beyond the scope of this page; read more about bundlers on their websites: + +- [Rollup](https://rollupjs.org/) +- [esbuild](https://esbuild.github.io/) +- [Parcel](https://parceljs.org/) +- [Webpack](https://webpack.js.org/) + +### JSPM + +{% alert title="Public CDNs", + state="warning" %} +

    JSPM and other public CDNs should not be used on corporate domains. Use them for development purposes only!

    +{% endalert %} + +Add an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to the ``, pointing to the CDN, or update any existing import map. + + + + + +Once the import map is established, you can load the element with the following module, containing a [bare module specifier](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). The example below shows how you'd load in <`rh-button>`. + + + + + +Note that Modules may be placed in the ``. Since they are deferred by default, they will not block rendering. + +{% feedback %} +

    Designers

    +

    To get started using our design system as a designer, go to the Designers page.

    +{% endfeedback %} \ No newline at end of file diff --git a/docs/get-started/developers/tokens.md b/docs/get-started/developers/tokens.md new file mode 100644 index 0000000000..9b0a9970c2 --- /dev/null +++ b/docs/get-started/developers/tokens.md @@ -0,0 +1,165 @@ +--- +layout: layout-with-subnav.njk +title: Tokens +heading: Developers +tags: + - developers +permalink: /get-started/developers/tokens/index.html +subNavCollection: sortedDevelopers +order: 30 +bodyClasses: element-docs +--- + +## How to install tokens + +Run the following git command to install RHDS tokens: + + + + + +## Usage + +We use [style-dictionary](https://amzn.github.io/style-dictionary/) to transform our tokens into multiple formats and helpers. + +### Import global CSS + +Apply defaults to the document root by importing the global stylesheet: + + + + + +### Reset the shadowroot + +Reset a component's styles (preventing inheritance) by adding resetStyles to its static Constructible Style Sheet list: + + + + + + + +### Import tokens as JavaScript objects + +{% alert title="Note", state="info" %} +We strongly recommend using CSS variables (and accompanying snippets), instead of importing tokens as JavaScript objects. +{% endalert %} + +Import tokens as JavaScript objects: + + + + + + + +or for tree-shakable imports: + + + + + + + +## Plugins + +### Using editor snippets + +Editor snippets complete prefixes like `--rh-color-brand` to their CSS custom properties, complete with fallback. + + + + + +They also provide reverse lookup. For example, if you want to choose between all the tokens with the value `#e00`, you can do so by completing the prefix `e00`. + +#### Load snippets in VSCode + +Download the VSIX bundle that’s linked at the bottom of our [“Release v1.0.0”](https://github.com/RedHat-UX/red-hat-design-tokens/releases/tag/v1.0.0) page. + +#### Load snippets in Neovim + +Use LuaSnip to load snippets in Neovim: + + + + + +### Stylelint plugin + +Install the stylelint plugin to automatically correct token values in your files. + +See the [Stylelint Plugin README](https://github.com/RedHat-UX/red-hat-design-tokens/blob/main/plugins/stylelint/README.md) for more info. + +### 11ty plugin + +The experimental 11ty plugin lets you display token values in an 11ty site. + +### vim-hexokinase + +Vim users can load the [vim-hexokinase](https://github.com/RRethy/vim-hexokinase) plugin to display color swatches next to their encoded values in their editor. + +Use the following config (lua syntax, for Neovim users) to configure hexokinase to display color values next to color aliases like `{color.brand.red}`. + + + + + + + + + +{% feedback %} +

    Designers

    +

    To get started using our design system as a designer, go to the Designers page.

    +{% endfeedback %} \ No newline at end of file diff --git a/docs/get-started/developers/usage.md b/docs/get-started/developers/usage.md new file mode 100644 index 0000000000..b571d486fc --- /dev/null +++ b/docs/get-started/developers/usage.md @@ -0,0 +1,114 @@ +--- +layout: layout-with-subnav.njk +title: Usage +heading: Developers +tags: + - developers +permalink: /get-started/developers/usage/index.html +subNavCollection: sortedDevelopers +order: 20 +bodyClasses: element-docs +--- + +## Usage + +Now that you've installed the Red Hat Design System, here's more information about how to use the web components. + +### Using react wrappers + +React wrappers make it possible to use web components within React JSX files. Follow the steps below to learn how. + +#### 1. Initial setup + +We'll bootstrap our React app using Vite. It's possible to use other tools for this, but that is out of the scope of this tutorial. + + + + + +This command will ask you to provide the project name, framework, and variant. + +#### 2. Install the `@lit/react` library + +Use the following command: + + + + + +#### 3. Import elements and patterns + +After installing the `@lit/react` library, you can import elements and patterns +to your file. Below is an example of importing `` and ``, and +managing app state between them using react. + + + + + +### Using RHDS elements with Vue + +To get web components to work with Vue, it’s pretty easy and straightforward. Follow the steps below to use web components in a Vue app. + +#### 1. Initial setup + +Add these two lines at the top of the `main.js` file in the `/src/` directory. + + + + + +#### 2. Import elements and patterns + +Add the import statements to the top of the ` + + +## Other resources + +- [Red Hat Design System Wiki](https://github.com/RedHat-UX/red-hat-design-system/wiki) +- [Red Hat Brand Standard](https://www.redhat.com/en/about/brand/standards) + +{% feedback %} +

    Designers

    +

    To get started using our design system as a designer, go to the Designers page.

    +{% endfeedback %} diff --git a/docs/patterns/announcement/index.md b/docs/patterns/announcement/index.md index 3c334be33b..95dc3a965e 100644 --- a/docs/patterns/announcement/index.md +++ b/docs/patterns/announcement/index.md @@ -16,8 +16,6 @@ audience. alt="Example of an announcement banner", src="./announcement-sample-1.svg" %} -{% repoStatus %} - ## Style An announcement banner can be used in light, dark, and saturated themes. It can diff --git a/docs/patterns/card/index.md b/docs/patterns/card/index.md index 714f1fb287..fe0a02205d 100644 --- a/docs/patterns/card/index.md +++ b/docs/patterns/card/index.md @@ -215,8 +215,6 @@ For more information, please see the [card css custom properties](/elements/card
    -{% repoStatus %} - {% include 'feedback.html' %} diff --git a/docs/patterns/disclosure/index.md b/docs/patterns/disclosure/index.md index 0c493c05fc..dc71b1f201 100644 --- a/docs/patterns/disclosure/index.md +++ b/docs/patterns/disclosure/index.md @@ -29,7 +29,6 @@ expands to reveal more information. -{% repoStatus %} ## Style diff --git a/docs/patterns/filter/index.md b/docs/patterns/filter/index.md index 465d490509..9425f6c97b 100644 --- a/docs/patterns/filter/index.md +++ b/docs/patterns/filter/index.md @@ -20,8 +20,6 @@ A Filter gives users the ability to sort a results listing by turning on and off src="./filter-sample-2.svg" %} -{% repoStatus %} - ## Style A filter can be used in the light theme only. It features a list of checkboxes and text that are wrapped in an [Accordion](../accordion) or a [Disclosure](../disclosure). Content categories can be represented by accordion panels that users can expand to view the checkboxes inside. When one or more checkboxes are selected, a button will appear that gives users the option to reset their selections. diff --git a/docs/patterns/form/index.md b/docs/patterns/form/index.md index bdfbfef210..030cce4789 100644 --- a/docs/patterns/form/index.md +++ b/docs/patterns/form/index.md @@ -13,8 +13,6 @@ A Form is a group of elements used to collect information from a user. It can in alt="Form component samples", src="./form-samples.svg" %} -{% repoStatus %} - ## Style {% example palette="lightest", diff --git a/docs/patterns/link-with-icon/index.md b/docs/patterns/link-with-icon/index.md index 3c0ca076a0..581b33d2c7 100644 --- a/docs/patterns/link-with-icon/index.md +++ b/docs/patterns/link-with-icon/index.md @@ -15,8 +15,6 @@ Link with icon features an icon that adds context to the link itself. It’s pos alt="Link with icon", src="./link-with-icon.svg" %} -{% repoStatus %} - ## Style Link with icon is available in light and dark themes. It’s a grouping of a small icon near a link, similar to how a list item is a grouping of a bullet point near text. The icon chosen should represent what a user will get when they click on or tap the link. It acts as a functional addition instead of only visual. diff --git a/docs/patterns/link/index.md b/docs/patterns/link/index.md index 4929227616..eaa2c3ee8e 100644 --- a/docs/patterns/link/index.md +++ b/docs/patterns/link/index.md @@ -15,8 +15,6 @@ Links are navigational elements that allow a user to move between content, pages alt="Link component examples", src="./example-links.svg" %} -{% repoStatus %} - ## Demo View a live version of the Call to action link and see how it can be customized. diff --git a/docs/patterns/search-bar/index.md b/docs/patterns/search-bar/index.md index ea4c2716e9..046f021302 100644 --- a/docs/patterns/search-bar/index.md +++ b/docs/patterns/search-bar/index.md @@ -16,8 +16,6 @@ a button. It allows a user to input text and then perform a search. alt="Search bar component sample", src="./search-bar-sample.svg" %} -{% repoStatus %} - ## Style A search bar includes a narrow but wide form field with placeholder text and a diff --git a/docs/patterns/skip-navigation/index.md b/docs/patterns/skip-navigation/index.md index 0051659d2b..0a9a16f182 100644 --- a/docs/patterns/skip-navigation/index.md +++ b/docs/patterns/skip-navigation/index.md @@ -6,6 +6,10 @@ tags: ## Overview + {% alert title="Warning", state="warning" %} + The skip navigation pattern is being deprecated. Please use the Skip link element instead. + {% endalert %} + Skip navigation is a styled link that appears at the top of a page when the Tab key is pressed. It bypasses the navigation and jumps users down to the main content when selected. ## Sample pattern @@ -14,8 +18,6 @@ Skip navigation is a styled link that appears at the top of a page when the Tab            alt="Skip navigation",            src="./skip-nav.svg" %} -{% repoStatus %} - ## Style Skip to main content is a styled link that consists of a text label and a background container. Even though it looks like a Button, it functions more like a jump link. diff --git a/docs/patterns/sticky-banner/index.md b/docs/patterns/sticky-banner/index.md index 8696718539..e819bf12d7 100644 --- a/docs/patterns/sticky-banner/index.md +++ b/docs/patterns/sticky-banner/index.md @@ -13,8 +13,6 @@ A Sticky banner slides into view at a certain scroll position and then anchors i alt="Sticky banner", src="./sticky-banner.svg" %} -{% repoStatus %} - ## Style A sticky banner can be used in the light theme only. The large size can include a thumbnail image on large screens, but both sizes can include a headline, text, a call to action, and a background container with a subtle drop shadow. A close button also needs to be included in both sizes. diff --git a/docs/patterns/sticky-card/index.md b/docs/patterns/sticky-card/index.md index f3792c1f46..9ae76479e9 100644 --- a/docs/patterns/sticky-card/index.md +++ b/docs/patterns/sticky-card/index.md @@ -13,8 +13,6 @@ Sticky cards slide into view at a certain scroll position and then anchor themse alt="Sticky card", src="./sticky-card.svg" %} -{% repoStatus %} - ## Style A sticky card acts as a small container for a limited amount of content. diff --git a/docs/patterns/video-thumbnail/index.md b/docs/patterns/video-thumbnail/index.md index 858d5c1b4d..6a4c3e5a72 100644 --- a/docs/patterns/video-thumbnail/index.md +++ b/docs/patterns/video-thumbnail/index.md @@ -13,8 +13,6 @@ A Video thumbnail is a graphical preview of a video overlayed with a play button alt="Video thumbnail", src="./video-thumbnail.svg" %} -{% repoStatus %} - ## Style A video thumbnail is a combination of a graphic with a slightly transparent play button on top. A video thumbnail can also include an optional caption underneath that explains what the video is. diff --git a/docs/playgrounds.11ty.cjs b/docs/playgrounds.11ty.cjs index 2f3ba701ff..fd6272d0e3 100644 --- a/docs/playgrounds.11ty.cjs +++ b/docs/playgrounds.11ty.cjs @@ -8,7 +8,7 @@ module.exports = class Playground { pagination: { data: 'elements', size: 1, - } + }, }; } diff --git a/docs/release-notes.md b/docs/release-notes.md index 7bf3667f5d..5dc4f87783 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,29 @@ We are continually making changes in order to improve and grow the Red Hat Desig
    +## Version 1.4.0 +Released April 22, 2024 + +### Highlights + +| Change | Notes {style="width: 70%" } | +| ------------------------------ | --------------------------------- | +| Added `` | Website status communicates the operational status of a website or domain using a status icon and link. It is usually located in the Footer component. | +| Added `` | Back to top component is a fragment link that allows users to quickly navigate to the top of a lengthy content. | A skip link is used to skip repetitive content on a page. It is hidden by default and can be activated by hitting the "Tab" key after loading/refreshing a page. | +| Added `` | A skip link is used to skip repetitive content on a page. It is hidden by default and can be activated by hitting the "Tab" key after loading/refreshing a page. | +| Updated `` | Added line numbers option, `Show more` toggle, copy and wrap actions, to `` | +| Updated `` | Improved focus accessibility for keyboard navigation users on firefox. | +| Updated `` | Improved focus accessibility on firefox. | +| Updated `` | Added an accents slot with placement options as inline and bottom. | +| Updated `` | Make sure alerts always have to correct (lightest) colour palette. | +| Updated `` | Allow tabs with long text content to fit into different-sized containers. | +| Updated PatternFly Elements tooling | [Patch update to dependencies](https://github.com/patternfly/patternfly-elements/releases/tag/%40patternfly%2Fpfe-core%403.0.0), including Lit version 3. | + +View version 1.4 release notes + +
    +
    + ## Version 1.3.0 Released January 11, 2024 diff --git a/docs/scss/components/_component-status-table.scss b/docs/scss/components/_component-status-table.scss index 8f4ae4a3a2..5a4100862b 100644 --- a/docs/scss/components/_component-status-table.scss +++ b/docs/scss/components/_component-status-table.scss @@ -1,75 +1,80 @@ -.component-status-table { - width: 100%; - border: 0; - border-collapse: collapse; - font-size: 0.9em; - line-height: 1.4; - text-align: center; - - :is(td, th) { - padding: 12px 16px; - text-align: center !important; +.component-status-list-heading { + display: flex; + justify-content: space-between; + align-items: flex-end; + p { + font-size: var(--rh-font-size-body-text-sm); } +} - th { - font-size: 14px; - } +.component-status-list-container { + border: var(--rh-border-width-sm) solid var(--rh-color-border-subtle-on-light); + border-radius: 8px; - tbody th { - font-size: var(--rh-font-size-body-text-md, 1rem) !important; - font-weight: var(--rh-font-weight-heading-medium, 500); + dl { + display: flex; + align-items: flex-start; + column-gap: var(--rh-space-2xl); + margin-block: var(--rh-space-lg) var(--rh-space-lg); + margin-inline: var(--rh-space-xl) var(--rh-space-xl); } - :is(th, td):first-child { - text-align: left !important; + dl div { + display: flex; + column-gap: var(--rh-space-md); + align-items: center; } - :is(th, td):nth-child(n + 8) { - display: none; + dl dd { + text-transform: capitalize; } - td { - border: 1px solid #d2d2d2; - border-left: 0; - border-right: 0; - - &:last-child { - border-right: 0; - } - - svg { - height: var(--rh-size-icon-01, 16px); - width: var(--rh-size-icon-01, 16px); - fill: var(--rh-color-text-primary-on-light, #151515); + dl dt { + font-size: var(--rh-font-size-body-text-md); + &:after { + content: ": "; } } - @media (max-width: 1000px) { - :is(td, th) { - padding: 12px 8px; - } + dl dt, + dl dd { + margin: 0; + padding: 0; } - + small { - align-self: end; - } } -.component-status-table-container { - display: flex; - flex-flow: column nowrap; +.component-status-table { + table tbody td:first-child { + text-transform: capitalize; + } } @media only screen and (max-width: 991px) { - .component-status-table-container { - overflow-x: scroll; + .component-status-list-container { + dl { + flex-direction: column; + row-gap: 16px; + } } - .component-status-table { - width: 912px; + .component-status-list-heading { + flex-direction: column; + align-items: flex-start; + } - + small { - align-self: start; + .component-status-table { + table thead ~ tbody tr:nth-child(even) { + background: none; + } + table thead ~ tbody tr:first-child { + border-top: none; + } + table thead ~ tbody tr:last-child { + border-bottom: none; + } + table tr td { + border: none; } } } diff --git a/elements/rh-accordion/context.ts b/elements/rh-accordion/context.ts new file mode 100644 index 0000000000..ba14bd0be7 --- /dev/null +++ b/elements/rh-accordion/context.ts @@ -0,0 +1,9 @@ +import type { RhAccordion } from './rh-accordion'; + +import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; + +export interface RhAccordionContext { + accents?: 'inline' | 'bottom'; +} + +export const context = createContextWithRoot(Symbol('rh-accordion-context')); diff --git a/elements/rh-accordion/demo/accents.html b/elements/rh-accordion/demo/accents.html new file mode 100644 index 0000000000..09ef6081e0 --- /dev/null +++ b/elements/rh-accordion/demo/accents.html @@ -0,0 +1,71 @@ +
    +

    Inline

    + + +

    Item One

    + Green + Red + Orange + Purple +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    + +

    Item Two

    + Green +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    + +

    Item Three

    + Red +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    +
    +
    + + +
    +

    Bottom

    + + +

    Item One

    + Green + Red + Orange + Purple +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    + +

    Item Two

    + Green +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    + +

    Item Three

    + Red +
    + +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    +
    +
    +
    + + + + \ No newline at end of file diff --git a/elements/rh-accordion/docs/00-overview.md b/elements/rh-accordion/docs/00-overview.md index e7ba52db40..cf05c9126f 100644 --- a/elements/rh-accordion/docs/00-overview.md +++ b/elements/rh-accordion/docs/00-overview.md @@ -5,6 +5,8 @@ alt="An accordion with four collapsed panels and one expanded panel", src="./accordion-sample-element.png" %} +{% repoStatusList %} + ## Sample element @@ -73,4 +75,4 @@ - When you need to condense a large amount of related information into sections - When you need a way for users to read or compare sections of content simultaneously -{% repoStatus type="Element" %} \ No newline at end of file +{% repoStatusChecklist %} diff --git a/elements/rh-accordion/docs/10-style.md b/elements/rh-accordion/docs/10-style.md index 63daecf22c..a5ad7facc0 100644 --- a/elements/rh-accordion/docs/10-style.md +++ b/elements/rh-accordion/docs/10-style.md @@ -14,7 +14,8 @@ Accordion panels include title text, a chevron icon, body text, and other conten 6) Emphasis 7) Content 8) Panel body region - {.example-notes} +9) Accent slot +{.example-notes} ### Sizes There are two available sizes and the only difference is the title text size. You can use the Small size on large breakpoints, but not the Large size on small breakpoints due to the potential of long title text wrapping to more than two lines. @@ -35,13 +36,22 @@ An accordion is available in both light and dark themes. The light theme expande alt="Dark theme accordion with an expanded panel", src="../accordion-theme-dark.png" %} -## Configuration +## Configuration + An expanded panel does not have a maximum height, but it may scroll if constrained by vertical space. The width of an accordion varies based on content and page layout. Title text and icons are horizontally aligned. {% example palette="light", alt="How an accordion is constructed showing alignment, space, scrolling, and width details", src="../accordion-configuration.png" %} +### Accent slot + +The accent slot can be positioned inline or below the panel's title. This can contain tags, badges, or other small elements with secondary information. + +{% example palette="light", + alt="Accordion panel with two tags in inline accent slot and an accordion with two tags below the title", + src="../accordion-accent-slot.png" %} + ### Nested panels Panels can be nested to help organize complex or granular sections of content. diff --git a/elements/rh-accordion/docs/20-guidelines.md b/elements/rh-accordion/docs/20-guidelines.md index 03cbcd3adc..697579e479 100644 --- a/elements/rh-accordion/docs/20-guidelines.md +++ b/elements/rh-accordion/docs/20-guidelines.md @@ -1,7 +1,9 @@ ## Usage Use an accordion to organize a large amount of content into sections. This allows users to scan through critical information first and then access additional information when needed. Users can also compare information by expanding multiple panels simultaneously. + ### When to use an accordion Using an accordion provides an easy way to organize content while reducing page scrolling, but at the expense of hiding information or burdening users with more clicks. There is a chance that important information will be missed or not immediately noticed by users. Therefore, if reading important information is critical to the user experience or if important information requires more focus and less clicking, it is advised to not use an accordion. + ### Accordion vs. disclosure An accordion is used to organize important information whereas a [Disclosure](/patterns/dislosure) can be used to organize secondary information that might not be critical to read or impact the experience. An accordion can also accommodate multiple sections of content, whereas a disclosure can only accommodate one. @@ -51,7 +53,7 @@ Title text can be two lines on small breakpoints, but no more. src="../accordion-long-title-text.png" %} ## Layout -The width of an accordion can be adjusted on large breakpoints to fit less columns if necessary. +The width of an accordion can be adjusted on large breakpoints to fit fewer columns if necessary. {% example palette="light", alt="A thinner accordion placed on a 12-column grid and occupying eight grid columns", diff --git a/elements/rh-accordion/docs/30-code.md b/elements/rh-accordion/docs/30-code.md index 91c0ebcbed..dff7558d55 100644 --- a/elements/rh-accordion/docs/30-code.md +++ b/elements/rh-accordion/docs/30-code.md @@ -1,6 +1,7 @@ {% renderInstall %}{% endrenderInstall %} -{% band header="Usage" %} +## Usage + ```html @@ -23,7 +24,6 @@ ``` -{% endband %} {% renderCodeDocs hideDescription=true %}{% endrenderCodeDocs %} diff --git a/elements/rh-accordion/docs/accordion-accent-slot.png b/elements/rh-accordion/docs/accordion-accent-slot.png new file mode 100644 index 0000000000..26d9dce956 Binary files /dev/null and b/elements/rh-accordion/docs/accordion-accent-slot.png differ diff --git a/elements/rh-accordion/docs/accordion-anatomy.png b/elements/rh-accordion/docs/accordion-anatomy.png old mode 100755 new mode 100644 index a8b8d19c77..91237da381 Binary files a/elements/rh-accordion/docs/accordion-anatomy.png and b/elements/rh-accordion/docs/accordion-anatomy.png differ diff --git a/elements/rh-accordion/docs/accordion-space.png b/elements/rh-accordion/docs/accordion-space.png old mode 100755 new mode 100644 index e18b19bde0..e4fb15ad89 Binary files a/elements/rh-accordion/docs/accordion-space.png and b/elements/rh-accordion/docs/accordion-space.png differ diff --git a/elements/rh-accordion/docs/rh-accordion.md b/elements/rh-accordion/docs/rh-accordion.md index 51573e69b4..caec4ac616 100644 --- a/elements/rh-accordion/docs/rh-accordion.md +++ b/elements/rh-accordion/docs/rh-accordion.md @@ -2,7 +2,7 @@ {% endrenderOverview %} -{% band header="Usage" %}{% endband %} +## Usage {% renderSlots %}{% endrenderSlots %} @@ -14,4 +14,4 @@ {% renderCssCustomProperties %}{% endrenderCssCustomProperties %} -{% renderCssParts %}{% endrenderCssParts %} \ No newline at end of file +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/rh-accordion/rh-accordion-header.css b/elements/rh-accordion/rh-accordion-header.css index a87afee5b6..dd1cd2235a 100644 --- a/elements/rh-accordion/rh-accordion-header.css +++ b/elements/rh-accordion/rh-accordion-header.css @@ -101,12 +101,27 @@ a { span { overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: calc(100% - var(--rh-space-xl, 24px)); text-align: start; } +[part="container"] { + display: flex; + gap: var(--rh-space-xl, 24px); + container-type: inline-size; +} + +#header-container { + display: flex; + flex-direction: column; + gap: var(--rh-space-md, 8px); +} + +[part="accents"] { + display: flex; + flex-wrap: wrap; + gap: var(--rh-space-md, 8px); +} + #button[aria-expanded="true"] #icon { rotate: calc(var(--_isRTL, -1) * 180deg); } @@ -145,3 +160,9 @@ span { inset-block: 0; inset-inline-start: 0; } + +@container (min-width: 576px) { + #header-container:not(.bottom) { + flex-direction: row; + } +} diff --git a/elements/rh-accordion/rh-accordion-header.ts b/elements/rh-accordion/rh-accordion-header.ts index 1e1eebbe64..511111880c 100644 --- a/elements/rh-accordion/rh-accordion-header.ts +++ b/elements/rh-accordion/rh-accordion-header.ts @@ -1,9 +1,11 @@ import type { RhAccordion } from './rh-accordion.js'; +import type { RhAccordionContext } from './context.js'; import { html, LitElement } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; @@ -11,6 +13,10 @@ import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import { DirController } from '../../lib/DirController.js'; import { colorContextConsumer, type ColorTheme } from '../../lib/context/color/consumer.js'; +import { consume } from '@lit/context'; + +import { context } from './context.js'; + import styles from './rh-accordion-header.css'; const isPorHeader = @@ -38,8 +44,8 @@ export class AccordionHeaderChangeEvent extends Event { * @slot * We expect the light DOM of the rh-accordion-header to be a heading level tag (h1, h2, h3, h4, h5, h6) * @slot accents - * These elements will appear inline with the accordion header, between the header and the chevron - * (or after the chevron and header in disclosure mode). + * These elements will appear inline by default with the header title, between the header and the chevron + * (or after the chevron and header in disclosure mode). There is an option to set the accents placement to bottom * * @fires {AccordionHeaderChangeEvent} change - when the open panels change * @@ -50,7 +56,10 @@ export class RhAccordionHeader extends LitElement { static readonly styles = [styles]; - static override readonly shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; + static override readonly shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; @property({ type: Boolean, reflect: true }) expanded = false; @@ -58,10 +67,15 @@ export class RhAccordionHeader extends LitElement { @property({ reflect: true, attribute: 'heading-tag' }) headingTag?: string; + /** @deprecated */ @property({ reflect: true }) icon = 'angle-down'; @colorContextConsumer() private on?: ColorTheme; + @consume({ context, subscribe: true }) + @property({ attribute: false }) + private ctx?: RhAccordionContext; + #generatedHtag?: HTMLHeadingElement; #logger = new Logger(this); @@ -118,13 +132,20 @@ export class RhAccordionHeader extends LitElement { } #renderHeaderContent() { + const { accents } = this.ctx ?? {}; const headingText = this.headingText?.trim() ?? this.#header?.textContent?.trim(); + return html` `)} `} @@ -632,18 +651,22 @@ export class RhAudioPlayer extends LitElement {
    diff --git a/elements/rh-code-block/demo/callout-badges.html b/elements/rh-code-block/demo/callout-badges.html new file mode 100644 index 0000000000..4c899f8349 --- /dev/null +++ b/elements/rh-code-block/demo/callout-badges.html @@ -0,0 +1,21 @@ + + + 1 + 2 +
    +
    1
    +
    Postfix callout
    +
    2
    +
    Inline callout
    +
    +
    + + + + diff --git a/elements/rh-code-block/demo/color-context.html b/elements/rh-code-block/demo/color-context.html index 6c494da250..72617692d4 100644 --- a/elements/rh-code-block/demo/color-context.html +++ b/elements/rh-code-block/demo/color-context.html @@ -1,11 +1,17 @@ - - + + Copy to Clipboard + + Toggle word wrap + + + diff --git a/elements/rh-code-block/demo/sizes.html b/elements/rh-code-block/demo/sizes.html index 0368baf4ef..7aeee18ec0 100644 --- a/elements/rh-code-block/demo/sizes.html +++ b/elements/rh-code-block/demo/sizes.html @@ -37,11 +37,11 @@

    Compact

    No fixed width

    - + - + +

    Responsive sizes

    + +
    + + Copy to Clipboard + + Toggle + line wrap + overflow + + + +
    + +
    + + Copy to Clipboard + + Toggle + line wrap + overflow + + + +
    + diff --git a/elements/rh-code-block/docs/10-overview.md b/elements/rh-code-block/docs/10-overview.md index 4d4782edfb..2ef2881529 100644 --- a/elements/rh-code-block/docs/10-overview.md +++ b/elements/rh-code-block/docs/10-overview.md @@ -7,6 +7,8 @@ alt="Image of a code block with black code text within a light gray container", src="./code-block-sample.png" %} +{% repoStatusList %} + ## Sample element @@ -29,9 +31,8 @@ View the `` demo in a new tab {% endcta %} - ## When to use - When you need to highlight a block of code while maintaining the formatting -{% repoStatus type="Element" %} +{% repoStatusChecklist %} \ No newline at end of file diff --git a/elements/rh-code-block/rh-code-block-lightdom.css b/elements/rh-code-block/rh-code-block-lightdom.css new file mode 100644 index 0000000000..f2240a9a21 --- /dev/null +++ b/elements/rh-code-block/rh-code-block-lightdom.css @@ -0,0 +1,5 @@ +rh-code-block { + & dd { + margin: 0; + } +} diff --git a/elements/rh-code-block/rh-code-block.css b/elements/rh-code-block/rh-code-block.css index 82c4f497fc..2b6167bbd4 100644 --- a/elements/rh-code-block/rh-code-block.css +++ b/elements/rh-code-block/rh-code-block.css @@ -1,61 +1,281 @@ :host { - position: relative; + --rh-code-block-callout-size: var(--rh-size-icon-02, 24px); + --_aspect-ratio: 1; + --_badge-size: var(--rh-code-block-callout-size); + display: block; + max-width: 1000px; + max-height: calc(10 * var(--rh-space-4xl, 64px)); } -#content { - display: block; - background-color: var(--rh-color-surface-lighter, #f2f2f2); - border: var(--rh-border-width-sm, 1px) solid var(--rh-color-border-subtle-on-light, #c7c7c7); +:host([full-height]) { + --_expand-toggle-rotate: 0deg; + + max-height: initial; +} + +[hidden] { + display: none !important; +} + +::slotted(pre) { + margin: 0 !important; + padding: 0 !important; + background: transparent !important; + border: none !important; +} + +.shadow-fab { + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + padding: var(--rh-space-md, 8px); + border-radius: var(--rh-border-radius-default, 3px); + width: var(--rh-length-3xl, 48px); + height: var(--rh-length-3xl, 48px); +} + +.shadow-fab:is(:hover, :focus, :active) { + background: + var( + --_code-action-hover-focus-active-background, + var(--rh-color-surface-light, #e0e0e0) + ); +} + +.shadow-fab svg { + width: var(--rh-size-icon-02, 24px); + height: var(--rh-size-icon-02, 24px); + color: var(--_code-action-color, var(--rh-color-text-primary-on-light, #151515)); +} + +.dark .shadow-fab { + --_code-action-color: var(--rh-color-text-primary-on-dark, #ffffff); + --_code-action-hover-focus-active-background: var(--rh-color-surface-dark, #383838); +} + +#container, +#content-lines, +#content, +#sizers { + max-width: 100%; +} + +#container { + --_code-main-spacer: var(--rh-space-xl, 24px); + + display: grid; + place-items: center; + grid-template-columns: auto min-content; + grid-template-areas: + "code actions" + "expand expand"; + column-gap: var(--_code-main-spacer); + padding-inline-start: var(--_code-main-spacer); + padding-block-end: var(--_code-main-spacer); + border-radius: var(--rh-border-radius-default, 3px); + background-color: var(--_code-background-color, var(--rh-color-surface-lighter, #f2f2f2)); + color: var(--_code-color, var(--rh-color-text-primary-on-light, #151515)); + border: + var(--rh-border-width-sm, 1px) + solid + var(--_code-border-color, var(--rh-color-border-subtle-on-light, #c7c7c7)); border-block-start-width: var( --rh-code-block-border-block-start-width, var(--rh-border-width-sm, 1px) ); - padding: var(--rh-space-xl, 24px); - font-family: var(--rh-font-family-code, RedHatMono, "Red Hat Mono", "Courier New", Courier, monospace); - color: var(--rh-color-text-primary-on-light, #151515); - max-width: 1000px; - max-height: 640px; - height: calc(100% - 2 * var(--rh-space-xl, 24px)); - overflow-y: auto; - border-radius: var(--rh-border-radius-default, 3px); } -#content.dark { - background-color: var(--rh-color-surface-darker, #1f1f1f); - border-color: var(--rh-color-border-subtle-on-dark, #707070); - color: var(--rh-color-text-secondary-on-dark, #c7c7c7); +#container.expandable { + padding-block-end: 0; +} + +#sizers, +#content { + display: block; + font-family: var(--rh-font-family-code, RedHatMono, "Red Hat Mono", "Courier New", Courier, monospace); + z-index: 1; + place-self: start; + grid-area: code; } +#sizers, #content::slotted(:is(script, pre)) { display: inline; - white-space: pre; + white-space: var(--_code-white-space, pre); + word-wrap: var(--_code-word-wrap, initial); color: inherit; } -:host([compact]) #content { - padding: var(--rh-space-lg, 16px); - height: calc(100% - 2 * var(--rh-space-lg, 16px)); +#content::slotted(rh-tag) { + width: var(--rh-size-icon-06, 64px); } -:host([resizable]) #content { +#content-lines { + display: grid; + column-gap: var(--rh-space-lg, 16px); + grid-area: code; + grid-template-areas: "lines code"; + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr; + position: relative; + overflow-y: auto; + margin-block-start: var(--_code-main-spacer); + width: 100%; +} + +#sizers { + position: absolute; + min-width: 100%; + width: 100%; + opacity: 0; + pointer-events: none; + z-index: -10000; +} + +#line-numbers { + pointer-events: none; + overflow-y: hidden; + margin: 0; + grid-area: lines; + list-style-type: none; + padding-inline: 0 var(--rh-space-md, 8px); + text-align: end; + font-family: var(--rh-font-family-code, RedHatMono, "Red Hat Mono", "Courier New", Courier, monospace); + color: var(--_code-line-numbers-color, var(--rh-color-gray-60, #4d4d4d)); + font-weight: var(--rh-font-weight-code-regular, 400); + border-inline-end: + var(--rh-border-width-sm, 1px) solid + var(--_code-line-numbers-border-color, var(--rh-color-border-subtle-on-light, #c7c7c7)); +} + +#actions { + display: flex; + grid-area: actions; + gap: var(--rh-space-md, 8px); + flex-flow: column; + margin-block-start: var(--rh-space-lg, 16px); + margin-inline-end: var(--rh-space-lg, 16px); + z-index: 2; + place-self: start center; + height: 100%; + position: relative; +} + +#actions rh-tooltip { + display: block; +} + +#expand { + --_code-secondary-spacer: var(--rh-space-md, 8px); + + display: inline-flex; + align-items: center; + border: 0; + background: transparent; + grid-area: expand; + gap: var(--rh-space-md, 8px); + inset-block-end: var(--_code-secondary-spacer); + margin-block: var(--_code-secondary-spacer); + color: var(--_expand-toggle-color, var(--rh-color-text-primary-on-light, #151515)); + font-family: var(--rh-font-family-body-text, RedHatText, "Red Hat Text", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans JP", "Noto Sans KR", "Noto Sans Malayalam", "Noto Sans SC", "Noto Sans TC", "Noto Sans Thai", Helvetica, Arial, sans-serif); + font-size: var(--rh-font-size-body-text-sm, 0.875rem); + font-weight: var(--rh-font-weight-body-text-regular, 400); + line-height: var(--rh-line-height-body-text, 1.5); +} + +#expand svg { + width: 11px; + height: 7px; + rotate: var(--_expand-toggle-rotate, 180deg); + transform: rotate 0.2s ease-in-out; + color: var(--_expand-toggle-icon-color, var(--rh-color-icon-secondary-on-light, #151515)); +} + +#container.compact { + --_code-main-spacer: var(--rh-space-lg, 16px); + --_code-secondary-spacer: var(--rh-space-sm, 6px); +} + +.resizable #content-lines { resize: vertical; overflow-x: scroll; } -.wrap::slotted(:is(script, pre)), -:host([wrap]) #content::slotted(:is(script, pre)) { - white-space: pre-wrap; +.truncated #content-lines { + max-height: calc(8 * var(--rh-font-size-code-md, 1rem)); } -:host([full-height]) #content { - max-height: initial; +.truncated #content-lines:before { + content: ""; + display: block; + position: sticky; + z-index: 2; + inset-block-end: 0; + inset-inline: 0; + height: var(--rh-space-3xl, 48px); + pointer-events: none; + grid-column: -1/1; + background: + var( + --_block-end-overflow-gradient, + linear-gradient( + 0deg, + #f2f2f2 0%, + rgba(242, 242, 242, 0) + 100% + ) + ); } -::slotted(pre) { - margin: 0 !important; - padding: 0 !important; - background: transparent !important; - border: none !important; +:not(.wrap) #actions:before { + content: ""; + display: block; + position: absolute; + z-index: 2; + inset-block: 0; + inset-inline-start: calc(-1 * var(--rh-space-4xl, 64px)); + width: var(--rh-space-4xl, 64px); + pointer-events: none; + background: + var( + --_inline-end-overflow-gradient, + linear-gradient( + 270deg, + #f2f2f2 0%, + rgba(242, 242, 242, 0) + 100% + ) + ); +} + +:not(.actions) #actions { + margin: 0; +} + +.wrap { + --_code-white-space: pre-wrap; + --_code-word-wrap: anywhere; +} + +.dark { + --_code-background-color: var(--rh-color-surface-dark-alt, #292929); + --_code-border-color: var(--rh-color-border-subtle-on-dark, #707070); + --_code-color: var(--rh-color-text-primary-on-dark, #ffffff); + --_code-line-numbers-color: var(--rh-color-text-secondary-on-dark, #c7c7c7); + --_code-line-numbers-border-color: var(--rh-color-border-subtle-on-dark, #707070); + --_expand-toggle-color: var(--rh-color-text-primary-on-dark, #ffffff); + --_expand-toggle-icon-color: var(--rh-color-icon-secondary-on-dark, #ffffff); + --_inline-end-overflow-gradient: linear-gradient(270deg, #292929 0%, rgba(41, 41, 41, 0) 100%); + --_block-end-overflow-gradient: linear-gradient(0deg, #292929 0%, rgba(41, 41, 41, 0) 100%); +} + +[name="legend"]::slotted(dl) { + display: grid; + grid-template-columns: max-content auto; + margin-block: var(--rh-space-lg, 16px); + gap: var(--rh-space-md, 8px); } diff --git a/elements/rh-code-block/rh-code-block.ts b/elements/rh-code-block/rh-code-block.ts index 55b5e25092..47d727f63b 100644 --- a/elements/rh-code-block/rh-code-block.ts +++ b/elements/rh-code-block/rh-code-block.ts @@ -1,22 +1,87 @@ -import { type ColorTheme, colorContextConsumer } from '../../lib/context/color/consumer.js'; -import { html, LitElement } from 'lit'; +import { LitElement, html, type PropertyValues } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { classMap } from 'lit/directives/class-map.js'; +import { styleMap } from 'lit/directives/style-map.js'; import { property } from 'lit/decorators/property.js'; +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; + +import { type ColorTheme, colorContextConsumer } from '../../lib/context/color/consumer.js'; + import style from './rh-code-block.css'; +/* TODO + * - style slotted and shadow fake-fabs + * - manage state of copy and wrap, including if they are slotted. see actions.html + */ + +interface CodeLineHeightsInfo { + lines: string[]; + lineHeights: (number | undefined)[]; + sizer: HTMLElement; + oneLinerHeight: number; +} + /** * A code block is formatted text within a container. * @summary Formats code strings within a container * @slot - A non-executable script tag containing the sample content. JavaScript * samples should use the type `text/sample-javascript`. HTML samples - * containing script tags must escape the closing `` tag. + * containing script tags must escape the closing `` tag. Can + * also be a `
    ` tag.
    + * @slot action-label-copy - tooltip content for the copy action button
    + * @slot action-label-wrap - tooltip content for the wrap action button
    + * @slot show-more - text content for the expandable toggle button when the code
    + *                   block is collapsed.
    + * @slot show-less - text content for the expandable toggle button when the code
    + *                   block is expanded.
    + * @slot legend - `
    ` element containing rh-badges in the `
    ` + * and legend text in the `
    ` elements */ @customElement('rh-code-block') export class RhCodeBlock extends LitElement { + private static actions = new Map([ + ['wrap', html` + + + + + `], + ['wrap-active', html` + + + + + + `], + ['copy', html` + + + + + `], + ]); + static styles = [style]; + @property({ + reflect: true, + converter: { + fromAttribute(value) { + return ((value ?? '').split(/\s+/) ?? []).map(x => x.trim()).filter(Boolean); + }, + toAttribute(value) { + return Array.isArray(value) ? value.join(' ') : ''; + }, + }, + }) actions: ('copy' | 'wrap')[] = []; + /** When set, the code block displays with compact spacing */ @property({ type: Boolean, reflect: true }) compact = false; @@ -26,14 +91,230 @@ export class RhCodeBlock extends LitElement { /** When set, the code block occupies it's full height, without scrolling */ @property({ type: Boolean, reflect: true, attribute: 'full-height' }) fullHeight = false; + /** When set, lines in the code snippet wrap */ + @property({ type: Boolean }) wrap = false; + @colorContextConsumer() private on?: ColorTheme; + #slots = new SlotController( + this, + null, + // 'actions', + 'action-label-copy', + 'action-label-wrap', + 'show-more', + 'show-less', + 'legend', + ); + + #ro = new ResizeObserver(() => this.#computeLineNumbers()); + + #lineHeights: `${string}px`[] = []; + + override connectedCallback() { + super.connectedCallback(); + this.#ro.observe(this); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.#ro.disconnect(); + } + render() { - const { on = '' } = this; + const { on = '', fullHeight, wrap, resizable, compact } = this; + const expandable = this.#lineHeights.length > 5; + const truncated = expandable && !fullHeight; + const actions = !!this.actions.length; return html` - +
    +
    + + + +
    + +
    + ${this.actions.map(x => html` + + + + `)} + +
    + + +
    + `; } + + protected override firstUpdated(): void { + this.#computeLineNumbers(); + } + + protected override updated(changed: PropertyValues): void { + if (changed.has('wrap')) { + this.#wrapChanged(); + } + } + + #wrapChanged() { + this.#computeLineNumbers(); + // TODO: handle slotted fabs + const slot = this.shadowRoot?.querySelector('slot[name="action-label-wrap"]'); + for (const el of slot?.assignedElements() ?? []) { + if (el instanceof HTMLElement) { + el.hidden = (el.dataset.codeBlockState !== 'active') === this.wrap; + } + } + this.requestUpdate(); + } + + /** + * Clone the text content and connect it to the document, in order to calculate the number of lines + * @license MIT + * Portions copyright prism.js authors (MIT license) + */ + #computeLineNumbers() { + const slot = this.shadowRoot?.getElementById('content') as HTMLSlotElement; + + const codes: HTMLElement[] = slot.assignedElements().flatMap(x => + x instanceof HTMLScriptElement + || x instanceof HTMLPreElement ? [x] + : []); + + const infos: CodeLineHeightsInfo[] = codes.map(element => { + const sizer = document.createElement('span'); + sizer.className = 'sizer'; + sizer.innerText = '0'; + sizer.style.display = 'block'; + this.shadowRoot?.getElementById('sizers')?.appendChild(sizer); + return { + lines: element.textContent?.split(/\n(?!$)/g) ?? [], + lineHeights: [], + sizer, + oneLinerHeight: sizer.getBoundingClientRect().height, + }; + }); + + for (const { lines, lineHeights, sizer, oneLinerHeight } of infos) { + lineHeights[lines.length - 1] = undefined; // why? + lines.forEach((line, i) => { + if (line.length > 1) { + const e = sizer.appendChild(document.createElement('span')); + e.style.display = 'block'; + e.textContent = line; + } else { + lineHeights[i] = oneLinerHeight; + } + }); + } + + for (const { sizer, lineHeights } of infos) { + let childIndex = 0; + for (let i = 0; i < lineHeights.length; i++) { + if (lineHeights[i] === undefined) { + lineHeights[i] = sizer.children[childIndex++].getBoundingClientRect()?.height ?? 0; + } + } + sizer.remove(); + } + + this.#lineHeights = infos.flatMap(x => + x.lineHeights?.map(y => + `${y ?? x.oneLinerHeight}px` as const)); + + this.requestUpdate('#linesNumbers', 0); + } + + #onActionsClick(event: Event) { + this.#onCodeAction(event); + } + + #onActionsKeyup(event: KeyboardEvent) { + switch (event.key) { + case 'Enter': + case ' ': + event.preventDefault(); + this.#onCodeAction(event); + } + } + + #onCodeAction(event: Event) { + const el = event.composedPath().find((x: EventTarget): x is HTMLElement => + x instanceof HTMLElement && !!x.dataset.codeBlockAction); + if (el) { + switch (el.dataset.codeBlockAction) { + case 'copy': + return this.#copy(); + case 'wrap': + this.wrap = !this.wrap; + this.requestUpdate(); + return; + } + } + } + + #onClickExpand() { + this.fullHeight = !this.fullHeight; + } + + async #copy() { + await navigator.clipboard.writeText( + Array.from( + this.querySelectorAll('script'), + x => x.textContent, + ).join('') + ); + // TODO: handle slotted fabs + const slot = this.shadowRoot?.querySelector('slot[name="action-label-copy"]'); + const tooltip = slot?.closest('rh-tooltip'); + tooltip?.hide(); + for (const el of slot?.assignedElements() ?? []) { + if (el instanceof HTMLElement) { + el.hidden = el.dataset.codeBlockState !== 'active'; + } + } + this.requestUpdate(); + tooltip?.show(); + await new Promise(r => setTimeout(r, 5_000)); + tooltip?.hide(); + for (const el of slot?.assignedElements() ?? []) { + if (el instanceof HTMLElement) { + el.hidden = el.dataset.codeBlockState === 'active'; + } + } + this.requestUpdate(); + tooltip?.show(); + } } declare global { @@ -41,3 +322,28 @@ declare global { 'rh-code-block': RhCodeBlock; } } + +/** + * TODO: slotted fabs like this: + * + *```html + + Copy to Clipboard + + + + + + Toggle linewrap + + + + ```` + * + */ diff --git a/elements/rh-cta/docs/00-overview.md b/elements/rh-cta/docs/00-overview.md index ed61ec4270..f634a13ad9 100644 --- a/elements/rh-cta/docs/00-overview.md +++ b/elements/rh-cta/docs/00-overview.md @@ -6,6 +6,8 @@ alt="Image of variants including Primary (red background and white text), Secondary (black border and black text), Brick (light gray border and blue text), and Default (blue text and blue icon)", src="./cta-sample.png" %} +{% repoStatusList %} + ## Sample element @@ -28,4 +30,4 @@ View a live version of this element to see how it can be customized. - When you need to arrange links in different arrangements like in a row or grid - When you need to hide or reveal content -{% repoStatus type="Element" %} \ No newline at end of file +{% repoStatusChecklist %} diff --git a/elements/rh-cta/docs/30-code.md b/elements/rh-cta/docs/30-code.md index e7fc1db4b7..b4107406cb 100644 --- a/elements/rh-cta/docs/30-code.md +++ b/elements/rh-cta/docs/30-code.md @@ -1,11 +1,11 @@ {% renderInstall lightdomcss=true %}{% endrenderInstall %} -{% band header="Usage" %} - ```html - - Default - - ``` -{% endband %} +## Usage + +```html + + Default + +``` {% renderCodeDocs hideDescription=true %}{% endrenderCodeDocs %} diff --git a/elements/rh-cta/rh-cta.ts b/elements/rh-cta/rh-cta.ts index bff4ec68be..84b6aaa83b 100644 --- a/elements/rh-cta/rh-cta.ts +++ b/elements/rh-cta/rh-cta.ts @@ -171,7 +171,9 @@ export class RhCta extends LitElement { } #onMutation() { - this.#logger.warn('The color-palette attribute is deprecated and will be removed in a future release.'); + this.#logger.warn( + 'The color-palette attribute is deprecated and will be removed in a future release.' + ); } // END DEPRECATION WARNING diff --git a/elements/rh-cta/test/rh-cta.e2e.ts b/elements/rh-cta/test/rh-cta.e2e.ts index 4332977d71..b941d5d82e 100644 --- a/elements/rh-cta/test/rh-cta.e2e.ts +++ b/elements/rh-cta/test/rh-cta.e2e.ts @@ -16,7 +16,9 @@ test.describe(tagName, () => { // hover await element.hover(); - await element.evaluate(e => Promise.all(e.getAnimations({ subtree: true }).map(animation => animation.finished))); + await element.evaluate(e => + Promise.all(e.getAnimations({ subtree: true }).map(animation => animation.finished)) + ); await componentPage.updateComplete(selector); await expect(locator).toHaveScreenshot(`${variant} 1 hover.png`); await page.mouse.click(0, 0); diff --git a/elements/rh-cta/test/rh-cta.spec.ts b/elements/rh-cta/test/rh-cta.spec.ts index 700463f274..208272191a 100644 --- a/elements/rh-cta/test/rh-cta.spec.ts +++ b/elements/rh-cta/test/rh-cta.spec.ts @@ -13,14 +13,14 @@ describe('', function() { const element = await createFixture(html``); const klass = customElements.get('rh-cta'); expect(element) - .to.be.an.instanceOf(klass) - .and - .to.be.an.instanceOf(RhCta); + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(RhCta); }); it('instanciates imperatively', function() { expect(document.createElement('rh-cta')) - .to.be.an.instanceof(RhCta); + .to.be.an.instanceof(RhCta); }); describe('on default context', function() { diff --git a/elements/rh-dialog/demo/events.html b/elements/rh-dialog/demo/events.html new file mode 100644 index 0000000000..f3e55d7310 --- /dev/null +++ b/elements/rh-dialog/demo/events.html @@ -0,0 +1,37 @@ +
    + +

    Modal dialog with a header

    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum.

    + + Learn more + +
    + Open +
    + Events Fired + No events yet +
    +
    + + + diff --git a/elements/rh-dialog/demo/no-header-content.html b/elements/rh-dialog/demo/no-header-content.html index 42c234f86b..120bf766ff 100644 --- a/elements/rh-dialog/demo/no-header-content.html +++ b/elements/rh-dialog/demo/no-header-content.html @@ -16,8 +16,5 @@

    This has no header content

    import '@rhds/elements/rh-button/rh-button.js'; import '@rhds/elements/rh-cta/rh-cta.js'; import '@rhds/elements/rh-dialog/rh-dialog.js'; - import '@patternfly/elements/pf-panel/pf-panel.js'; - import '@patternfly/elements/pf-button/pf-button.js'; - import '@patternfly/elements/pf-card/pf-card.js'; diff --git a/elements/rh-dialog/docs/00-overview.md b/elements/rh-dialog/docs/00-overview.md index bc7527a80b..d21c008f4b 100644 --- a/elements/rh-dialog/docs/00-overview.md +++ b/elements/rh-dialog/docs/00-overview.md @@ -6,7 +6,7 @@ alt="A dialog container with a black headline, black body text, two blue buttons, and a dark gray close button all on a white background on top of a slightly transparent black background", src="./dialog-sample.png" %} - +{% repoStatusList %} ## Sample element @@ -30,15 +30,10 @@ View a live version of this element to see how it can be customized. View the demo {% endcta %} - - ## When to use - When you need to confirm user decisions - When you need an immediate response from users - When you need to notify users of urgent information concerning their current workflow - - -{% repoStatus type="Element" %} - +{% repoStatusChecklist %} \ No newline at end of file diff --git a/elements/rh-dialog/docs/30-code.md b/elements/rh-dialog/docs/30-code.md index add15fd861..0c7278d44b 100644 --- a/elements/rh-dialog/docs/30-code.md +++ b/elements/rh-dialog/docs/30-code.md @@ -1,6 +1,7 @@ {% renderInstall %}{% endrenderInstall %} -{% band header="Usage" %} +## Usage + ```html

    Leave page

    @@ -10,6 +11,5 @@
    Open modal dialog ``` -{% endband %} {% renderCodeDocs hideDescription=true %}{% endrenderCodeDocs %} diff --git a/elements/rh-dialog/rh-dialog.ts b/elements/rh-dialog/rh-dialog.ts index 66411dba6b..66746dd133 100644 --- a/elements/rh-dialog/rh-dialog.ts +++ b/elements/rh-dialog/rh-dialog.ts @@ -10,10 +10,11 @@ import { ScreenSizeController } from '../../lib/ScreenSizeController.js'; import styles from './rh-dialog.css'; -import '@rhds/elements/rh-surface/rh-surface.js'; import { query } from 'lit/decorators/query.js'; import { ifDefined } from 'lit/directives/if-defined.js'; +import '@rhds/elements/rh-surface/rh-surface.js'; + export class DialogCancelEvent extends Event { constructor() { super('cancel', { bubbles: true, cancelable: true }); @@ -40,16 +41,6 @@ async function pauseYoutube(iframe: HTMLIFrameElement) { await pauseVideo(iframe); } -function openChanged(this: RhDialog, oldValue: unknown) { - if (this.type === 'video' && oldValue === true && this.open === false) { - this.querySelector('video')?.pause?.(); - const iframe = this.querySelector('iframe'); - if (iframe?.src.match(/youtube/)) { - pauseYoutube(iframe); - } - } -} - /** * A dialog displays important information to users without requiring them to navigate away from the page. * @summary Communicates information requiring user input or action @@ -94,7 +85,7 @@ export class RhDialog extends LitElement { */ @property({ reflect: true }) position?: 'top'; - @observed(openChanged) + @observed @property({ type: Boolean, reflect: true }) open = false; /** Optional ID of the trigger element */ @@ -204,9 +195,19 @@ export class RhDialog extends LitElement { } protected async _openChanged(oldValue?: boolean, newValue?: boolean) { - // loosening types to prevent running these effects in unexpected circumstances - // eslint-disable-next-line eqeqeq - if (oldValue == null || newValue == null || oldValue == newValue) { + if (this.type === 'video') { + if (oldValue === true && this.open === false) { + this.querySelector('video')?.pause?.(); + const iframe = this.querySelector('iframe'); + if (iframe?.src.match(/youtube/)) { + pauseYoutube(iframe); + } + } + } else if (oldValue == null + || newValue == null + // loosening types to prevent running these effects in unexpected circumstances + // eslint-disable-next-line eqeqeq + || oldValue == newValue) { return; } else if (this.open) { // This prevents background scroll @@ -231,7 +232,8 @@ export class RhDialog extends LitElement { protected _triggerChanged() { if (this.trigger) { - this.#triggerElement = (this.getRootNode() as Document | ShadowRoot).getElementById(this.trigger); + this.#triggerElement = + (this.getRootNode() as Document | ShadowRoot).getElementById(this.trigger); this.#triggerElement?.addEventListener('click', this.onTriggerClick); } } diff --git a/elements/rh-dialog/test/rh-dialog.spec.ts b/elements/rh-dialog/test/rh-dialog.spec.ts index 221308cf21..cacfdcf5df 100644 --- a/elements/rh-dialog/test/rh-dialog.spec.ts +++ b/elements/rh-dialog/test/rh-dialog.spec.ts @@ -1,18 +1,97 @@ -import { expect, html } from '@open-wc/testing'; +import { expect, html, oneEvent } from '@open-wc/testing'; import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { clickElementAtOffset } from '@patternfly/pfe-tools/test/utils.js'; +import { sendKeys } from '@web/test-runner-commands'; import { RhDialog } from '@rhds/elements/rh-dialog/rh-dialog.js'; +import { RhButton } from '@rhds/elements/rh-button/rh-button.js'; -const element = html` - -`; +function press(key: string) { + return async function() { + await sendKeys({ press: key }); + }; +} describe('', function() { it('should upgrade', async function() { - const el = await createFixture(element); + const el = await createFixture(html` + + `); const klass = customElements.get('rh-dialog'); expect(el) - .to.be.an.instanceOf(klass) - .and - .to.be.an.instanceOf(RhDialog); + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(RhDialog); + }); + describe('with a trigger', function() { + let element: RhDialog; + let trigger: RhButton; + const updateComplete = () => element.updateComplete; + beforeEach(async function() { + element = await createFixture(html` + +

    Header

    +

    Body

    + Footer Action +
    + Open + `); + trigger = document.getElementById('trigger')!; + }); + describe('clicking the trigger', function() { + let openEventPromise: Promise; + let closeEventPromise: Promise; + let cancelEventPromise: Promise; + beforeEach(function() { + openEventPromise = oneEvent(element, 'open'); + closeEventPromise = oneEvent(element, 'close'); + cancelEventPromise = oneEvent(element, 'cancel'); + }); + beforeEach(() => trigger.click()); + beforeEach(updateComplete); + it('opens the dialog', function() { + expect(element.open).to.be.true; + }); + it('fires "open" event', async function() { + const openEvent = await openEventPromise; + expect(openEvent.type).to.equal('open'); + }); + describe('pressing Escape', function() { + beforeEach(press('Escape')); + beforeEach(updateComplete); + it('closes the dialog', function() { + expect(element.open).to.be.false; + }); + it('fires the cancel event', async function() { + const cancelEvent = await cancelEventPromise; + expect(cancelEvent.type).to.equal('cancel'); + }); + }); + describe('clicking outside the dialog', function() { + beforeEach(() => clickElementAtOffset(document.body, [10, 10])); + beforeEach(updateComplete); + it('closes the dialog', function() { + expect(element.open).to.be.false; + }); + it('fires the cancel event', async function() { + const cancelEvent = await cancelEventPromise; + expect(cancelEvent.type).to.equal('cancel'); + }); + }); + describe('clicking the close button', function() { + // ordinarily we try our best to avoid querying the shadow root in test files + // in this case, we feel justified in making an exception, because the "close-button" + // css part is already included in the element's public API. + // NOTE: we query specifically for the element with that part, not by shadow class or id + beforeEach(() => element.shadowRoot.querySelector('[part="close-button"]')?.click()); + beforeEach(updateComplete); + it('closes the dialog', function() { + expect(element.open).to.be.false; + }); + it('fires the close event', async function() { + const closeEvent = await closeEventPromise; + expect(closeEvent.type).to.equal('close'); + }); + }); + }); }); }); diff --git a/elements/rh-dialog/yt-api.ts b/elements/rh-dialog/yt-api.ts index 1151d5f6a6..bb1a28fd32 100644 --- a/elements/rh-dialog/yt-api.ts +++ b/elements/rh-dialog/yt-api.ts @@ -5,7 +5,7 @@ declare class Player { }; }); - pauseVideo(): void + pauseVideo(): void; } declare global { @@ -28,8 +28,8 @@ async function getPlayer(iframe: HTMLIFrameElement): Promise { onReady() { player = players.get(iframe); r(player); - } - } + }, + }, })); } else { requestAnimationFrame(() => r(player)); diff --git a/elements/rh-footer/docs/00-overview.md b/elements/rh-footer/docs/00-overview.md index d933aee306..62d9bec936 100644 --- a/elements/rh-footer/docs/00-overview.md +++ b/elements/rh-footer/docs/00-overview.md @@ -1,6 +1,8 @@ ## Overview {{ tagName | getElementDescription }} +{% repoStatusList %} + ## Sample element @@ -92,4 +94,4 @@ - When you want to give users persistent access to secondary content outside of the navigation - When you need a place to put copyright or legal information -{% repoStatus type="Element" %} \ No newline at end of file + {% repoStatusChecklist %} diff --git a/elements/rh-footer/rh-footer-universal.ts b/elements/rh-footer/rh-footer-universal.ts index 8819952c61..07799d6081 100644 --- a/elements/rh-footer/rh-footer-universal.ts +++ b/elements/rh-footer/rh-footer-universal.ts @@ -64,8 +64,13 @@ export class RhFooterUniversal extends LitElement { let footer: HTMLElement | null | undefined = node?.closest('footer'); let h2: HTMLElement | null | undefined = null; while (!!node && !footer) { - h2 = h2 || node?.closest('h2') || node?.querySelector('h2') || node?.shadowRoot?.querySelector('h2'); - footer = node?.closest('footer') || node?.querySelector('footer') || node?.shadowRoot?.querySelector('footer'); + h2 = h2 + || node?.closest('h2') + || node?.querySelector('h2') + || node?.shadowRoot?.querySelector('h2'); + footer = node?.closest('footer') + || node?.querySelector('footer') + || node?.shadowRoot?.querySelector('footer'); node = node.parentElement; } diff --git a/elements/rh-footer/rh-footer.ts b/elements/rh-footer/rh-footer.ts index fca9fad996..6423941659 100644 --- a/elements/rh-footer/rh-footer.ts +++ b/elements/rh-footer/rh-footer.ts @@ -94,7 +94,7 @@ export class RhFooter extends LitElement { protected screenSize = new ScreenSizeController(this, 'md', { onChange: matches => { this.#compact = !matches; - } + }, }); override connectedCallback() { @@ -191,15 +191,21 @@ export class RhFooter extends LitElement { * and synchronously update each list and header if we need to. */ public updateAccessibility(): void { - const listsSelector = ':is([slot^=links],[slot=footer-links-primary],[slot=footer-links-secondary]):is(ul)'; + const listsSelector = + ':is([slot^=links],[slot=footer-links-primary],[slot=footer-links-secondary]):is(ul)'; for (const list of this.querySelectorAll(listsSelector)) { // if we already have a label then we assume that the user // has wired this up themselves. if (!list.hasAttribute('aria-labelledby')) { // get the corresponding header that should be the previous sibling - const header = isHeaderTagName(list.previousElementSibling?.tagName ?? '') ? list.previousElementSibling : null; + const header = + isHeaderTagName(list.previousElementSibling?.tagName ?? '') ? + list.previousElementSibling + : null; if (!header) { - return this.#logger.warn('This links set doesn\'t have a valid header associated with it.'); + return this.#logger.warn( + 'This links set doesn\'t have a valid header associated with it.' + ); } else { // add an ID to the header if we need it header.id ||= getRandomId('rh-footer'); diff --git a/elements/rh-footer/test/rh-footer.spec.ts b/elements/rh-footer/test/rh-footer.spec.ts index 76a11783ad..c849ba7c36 100644 --- a/elements/rh-footer/test/rh-footer.spec.ts +++ b/elements/rh-footer/test/rh-footer.spec.ts @@ -1,5 +1,5 @@ import { html } from 'lit'; -import { fixture, expect, aTimeout, nextFrame, oneEvent } from '@open-wc/testing'; +import { fixture, expect, aTimeout, nextFrame } from '@open-wc/testing'; import { setViewport } from '@web/test-runner-commands'; import { tokens } from '@rhds/tokens'; import { RhFooter, RhFooterUniversal } from '../rh-footer.js'; @@ -158,9 +158,9 @@ describe('', function() { it('should upgrade', async function() { const klass = customElements.get('rh-footer'); expect(element) - .to.be.an.instanceOf(klass) - .and - .to.be.an.instanceOf(RhFooter); + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(RhFooter); }); it('passes the a11y audit', function() { @@ -177,9 +177,9 @@ describe('', function() { it('universal should upgrade', async function() { const klass = customElements.get('rh-footer-universal'); expect(universalFooter) - .to.be.an.instanceOf(klass) - .and - .to.be.an.instanceOf(RhFooterUniversal); + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(RhFooterUniversal); }); it('universal passes the a11y audit', async function() { @@ -383,7 +383,7 @@ describe('', function() { await element.updateComplete; expect(Math.abs(base.getBoundingClientRect().top - logo.getBoundingClientRect().top)).to.equal(32); - expect(Math.abs(logo.getBoundingClientRect().right - primary.getBoundingClientRect().left)).to.equal(32); + expect(Math.floor(Math.abs(logo.getBoundingClientRect().right - primary.getBoundingClientRect().left))).to.equal(32); expect(Math.abs(primary.getBoundingClientRect().bottom - secondaryContent.getBoundingClientRect().top)).to.equal(24); expect(Math.abs(base.getBoundingClientRect().bottom - tertiary.getBoundingClientRect().bottom)).to.equal(32); }); @@ -429,7 +429,7 @@ describe('', function() { it('should have an icon size of --rh-icon-size-02', async function() { const element = await fixture(KITCHEN_SINK_TEMPLATE); const socialLink = element.querySelector('rh-footer-social-link'); - await oneEvent(element, 'load'); + await aTimeout(200); // we need to reach into pf-icon to get the actual size of the svg. const icon = socialLink?.querySelector('pf-icon')?.shadowRoot?.querySelector('svg'); if (icon) { diff --git a/elements/rh-jump-links/docs/00-overview.md b/elements/rh-jump-links/docs/00-overview.md index ef6c59f8b5..0f8049f695 100644 --- a/elements/rh-jump-links/docs/00-overview.md +++ b/elements/rh-jump-links/docs/00-overview.md @@ -5,14 +5,11 @@ the link selected. A link is displayed as active when the content it links to is visible in the browser window. - ## Sample element ![Jump links sample component]({{ './jump-links-sample.svg' | url }}){style="--inline-img-max-width:128px;"} -{% repoStatus %} - {# ## Demos @@ -23,4 +20,3 @@ {% endcta %} #} - diff --git a/elements/rh-menu/demo/color-context.html b/elements/rh-menu/demo/color-context.html index 99296bf0b3..d647ab4aca 100644 --- a/elements/rh-menu/demo/color-context.html +++ b/elements/rh-menu/demo/color-context.html @@ -1,6 +1,5 @@ - Toggle Links Link1 Link2 Link3 @@ -13,4 +12,3 @@ import '@rhds/elements/rh-button/rh-button.js'; import '@rhds/elements/lib/elements/rh-context-demo/rh-context-demo.js'; - diff --git a/elements/rh-menu/demo/position-left.html b/elements/rh-menu/demo/position-left.html index a815355026..d6e4795ca2 100644 --- a/elements/rh-menu/demo/position-left.html +++ b/elements/rh-menu/demo/position-left.html @@ -1,5 +1,4 @@ - Toggle Settings Menuitem1 Menuitem2 Menuitem3 diff --git a/elements/rh-menu/demo/position-right.html b/elements/rh-menu/demo/position-right.html index 2a19b66cd2..eec565dae8 100644 --- a/elements/rh-menu/demo/position-right.html +++ b/elements/rh-menu/demo/position-right.html @@ -1,5 +1,4 @@ - Toggle Links Link1 Link2 Link3 diff --git a/elements/rh-menu/demo/position-top.html b/elements/rh-menu/demo/position-top.html index 4890d9d12a..8cbd73e815 100644 --- a/elements/rh-menu/demo/position-top.html +++ b/elements/rh-menu/demo/position-top.html @@ -1,5 +1,4 @@ - Toggle Menu Menuitem1 Menuitem2 Menuitem3 diff --git a/elements/rh-menu/demo/rh-menu.html b/elements/rh-menu/demo/rh-menu.html index 57439f7bb0..21a763837f 100644 --- a/elements/rh-menu/demo/rh-menu.html +++ b/elements/rh-menu/demo/rh-menu.html @@ -1,5 +1,4 @@ - Toggle Menu Menuitem1 Menuitem2 Menuitem3 diff --git a/elements/rh-menu/docs/rh-menu.md b/elements/rh-menu/docs/rh-menu.md index 908eeddf06..e57d1513de 100644 --- a/elements/rh-menu/docs/rh-menu.md +++ b/elements/rh-menu/docs/rh-menu.md @@ -1,17 +1,16 @@ -{% renderOverview %} - -{% endrenderOverview %} +{% renderInstall %}{% endrenderInstall %} -{% band header="Usage" %}{% endband %} +## Usage -{% renderSlots %}{% endrenderSlots %} +```html + + Toggle Menu + Menuitem1 + Menuitem2 + Menuitem3 + Menuitem4 + +``` -{% renderAttributes %}{% endrenderAttributes %} +{% renderCodeDocs hideDescription=true %}{% endrenderCodeDocs %} -{% renderMethods %}{% endrenderMethods %} - -{% renderEvents %}{% endrenderEvents %} - -{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} - -{% renderCssParts %}{% endrenderCssParts %} \ No newline at end of file diff --git a/elements/rh-menu/rh-menu.css b/elements/rh-menu/rh-menu.css index bb63c97a09..d877df03e2 100644 --- a/elements/rh-menu/rh-menu.css +++ b/elements/rh-menu/rh-menu.css @@ -9,9 +9,12 @@ slot { width: max-content; } +::slotted(a) { + padding: 5px !important; /* WARNING: not a token value */ +} + .dark::slotted(a) { color: var(--rh-color-interactive-blue-lightest, #b9dafc) !important; - padding: 5px !important; /* WARNING: not a token value */ } .dark::slotted(a:hover) { diff --git a/elements/rh-menu/rh-menu.ts b/elements/rh-menu/rh-menu.ts index c7a3b9673a..02a27fb8d0 100644 --- a/elements/rh-menu/rh-menu.ts +++ b/elements/rh-menu/rh-menu.ts @@ -1,6 +1,7 @@ import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { classMap } from 'lit/directives/class-map.js'; +import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js'; @@ -26,9 +27,15 @@ export class MenuToggleEvent extends Event { export class RhMenu extends LitElement { static readonly styles = [styles]; + static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; + + @queryAssignedElements() private _menuItems!: HTMLElement[]; + @colorContextConsumer() private on?: ColorTheme; - #tabindex = new RovingTabindexController(this); + #tabindex = new RovingTabindexController(this, { + getItems: () => this._menuItems ?? [], + }); get activeItem() { return this.#tabindex.activeItem; @@ -38,42 +45,31 @@ export class RhMenu extends LitElement { super.connectedCallback(); this.id ||= getRandomId('menu'); this.setAttribute('role', 'menu'); // TODO: use InternalsController.role when support/polyfill is better - this.#initItems(); + this.#onSlotchange(); } render() { const { on = '' } = this; return html` - + `; } - /** - * finds menu items and sets attributes accordingly - */ - #initItems() { - const items = Array.from(this.children) - .map(getItemElement) - .filter((x): x is HTMLElement => x instanceof HTMLElement); - items.forEach(item => item?.setAttribute('role', 'menuitem')); - this.#tabindex.initItems(items); - this.requestUpdate(); + #onSlotchange() { + for (const item of this._menuItems ?? []) { + item.setAttribute('role', 'menuitem'); + } } activateItem(item: HTMLElement) { - this.#tabindex.updateActiveItem(item); - this.#tabindex.focusOnItem(item); + this.#tabindex.setActiveItem(item); } -} -/** - * Given an element, returns self, or child that is not an rh-tooltip - */ -function getItemElement(element: Element) { - return ( - element.localName !== 'rh-tooltip' ? element - : element.querySelector(':not([slot=content])') - ); + focus() { + this.#tabindex.activeItem?.focus(); + } } declare global { diff --git a/elements/rh-menu/test/rh-menu.spec.ts b/elements/rh-menu/test/rh-menu.spec.ts index 63f3d9a600..8edd8a7b85 100644 --- a/elements/rh-menu/test/rh-menu.spec.ts +++ b/elements/rh-menu/test/rh-menu.spec.ts @@ -27,9 +27,9 @@ describe('', function() { it('should upgrade', function() { const klass = customElements.get('rh-menu'); expect(element) - .to.be.an.instanceOf(klass) - .and - .to.be.an.instanceOf(RhMenu); + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(RhMenu); }); it('instantiates imperatively', function() { @@ -45,184 +45,180 @@ describe('', function() { describe('tabbing to the element', function() { beforeEach(press('Tab')); + it('focuses the first item', function() { expect(document.activeElement).to.have.id('item1'); }); + describe('Tab', function() { beforeEach(press('Tab')); + it('focuses the document', function() { expect(document.activeElement).to.equal(document.body); }); }); + describe('Shift+Tab', function() { beforeEach(press('Shift+Tab')); + it('focuses the document', function() { expect(document.activeElement).to.equal(document.body); }); }); + describe('ArrowRight', function() { beforeEach(press('ArrowRight')); + it('focuses the second item', function() { expect(document.activeElement).to.have.id('item2'); }); + describe('ArrowRight', function() { beforeEach(press('ArrowRight')); + it('should focus on item3', function() { expect(document.activeElement).to.have.id('item3'); }); + describe('ArrowRight', function() { beforeEach(press('ArrowRight')); + it('should focus on item1', function() { expect(document.activeElement).to.have.id('item1'); }); }); }); + describe('End', function() { beforeEach(press('End')); it('should focus on item3', function() { expect(document.activeElement).and.to.have.id('item3'); }); }); + describe('Home', function() { beforeEach(press('Home')); - it('should focus on item1', function() { - expect(document.activeElement).and.to.have.id('item1'); - }); - }); - describe('PageDown', function() { - beforeEach(press('PageDown')); - it('should focus on item3', function() { - expect(document.activeElement).and.to.have.id('item3'); - }); - }); - describe('PageUp', function() { - beforeEach(press('PageUp')); + it('should focus on item1', function() { expect(document.activeElement).and.to.have.id('item1'); }); }); }); + describe('ArrowDown', function() { beforeEach(press('ArrowDown')); + it('focuses the second item', function() { expect(document.activeElement).to.have.id('item2'); }); + describe('ArrowDown', function() { beforeEach(press('ArrowDown')); + it('should focus on item3', function() { expect(document.activeElement).to.have.id('item3'); }); + describe('ArrowDown', function() { beforeEach(press('ArrowDown')); + it('should focus on item1', function() { expect(document.activeElement).to.have.id('item1'); }); }); }); + describe('End', function() { beforeEach(press('End')); + it('should focus on item3', function() { expect(document.activeElement).and.to.have.id('item3'); }); }); + describe('Home', function() { beforeEach(press('Home')); - it('should focus on item1', function() { - expect(document.activeElement).and.to.have.id('item1'); - }); - }); - describe('PageDown', function() { - beforeEach(press('PageDown')); - it('should focus on item3', function() { - expect(document.activeElement).and.to.have.id('item3'); - }); - }); - describe('PageUp', function() { - beforeEach(press('PageUp')); + it('should focus on item1', function() { expect(document.activeElement).and.to.have.id('item1'); }); }); }); + describe('ArrowLeft', function() { beforeEach(press('ArrowLeft')); + it('focuses the second item', function() { expect(document.activeElement).to.have.id('item3'); }); + describe('ArrowLeft', function() { beforeEach(press('ArrowLeft')); + it('should focus on item3', function() { expect(document.activeElement).to.have.id('item2'); }); + describe('ArrowLeft', function() { beforeEach(press('ArrowLeft')); + it('should focus on item1', function() { expect(document.activeElement).to.have.id('item1'); }); }); }); + describe('End', function() { beforeEach(press('End')); + it('should focus on item3', function() { expect(document.activeElement).and.to.have.id('item3'); }); }); + describe('Home', function() { beforeEach(press('Home')); - it('should focus on item1', function() { - expect(document.activeElement).and.to.have.id('item1'); - }); - }); - describe('PageDown', function() { - beforeEach(press('PageDown')); - it('should focus on item3', function() { - expect(document.activeElement).and.to.have.id('item3'); - }); - }); - describe('PageUp', function() { - beforeEach(press('PageUp')); + it('should focus on item1', function() { expect(document.activeElement).and.to.have.id('item1'); }); }); }); + describe('ArrowUp', function() { beforeEach(press('ArrowUp')); + it('focuses the second item', function() { expect(document.activeElement).to.have.id('item3'); }); + describe('ArrowUp', function() { beforeEach(press('ArrowUp')); + it('should focus on item3', function() { expect(document.activeElement).to.have.id('item2'); }); + describe('ArrowUp', function() { beforeEach(press('ArrowUp')); + it('should focus on item1', function() { expect(document.activeElement).to.have.id('item1'); }); }); }); + describe('End', function() { beforeEach(press('End')); + it('should focus on item3', function() { expect(document.activeElement).and.to.have.id('item3'); }); }); + describe('Home', function() { beforeEach(press('Home')); - it('should focus on item1', function() { - expect(document.activeElement).and.to.have.id('item1'); - }); - }); - describe('PageDown', function() { - beforeEach(press('PageDown')); - it('should focus on item3', function() { - expect(document.activeElement).and.to.have.id('item3'); - }); - }); - describe('PageUp', function() { - beforeEach(press('PageUp')); + it('should focus on item1', function() { expect(document.activeElement).and.to.have.id('item1'); }); diff --git a/elements/rh-navigation-secondary/docs/00-overview.md b/elements/rh-navigation-secondary/docs/00-overview.md index ecc5f5794c..e36b119d99 100644 --- a/elements/rh-navigation-secondary/docs/00-overview.md +++ b/elements/rh-navigation-secondary/docs/00-overview.md @@ -1,6 +1,8 @@ ## Overview {{ tagName | getElementDescription }} +{% repoStatusList %} + ## Sample element {% example palette="light", @@ -18,5 +20,4 @@ - When you need to provide a more granular navigation that is specific to a topic - When you need to prevent other navigations from getting overloaded - {% repoStatus type="Element" %} - +{% repoStatusChecklist %} diff --git a/elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.ts b/elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.ts index 15e897e52a..a64563d31d 100644 --- a/elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.ts +++ b/elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.ts @@ -30,13 +30,10 @@ import styles from './rh-navigation-secondary-dropdown.css'; /** * Upgrades a top level nav link to include dropdown functionality * @summary Upgrades a top level nav link to include dropdown functionality - * * @slot link - Link for dropdown, expects `` element * @slot menu - Menu for dropdown, expects `` element - * * @fires { SecondaryNavDropdownExpandEvent } change - Fires when a dropdown is clicked - * -**/ + */ @customElement('rh-navigation-secondary-dropdown') export class RhNavigationSecondaryDropdown extends LitElement { static readonly styles = [styles]; @@ -62,11 +59,13 @@ export class RhNavigationSecondaryDropdown extends LitElement { const [link] = this.#slots.getSlotted('link'); const [menu] = this.#slots.getSlotted('menu'); if (link === undefined) { - this.#logger.warn('[rh-navigation-secondary-dropdown][slot="link"] expects a slotted tag'); + this.#logger.warn( + '[rh-navigation-secondary-dropdown][slot="link"] expects a slotted tag' + ); return; } if (menu === undefined) { - this.#logger.warn('[rh-navigation-secondary-dropdown][slot="menu"] expects a slotted tag'); + this.#logger.warn(`[rh-navigation-secondary-dropdown][slot="menu"] expects a slotted tag`); return; } @@ -95,7 +94,7 @@ export class RhNavigationSecondaryDropdown extends LitElement { * run the `#open()` method, if false run the `#close()` method. * @param oldVal {string} - Boolean value in string form * @param newVal {string} - Boolean value in string form - * @returns {void} + * @returns */ protected _expandedChanged(oldVal?: 'false' | 'true', newVal?: 'false' | 'true'): void { if (newVal === oldVal) { @@ -119,7 +118,7 @@ export class RhNavigationSecondaryDropdown extends LitElement { /** * Sets or removes attributes needed to open a dropdown menu - * @returns {void} + * @returns */ #open(): void { const link = this.#slots.getSlotted('link').find(child => child instanceof HTMLAnchorElement); @@ -127,21 +126,24 @@ export class RhNavigationSecondaryDropdown extends LitElement { // menu as a RhNavigationSecondaryMenu in the slotted child is specific to rh-navigation-secondary. // If this component is abstracted to a standalone component. The RhNavigationSecondaryMenu // could possibly become a sub component of the abstraction instead. - const menu = this.#slots.getSlotted('menu').find(child => child instanceof RhNavigationSecondaryMenu) as RhNavigationSecondaryMenu; + const menu = this.#slots.getSlotted('menu').find(child => + child instanceof RhNavigationSecondaryMenu + ) as RhNavigationSecondaryMenu; menu.visible = true; } - /** - * Sets or removes attributes needed to close a dropdown menu - * @returns {void} - */ - #close(): void { + /** Sets or removes attributes needed to close a dropdown menu */ + #close() { const link = this.#slots.getSlotted('link').find(child => child instanceof HTMLAnchorElement); link?.setAttribute('aria-expanded', 'false'); // Same as comment in #open() // The RhNavigationSecondaryMenu could possibly become a sub component of the abstraction instead. - const menu = this.#slots.getSlotted('menu').find(child => child instanceof RhNavigationSecondaryMenu) as RhNavigationSecondaryMenu; - menu.visible = false; + const menu = this.#slots.getSlotted('menu').find( + (child: Node): child is RhNavigationSecondaryMenu => + child instanceof RhNavigationSecondaryMenu); + if (menu) { + menu.visible = false; + } } async #mutationsCallback(): Promise { @@ -151,13 +153,16 @@ export class RhNavigationSecondaryDropdown extends LitElement { } } +/** @deprecated use rh-navigation-secondary-dropdown */ @customElement('rh-secondary-nav-dropdown') class RhSecondaryNavDropdown extends RhNavigationSecondaryDropdown { #logger = new Logger(this); constructor() { super(); - this.#logger.warn('rh-secondary-nav-dropdown is deprecated. Use rh-navigation-secondary-dropdown instead.'); + this.#logger.warn( + 'rh-secondary-nav-dropdown is deprecated. Use rh-navigation-secondary-dropdown instead.' + ); } } diff --git a/elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.ts b/elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.ts index 25554a1354..95c07cf607 100644 --- a/elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.ts +++ b/elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.ts @@ -11,15 +11,11 @@ import styles from './rh-navigation-secondary-menu-section.css'; /** * A menu section which auto upgrades accessibility for headers and sibling list * @summary 'A menu section which auto upgrades accessibility for headers and sibling list' - * * @slot header - Adds a header tag to section, expects `

    |

    |

    |

    |

    |
    ` element * @slot links - Adds a ul tag to section, expects `
      |
        ` element * @slot cta - Adds a section level CTA, expects `` element - * * @csspart container - container,
        element - * - * -**/ + */ @customElement('rh-navigation-secondary-menu-section') export class RhNavigationSecondaryMenuSection extends LitElement { static readonly styles = [styles]; @@ -47,16 +43,19 @@ export class RhNavigationSecondaryMenuSection extends LitElement { * `aria-labelledby` attribute finds the previousElementSibling header * `` tags if available assigns an id or uses preexisting id * to that header, then uses that id to the list on the `aria-labelledby`. - * @returns {void} */ - #updateAccessibility(): void { + #updateAccessibility() { const lists = this.querySelectorAll(':is([slot="links"]):is(ul, ol)'); for (const list of lists) { if (!list.hasAttribute('aria-labelledby')) { - const header = isHeadingElement(list.previousElementSibling) ? list.previousElementSibling : null; + const header = isHeadingElement(list.previousElementSibling) ? + list.previousElementSibling + : null; if (!header) { - return this.#logger.warn('This links set doesn\'t have a valid header associated with it.'); + return this.#logger.warn( + 'This links set doesn\'t have a valid header associated with it.' + ); } else { // add an ID to the header if we need it header.id ||= getRandomId('rh-navigation-secondary-menu-section'); @@ -68,13 +67,16 @@ export class RhNavigationSecondaryMenuSection extends LitElement { } } +/** @deprecated use rh-navigation-secondary-menu-section */ @customElement('rh-secondary-nav-menu-section') class RhSecondaryNavMenuSection extends RhNavigationSecondaryMenuSection { #logger = new Logger(this); constructor() { super(); - this.#logger.warn('rh-secondary-nav-menu-section is deprecated. Use rh-navigation-secondary-menu-section instead.'); + this.#logger.warn( + `rh-secondary-nav-menu-section is deprecated. Use rh-navigation-secondary-menu-section instead.` + ); } } diff --git a/elements/rh-navigation-secondary/rh-navigation-secondary-menu.ts b/elements/rh-navigation-secondary/rh-navigation-secondary-menu.ts index 963530969c..fa6605760b 100644 --- a/elements/rh-navigation-secondary/rh-navigation-secondary-menu.ts +++ b/elements/rh-navigation-secondary/rh-navigation-secondary-menu.ts @@ -85,7 +85,9 @@ class RhSecondaryNavMenu extends RhNavigationSecondaryMenu { constructor() { super(); - this.#logger.warn('rh-secondary-nav-menu is deprecated. Use rh-navigation-secondary-menu instead.'); + this.#logger.warn( + 'rh-secondary-nav-menu is deprecated. Use rh-navigation-secondary-menu instead.' + ); } } diff --git a/elements/rh-navigation-secondary/rh-navigation-secondary.ts b/elements/rh-navigation-secondary/rh-navigation-secondary.ts index 1060191e01..7a282f74d4 100644 --- a/elements/rh-navigation-secondary/rh-navigation-secondary.ts +++ b/elements/rh-navigation-secondary/rh-navigation-secondary.ts @@ -14,7 +14,10 @@ import '@rhds/elements/rh-surface/rh-surface.js'; import './rh-navigation-secondary-menu-section.js'; import './rh-navigation-secondary-overlay.js'; -import { RhNavigationSecondaryDropdown, SecondaryNavDropdownExpandEvent } from './rh-navigation-secondary-dropdown.js'; +import { + RhNavigationSecondaryDropdown, + SecondaryNavDropdownExpandEvent, +} from './rh-navigation-secondary-dropdown.js'; import { DirController } from '../../lib/DirController.js'; import { ScreenSizeController } from '../../lib/ScreenSizeController.js'; @@ -36,28 +39,35 @@ export type NavPalette = Extract { + return parent.querySelectorAll(`a, + button:not([disabled]), + input:not([disabled]), + select:not([disabled]), + textarea:not([disabled]), + [tabindex]:not([tabindex="-1"]):not([disabled]), + details:not([disabled]), + summary:not(:disabled)`); +} + /** * The Secondary navigation is used to connect a series of pages together. It displays wayfinding content and links relevant to the page it is placed on. It should be used in conjunction with the [primary navigation](../navigation-primary). * * @summary Propagates related content across a series of pages - * * @slot logo - Logo added to the main nav bar, expects `Text | | ` element * @slot nav - Navigation list added to the main nav bar, expects `
          ` element * @slot cta - Nav bar level CTA, expects `` element * @slot mobile-menu - Text label for the mobile menu button, for l10n. Defaults to "Menu" - * * @csspart nav - container, `