diff --git a/.github/workflows/github-actions-check-labels.yml b/.github/workflows/github-actions-check-labels.yml index e69de29bb2..e369390c95 100644 --- a/.github/workflows/github-actions-check-labels.yml +++ b/.github/workflows/github-actions-check-labels.yml @@ -0,0 +1,35 @@ +name: Check Labels +run-name: ${{ github.actor }} is checking labels πŸš€ +on: + pull_request: + types: [ labeled, unlabeled ] +jobs: + Checking-Labels: + runs-on: ubuntu-latest + steps: + - name: Check for Semver label + run: | + LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH") + SEMVER_PATTERN="^(major|minor|patch)$" + + SEMVER_LABELS=$(echo "$LABELS" | grep -iE "$SEMVER_PATTERN" || true) + + # Check if SEMVER_LABELS is empty + if [ -z "$SEMVER_LABELS" ]; then + echo "Error: No Semver label found. Please add exactly one of: major, minor, patch (case-insensitive)" + exit 1 + fi + + SEMVER_LABEL_COUNT=$(echo "$SEMVER_LABELS" | wc -l) + + if [ "$SEMVER_LABEL_COUNT" -eq 0 ]; then + echo "Error: No Semver label found. Please add exactly one of: major, minor, patch (case-insensitive)" + exit 1 + elif [ "$SEMVER_LABEL_COUNT" -gt 1 ]; then + echo "Error: Multiple Semver labels found. Please ensure only one is present:" + echo "$SEMVER_LABELS" + exit 1 + else + NORMALIZED_LABEL=$(echo "$SEMVER_LABELS" | tr '[:upper:]' '[:lower:]') + echo "Valid Semver label found: $NORMALIZED_LABEL" + fi \ No newline at end of file diff --git a/.github/workflows/github-actions-release-candidate.yml b/.github/workflows/github-actions-release-candidate.yml index 0df9bed6c3..633f7698f0 100644 --- a/.github/workflows/github-actions-release-candidate.yml +++ b/.github/workflows/github-actions-release-candidate.yml @@ -30,29 +30,88 @@ jobs: with: bundler-cache: true - name: Python Setup - uses: actions/setup-python@v4 - with: + uses: actions/setup-python@v4 + with: python-version: '3.9' - name: Set Git Config run: | git config --local user.name "${{ github.actor }}" git config --local user.email "${{ github.actor }}@users.noreply.github.com" - - name: Grab Current Version and Set New RC Version - id: set-version + - name: Get Semver Label + id: get-label run: | - current_npm_version=$(node -pe "require('./package.json').version") + PR_NUMBER=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \ + | jq '.[0].number') + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - if [[ $current_npm_version == *"-rc."* ]]; then - new_npm_version=$(yarn version --prerelease --preid rc --no-git-tag-version | grep "New version:" | awk '{print $4}') + if [ ! -z "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "βœ… Successfully found PR number: $PR_NUMBER" else - new_npm_version=$(yarn version --preminor --preid rc --no-git-tag-version | grep "New version:" | awk '{print $4}') + echo "❌ Unable to find PR number" fi - + + echo "Fetching labels for PR #$PR_NUMBER..." + LABELS=$(gh pr view $PR_NUMBER --json labels -q '.labels[].name' || echo "Failed to fetch labels") + echo "Found labels: $LABELS" + + if [ -z "$LABELS" ]; then + echo "β›” Error: Failed to fetch PR labels" + exit 1 + fi + + SEMVER_LABEL=$(echo "$LABELS" | grep -iE '^(major|minor|patch)$' || true) + echo "Found Semver labels: $SEMVER_LABEL" + + if [ -z "$SEMVER_LABEL" ]; then + echo "β›” Error: No valid Semver label (major, minor, patch) found on PR #$PR_NUMBER." + exit 1 + fi + + LABEL_COUNT=$(echo "$SEMVER_LABEL" | wc -l) + echo "Number of Semver labels found: $LABEL_COUNT" + + if [ "$LABEL_COUNT" -ne 1 ]; then + echo "β›” Error: Expected exactly one Semver label, found $LABEL_COUNT on PR #$PR_NUMBER." + exit 1 + fi + + echo "SEMVER_LABEL=$SEMVER_LABEL" >> $GITHUB_ENV + echo "βœ… Successfully found Semver label: $SEMVER_LABEL" + + echo "SEMVER_LABEL=$SEMVER_LABEL" >> $GITHUB_ENV + echo "Semver label found: $SEMVER_LABEL" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Grab Current Version and Set New RC Version + id: set-version + run: | + current_npm_version=$(node -pe "require('./package.json').version") + + case ${{ env.SEMVER_LABEL }} in + Major) + new_npm_version=$(yarn version --premajor --preid rc --no-git-tag-version | grep "New version:" | awk '{print $4}') + ;; + Minor) + new_npm_version=$(yarn version --preminor --preid rc --no-git-tag-version | grep "New version:" | awk '{print $4}') + ;; + Patch) + new_npm_version=$(yarn version --prepatch --preid rc --no-git-tag-version | grep "New version:" | awk '{print $4}') + ;; + *) + echo "Error: Invalid Semver label: ${{ env.SEMVER_LABEL }}" + exit 1 + ;; + esac + new_npm_version=${new_npm_version#v} new_ruby_version=$(echo $new_npm_version | sed 's/-rc\./.pre.rc./') - + echo "new_npm_version=${new_npm_version}" >> $GITHUB_ENV echo "new_ruby_version=${new_ruby_version}" >> $GITHUB_ENV + - name: Check if version exists and increment if necessary run: | max_attempts=100 @@ -140,10 +199,10 @@ jobs: issue_number: pullRequestNumber, owner: context.repo.owner, repo: context.repo.repo, - body: `You merged this pr to master branch: + body: `You merged this pr to master branch: - Ruby Gem: [${{env.RUBY_GEM_VERSION}}](${{env.RUBY_GEM_LINK}}) - NPM: [${{env.NPM_VERSION}}](${{env.NPM_LINK}})` }); } else { console.log('No pull request found for this commit'); - } + } \ No newline at end of file diff --git a/playbook-website/Gemfile.lock b/playbook-website/Gemfile.lock index 884c2fb3af..2909c99b95 100644 --- a/playbook-website/Gemfile.lock +++ b/playbook-website/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../playbook specs: - playbook_ui (14.6.2) + playbook_ui (14.7.0) actionpack (>= 5.2.4.5) actionview (>= 5.2.4.5) activesupport (>= 5.2.4.5) diff --git a/playbook-website/app/javascript/components/MainSidebar/MenuData/GuidelinesNavItems.ts b/playbook-website/app/javascript/components/MainSidebar/MenuData/GuidelinesNavItems.ts index 9303a1d3f7..da7e613fbd 100644 --- a/playbook-website/app/javascript/components/MainSidebar/MenuData/GuidelinesNavItems.ts +++ b/playbook-website/app/javascript/components/MainSidebar/MenuData/GuidelinesNavItems.ts @@ -63,6 +63,10 @@ export const VisualGuidelinesItems = [ name: "Hover", link: "/visual_guidelines/hover" }, + { + name: "Group Hover", + link: "/visual_guidelines/group_hover" + }, { name: "Text Align", link: "/visual_guidelines/text_align" diff --git a/playbook-website/app/javascript/components/VisualGuidelines/Examples/GroupHover.tsx b/playbook-website/app/javascript/components/VisualGuidelines/Examples/GroupHover.tsx new file mode 100644 index 0000000000..4e262da48b --- /dev/null +++ b/playbook-website/app/javascript/components/VisualGuidelines/Examples/GroupHover.tsx @@ -0,0 +1,49 @@ +/* eslint-disable flowtype/no-types-missing-file-annotation */ + +import React from 'react' + +import { + Card, + Title, +} from 'playbook-ui' + +import Example from '../Templates/Example' + +const GroupHoverDescription = () => ( + <> + You can hover over a kit and its children's hover affects will be applied. Check out {"our hover affects here."} + +) + +const GroupHover = ({ example }: {example: string}) => ( + } + example={example} + title="Group Hover" + > + + + <Title + alignSelf="center" + paddingY="lg" + text="I don't have any hover effect on me" + /> + <Title + alignSelf="center" + hover={{ scale: "lg"}} + text="I need to be hovered over directly" + /> + </Card> + </Example> +) + +export default GroupHover diff --git a/playbook-website/app/javascript/components/VisualGuidelines/index.tsx b/playbook-website/app/javascript/components/VisualGuidelines/index.tsx index 98356e6aa8..043d89d30a 100644 --- a/playbook-website/app/javascript/components/VisualGuidelines/index.tsx +++ b/playbook-website/app/javascript/components/VisualGuidelines/index.tsx @@ -18,6 +18,7 @@ import Cursor from "../VisualGuidelines/Examples/Cursor"; import FlexBox from "../VisualGuidelines/Examples/FlexBox"; import Position from "../VisualGuidelines/Examples/Position"; import Hover from "../VisualGuidelines/Examples/Hover"; +import GroupHover from "../VisualGuidelines/Examples/GroupHover"; import TextAlign from "../VisualGuidelines/Examples/TextAlign"; import Overflow from "./Examples/Overflow"; import Truncate from "./Examples/Truncate"; @@ -82,6 +83,8 @@ const VisualGuidelines = ({ return <VerticalAlign example={examples.vertical_align_jsx}/>; case "hover": return <Hover example={examples.hover_jsx}/>; + case "group_hover": + return <GroupHover example={examples.group_hover_jsx}/>; case "text_align": return <TextAlign example={examples.text_align_jsx} /> case "overflow": diff --git a/playbook-website/app/views/guides/getting_started/dependencies.md b/playbook-website/app/views/guides/getting_started/dependencies.md new file mode 100644 index 0000000000..66550e3e03 --- /dev/null +++ b/playbook-website/app/views/guides/getting_started/dependencies.md @@ -0,0 +1,67 @@ +--- +title: Dependencies +icon: code +description: Some of our kits require additional libraries to run properly. +--- + +## Playbook UI Dependencies | React + +Playbook UI's React library needs the following packages installed in your project to work properly: + +```json +"react" + "react-dom" + "react-is" + "react-trix" +``` + +## Playbook UI Dependencies | Rails + +Playbook UI's Rails gem requires React for its components javascript to fully function. Follow the instructions in the [Ruby & React Setup](/guides/getting_started/rails_&_react_setup) guide to add react to your Rails app. + +## Unbundled Dependencies + +These kits require you to install additional libraries to get full functionality. + +To install them add them to your project using `yarn add`, `npm install`, or manually add them to your `package.json` file. + +| Kit | Kit Link | NPM Link(s) | Dependency(s) | +|---------------------|-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------------------------------------------| +| **Icon** | [Icon](https://playbook.powerapp.cloud/kits/icon/react) | [fontawesome-free](https://www.npmjs.com/package/fontawesome-free) | fontawesome-free | +| **Icon Circle** | [Icon Circle](https://playbook.powerapp.cloud/kits/icon_circle/react) | [fontawesome-free](https://www.npmjs.com/package/fontawesome-free) | fontawesome-free | +| **Icon Stat Value** | [Icon Stat Value](https://playbook.powerapp.cloud/kits/icon_stat_value/react) | [fontawesome-free](https://www.npmjs.com/package/fontawesome-free) | fontawesome-free | +| **Icon Value** | [Icon Value](https://playbook.powerapp.cloud/kits/icon_value/react) | [fontawesome-free](https://www.npmjs.com/package/fontawesome-free) | fontawesome-free | +| **Map** | [Map](https://playbook.powerapp.cloud/kits/map/react) | [maplibre-gl](https://www.npmjs.com/package/maplibre-gl) | maplibre-gl | +| **Rich Text Editor**<br>(TipTap Editor) | [Rich Text Editor](https://playbook.powerapp.cloud/kits/rich_text_editor/react) | - [@tiptap/core](https://www.npmjs.com/package/@tiptap/core)<br>- [@tiptap/react](https://www.npmjs.com/package/@tiptap/react)<br>- [@tiptap/starter-kit](https://www.npmjs.com/package/@tiptap/starter-kit)<br>- [@tiptap/extension-document](https://www.npmjs.com/package/@tiptap/extension-document)<br>- [@tiptap/extension-highlight](https://www.npmjs.com/package/@tiptap/extension-highlight)<br>- [@tiptap/extension-horizontal-rule](https://www.npmjs.com/package/@tiptap/extension-horizontal-rule)<br>- [@tiptap/extension-link](https://www.npmjs.com/package/@tiptap/extension-link)<br>- [@tiptap/extension-paragraph](https://www.npmjs.com/package/@tiptap/extension-paragraph)<br>- [@tiptap/extension-text](https://www.npmjs.com/package/@tiptap/extension-text)<br>- [@tiptap/pm](https://www.npmjs.com/package/@tiptap/pm) | - @tiptap/core<br>- @tiptap/react<br>- @tiptap/starter-kit<br>- @tiptap/extension-document<br>- @tiptap/extension-highlight<br>- @tiptap/extension-horizontal-rule<br>- @tiptap/extension-link<br>- @tiptap/extension-paragraph<br>- @tiptap/extension-text<br>- @tiptap/pm | + +## Bundled Dependencies + +These kits use dependencies that are bundled with them; no additional installation is required. + +| Kit | Kit Link | NPM Link(s) | Dependency(s) | +|------------------------|-----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|-----------------------------------------| +| **Advanced Table** | [Advanced Table](https://playbook.powerapp.cloud/kits/advanced_table/react) | [@tanstack/react-table](https://www.npmjs.com/package/@tanstack/react-table) | @tanstack/react-table | +| **Bar Graph** | [Bar Graph](https://playbook.powerapp.cloud/kits/bar_graph/react) | [highcharts](https://www.npmjs.com/package/highcharts),<br>[highcharts-react-official](https://www.npmjs.com/package/highcharts-react-official) | highcharts,<br>highcharts-react-official | +| **Circle Chart** | [Circle Chart](https://playbook.powerapp.cloud/kits/circle_chart/react) | [highcharts](https://www.npmjs.com/package/highcharts),<br>[highcharts-react-official](https://www.npmjs.com/package/highcharts-react-official) | highcharts,<br>highcharts-react-official | +| **Date Picker** | [Date Picker](https://playbook.powerapp.cloud/kits/date_picker/react) | [flatpickr](https://www.npmjs.com/package/flatpickr) | flatpickr | +| **Dialog** | [Dialog](https://playbook.powerapp.cloud/kits/dialog/react) | [react-modal](https://www.npmjs.com/package/react-modal) | react-modal | +| **File Upload** | [File Upload](https://playbook.powerapp.cloud/kits/file_upload/react) | [react-dropzone](https://www.npmjs.com/package/react-dropzone) | react-dropzone | +| **Filter** | [Filter](https://playbook.powerapp.cloud/kits/filter/react) | [react-popper](https://www.npmjs.com/package/react-popper) | react-popper | +| **Gauge** | [Gauge](https://playbook.powerapp.cloud/kits/gauge/react) | [highcharts](https://www.npmjs.com/package/highcharts),<br>[highcharts-react-official](https://www.npmjs.com/package/highcharts-react-official) | highcharts,<br>highcharts-react-official | +| **Highlight** | [Highlight](https://playbook.powerapp.cloud/kits/highlight/react) | [react-highlight-words](https://www.npmjs.com/package/react-highlight-words) | react-highlight-words | +| **LightBox** | [LightBox](https://playbook.powerapp.cloud/kits/lightbox/react) | [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch) | react-zoom-pan-pinch | +| **Line Graph** | [Line Graph](https://playbook.powerapp.cloud/kits/line_graph/react) | [highcharts](https://www.npmjs.com/package/highcharts),<br>[highcharts-react-official](https://www.npmjs.com/package/highcharts-react-official) | highcharts,<br>highcharts-react-official | +| **Multi Level Select** | [Multi Level Select](https://playbook.powerapp.cloud/kits/multi_level_select/react) | [lodash](https://www.npmjs.com/package/lodash) | lodash | +| **Passphrase** | [Passphrase](https://playbook.powerapp.cloud/kits/passphrase/react) | [react-popper](https://www.npmjs.com/package/react-popper) | react-popper | +| **Phone Number Input** | [Phone Number Input](https://playbook.powerapp.cloud/kits/phone_number_input/react) | [intl-tel-input](https://www.npmjs.com/package/intl-tel-input) | intl-tel-input | +| **Popover** | [Popover](https://playbook.powerapp.cloud/kits/popover/react) | [lodash](https://www.npmjs.com/package/lodash),<br>[react-popper](https://www.npmjs.com/package/react-popper) | lodash,<br>react-popper | +| **Rich Text Editor**<br>(Trix Editor) | [Rich Text Editor](https://playbook.powerapp.cloud/kits/rich_text_editor/react) | [trix](https://www.npmjs.com/package/trix),<br>[react-trix](https://www.npmjs.com/package/react-trix) | trix,<br>react-trix | +| **Tooltip** | [Tooltip](https://playbook.powerapp.cloud/kits/tooltip/react) | [@floating-ui/react](https://www.npmjs.com/package/@floating-ui/react) | @floating-ui/react | +| **Treemap Chart** | [Treemap Chart](https://playbook.powerapp.cloud/kits/treemap_chart/react) | [highcharts](https://www.npmjs.com/package/highcharts),<br>[highcharts-react-official](https://www.npmjs.com/package/highcharts-react-official) | highcharts,<br>highcharts-react-official | +| **Typeahead** | [Typeahead](https://playbook.powerapp.cloud/kits/typeahead/react) | [react-select](https://www.npmjs.com/package/react-select),<br>[lodash](https://www.npmjs.com/package/lodash) | react-select,<br>lodash | +| **Walkthrough** | [Walkthrough](https://playbook.powerapp.cloud/kits/walkthrough/react) | [react-joyride](https://www.npmjs.com/package/react-joyride) | react-joyride | + +## Notes +**Rich Text Editor**: This kit supports two different editors: +**TipTap Editor**: Requires manual installation of `tiptap` and various `@tiptap/*` extensions (listed above under Unbundled Dependencies). +**Trix Editor**: Dependencies (`trix` and `react-trix`) are bundled with the kit; no extra installation is needed. diff --git a/playbook-website/app/views/guides/getting_started/font_awesome.md b/playbook-website/app/views/guides/getting_started/font_awesome.md new file mode 100644 index 0000000000..d207dda012 --- /dev/null +++ b/playbook-website/app/views/guides/getting_started/font_awesome.md @@ -0,0 +1,127 @@ +--- +title: Font Awesome Setup +description: Playbook seamlessly integrates with Font Awesome, a leading icon library known for its extensive collection of high-quality, scalable icons. This integration not only enhances the visual appeal of websites and applications but also improves overall usability. +icon: font-awesome +--- + +Playbook seamlessly integrates with [Font Awesome](https://fontawesome.com/), a leading icon library known for its extensive collection of high-quality, scalable icons. This integration not only enhances the visual appeal of websites and applications but also improves overall usability. + +Some Font Awesome benefits: + +**1. Wide Range of Icons:** Font Awesome offers a vast selection of icons to suit a variety of needs. You can easily find the perfect icon for your project through their [icon search](https://fontawesome.com/search). +**2. Ease of Use:** The icons are straightforward to implement. With just a few lines of code, you can quickly and easily add visual elements to your web projects. Note, a Pro subscription is required for access to a wider range of icons beyond the [Free set](https://fontawesome.com/search?o=r&m=free&s=regular). +**3. Visual Appeal:** Incorporating these icons can improve the aesthetic of your site or application, making it more attractive to users. With Playbook, you have the flexibility to customize color, size, and animations. +**4. User-Friendliness:** Icons can help users navigate and understand your website or application more efficiently, enhancing their overall experience. Font Awesome icons are web fonts compatible with most browsers and are optimized for performance and accessibility. + +Integrating Font Awesome with Playbook ensures that you have access to these benefits, making your projects more polished and professional. + +![fontawesome](https://github.com/user-attachments/assets/638b63ad-56d3-4819-8e05-fcbb175bedc7) + +### Ruby on Rails Setup + +<details class="mt_sm"> + <summary class="mb_sm"><strong><img src="https://github.com/user-attachments/assets/781b1ec8-954c-4919-a79c-7009521849a6" alt="rails logo" class="pb_custom_icon svg-inline--fa svg_fw mr_xxs" style="margin: 0;" />Default with an Asset Pipeline</strong></summary> + <strong>Make sure you are on Rails 7 or higher.</strong> + <p> + <strong>1.</strong> Follow the <a href="/guides/getting_started/ruby_on_rails_setup">Ruby on Rails Setup getting started page</a> to setup Playbook with your Rails project. + </p> + <p> + <strong>2.</strong> Setup Pro or Free Font Awesome to use our Icon Component. + </p> + <p><strong>Pro:</strong></p> + <pre><code class="rb"># app/assets/stylesheets/application.scss + @import "font-awesome-pro"; + @import "font-awesome-pro/solid"; + @import "font-awesome-pro/regular"; + @import "playbook";</code></pre> + <pre><code class="rb"># app/Gemfile + source "https://token:TOKEN@dl.fontawesome.com/basic/fontawesome-pro/ruby/" do + gem "font-awesome-pro-sass", "6.2.0" + end</code></pre> + <strong>Free:</strong> + <p><em>Currently only <a href="https://fontawesome.com/search?o=r&m=free&s=regular">Free Regular</a> icons are supported in our icon component structure.</em></p> + + <pre><code class="rb"># app/assets/stylesheets/application.scss + @import "font-awesome";</code></pre> + + <pre><code class="rb"># app/Gemfile + source "https://token:TOKEN@dl.fontawesome.com/basic/fontawesome-pro/ruby/" do + gem "font-awesome-pro-sass", "6.2.0" + end</code></pre> + + <strong>3.</strong> Bundle all the things! + + <pre><code class="sh">bundle install</code></pre> + + <strong>4.</strong> <strong>Go build awesome stuff!</strong> + + <p>Refer to our <a href="/kits/icon">Icon kit</a> to get started with Font Awesome icons in Playbook.</p> + + <pre><code class="rb"><%= pb_rails("icon", props: { icon: "font-awesome", fixed_width: true }) %></code></pre> +</details> + +<details class="mt_sm"> + <summary class="mb_sm"><strong><img src="https://github.com/user-attachments/assets/781b1ec8-954c-4919-a79c-7009521849a6" alt="rails logo" class="pb_custom_icon svg-inline--fa svg_fw mr_xxs" style="margin: 0;" />With a JavaScript Bundler</strong></summary> + <strong>Make sure you are on Rails 7 or higher.</strong> + <p> + <strong>1.</strong> Follow the <a href="/guides/getting_started/ruby_on_rails_setup">Ruby on Rails Setup getting started page</a> to setup Playbook with your Rails project. + </p> + <p> + Use your desired bundler: + <pre><code class="sh">rails new CoolNewApp -j webpack</code></pre> + </p> + <p> + <strong>2.</strong> Follow the <a href="/guides/getting_started/rails_&_react_setup">Ruby & React page</a> if you want to use React with your project. + </p> + <p> + <strong>3.</strong> Setup Pro or Free Font Awesome to use our Icon Component. + </p> + <p><strong>Pro:</strong></p> + <pre><code class="rb"># app/assets/stylesheets/application.scss + @import "font-awesome-pro"; + @import "font-awesome-pro/solid"; + @import "font-awesome-pro/regular"; + @import "playbook";</code></pre> + <pre><code class="rb"># app/Gemfile + source "https://token:TOKEN@dl.fontawesome.com/basic/fontawesome-pro/ruby/" do + gem "font-awesome-pro-sass", "6.2.0" + end</code></pre> + <p>If you prefer, you can install with JavaScript:</p> + <pre><code class="sh">FONTAWESOME_PACKAGE_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX yarn add @fortawesome/fontawesome-pro</code></pre> + <strong>Free:</strong> + <p><em>Currently only <a href="https://fontawesome.com/search?o=r&m=free&s=regular">Free Regular</a> icons are supported in our icon component structure.</em></p> + + <pre><code class="rb"># app/assets/stylesheets/application.scss + @import "font-awesome";</code></pre> + + <pre><code class="rb"># app/Gemfile + source "https://token:TOKEN@dl.fontawesome.com/basic/fontawesome-pro/ruby/" do + gem "font-awesome-pro-sass", "6.2.0" + end</code></pre> + + <p>If you prefer, you can install with JavaScript:</p> + <pre><code class="sh">yarn add @fortawesome/fontawesome-free</code></pre> + + <strong>4.</strong> Bundle all the things! + + <pre><code class="sh">bundle install</code></pre> + + <pre><code class="sh">yarn</code></pre> + + <pre><code class="sh">npm install</code></pre> + + <strong>5.</strong> Build JavaScript for development + <p>When using a bundling option, use <code>bin/dev</code> to start the Rails server and build JavaScript for development. Don't forget to add a build script in your package.json file:</p> + + <pre><code class="js">"scripts": { + "build": "webpack" + },</code></pre> + + <strong>6.</strong> <strong>Go build awesome stuff!</strong> + + <p>Refer to our <a href="/kits/icon">Icon kit</a> to get started with Font Awesome icons in Playbook.</p> + + <pre><code class="rb"><%= pb_rails("icon", props: { icon: "font-awesome", fixed_width: true }) %></code></pre> + + <pre><code class="react"><Icon fixedWidth icon="font-awesome" /></code></pre> +</details> \ No newline at end of file diff --git a/playbook-website/app/views/guides/getting_started/react_setup.md b/playbook-website/app/views/guides/getting_started/react_setup.md index f6304d8d6a..1b0bf4dd4f 100644 --- a/playbook-website/app/views/guides/getting_started/react_setup.md +++ b/playbook-website/app/views/guides/getting_started/react_setup.md @@ -8,11 +8,13 @@ description: React applications. Endlessly flexible presentational UI components ```sh yarn add playbook-ui ``` -#### Match your project's versions of React and ReactDOM with Playbook's versions +#### Match your project's versions of React, ReactDOM, react-is and React Trix with Playbook's versions ```json "react": "17.0.2", "react-dom": "17.0.2", + "react-is": "^17.0.2", + "react-trix": "0.10.1", ``` #### Import fonts and CSS styles Can be imported in your Index.js file or top level app Component @@ -28,4 +30,7 @@ import 'playbook-ui/dist/fonts/fontawesome-min'; import { Avatar, Button } from 'playbook-ui'; ``` #### CodeSandbox React Setup Example -[Link to CodeSandbox Example](https://codesandbox.io/s/playbook-empty-6ixcw) \ No newline at end of file +[Link to CodeSandbox Example](https://codesandbox.io/s/playbook-empty-6ixcw) + +### Dependencies +[More details about Playbook dependencies](/guides/getting_started/dependencies) diff --git a/playbook-website/app/views/pages/code_snippets/group_hover_jsx.txt b/playbook-website/app/views/pages/code_snippets/group_hover_jsx.txt new file mode 100644 index 0000000000..6366242b5d --- /dev/null +++ b/playbook-website/app/views/pages/code_snippets/group_hover_jsx.txt @@ -0,0 +1,21 @@ +<Card + cursor="pointer" + groupHover +> + <Title + alignSelf="center" + groupHover + hover={{ scale: "lg"}} + text="If the card is hovered, I'm hovered too!" + /> + <Title + alignSelf="center" + paddingY="lg" + text="I don't have any hover effect on me" + /> + <Title + alignSelf="center" + hover={{ scale: "lg"}} + text="I need to be hovered over directly" + /> +</Card> diff --git a/playbook-website/config/initializers/global_variables.rb b/playbook-website/config/initializers/global_variables.rb index 68052286d5..c88293355d 100644 --- a/playbook-website/config/initializers/global_variables.rb +++ b/playbook-website/config/initializers/global_variables.rb @@ -69,10 +69,11 @@ ], } -# Move HTML figma to the end +# Move these pages to the end of the Getting Started page +page_names = ["HTML&_CSS", "figma_setup", "how_to_theme", "dependencies", "font_awesome"] -move_pages = navigation[:getting_started][:pages].select { |page| ["HTML&_CSS", "figma_setup", "how_to_theme"].include?(page[:page_id]) } -navigation[:getting_started][:pages].reject! { |page| ["HTML&_CSS", "figma_setup", "how_to_theme"].include?(page[:page_id]) } +move_pages = navigation[:getting_started][:pages].select { |page| page_names.include?(page[:page_id]) } +navigation[:getting_started][:pages].reject! { |page| page_names.include?(page[:page_id]) } navigation[:getting_started][:pages].concat(move_pages) DOCS = navigation diff --git a/playbook-website/config/menu.yml b/playbook-website/config/menu.yml index 61f8012c84..61472e88ab 100644 --- a/playbook-website/config/menu.yml +++ b/playbook-website/config/menu.yml @@ -44,7 +44,7 @@ kits: description: status: stable - name: drawer - platforms: *1 + platforms: *1 status: beta - category: buttons description: Buttons are used primarily for actions, such as β€œSave” and β€œCancel”. @@ -431,7 +431,7 @@ kits: platforms: *1 status: stable - name: draggable - platforms: *2 + platforms: *1 description: status: stable - category: message_text_patterns @@ -494,6 +494,9 @@ kits: description: The timeline kit can use two different line styles in the same timeline - solid and dotted line styles. status: stable + - name: skeleton_loading + platforms: *1 + status: beta - category: tags description: components: diff --git a/playbook-website/package.json b/playbook-website/package.json index 2e36f4abf6..e3f29aa367 100644 --- a/playbook-website/package.json +++ b/playbook-website/package.json @@ -22,7 +22,7 @@ "@babel/preset-typescript": "^7.24.7", "@hotwired/turbo-rails": "^7.3.0", "@originjs/vite-plugin-commonjs": "^1.0.3", - "@tiptap/extension-document": "^2.1.12", + "@tiptap/extension-document": "^2.6.6", "@tiptap/extension-highlight": "^2.0.3", "@tiptap/extension-horizontal-rule": "^2.0.3", "@tiptap/extension-link": "^2.0.2", diff --git a/playbook/CHANGELOG.md b/playbook/CHANGELOG.md index 53cc2c1163..73a075a7e3 100644 --- a/playbook/CHANGELOG.md +++ b/playbook/CHANGELOG.md @@ -1,3 +1,45 @@ +# Go Further With Our Custom Timelines! +##### November 8, 2024 + +![timelinewithanykit](https://github.com/user-attachments/assets/f0e500bb-e7ea-4eb7-990f-68e6eebdacf1) + +The Timeline kit now lets you swap in custom components, allowing clear, adaptable tracking for projects, processes, and user journeys. This enables your Timelines to be more informative and lets you adjust it to fit your needs! + +[14.7.0](https://github.com/powerhome/playbook/tree/14.7.0) full list of changes: + +**Kit Enhancements:** + +- Custom Cells for Advanced Table First Column ([\#3889](https://github.com/powerhome/playbook/pull/3889)) ([@nidaqg](https://github.com/nidaqg)) +- Group hover Global Prop ([\#3833](https://github.com/powerhome/playbook/pull/3833)) ([@markdoeswork](https://github.com/markdoeswork)) +- Timeline Sub Components ([\#3801](https://github.com/powerhome/playbook/pull/3801)) ([@markdoeswork](https://github.com/markdoeswork)) +- Tooltip for Truncated Form Pills ([\#3856](https://github.com/powerhome/playbook/pull/3856)) ([@ElisaShapiro](https://github.com/ElisaShapiro)) +- Currency Kit Comma Separator ([\#3867](https://github.com/powerhome/playbook/pull/3867)) ([@jasperfurniss](https://github.com/jasperfurniss)) + +**Fixed Bugs:** + - Fix Height Prop for HtmlOptions Support ([\#3873](https://github.com/powerhome/playbook/pull/3873)) ([@jasperfurniss](https://github.com/jasperfurniss)) + - Fix Overflow Container Bug by Using Padding Instead of Outline ([\#3807](https://github.com/powerhome/playbook/pull/3807)) ([@kangaree](https://github.com/kangaree)) + + +**Improvements:** +- Bump @tiptap/extension-document from 2.1.12 to 2.6.6 ([\#3879](https://github.com/powerhome/playbook/pull/3879)) ([@skduncan](https://github.com/skduncan)) +- Fixing parsing kits props ([\#3857](https://github.com/powerhome/playbook/pull/3857)) ([@carloslimasd](https://github.com/carloslimasd)) +- Bump lazysizes from 5.2.2 to 5.3.2 ([\#3855](https://github.com/powerhome/playbook/pull/3855)) ([@skduncan](https://github.com/skduncan)) +- Font Awesome Setup Docs: JS Bundle and Asset Pipeline - Phase 2 ([\#3878](https://github.com/powerhome/playbook/pull/3878)) ([@kangaree](https://github.com/kangaree)) +- Add Details to Dependencies Docs ([\#3885](https://github.com/powerhome/playbook/pull/3885)) ([@markdoeswork](https://github.com/markdoeswork)) +- Font Awesome Setup Documentation - Phase 1 ([\#3865](https://github.com/powerhome/playbook/pull/3865)) ([@kangaree](https://github.com/kangaree)) +- Add Dependencies to Getting Started Page ([\#3864](https://github.com/powerhome/playbook/pull/3864)) ([@markdoeswork](https://github.com/markdoeswork)) +- Swift Changelog Update ([\#3854](https://github.com/powerhome/playbook/pull/3854)) ([@RachelRadford21](https://github.com/RachelRadford21)) +- Add README.md to Root in Dist for NPM ([\#3853](https://github.com/powerhome/playbook/pull/3853)) ([@kangaree](https://github.com/kangaree)) +Update Sentry DSNs ([\#3843](https://github.com/powerhome/playbook/pull/3843)) ([@benlangfeld](https://github.com/benlangfeld)) + +**New Kits:** + - Beta Skeleton Loading Kit - React ([\#3876](https://github.com/powerhome/playbook/pull/3876)) ([@ElisaShapiro](https://github.com/ElisaShapiro)) + - Beta Link Kit ([\#3852](https://github.com/powerhome/playbook/pull/3852)) ([@kangaree](https://github.com/kangaree)) + + +[Full Changelog](https://github.com/powerhome/playbook/compare/14.6.2...14.7.0) + + # Supercharge Your Tables: Custom Components in Advanced Table Cells! ##### October 22, 2024 diff --git a/playbook/Gemfile.lock b/playbook/Gemfile.lock index b644f93b20..9f8b55b9c7 100644 --- a/playbook/Gemfile.lock +++ b/playbook/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - playbook_ui (14.6.2) + playbook_ui (14.7.0) actionpack (>= 5.2.4.5) actionview (>= 5.2.4.5) activesupport (>= 5.2.4.5) diff --git a/playbook/app/entrypoints/playbook-doc.js b/playbook/app/entrypoints/playbook-doc.js index c4491a7449..daa9041213 100755 --- a/playbook/app/entrypoints/playbook-doc.js +++ b/playbook/app/entrypoints/playbook-doc.js @@ -88,6 +88,7 @@ import * as SelectableCard from 'kits/pb_selectable_card/docs' import * as SelectableCardIcon from 'kits/pb_selectable_card_icon/docs' import * as SelectableIcon from 'kits/pb_selectable_icon/docs' import * as SelectableList from 'kits/pb_selectable_list/docs' +import * as SkeletonLoading from 'kits/pb_skeleton_loading/docs' import * as Source from 'kits/pb_source/docs' import * as StarRating from 'kits/pb_star_rating/docs' import * as StatChange from 'kits/pb_stat_change/docs' @@ -197,6 +198,7 @@ WebpackerReact.registerComponents({ ...SelectableCardIcon, ...SelectableIcon, ...SelectableList, + ...SkeletonLoading, ...Source, ...StarRating, ...StatChange, diff --git a/playbook/app/entrypoints/playbook-rails.js b/playbook/app/entrypoints/playbook-rails.js index 7769cb5fb9..7c73e41eb8 100644 --- a/playbook/app/entrypoints/playbook-rails.js +++ b/playbook/app/entrypoints/playbook-rails.js @@ -50,6 +50,9 @@ PbStarRating.start() import PbRadio from 'kits/pb_radio' PbRadio.start() +import PbDraggable from 'kits/pb_draggable' +PbDraggable.start() + import 'flatpickr' // React-Rendered Rails Kits ===== diff --git a/playbook/app/entrypoints/playbook.scss b/playbook/app/entrypoints/playbook.scss index 8ebaa9cf11..b790495672 100755 --- a/playbook/app/entrypoints/playbook.scss +++ b/playbook/app/entrypoints/playbook.scss @@ -1,4 +1,3 @@ - @import 'kits/pb_advanced_table/advanced_table'; @import 'kits/pb_avatar/avatar'; @import 'kits/pb_avatar_action_button/avatar_action_button'; @@ -85,6 +84,7 @@ @import 'kits/pb_selectable_card_icon/selectable_card_icon'; @import 'kits/pb_selectable_icon/selectable_icon'; @import 'kits/pb_selectable_list/selectable_list'; +@import 'kits/pb_skeleton_loading/skeleton_loading'; @import 'kits/pb_source/source'; @import 'kits/pb_star_rating/star_rating'; @import 'kits/pb_stat_change/stat_change'; diff --git a/playbook/app/javascript/kits.js b/playbook/app/javascript/kits.js index 15de7bb5b1..eab34edecf 100644 --- a/playbook/app/javascript/kits.js +++ b/playbook/app/javascript/kits.js @@ -93,6 +93,7 @@ export { default as SelectableCardIcon } from '../pb_kits/playbook/pb_selectable export { default as SelectableIcon } from '../pb_kits/playbook/pb_selectable_icon/_selectable_icon' export { default as SelectableList } from '../pb_kits/playbook/pb_selectable_list/_selectable_list' export { default as SelectableListItem } from '../pb_kits/playbook/pb_selectable_list/_item' +export { default as SkeletonLoading} from '../pb_kits/playbook/pb_skeleton_loading/_skeleton_loading' export { default as Source } from '../pb_kits/playbook/pb_source/_source' export { default as StarRating } from '../pb_kits/playbook/pb_star_rating/_star_rating' export { default as StatChange } from '../pb_kits/playbook/pb_stat_change/_stat_change' diff --git a/playbook/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx b/playbook/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx index ad90a37419..31625a9781 100644 --- a/playbook/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx +++ b/playbook/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx @@ -16,6 +16,7 @@ interface CustomCellProps { onRowToggleClick?: (arg: Row<GenericObject>) => void row: Row<GenericObject> value?: string + customRenderer?: (row: Row<GenericObject>, value: string | undefined) => React.ReactNode } export const CustomCell = ({ @@ -23,6 +24,7 @@ export const CustomCell = ({ onRowToggleClick, row, value, + customRenderer, }: CustomCellProps & GlobalProps) => { const { setExpanded, expanded, expandedControl, inlineRowLoading } = useContext(AdvancedTableContext); @@ -61,7 +63,12 @@ export const CustomCell = ({ </button> ) : null} <FlexItem paddingLeft={renderButton? "none" : "xs"}> - {row.depth === 0 ? getValue() : value} + {row.depth === 0 ? ( + customRenderer ? customRenderer(row, getValue()) : getValue() + ) :( + customRenderer ? customRenderer(row, value) : value + ) + } </FlexItem> </Flex> </div> diff --git a/playbook/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx b/playbook/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx index 52b0c7fcf4..8ae25edbfd 100644 --- a/playbook/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +++ b/playbook/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx @@ -90,8 +90,8 @@ const AdvancedTable = (props: AdvancedTableProps) => { const columnHelper = createColumnHelper() - //Create cells for first columns - const createCellFunction = (cellAccessors: string[], customRenderer?: (row: Row<GenericObject>, value: any) => JSX.Element) => { + //Create cells for columns, with customization for first column + const createCellFunction = (cellAccessors: string[], customRenderer?: (row: Row<GenericObject>, value: any) => JSX.Element, index?: number) => { const columnCells = ({ row, getValue, @@ -101,19 +101,16 @@ const AdvancedTable = (props: AdvancedTableProps) => { }) => { const rowData = row.original - // Use customRenderer if provided, otherwise default rendering - if (customRenderer) { - return customRenderer(row, getValue()) - } - + if (index === 0) { switch (row.depth) { case 0: { return ( - <CustomCell - getValue={getValue} - onRowToggleClick={onRowToggleClick} - row={row} - /> + <CustomCell + customRenderer={customRenderer} + getValue={getValue} + onRowToggleClick={onRowToggleClick} + row={row} + /> ) } default: { @@ -122,6 +119,7 @@ const AdvancedTable = (props: AdvancedTableProps) => { const accessorValue = rowData[depthAccessor] return accessorValue ? ( <CustomCell + customRenderer={customRenderer} onRowToggleClick={onRowToggleClick} row={row} value={accessorValue} @@ -132,11 +130,13 @@ const AdvancedTable = (props: AdvancedTableProps) => { } } } - + return customRenderer + ? customRenderer(row, getValue()) + : getValue() + } return columnCells } - - //Create column array in format needed by Tanstack +//Create column array in format needed by Tanstack const columns = columnDefinitions && columnDefinitions.map((column, index) => { @@ -147,19 +147,12 @@ const AdvancedTable = (props: AdvancedTableProps) => { }), } - // Use the custom renderer if provided, EXCEPT for the first column - if (index !== 0) { - if (column.cellAccessors || column.customRenderer) { - columnStructure.cell = createCellFunction( - column.cellAccessors, - column.customRenderer - ) - } - } else { - // For the first column, apply createCellFunction without customRenderer - if (column.cellAccessors) { - columnStructure.cell = createCellFunction(column.cellAccessors) - } + if (column.cellAccessors || column.customRenderer) { + columnStructure.cell = createCellFunction( + column.cellAccessors, + column.customRenderer, + index + ) } return columnStructure diff --git a/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx b/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx index 2cb8a51674..7165f218d4 100644 --- a/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +++ b/playbook/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx @@ -1,7 +1,7 @@ import React, {useState} from "react" import { render, screen, waitFor } from "../utilities/test-utils" -import { AdvancedTable } from "playbook-ui" +import { AdvancedTable, Pill } from "playbook-ui" const MOCK_DATA = [ { @@ -88,6 +88,28 @@ const columnDefinitions = [ }, ] +const columnDefinitionsCustomRenderer = [ + { + accessor: "year", + label: "Year", + cellAccessors: ["quarter", "month", "day"], + }, + { + accessor: "newEnrollments", + label: "New Enrollments", + customRenderer: (row, value) => ( + <Pill text={value} + variant="success" + /> + ), + }, + { + accessor: "scheduledMeetings", + label: "Scheduled Meetings", + }, +] + + const subRowHeaders = ["Quarter"] const testId = "advanced_table" @@ -463,3 +485,17 @@ test("responsive none prop functions as expected", () => { const kit = screen.getByTestId(testId) expect(kit).toHaveClass("pb_advanced_table table-responsive-none") }) + +test("customRenderer prop functions as expected", () => { + render( + <AdvancedTable + columnDefinitions={columnDefinitionsCustomRenderer} + data={{ testid: testId }} + tableData={MOCK_DATA} + /> + ) + + const kit = screen.getByTestId(testId) + const pill = kit.querySelector(".pb_pill_kit_success_lowercase") + expect(pill).toBeInTheDocument() +}) \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_custom_cell.jsx b/playbook/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_custom_cell.jsx index b1c5bad58d..78b586bdf7 100644 --- a/playbook/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_custom_cell.jsx +++ b/playbook/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_custom_cell.jsx @@ -1,5 +1,5 @@ import React from "react" -import { AdvancedTable, Pill, Body, Flex, Detail, Caption } from "playbook-ui" +import { AdvancedTable, Pill, Body, Flex, Detail, Caption, Badge, Title } from "playbook-ui" import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableCustomCell = (props) => { @@ -8,7 +8,18 @@ const AdvancedTableCustomCell = (props) => { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], - + customRenderer: (row, value) => ( + <Flex> + <Title size={4} + text={value} + /> + <Badge dark + marginLeft="xxs" + text={row.original.newEnrollments > 20 ? "High" : "Low"} + variant="neutral" + /> + </Flex> + ), }, { accessor: "newEnrollments", diff --git a/playbook/app/pb_kits/playbook/pb_card/_card.tsx b/playbook/app/pb_kits/playbook/pb_card/_card.tsx index 149130d8b2..225f40def0 100755 --- a/playbook/app/pb_kits/playbook/pb_card/_card.tsx +++ b/playbook/app/pb_kits/playbook/pb_card/_card.tsx @@ -5,7 +5,7 @@ import { get } from 'lodash' import classnames from 'classnames' import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props' -import { GlobalProps, globalProps } from '../utilities/globalProps' +import { GlobalProps, globalProps, globalInlineProps } from '../utilities/globalProps' import type { ProductColors, CategoryColors, BackgroundColors } from '../types/colors' import Icon from '../pb_icon/_icon' @@ -49,6 +49,7 @@ type CardBodyProps = { padding?: string, } & GlobalProps + // Header component const Header = (props: CardHeaderProps) => { const { children, className, headerColor = 'category_1', headerColorStriped = false } = props @@ -107,6 +108,10 @@ const Card = (props: CardPropTypes): React.ReactElement => { // coerce to array const cardChildren = React.Children.toArray(children) + const dynamicInlineProps = globalInlineProps(props); + const { style: htmlStyle = {}, ...restHtmlProps } = htmlProps as { style?: React.CSSProperties }; + const mergedStyles: React.CSSProperties = { ...htmlStyle, ...dynamicInlineProps }; + const subComponentTags = (tagName: string) => { return cardChildren.filter((c: string) => ( @@ -122,7 +127,7 @@ const Card = (props: CardPropTypes): React.ReactElement => { const tagOptions = ['div', 'section', 'footer', 'header', 'article', 'aside', 'main', 'nav'] const Tag = tagOptions.includes(tag) ? tag : 'div' - + return ( <> { @@ -133,8 +138,9 @@ const Card = (props: CardPropTypes): React.ReactElement => { <Tag {...ariaProps} {...dataProps} - {...htmlProps} className={classnames(cardCss, globalProps(props), className)} + {...restHtmlProps} + style={mergedStyles} > {subComponentTags('Header')} { @@ -161,8 +167,9 @@ const Card = (props: CardPropTypes): React.ReactElement => { <Tag {...ariaProps} {...dataProps} - {...htmlProps} className={classnames(cardCss, globalProps(props), className)} + {...restHtmlProps} + style={mergedStyles} > {subComponentTags('Header')} {nonHeaderChildren} diff --git a/playbook/app/pb_kits/playbook/pb_card/_card_mixin.scss b/playbook/app/pb_kits/playbook/pb_card/_card_mixin.scss index a3781bf2ac..f15d002ee0 100755 --- a/playbook/app/pb_kits/playbook/pb_card/_card_mixin.scss +++ b/playbook/app/pb_kits/playbook/pb_card/_card_mixin.scss @@ -28,8 +28,7 @@ $pb_card_header_colors: map-merge(map-merge($product_colors, $additional_colors) @mixin pb_card_selected($border_color: $primary) { border-color: $border_color; - border-width: $pb_card_border_width; - outline: 1px solid $border_color; + border-width: $pb_card_border_width * 2; } @mixin pb_card_selected_dark { diff --git a/playbook/app/pb_kits/playbook/pb_date/_date.scss b/playbook/app/pb_kits/playbook/pb_date/_date.scss index 56efa4a0b1..a20659bafe 100644 --- a/playbook/app/pb_kits/playbook/pb_date/_date.scss +++ b/playbook/app/pb_kits/playbook/pb_date/_date.scss @@ -28,5 +28,8 @@ [class^=pb_title_kit] { color: $text_dk_default !important; } + [class^=pb_body_kit], [class^=pb_caption_kit] { + color: $text_dk_light !important; + } } } \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_dialog/_dialog.tsx b/playbook/app/pb_kits/playbook/pb_dialog/_dialog.tsx index e25eb702ae..32e5443019 100644 --- a/playbook/app/pb_kits/playbook/pb_dialog/_dialog.tsx +++ b/playbook/app/pb_kits/playbook/pb_dialog/_dialog.tsx @@ -6,7 +6,7 @@ import classnames from "classnames"; import Modal from "react-modal"; import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from "../utilities/props"; -import { globalProps } from "../utilities/globalProps"; +import { globalProps, globalInlineProps } from "../utilities/globalProps"; import Body from "../pb_body/_body"; import Button from "../pb_button/_button"; @@ -91,6 +91,8 @@ const Dialog = (props: DialogProps): React.ReactElement => { beforeClose: "pb_dialog_overlay_before_close", }; + const dynamicInlineProps = globalInlineProps(props); + const classes = classnames( buildCss("pb_dialog_wrapper"), globalProps(props), @@ -184,6 +186,7 @@ const Dialog = (props: DialogProps): React.ReactElement => { overlayClassName={overlayClassNames} portalClassName={portalClassName} shouldCloseOnOverlayClick={shouldCloseOnOverlayClick && !loading} + style={{ content: dynamicInlineProps }} > <> {title && !status ? <Dialog.Header>{title}</Dialog.Header> : null} @@ -192,6 +195,7 @@ const Dialog = (props: DialogProps): React.ReactElement => { <Dialog.Body className="dialog_status_text_align" padding="md" + > <Flex align="center" orientation="column" diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.html.erb new file mode 100644 index 0000000000..5424668b74 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.html.erb @@ -0,0 +1,26 @@ +<% initial_items = [ + { + id: "1", + url: "https://unsplash.it/500/400/?image=633", + }, + { + id: "2", + url: "https://unsplash.it/500/400/?image=634", + }, + { + id: "3", + url: "https://unsplash.it/500/400/?image=637", + }, +] %> + +<%= pb_rails("draggable", props: {initial_items: initial_items}) do %> + <%= pb_rails("draggable/draggable_container") do %> + <%= pb_rails("flex") do %> + <% initial_items.each do |item| %> + <%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %> + <%= pb_rails("image", props: { alt: item[:id], size: "md", url: item[:url], margin: "xs" }) %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.md b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.md new file mode 100644 index 0000000000..23ee195754 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_default_rails.md @@ -0,0 +1,7 @@ +The `draggable` kit gives you a full subcomponent structure that allows it to be used with almost any kit. + +`initial_items` is a REQUIRED prop, which is the array of objects that contains data for the the draggable items. + +`draggable/draggable_container` = This specifies the container within which items can be dropped. + +`draggable/draggable_item` = This specifies the items that can be dragged and dropped. `drag_id` is a REQUIRED prop for draggable_item and must match the id on the items within `initial_items`. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards_rails.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards_rails.html.erb new file mode 100644 index 0000000000..ce95ab7ead --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards_rails.html.erb @@ -0,0 +1,38 @@ +<% initial_items = [ + { id: "21", name: "Joe Black" }, + { id: "22", name: "Nancy White" }, + { id: "23", name: "Bill Green" }, +] %> + +<%= pb_rails("draggable", props: {initial_items: initial_items}) do %> + <%= pb_rails("draggable/draggable_container") do %> + <% initial_items.each do |item| %> + <%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %> + <%= pb_rails("card", props: {highlight: {position: "side", color:"primary"}, margin_bottom: "xs", padding: "xs"}) do %> + <%= pb_rails("flex", props:{align_items: "stretch", flex_direction:"column"}) do %> + <%= pb_rails("flex", props:{gap: "xs"}) do %> + <%= pb_rails("title", props: { text: item[:name], tag: "h4", size: 4 }) %> + <%= pb_rails("badge", props: {text:"35-12345" ,variant: "primary"}) %> + <% end %> + <%= pb_rails("caption", props: { size: "xs", text: "8:00A β€’ Township Name β€’ 90210" }) %> + <%= pb_rails("flex", props:{gap: "xxs", spacing:"between"}) do %> + <%= pb_rails("flex", props:{gap: "xxs"}) do %> + <%= pb_rails("caption", props: { size: "xs" , color: "error" }) do %> + <%= pb_rails("icon", props: { icon: "house-circle-exclamation", fixed_width: true }) %> + <% end %> + <%= pb_rails("caption", props: { size: "xs" , color: "success" }) do %> + <%= pb_rails("icon", props: { icon: "file-circle-check", fixed_width: true }) %> + <% end %> + <% end %> + <%= pb_rails("flex") do %> + <%= pb_rails("badge", props: {text:"Schedule QA" ,variant: "warning", rounded: true}) %> + <%= pb_rails("badge", props: {text:"Flex" ,variant: "primary", rounded: true}) %> + <%= pb_rails("badge", props: {text:"R99" ,variant: "primary", rounded: true}) %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards_rails.md b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_cards_rails.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_list_rails.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_list_rails.html.erb new file mode 100644 index 0000000000..485e9c0915 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/docs/_draggable_with_list_rails.html.erb @@ -0,0 +1,19 @@ +<% initial_items = [ + { id: "31", name: "Philadelphia" }, + { id: "32", name: "New Jersey" }, + { id: "33", name: "Maryland" }, + { id: "34", name: "Connecticut" }, + +] %> + +<%= pb_rails("draggable", props: {initial_items: initial_items}) do %> + <%= pb_rails("draggable/draggable_container") do %> + <%= pb_rails("list", props: {ordered: false}) do %> + <% initial_items.each do |item| %> + <%= pb_rails("draggable/draggable_item", props:{drag_id: item[:id]}) do %> + <%= pb_rails("list/item") do %><%= item[:name] %><% end %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/docs/example.yml b/playbook/app/pb_kits/playbook/pb_draggable/docs/example.yml index 141356a735..681ce6c7a1 100644 --- a/playbook/app/pb_kits/playbook/pb_draggable/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_draggable/docs/example.yml @@ -8,4 +8,10 @@ examples: - draggable_with_cards: Draggable with Cards - draggable_multiple_containers: Dragging Across Multiple Containers + rails: + - draggable_default_rails: Default + - draggable_with_list_rails: Draggable with List Kit + - draggable_with_cards_rails: Draggable with Cards + + diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/draggable.html.erb new file mode 100644 index 0000000000..919544cc91 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable.html.erb @@ -0,0 +1,3 @@ +<%= pb_content_tag do %> + <%= content.presence %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable.rb b/playbook/app/pb_kits/playbook/pb_draggable/draggable.rb new file mode 100644 index 0000000000..fd1dfff29c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Playbook + module PbDraggable + class Draggable < ::Playbook::KitBase + prop :initial_items, type: Playbook::Props::Array, + default: [] + + def data + Hash(prop(:data)).merge(pb_draggable: true) + end + + def classname + generate_classname("pb_draggable") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.html.erb new file mode 100644 index 0000000000..0e15ba7c0b --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.html.erb @@ -0,0 +1,3 @@ +<%= pb_content_tag do %> + <%= content.presence %> + <% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.rb b/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.rb new file mode 100644 index 0000000000..6c51290350 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable_container.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Playbook + module PbDraggable + class DraggableContainer < ::Playbook::KitBase + def data + Hash(prop(:data)).merge(pb_draggable_container: true) + end + + def classname + generate_classname("pb_draggable_container") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.html.erb b/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.html.erb new file mode 100644 index 0000000000..cb55c38284 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.html.erb @@ -0,0 +1,7 @@ +<%= pb_content_tag(:div, { +id: "item_#{object.drag_id}", +draggable: true +}) do %> + <%= content.presence %> +<% end %> + \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.rb b/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.rb new file mode 100644 index 0000000000..ae9d094228 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/draggable_item.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Playbook + module PbDraggable + class DraggableItem < ::Playbook::KitBase + prop :drag_id, type: Playbook::Props::String, + default: "" + + def data + Hash(prop(:data)).merge(pb_draggable_item: true) + end + + def classname + generate_classname("pb_draggable_item") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_draggable/index.js b/playbook/app/pb_kits/playbook/pb_draggable/index.js new file mode 100644 index 0000000000..e16f8806cf --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_draggable/index.js @@ -0,0 +1,125 @@ +import PbEnhancedElement from "../pb_enhanced_element"; + +const DRAGGABLE_SELECTOR = "[data-pb-draggable]"; +const DRAGGABLE_CONTAINER = ".pb_draggable_container"; + +export default class PbDraggable extends PbEnhancedElement { + static get selector() { + return DRAGGABLE_SELECTOR; + } + + connect() { + this.draggedItem = null; + this.draggedItemId = null; + document.addEventListener("DOMContentLoaded", () => this.bindEventListeners()); + } + + bindEventListeners() { + // Needed to prevent images within draggable items from being independently draggable + // Needed if using Image kit in draggable items + this.element.querySelectorAll(".pb_draggable_item img").forEach(img => { + img.setAttribute("draggable", "false"); + }); + + this.element.querySelectorAll(".pb_draggable_item").forEach(item => { + item.addEventListener("dragstart", this.handleDragStart.bind(this)); + item.addEventListener("dragend", this.handleDragEnd.bind(this)); + item.addEventListener("dragenter", this.handleDragEnter.bind(this)); + }); + + const container = this.element.querySelector(DRAGGABLE_CONTAINER); + if (container) { + container.addEventListener("dragover", this.handleDragOver.bind(this)); + container.addEventListener("drop", this.handleDrop.bind(this)); + } + } + + handleDragStart(event) { + // Needed to prevent images within draggable items from being independently draggable + // Needed if using Image kit in draggable items + if (event.target.tagName.toLowerCase() === 'img') { + event.preventDefault(); + return; + } + + this.draggedItem = event.target; + this.draggedItemId = event.target.id; + event.target.classList.add("is_dragging"); + + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', this.draggedItemId); + } + + setTimeout(() => { + event.target.style.opacity = '0.5'; + }, 0); + } + + handleDragEnter(event) { + if (!this.draggedItem || event.target === this.draggedItem) return; + + const targetItem = event.target.closest('.pb_draggable_item'); + if (!targetItem) return; + + const container = targetItem.parentNode; + const items = Array.from(container.children); + const draggedIndex = items.indexOf(this.draggedItem); + const targetIndex = items.indexOf(targetItem); + + if (draggedIndex > targetIndex) { + container.insertBefore(this.draggedItem, targetItem); + } else { + container.insertBefore(this.draggedItem, targetItem.nextSibling); + } + } + + handleDragOver(event) { + event.preventDefault(); + const container = event.target.closest(DRAGGABLE_CONTAINER); + + if (container) { + container.classList.add("active_container"); + } + } + + handleDrop(event) { + event.preventDefault(); + const container = event.target.closest(DRAGGABLE_CONTAINER); + if (!container || !this.draggedItem) return; + + container.classList.remove("active_container"); + this.draggedItem.style.opacity = '1'; + + // Updated order of items as an array of item IDs + const reorderedItems = Array.from(container.children) + .filter(item => item.classList.contains("pb_draggable_item")) + .map(item => item.id.replace("item_", "")); + + // Store reordered items in a data attribute on the container + container.setAttribute("data-reordered-items", JSON.stringify(reorderedItems)); + + const customEvent = new CustomEvent('pb-draggable-reorder', { + detail: { + reorderedItems, + containerId: container.id, + } + }); + this.element.dispatchEvent(customEvent); + + this.draggedItem = null; + this.draggedItemId = null; + } + + + handleDragEnd(event) { + event.target.classList.remove("is_dragging"); + event.target.style.opacity = '1'; + this.draggedItem = null; + this.draggedItemId = null; + + this.element.querySelectorAll(DRAGGABLE_CONTAINER).forEach(container => { + container.classList.remove("active_container"); + }); + } +} diff --git a/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx b/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx index 13af9cb3c5..b946f7f295 100644 --- a/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +++ b/playbook/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx @@ -47,7 +47,7 @@ interface DropdownComponent Container: typeof DropdownContainer; } -const Dropdown = forwardRef((props: DropdownProps, ref: any) => { +let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => { const { aria = {}, autocomplete = false, @@ -260,7 +260,7 @@ const Dropdown = forwardRef((props: DropdownProps, ref: any) => { <DropdownContainer> {optionsWithBlankSelection && optionsWithBlankSelection?.map((option: GenericObject) => ( - <Dropdown.Option key={option.id} + <DropdownOption key={option.id} option={option} /> ))} @@ -278,11 +278,12 @@ const Dropdown = forwardRef((props: DropdownProps, ref: any) => { </DropdownContext.Provider> </div> ) -}) as DropdownComponent +} -Dropdown.displayName = "Dropdown"; -Dropdown.Option = DropdownOption; -Dropdown.Trigger = DropdownTrigger; -Dropdown.Container = DropdownContainer; +Dropdown = forwardRef(Dropdown) as unknown as DropdownComponent; +(Dropdown as DropdownComponent).displayName = "Dropdown"; +(Dropdown as DropdownComponent).Option = DropdownOption; +(Dropdown as DropdownComponent).Trigger = DropdownTrigger; +(Dropdown as DropdownComponent).Container = DropdownContainer; export default Dropdown; diff --git a/playbook/app/pb_kits/playbook/pb_flex/_flex.tsx b/playbook/app/pb_kits/playbook/pb_flex/_flex.tsx index e683f5407a..d5de3e6067 100644 --- a/playbook/app/pb_kits/playbook/pb_flex/_flex.tsx +++ b/playbook/app/pb_kits/playbook/pb_flex/_flex.tsx @@ -1,7 +1,7 @@ import React from 'react' import classnames from 'classnames' import { buildCss, buildDataProps, buildHtmlProps } from '../utilities/props' -import { GlobalProps, globalProps } from '../utilities/globalProps' +import { GlobalProps, globalProps, globalInlineProps } from '../utilities/globalProps' import { GenericObject, Sizes } from '../types' type FlexProps = { @@ -61,6 +61,7 @@ const Flex = (props: FlexProps): React.ReactElement => { const alignSelfClass = alignSelf !== 'none' ? `align_self_${alignSelf}` : '' const dataProps = buildDataProps(data) const htmlProps = buildHtmlProps(htmlOptions) + const dynamicInlineProps = globalInlineProps(props) return ( @@ -83,6 +84,7 @@ const Flex = (props: FlexProps): React.ReactElement => { globalProps(props), className )} + style={dynamicInlineProps} {...dataProps} {...htmlProps} > diff --git a/playbook/app/pb_kits/playbook/pb_flex/_flex_item.tsx b/playbook/app/pb_kits/playbook/pb_flex/_flex_item.tsx index 6c61d55cb0..b8a27e9704 100644 --- a/playbook/app/pb_kits/playbook/pb_flex/_flex_item.tsx +++ b/playbook/app/pb_kits/playbook/pb_flex/_flex_item.tsx @@ -1,7 +1,7 @@ import React from 'react' import classnames from 'classnames' import { buildCss, buildHtmlProps } from '../utilities/props' -import { globalProps, GlobalProps } from '../utilities/globalProps' +import { globalProps, GlobalProps, globalInlineProps} from '../utilities/globalProps' type FlexItemPropTypes = { children: React.ReactNode[] | React.ReactNode, fixedSize?: string, @@ -35,14 +35,20 @@ const FlexItem = (props: FlexItemPropTypes): React.ReactElement => { const fixedStyle = fixedSize !== undefined ? { flexBasis: `${fixedSize}` } : null const orderClass = order !== 'none' ? `order_${order}` : null + const dynamicInlineProps = globalInlineProps(props) + const combinedStyles = { + ...fixedStyle, + ...dynamicInlineProps + } const htmlProps = buildHtmlProps(htmlOptions) + return ( <div {...htmlProps} className={classnames(buildCss('pb_flex_item_kit', growClass, shrinkClass, flexClass, displayFlexClass), orderClass, alignSelfClass, globalProps(props), className)} - style={fixedStyle} + style={combinedStyles} > {children} </div> diff --git a/playbook/app/pb_kits/playbook/pb_flex/flex_item.html.erb b/playbook/app/pb_kits/playbook/pb_flex/flex_item.html.erb index 240e1ce109..96c6c7af2f 100644 --- a/playbook/app/pb_kits/playbook/pb_flex/flex_item.html.erb +++ b/playbook/app/pb_kits/playbook/pb_flex/flex_item.html.erb @@ -1,8 +1,5 @@ -<%= content_tag(:div, - id: object.id, - data: object.data, - class: object.classname, - style: object.style_value, - **combined_html_options) do %> +<%= pb_content_tag(:div, + style: object.inline_styles +) do %> <%= content.presence %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_flex/flex_item.rb b/playbook/app/pb_kits/playbook/pb_flex/flex_item.rb index dc99471511..caa11d6c9f 100644 --- a/playbook/app/pb_kits/playbook/pb_flex/flex_item.rb +++ b/playbook/app/pb_kits/playbook/pb_flex/flex_item.rb @@ -20,8 +20,13 @@ def classname generate_classname("pb_flex_item_kit", fixed_size_class, grow_class, shrink_class, display_flex_class) + align_self_class end - def style_value - "flex-basis: #{fixed_size};" if fixed_size.present? + def inline_styles + styles = [] + styles << "flex-basis: #{fixed_size};" if fixed_size.present? + styles << "height: #{height};" if height.present? + styles << "min-height: #{min_height};" if min_height.present? + styles << "max-height: #{max_height};" if max_height.present? + styles.join(" ") end private diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss index 87f00ef302..2541ef05f8 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss +++ b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.scss @@ -142,7 +142,9 @@ $form_pill_colors: map-merge($status_color_text, map-merge($data_colors, $produc height: 12px !important; width: 12px !important; padding-right: $space_xs; - + .pb_form_pill_text, + .pb_form_pill_tag { + + .pb_form_pill_text, + .pb_form_pill_tag, + + .pb_tooltip_kit .pb_form_pill_text, + .pb_tooltip_kit .pb_form_pill_tag, + + div .pb_form_pill_text, + div .pb_form_pill_tag { padding-left: 0; } } @@ -169,7 +171,9 @@ $form_pill_colors: map-merge($status_color_text, map-merge($data_colors, $produc } .pb_form_pill_icon { padding-right: $space_xxs; - + .pb_form_pill_text, + .pb_form_pill_tag { + + .pb_form_pill_text, + .pb_form_pill_tag, + + .pb_tooltip_kit .pb_form_pill_text, + .pb_tooltip_kit .pb_form_pill_tag, + + div .pb_form_pill_text, + div .pb_form_pill_tag { padding-left: 0; } } diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx index 9cbff71a4c..0ea0f7d02b 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx @@ -3,6 +3,7 @@ import classnames from 'classnames' import Title from '../pb_title/_title' import Icon from '../pb_icon/_icon' import Avatar from '../pb_avatar/_avatar' +import Tooltip from '../pb_tooltip/_tooltip' import { globalProps, GlobalProps } from '../utilities/globalProps' import { buildDataProps, buildHtmlProps } from '../utilities/props' @@ -62,6 +63,30 @@ const FormPill = (props: FormPillProps): React.ReactElement => { const dataProps = buildDataProps(data) const htmlProps = buildHtmlProps(htmlOptions) + const renderTitle = (content: string, className: string) => { + const titleComponent = ( + <Title + className={className} + size={4} + text={content} + truncate={props.truncate} + /> + ) + if (props.truncate) { + return ( + <Tooltip + interaction + placement="top" + position="fixed" + text={content} + > + {titleComponent} + </Tooltip> + ) + } + return titleComponent + } + return ( <div className={css} id={id} @@ -77,12 +102,7 @@ const FormPill = (props: FormPillProps): React.ReactElement => { size="xxs" status={null} /> - <Title - className="pb_form_pill_text" - size={4} - text={name} - truncate={props.truncate} - /> + {renderTitle(name, "pb_form_pill_text")} </> )} {((name && icon && !text) || (name && icon && text)) && ( @@ -93,12 +113,7 @@ const FormPill = (props: FormPillProps): React.ReactElement => { size="xxs" status={null} /> - <Title - className="pb_form_pill_text" - size={4} - text={name} - truncate={props.truncate} - /> + {renderTitle(name, "pb_form_pill_text")} <Icon className="pb_form_pill_icon" color={color} @@ -113,22 +128,10 @@ const FormPill = (props: FormPillProps): React.ReactElement => { color={color} icon={icon} /> - <Title - className="pb_form_pill_tag" - size={4} - text={text} - truncate={props.truncate} - /> + {renderTitle(text, "pb_form_pill_tag")} </> )} - {(!name && !icon && text) && ( - <Title - className="pb_form_pill_tag" - size={4} - text={text} - truncate={props.truncate} - /> - )} + {(!name && !icon && text) && renderTitle(text, "pb_form_pill_tag")} <div className="pb_form_pill_close" onClick={onClick} @@ -143,4 +146,5 @@ const FormPill = (props: FormPillProps): React.ReactElement => { </div> ) } + export default FormPill diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb index 3e66bb7446..a298f19a09 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb @@ -13,7 +13,30 @@ id: "typeahead-form-pill", is_multi: true, options: names, - label: "Names", + label: "Truncation Within Typeahead", pills: true, truncate: 1, }) %> + +<%= pb_rails("caption", props: { text: "Form Pill Truncation" }) %> +<%= pb_rails("card", props: { max_width: "xs" }) do %> + <%= pb_rails("form_pill", props: { + name: "Princess Amelia Mignonette Grimaldi Thermopolis Renaldo", + avatar_url: "https://randomuser.me/api/portraits/women/44.jpg", + tabindex: 0, + truncate: 1, + id: "truncation-1" + }) %> + <%= pb_rails("form_pill", props: { + icon: "badge-check", + text: "icon and a very long tag to show truncation", + tabindex: 0, + truncate: 1, + id: "truncation-2" + }) %> + <%= pb_rails("form_pill", props: { + text: "form pill long tag no tooltip show truncation", + tabindex: 0, + truncate: 1, + }) %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx index b599017782..51a8a3e2a5 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx @@ -1,5 +1,5 @@ import React from 'react' -import Typeahead from '../../pb_typeahead/_typeahead' +import { Card, Caption, FormPill, Typeahead } from 'playbook-ui' const names = [ { label: 'Alexander Nathaniel Montgomery', value: 'Alexander Nathaniel Montgomery' }, @@ -15,11 +15,34 @@ const FormPillTruncatedText = (props) => { <Typeahead htmlOptions={{ style: { maxWidth: "240px" }}} isMulti - label="Names" + label="Truncation Within Typeahead" options={names} truncate={1} {...props} /> + <Caption text="Form Pill Truncation"/> + <Card maxWidth="xs"> + <FormPill + avatarUrl="https://randomuser.me/api/portraits/women/44.jpg" + name="Princess Amelia Mignonette Grimaldi Thermopolis Renaldo" + onClick={() => alert('Click!')} + tabIndex={0} + truncate={1} + /> + <FormPill + icon="badge-check" + onClick={() => {alert('Click!')}} + tabIndex={0} + text="icon and a very long tag to show truncation" + truncate={1} + /> + <FormPill + onClick={() => {alert('Click!')}} + tabIndex={0} + text="form pill with a very long tag to show truncation" + truncate={1} + /> + </Card> </> ) } diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.md b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.md deleted file mode 100644 index e960dc2deb..0000000000 --- a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.md +++ /dev/null @@ -1 +0,0 @@ -For pills with longer text, the `truncate` global prop can be used to truncate the label within each Form Pill. See [here](https://playbook.powerapp.cloud/visual_guidelines/truncate) for more information on the truncate global prop. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_rails.md b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_rails.md new file mode 100644 index 0000000000..fc0593464e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_rails.md @@ -0,0 +1,3 @@ +For Form Pills with longer text, the truncate global prop can be used to truncate the label within each Form Pill. See [here](https://playbook.powerapp.cloud/visual_guidelines/truncate) for more information on the truncate global prop. + +__NOTE__: For Rails Form Pills (not ones embedded within a React-rendered Typeahead or MultiLevelSelect), a unique `id` is required to enable the Tooltip functionality displaying the text or tag section of the Form Pill. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_react.md b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_react.md new file mode 100644 index 0000000000..5b35756d8c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text_react.md @@ -0,0 +1 @@ +For Form Pills with longer text, the `truncate` global prop can be used to truncate the label within each Form Pill. Hover over the truncated Form Pill and a Tooltip containing the text or tag section of the Form Pill will appear. See [here](https://playbook.powerapp.cloud/visual_guidelines/truncate) for more information on the truncate global prop. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb index b9ddf68b86..4e42ddabb6 100644 --- a/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb +++ b/playbook/app/pb_kits/playbook/pb_form_pill/form_pill.html.erb @@ -1,19 +1,57 @@ <%= content_tag(:div, id: object.id, data: object.data, class: object.classname + object.size_class, tabindex: object.tabindex, **combined_html_options) do %> <% if object.name.present? %> <%= pb_rails("avatar", props: { name: object.name, image_url: object.avatar_url, size: "xxs" }) %> - <%= pb_rails("title", props: { text: object.name, size: 4, classname: "pb_form_pill_text" }) %> + <% if object.truncate %> + <div> + <%= pb_rails("title", props: { + classname: "pb_form_pill_text truncate_#{object.truncate}", + id: object.id, + size: 4, + text: object.name, + }) %> + <% if object.id.present? %> + <%= pb_rails("tooltip", props: { + position: "top", + tooltip_id: "tooltip-#{object.id}", + trigger_element_selector: "##{object.id}" + }) do %> + <%= object.name %> + <% end %> + <% end %> + </div> + <% else %> + <%= pb_rails("title", props: { classname: "pb_form_pill_text", id: object.id, size: 4, text: object.name }) %> + <% end %> <% if object.icon.present? %> <%= pb_rails("icon", props: { classname: "pb_form_pill_icon", color: object.color, icon: object.icon }) %> <% end %> <% elsif object.text.present? %> - <% if object.icon.present? %> - <%= pb_rails("icon", props: { classname: "pb_form_pill_icon", color: object.color, icon: object.icon }) %> - <% end %> - <% if object.text.present? %> - <%= pb_rails("title", props: { text: object.text, size: 4, classname: "pb_form_pill_tag" }) %> - <% end %> + <% if object.icon.present? %> + <%= pb_rails("icon", props: { classname: "pb_form_pill_icon", color: object.color, icon: object.icon }) %> + <% end %> + <% if object.truncate %> + <div> + <%= pb_rails("title", props: { + classname: "pb_form_pill_tag truncate_#{object.truncate}", + id: object.id, + size: 4, + text: object.text, + }) %> + <% if object.id.present? %> + <%= pb_rails("tooltip", props: { + position: "top", + tooltip_id: "tooltip-#{object.id}", + trigger_element_selector: "##{object.id}" + }) do %> + <%= object.text %> + <% end %> + <% end %> + </div> + <% else %> + <%= pb_rails("title", props: { classname: "pb_form_pill_tag", id: object.id, size: 4, text: object.text, }) %> + <% end %> <% end %> <%= pb_rails("body", props: { classname: "pb_form_pill_close" }) do %> <%= pb_rails("icon", props: { icon: 'times', fixed_width: true, size: object.close_icon_size }) %> <% end %> -<% end %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_popover/_popover.tsx b/playbook/app/pb_kits/playbook/pb_popover/_popover.tsx index d67921b5c7..4cf9eec90a 100644 --- a/playbook/app/pb_kits/playbook/pb_popover/_popover.tsx +++ b/playbook/app/pb_kits/playbook/pb_popover/_popover.tsx @@ -21,7 +21,7 @@ import classnames from "classnames"; import { globalProps, GlobalProps } from "../utilities/globalProps"; import { uniqueId } from 'lodash'; -type ModifiedGlobalProps = Omit<GlobalProps, 'minWidth'> +type ModifiedGlobalProps = Omit<GlobalProps, 'minWidth' | 'maxHeight' | 'minHeight'> type PbPopoverProps = { aria?: { [key: string]: string }; diff --git a/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss b/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss index f2dda25c0d..e84ccb6d81 100644 --- a/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss +++ b/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.scss @@ -10,6 +10,24 @@ $pb_selectable_card_indicator_size: 22px; $pb_selectable_card_border: 2px; +$pb_selectable_space_classes: ( + xxs: $space_xxs, + xs: $space_xs, + sm: $space_sm, + md: $space_md, + lg: $space_lg, + xl: $space_xl, +); +$pb_selectable_paddings: ( + p: "padding", + pr: "padding-right", + pl: "padding-left", + pt: "padding-top", + pb: "padding-bottom", + px: ("padding-left", "padding-right"), + py: ("padding-top", "padding-bottom") +); + [class^=pb_selectable_card_kit] { display: block; margin-bottom: 0; @@ -28,7 +46,6 @@ $pb_selectable_card_border: 2px; padding: $space_sm; margin-bottom: $space_sm; cursor: pointer; - outline: 1px solid transparent; @media (hover:hover) { &:hover { @@ -74,6 +91,7 @@ $pb_selectable_card_border: 2px; position: relative; @include pb_card_selected; + padding: calc(#{$space_sm} - 1px); transition-property: none; transition-duration: 0s; @@ -88,6 +106,54 @@ $pb_selectable_card_border: 2px; background-color: $royal; } } + + // Selected card has 1px more border + // Remove 1px so content does not "jump" + @each $position_name, + $position in $pb_selectable_paddings { + @each $space_name, + $space in $pb_selectable_space_classes { + ~ label.#{$position_name}_#{$space_name} { + @if type-of($position)=="list" { + @each $coordinate in $position { + #{$coordinate}: calc(#{$space} - 1px) !important; + } + } + + @else { + #{$position}: calc(#{$space} - 1px) !important; + } + } + } + } + } + } + + &.display_input { + input[type="checkbox"], + input[type="radio"] { + &:checked { + ~label { + border-width: $pb_card_border_width; + outline: 1px solid $primary; + } + + } + } + + > label { + outline: 1px solid transparent; + padding: $space_sm; + } + + &.dark { + input[type="checkbox"], + input[type="radio"] { + &:checked ~ label { + border-width: $pb_card_border_width; + outline: 1px solid $primary; + } + } } } diff --git a/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.tsx b/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.tsx index 923f602710..8b53177f62 100644 --- a/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.tsx +++ b/playbook/app/pb_kits/playbook/pb_selectable_card/_selectable_card.tsx @@ -67,6 +67,7 @@ const SelectableCard = (props: SelectableCardProps) => { 'disabled': disabled, 'enabled': !disabled, }), + variant === 'displayInput' ? 'display_input' : '', { error }, dark ? 'dark' : '', className diff --git a/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb b/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb index 0618d35d17..e3f997a773 100644 --- a/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb +++ b/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.html.erb @@ -25,7 +25,7 @@ <% end %> <div class="separator"></div> <div class="psuedo_separator"></div> - <%= pb_rails("card", props: { padding: "sm", status: object.status, border_none: true }) do %> + <%= pb_rails("card", props: { padding: "sm", status: object.status, border_none: true, dark: object.dark }) do %> <% if content.nil? %> <%= pb_rails("body", props: { text: object.text }) %> <% else %> diff --git a/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.rb b/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.rb index a9934da1d4..6b6b2e6158 100644 --- a/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.rb +++ b/playbook/app/pb_kits/playbook/pb_selectable_card/selectable_card.rb @@ -25,7 +25,7 @@ class SelectableCard < Playbook::KitBase def classname [ - generate_classname_without_spacing("pb_selectable_card_kit", checked_class, enable_disabled_class), + generate_classname_without_spacing("pb_selectable_card_kit", checked_class, enable_disabled_class) + display_input_class, error_class, dark_props, ].compact.join(" ") @@ -79,6 +79,10 @@ def enable_disabled_class def error_class error ? "error" : nil end + + def display_input_class + variant == "display_input" ? " display_input" : "" + end end end end diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.scss b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.scss new file mode 100644 index 0000000000..75f0976224 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.scss @@ -0,0 +1,37 @@ +@import 'skeleton_loading_mixins'; + +.pb_skeleton_loading { + display: "flex"; + flex-direction: "column"; + height: 100%; + .color_default { + @include skeleton-shimmer($silver); + } + .color_white { + @include skeleton-shimmer-light($white); + } + .dark { + @include skeleton-shimmer($border_dark); + } + .gap_xxs { + margin-top: 4px; + } + .gap_xs { + margin-top: 8px; + } + .gap_sm { + margin-top: 16px; + } + .gap_md { + margin-top: 24px; + } + .gap_lg { + margin-top: 32px; + } + .gap_xl { + margin-top: 40px; + } + .gap_xxl { + margin-top: 48px; + } +} diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.tsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.tsx new file mode 100644 index 0000000000..f7e155408b --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading.tsx @@ -0,0 +1,67 @@ + +import React from 'react' +import classnames from 'classnames' +import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props' +import { globalProps, GlobalProps } from '../utilities/globalProps' +import { Sizes } from '../types' + + +type SkeletonLoadingProps = { + aria?: { [key: string]: string }, + className?: string, + data?: { [key: string]: string }, + id?: string, + htmlOptions?: {[key: string]: string | number | boolean | (() => void)}, + height?: string, + width?: string, + borderRadius?: string, + gap?: Sizes | "none", + stack?: number, + color?: "default" | "white", + dark?: boolean, +} & GlobalProps + +const SkeletonLoading = (props: SkeletonLoadingProps): React.ReactElement => { + const { + aria = {}, + className, + data = {}, + id, + htmlOptions = {}, + height = "16px", + width = "100%", + borderRadius = "sm", + gap = "xxs", + stack = 1, + color = "default", + dark = false, + } = props + + const ariaProps = buildAriaProps(aria) + const dataProps = buildDataProps(data) + const htmlProps = buildHtmlProps(htmlOptions) + const skeletonContainerCss = classnames(buildCss('pb_skeleton_loading'), globalProps(props), className) + const gapClass = gap !== 'none' ? `gap_${gap}` : '' + const innerSkeletonCss = classnames(`border_radius_${borderRadius}`,`color_${color}`, dark && 'dark', ) + const innerSizeStyle = { height, width } + + return ( + <div + {...ariaProps} + {...dataProps} + {...htmlProps} + className={skeletonContainerCss} + id={id} + > + {Array.from({ length: Number(stack) }).map((_, index) => ( + <div + className={classnames(buildCss('pb_skeleton_loading_item'), innerSkeletonCss, index > 0 && gapClass)} + key={index} + style={innerSizeStyle} + /> + ))} + </div> + ) +} + +export default SkeletonLoading diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading_mixins.scss b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading_mixins.scss new file mode 100644 index 0000000000..2fe5d33376 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/_skeleton_loading_mixins.scss @@ -0,0 +1,40 @@ +// Animation +@keyframes wave { + 0% { + background-position: -468px 0; + } + 100% { + background-position: 468px 0; + } +} + +// Shimmer animation and gradient mixin based on color +@mixin skeleton-shimmer($color) { + background: $color; + background-color: $color; + background-image: linear-gradient( + to left, + $color 0%, + lighten($color, 1%) 50%, + lighten($color, 1%) 60%, + $color 80%, + $color 100% + ); + background-repeat: no-repeat; + animation: wave 1.5s linear infinite forwards; +} + +@mixin skeleton-shimmer-light($color) { + background: $color; + background-color: $color; + background-image: linear-gradient( + to left, + $color 0%, + darken($color, 1%) 50%, + darken($color, 1%) 60%, + $color 80%, + $color 100% + ); + background-repeat: no-repeat; + animation: wave 1.5s linear infinite forwards; +} \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.jsx new file mode 100644 index 0000000000..800685d00a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.jsx @@ -0,0 +1,51 @@ +import React from 'react' +import { Flex, SkeletonLoading } from "playbook-ui" + + +const SkeletonLoadingBorderRadius = (props) => ( + <Flex justify="evenly"> + <SkeletonLoading + borderRadius="rounded" + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + borderRadius="xl" + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + borderRadius="lg" + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + borderRadius="md" + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + borderRadius="xs" + height="50px" + width="100px" + {...props} + /> + <SkeletonLoading + borderRadius="none" + height="50px" + width="100px" + {...props} + /> + </Flex> +) + +export default SkeletonLoadingBorderRadius diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.md b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.md new file mode 100644 index 0000000000..52c493272c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_border_radius.md @@ -0,0 +1 @@ +The `borderRadius` prop accepts all of our [BorderRadius](https://playbook.powerapp.cloud/visual_guidelines/border_radius) tokens, with `sm` as default. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.jsx new file mode 100644 index 0000000000..82ad6d3323 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.jsx @@ -0,0 +1,26 @@ +import React from 'react' +import { Card, SkeletonLoading } from "playbook-ui" + + +const SkeletonLoadingColor = (props) => ( + <div> + <Card + borderNone + {...props} + > + <SkeletonLoading {...props}/> + </Card> + <Card + background="light" + borderNone + {...props} + > + <SkeletonLoading + color="white" + {...props} + /> + </Card> + </div> +) + +export default SkeletonLoadingColor diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.md b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.md new file mode 100644 index 0000000000..1837383ba8 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_color.md @@ -0,0 +1 @@ +The SkeletonLoading component has a default and a white `color` variant. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.html.erb b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.html.erb new file mode 100644 index 0000000000..542c88de2a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.html.erb @@ -0,0 +1 @@ +<%= pb_rails("skeleton_loading") %> diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.jsx new file mode 100644 index 0000000000..71f7986e57 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_default.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import { SkeletonLoading } from "playbook-ui" + + +const SkeletonLoadingDefault = (props) => ( + <div> + <SkeletonLoading {...props} /> + </div> +) + +export default SkeletonLoadingDefault diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.jsx new file mode 100644 index 0000000000..9760332ca3 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.jsx @@ -0,0 +1,59 @@ +import React from 'react' +import { Card, SkeletonLoading } from "playbook-ui" + + +const SkeletonLoadingHeightWidth = (props) => ( + <div> + <SkeletonLoading + height="100px" + width="50%" + {...props} + /> + <SkeletonLoading + gap="md" + height="20px" + marginY="md" + stack="3" + width="50px" + {...props} + /> + <Card htmlOptions={{ style: { height: '200px', width: '100%' }}} + marginBottom="md" + padding="none" + > + <SkeletonLoading + borderRadius="md" + gap="xl" + height="50%" + width="300px" + {...props} + /> + </Card> + <Card htmlOptions={{ style: { height: '200px', width: '100%' }}} + padding="none" + > + <SkeletonLoading + borderRadius="md" + gap="xl" + height="30%" + stack="2" + width="70%" + {...props} + /> + </Card> + <SkeletonLoading + height="150px" + marginY="md" + width="150px" + {...props} + /> + <SkeletonLoading + borderRadius="rounded" + height="150px" + width="150px" + {...props} + /> + </div> +) + +export default SkeletonLoadingHeightWidth diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.md b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.md new file mode 100644 index 0000000000..12b931c066 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_height_width.md @@ -0,0 +1,3 @@ +The `height` and `width` props accept pixel and percentage values. If using a percentage for `height`, the parent container must have a set height. + +Set the `height` and `width` props to the same value to make a square. A `rounded` borderRadius will make a square a circle. If using percentages to make a square, your parent container must also be a square. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.jsx new file mode 100644 index 0000000000..babb83e293 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { SkeletonLoading } from "playbook-ui" + + +const SkeletonLoadingLayout = (props) => ( + <div> + <SkeletonLoading + stack="5" + {...props} + /> + <SkeletonLoading + gap="md" + paddingTop="xl" + stack="3" + {...props} + /> + </div> +) + +export default SkeletonLoadingLayout diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.md b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.md new file mode 100644 index 0000000000..11d2958cdc --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/_skeleton_loading_layout.md @@ -0,0 +1,3 @@ +Use the `stack` and `gap` props in conjunction to layer multiple Skeleton loading bars on top of each other. + +`stack` accepts a number that correlates to the number of rows (1 is default), and `gap` accepts a portion of our [spacing props](https://playbook.powerapp.cloud/visual_guidelines/spacing) (`xxs` as default, `xs`, `sm`, `md`, `lg`, `xl`, `xxl`) to set the pixel distance between each row. `gap` will not do anything if there is no corresponding `stack` prop set. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/example.yml b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/example.yml new file mode 100644 index 0000000000..df125288c6 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/example.yml @@ -0,0 +1,13 @@ +examples: + + rails: + # - skeleton_loading_default: Default + + + react: + - skeleton_loading_default: Default + - skeleton_loading_color: Color + - skeleton_loading_layout: Layout + - skeleton_loading_border_radius: Border Radius + - skeleton_loading_height_width: Height & Width + diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/index.js b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/index.js new file mode 100644 index 0000000000..862e8b3742 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/docs/index.js @@ -0,0 +1,5 @@ +export { default as SkeletonLoadingDefault } from './_skeleton_loading_default.jsx' +export { default as SkeletonLoadingColor } from './_skeleton_loading_color.jsx' +export { default as SkeletonLoadingLayout } from './_skeleton_loading_layout.jsx' +export { default as SkeletonLoadingBorderRadius } from './_skeleton_loading_border_radius.jsx' +export { default as SkeletonLoadingHeightWidth } from './_skeleton_loading_height_width.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.html.erb b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.html.erb new file mode 100644 index 0000000000..85230e2574 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.html.erb @@ -0,0 +1,12 @@ +<!-- Go to pb_content_tag definition in kit_base.rb for usage information. Commented out options are default (showing the default shape), and each can be deleted if not customizing that param. --> +<!-- If using nonstandard params please un-comment out and replace with your custom params. --> +<%= pb_content_tag( + # :div, + # aria: object.aria, + # class: object.classname, + # data: object.data, + # id: object.id, + # **combined_html_options +) do %> + <span>SKELETON_LOADING CONTENT</span> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.rb b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.rb new file mode 100644 index 0000000000..d91d02ea78 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Playbook + module PbSkeletonLoading + class SkeletonLoading < ::Playbook::KitBase + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.test.jsx b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.test.jsx new file mode 100644 index 0000000000..57b7215622 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading.test.jsx @@ -0,0 +1,81 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { ensureAccessible } from '../utilities/test-utils' +import { SkeletonLoading } from 'playbook-ui' + +/* See these resources for more testing info: + - https://github.com/testing-library/jest-dom#usage for useage and examples + - https://jestjs.io/docs/en/using-matchers +*/ + +describe('SkeletonLoading', () => { + const defaultProps = { + data: { testid: 'skeleton-loading' } + } + + it('should be accessible', async () => { + ensureAccessible(SkeletonLoading, defaultProps) + }) + + it('renders with default props', () => { + const { container } = render(<SkeletonLoading {...defaultProps} />) + const skeleton = screen.getByTestId('skeleton-loading') + + expect(skeleton).toBeInTheDocument() + expect(skeleton).toHaveClass('pb_skeleton_loading') + expect(container.querySelectorAll('div[class*="border_radius_"]')).toHaveLength(1) + }) + + it('renders multiple skeleton items based on stack prop', () => { + const props = { + ...defaultProps, + stack: 3 + } + const { container } = render(<SkeletonLoading {...props} />) + + expect(container.querySelectorAll('div[class*="border_radius_"]')).toHaveLength(3) + }) + + it('applies custom styles correctly', () => { + const props = { + ...defaultProps, + height: '24px', + width: '50%', + borderRadius: 'lg', + color: 'light', + dark: true + } + const { container } = render(<SkeletonLoading {...props} />) + const skeletonItem = container.querySelector('div[class*="border_radius_"]') + + expect(skeletonItem).toHaveClass('border_radius_lg') + expect(skeletonItem).toHaveClass('dark') + }) + + it('applies gap class to items after first one', () => { + const props = { + ...defaultProps, + stack: 3, + gap: 'md' + } + const { container } = render(<SkeletonLoading {...props} />) + const skeletonItems = container.querySelectorAll('div[class*="border_radius_"]') + + expect(skeletonItems[0]).not.toHaveClass('gap_md') + expect(skeletonItems[1]).toHaveClass('gap_md') + expect(skeletonItems[2]).toHaveClass('gap_md') + }) + + it('handles no gap properly', () => { + const props = { + ...defaultProps, + stack: 2, + gap: 'none' + } + const { container } = render(<SkeletonLoading {...props} />) + const skeletonItems = container.querySelectorAll('div[class*="border_radius_"]') + + expect(skeletonItems[0]).not.toHaveClass('gap_none') + expect(skeletonItems[1]).not.toHaveClass('gap_none') + }) +}) \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx b/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx index 3d6ef9eafd..f5aa0d6d2b 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx +++ b/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx @@ -1,12 +1,15 @@ import React from 'react' import classnames from 'classnames' - import { buildCss, buildHtmlProps } from '../utilities/props' -import { globalProps, GlobalProps } from "../utilities/globalProps"; +import { globalProps, GlobalProps } from "../utilities/globalProps" import DateStacked from '../pb_date_stacked/_date_stacked' import IconCircle from '../pb_icon_circle/_icon_circle' +import TimelineLabel from './subcomponents/Label' +import TimelineStep from './subcomponents/Step' +import TimelineDetail from './subcomponents/Detail' + type ItemProps = { className?: string, children?: React.ReactNode[] | React.ReactNode, @@ -17,6 +20,13 @@ type ItemProps = { lineStyle?: 'solid' | 'dotted', } & GlobalProps +function isElementOfType<P>( + element: React.ReactNode, + component: React.ComponentType<P> +): element is React.ReactElement<P> { + return React.isValidElement<P>(element) && element.type === component +} + const TimelineItem = ({ className, children, @@ -31,31 +41,57 @@ const TimelineItem = ({ const htmlProps = buildHtmlProps(htmlOptions) + const childrenArray = React.Children.toArray(children) + + const labelChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineLabel) + ) + + const stepChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineStep) + ) + + const detailChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineDetail) + ) + + const otherChildren = childrenArray.filter( + (child) => + !isElementOfType(child, TimelineLabel) && + !isElementOfType(child, TimelineStep) && + !isElementOfType(child, TimelineDetail) + ) + return ( - <div + <div {...htmlProps} className={classnames(timelineItemCss, globalProps(props), className)} > - <div className="pb_timeline_item_left_block"> - {date && - <DateStacked - align="center" - date={date} - size="sm" - /> - } - </div> - <div className="pb_timeline_item_step"> - <IconCircle - icon={icon} - size="xs" - variant={iconColor} - /> - <div className="pb_timeline_item_connector" /> - </div> - <div className="pb_timeline_item_right_block"> - {children} - </div> + {labelChild || ( + <div className="pb_timeline_item_left_block"> + {date && ( + <DateStacked + align="center" + date={date} + size="sm" + /> + )} + </div> + )} + {stepChild || ( + <div className="pb_timeline_item_step"> + <IconCircle icon={icon} + size="xs" + variant={iconColor} + /> + <div className="pb_timeline_item_connector" /> + </div> + )} + {detailChild || ( + <div className="pb_timeline_item_right_block"> + { otherChildren } + </div> + )} </div> ) } diff --git a/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx b/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx index e58d9f8acb..a0e3f53af6 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx +++ b/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx @@ -5,6 +5,11 @@ import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../uti import { GlobalProps, globalProps } from '../utilities/globalProps' import TimelineItem from './_item' +import { + TimelineStep, + TimelineLabel, + TimelineDetail, +} from './subcomponents' type TimelineProps = { aria?: { [key: string]: string }, @@ -47,5 +52,8 @@ const Timeline = ({ } Timeline.Item = TimelineItem +Timeline.Step = TimelineStep +Timeline.Label = TimelineLabel +Timeline.Detail = TimelineDetail export default Timeline diff --git a/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb new file mode 100644 index 0000000000..919544cc91 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb @@ -0,0 +1,3 @@ +<%= pb_content_tag do %> + <%= content.presence %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/detail.rb b/playbook/app/pb_kits/playbook/pb_timeline/detail.rb new file mode 100644 index 0000000000..810248dae4 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/detail.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Detail < Playbook::KitBase + def classname + generate_classname("pb_timeline_item_right_block") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb new file mode 100644 index 0000000000..1725989c17 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb @@ -0,0 +1,43 @@ +<%= pb_rails("timeline", props: {orientation: "horizontal", show_date: true}) do %> + <%= pb_rails("timeline/item", props: { line_style: "solid"}) do |item| %> + + <% item.label do %> + <%= pb_rails("timeline/label") do %> + <%= pb_rails("title", props: { text: "Any Kit Here", size: 2 }) %> + <% end %> + <% end %> + + <% item.step do %> + <%= pb_rails("timeline/step", props: { icon: 'check', icon_color: 'teal' }) %> + <% end %> + + <% item.detail do %> + <%= pb_rails("title_detail", props: { + title: "Jackson Heights", + detail: "37-27 74th Street" + }) %> + <% end %> + <% end %> + <%= pb_rails("timeline/item", props: { line_style: "dotted"}) do |item| %> + + <% item.step do %> + <%= pb_rails("timeline/step") do %> + <%= pb_rails("pill", props: { text: "Any Kit" , variant: "success" }) %> + <% end %> + <% end %> + + <% item.detail do %> + <%= pb_rails("title_detail", props: { + title: "Greenpoint", + detail: "81 Gate St Brooklyn" + }) %> + <% end %> + <% end %> + + <%= pb_rails("timeline/item", props: {icon: "map-marker-alt", icon_color: "purple", date: Date.today+1 }) do |item| %> + <%= pb_rails("title_detail", props: { + title: "Society Hill", + detail: "72 E St Astoria" + }) %> + <% end %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx new file mode 100644 index 0000000000..c426fbc066 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx @@ -0,0 +1,68 @@ +import React from 'react' + +import Timeline from '../_timeline' +import Title from '../../pb_title/_title' +import Pill from '../../pb_pill/_pill' + +import TitleDetail from '../../pb_title_detail/_title_detail' + +const TimelineWithChildren = (props) => ( + <div> + <Timeline orientation="horizontal" + showDate + {...props} + > + <Timeline.Item lineStyle="solid" + {...props} + > + <Timeline.Label> + <Title size={2} + text='Any Kit Here' + /> + </Timeline.Label> + <Timeline.Step icon="user" + iconColor="royal" + /> + <Timeline.Detail> + <TitleDetail detail="37-27 74th Street" + title="Jackson Heights" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + + <Timeline.Item lineStyle="dotted" + {...props} + > + <Timeline.Step> + <Pill text="Any Kit" + variant="success" + /> + </Timeline.Step> + <Timeline.Detail> + <TitleDetail detail="81 Gate St Brooklyn" + title="Greenpoint" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + + <Timeline.Item lineStyle="solid" + {...props} + > + <Timeline.Label date={new Date(new Date().setDate(new Date().getDate() + 1))} /> + <Timeline.Step icon="map-marker-alt" + iconColor="purple" + /> + <Timeline.Detail> + <TitleDetail detail="72 E St Astoria" + title="Society Hill" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + </Timeline> + </div> +) + +export default TimelineWithChildren diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md new file mode 100644 index 0000000000..dc0264dd9e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md @@ -0,0 +1,2 @@ +Any kit can be used inside of our compound components of label, step, or detail. Expand the code snippet below to see how to use these children elements. + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml b/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml index 2cf2a99a73..1cd961b5d5 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml @@ -4,10 +4,11 @@ examples: - timeline_default: Default - timeline_vertical: Vertical - timeline_with_date: With Date + - timeline_with_children: With Children react: - timeline_default: Default - timeline_vertical: Vertical - timeline_with_date: With Date - + - timeline_with_children: With Children diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js b/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js index 35398d22d6..8da0d1e1f0 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js @@ -1,3 +1,4 @@ export { default as TimelineDefault } from './_timeline_default.jsx' export { default as TimelineVertical } from './_timeline_vertical.jsx' export { default as TimelineWithDate } from './_timeline_with_date.jsx' +export { default as TimelineWithChildren } from './_timeline_with_children.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb index cb815cb6e2..8f7153b22b 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb +++ b/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb @@ -1,25 +1,21 @@ <%= pb_content_tag do %> + <% if label %> + <%= label %> + <% else %> + <%= pb_rails("timeline/label", props: { date: date }) %> + <% end %> - <div class="pb_timeline_item_left_block"> - <% if object.date.present? %> - <%= pb_rails("date_stacked", props: { - date: object.date, - size: "sm", - align: "center" - }) %> - <% end %> - </div> - - <div class="pb_timeline_item_step"> - <%= pb_rails("icon_circle", props: { - icon: object.icon, - variant: object.icon_color, - size: "xs" - }) %> - <div class="pb_timeline_item_connector"></div> - </div> + <% if step %> + <%= step %> + <% else %> + <%= pb_rails("timeline/step", props: { icon: icon, icon_color: icon_color }) %> + <% end %> - <div class="pb_timeline_item_right_block"> - <%= content.presence %> - </div> + <% if detail%> + <%= detail%> + <% else %> + <%= pb_rails("timeline/detail") do %> + <%= content %> + <% end %> + <% end %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/item.rb b/playbook/app/pb_kits/playbook/pb_timeline/item.rb index f5c5830777..9a954cc413 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/item.rb +++ b/playbook/app/pb_kits/playbook/pb_timeline/item.rb @@ -13,6 +13,10 @@ class Item < Playbook::KitBase values: %w[solid dotted], default: "solid" + renders_one :label + renders_one :step + renders_one :detail + def classname generate_classname("pb_timeline_item_kit", line_style) end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb new file mode 100644 index 0000000000..c7b4b3b18b --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb @@ -0,0 +1,12 @@ +<%= pb_content_tag do %> + <% if object.date.present? %> + <%= pb_rails("date_stacked", props: { + date: object.date, + size: "sm", + align: "center" + }) %> + <% else %> + <%= content.presence %> + <% end %> +<% end %> + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/label.rb b/playbook/app/pb_kits/playbook/pb_timeline/label.rb new file mode 100644 index 0000000000..bfb74a469f --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/label.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Label < Playbook::KitBase + prop :date + + def classname + generate_classname("pb_timeline_item_left_block") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb new file mode 100644 index 0000000000..533e1cb383 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb @@ -0,0 +1,14 @@ +<%= pb_content_tag do %> + <% if object.icon.present? %> + <%= pb_rails("icon_circle", props: { + icon: object.icon, + variant: object.icon_color, + size: "xs" + }) %> + <% else %> + <%= content.presence %> + <% end %> + <div class="pb_timeline_item_connector"></div> +<% end %> + + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/step.rb b/playbook/app/pb_kits/playbook/pb_timeline/step.rb new file mode 100644 index 0000000000..a80d28d51a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/step.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Step < Playbook::KitBase + prop :icon, type: Playbook::Props::String + prop :icon_color, type: Playbook::Props::Enum, + values: %w[default royal blue purple teal red yellow green], + default: "default" + + def classname + generate_classname("pb_timeline_item_step") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx new file mode 100644 index 0000000000..0365376132 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" + +type TimelineDetailProps = { + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineDetail: React.FC<TimelineDetailProps> = ({ + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_right_block', globalProps(props), className)} + > + {children} + </div> + ) +} + +export default TimelineDetail diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx new file mode 100644 index 0000000000..717e92aeaf --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" +import DateStacked from '../../pb_date_stacked/_date_stacked' + +type TimelineLabelProps = { + date?: Date, + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineLabel: React.FC<TimelineLabelProps> = ({ + date, + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_left_block', globalProps(props), className)} + > + {children} + {date && ( + <DateStacked align="center" + date={date} + size="sm" + /> + )} + </div> + ) +} + +export default TimelineLabel diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx new file mode 100644 index 0000000000..648c1d9b0a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" +import IconCircle from '../../pb_icon_circle/_icon_circle' + +type TimelineStepProps = { + icon?: string, + iconColor?: 'default' | 'royal' | 'blue' | 'purple' | 'teal' | 'red' | 'yellow' | 'green', + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineStep: React.FC<TimelineStepProps> = ({ + icon = 'user', + iconColor = 'default', + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_step', globalProps(props), className)} + > + {children ? ( + children + ) : ( + <IconCircle icon={icon} + size="xs" + variant={iconColor} + /> + )} + <div className="pb_timeline_item_connector" /> + </div> + ) +} + +export default TimelineStep diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx new file mode 100644 index 0000000000..693e027a7d --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx @@ -0,0 +1,3 @@ +export { default as TimelineLabel } from './Label'; +export { default as TimelineDetail } from './Detail'; +export { default as TimelineStep } from './Step'; diff --git a/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js b/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js index fa71db5bad..9468e2070d 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js +++ b/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js @@ -2,6 +2,10 @@ import React from 'react' import { render, screen } from '../utilities/test-utils' import Timeline from './_timeline' +import TimelineItem from './_item' +import TimelineLabel from './subcomponents/Label' +import TimelineStep from './subcomponents/Step' +import TimelineDetail from './subcomponents/Detail' import TitleDetail from '../pb_title_detail/_title_detail' const testId = 'timeline' @@ -43,18 +47,91 @@ const TimelineDefault = (props) => ( </> ) +const TimelineWithChildren = (props) => ( + <> + <Timeline + className={className} + data={{ testid: testId }} + orientation="horizontal" + showDate + {...props} + > + <TimelineItem lineStyle="solid" + {...props} + > + <TimelineLabel date={new Date()} /> + <TimelineStep icon="user" + iconColor="royal" + /> + <TimelineDetail> + <TitleDetail + detail="37-27 74th Street" + title="Jackson Heights" + {...props} + /> + </TimelineDetail> + </TimelineItem> + + <TimelineItem lineStyle="dotted" + {...props} + > + <TimelineStep icon="check" + iconColor="teal" + /> + <TimelineDetail> + <TitleDetail + detail="81 Gate St Brooklyn" + title="Greenpoint" + {...props} + /> + </TimelineDetail> + </TimelineItem> + + <TimelineItem lineStyle="solid" + {...props} + > + <TimelineLabel + date={new Date(new Date().setDate(new Date().getDate() + 1))} + /> + <TimelineStep icon="map-marker-alt" + iconColor="purple" + /> + <TimelineDetail> + <TitleDetail + detail="72 E St Astoria" + title="Society Hill" + {...props} + /> + </TimelineDetail> + </TimelineItem> + </Timeline> + </> +) + test('should pass data prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) expect(kit).toBeInTheDocument() }) +test('should pass data prop using children', () => { + render(<TimelineWithChildren />) + const kit = screen.getByTestId(testId) + expect(kit).toBeInTheDocument() +}) + test('should pass className prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) expect(kit).toHaveClass(className) }) +test('should pass className prop with children', () => { + render(<TimelineWithChildren />) + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass(className) +}) + test('should pass aria prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) @@ -86,3 +163,10 @@ test('should pass showDate prop', () => { const kit = screen.getByTestId(testId) expect(kit).toHaveClass('pb_timeline_kit__horizontal__with_date') }) + +test('should pass showDate prop with Children', () => { + const props = { showDate: true } + render(<TimelineWithChildren {...props} />) + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass('pb_timeline_kit__horizontal__with_date') +}) diff --git a/playbook/app/pb_kits/playbook/utilities/_hover.scss b/playbook/app/pb_kits/playbook/utilities/_hover.scss index 1974908e43..ed571aa400 100644 --- a/playbook/app/pb_kits/playbook/utilities/_hover.scss +++ b/playbook/app/pb_kits/playbook/utilities/_hover.scss @@ -1,5 +1,25 @@ @import "../tokens/exports/scale.module"; +@mixin hover-scale-classes($scales-list) { + @each $name, $scale in $scales-list { + .hover_#{"" + $name}:hover, + .group_hover:hover .group_hover.hover_#{"" + $name} { + transform: $scale; + transition: transform $transition-speed ease; + } + } +} + +@mixin hover-shadow-classes($shadows-list) { + @each $name, $shadow in $shadows-list { + .hover_#{"" + $name}:hover, + .group_hover:hover .group_hover.hover_#{"" + $name} { + box-shadow: $shadow; + transition: box-shadow $transition-speed ease; + } + } +} + @mixin hover-color-classes($colors-list) { @each $name, $color in $colors-list { .hover_background-#{"" + $name}:hover { @@ -13,25 +33,6 @@ } } - @mixin hover-shadow-classes($shadows-list) { - @each $name, $shadow in $shadows-list { - .hover_#{"" + $name}:hover { - box-shadow: $shadow; - transition: box-shadow $transition-speed ease; - } - } - } - - @mixin hover-scale-classes($scales-list) { - @each $name, $scale in $scales-list { - .hover_#{"" + $name}:hover { - transform: $scale; - transition: transform $transition-speed ease; - } - } - } - - @include hover-scale-classes($scales); @include hover-shadow-classes($box_shadows); @include hover-color-classes($product_colors); diff --git a/playbook/app/pb_kits/playbook/utilities/globalPropNames.mjs b/playbook/app/pb_kits/playbook/utilities/globalPropNames.mjs index d41b00be0b..639cc31619 100644 --- a/playbook/app/pb_kits/playbook/utilities/globalPropNames.mjs +++ b/playbook/app/pb_kits/playbook/utilities/globalPropNames.mjs @@ -1,9 +1,13 @@ export default [ + "minHeight", + "maxHeight", + "height", "left", "bottom", "right", "top", "hover", + "groupHover", "zIndex", "verticalAlign", "truncate", diff --git a/playbook/app/pb_kits/playbook/utilities/globalProps.ts b/playbook/app/pb_kits/playbook/utilities/globalProps.ts index 02338294bc..f29b4a6706 100644 --- a/playbook/app/pb_kits/playbook/utilities/globalProps.ts +++ b/playbook/app/pb_kits/playbook/utilities/globalProps.ts @@ -66,6 +66,10 @@ type Hover = Shadow & { scale?: "sm" | "md" | "lg" } +type GroupHover = { + groupHover?: boolean, +} + type JustifyContent = { justifyContent?: Alignment & Space } @@ -170,12 +174,24 @@ type ZIndex = { zIndex?: ZIndexType, } | ZIndexResponsiveType +type Height = { + height?: string +} + +type MaxHeight = { + maxHeight?: string +} + +type MinHeight = { + minHeight?: string +} + // keep this as the last type definition export type GlobalProps = AlignContent & AlignItems & AlignSelf & BorderRadius & Cursor & Dark & Display & DisplaySizes & Flex & FlexDirection & FlexGrow & FlexShrink & FlexWrap & JustifyContent & JustifySelf & LineHeight & Margin & MinWidth & MaxWidth & NumberSpacing & Order & Overflow & Padding & - Position & Shadow & TextAlign & Truncate & VerticalAlign & ZIndex & { hover?: string } & Top & Right & Bottom & Left; + Position & Shadow & TextAlign & Truncate & VerticalAlign & ZIndex & { hover?: string } & Top & Right & Bottom & Left & Height & MaxHeight & MinHeight; const getResponsivePropClasses = (prop: {[key: string]: string}, classPrefix: string) => { const keys: string[] = Object.keys(prop) @@ -209,6 +225,7 @@ const filterClassName = (value: string): string => { // Prop categories const PROP_CATEGORIES: {[key:string]: (props: {[key: string]: any}) => string} = { + groupHoverProps: ({ groupHover }: GroupHover ) => groupHover ? 'group_hover ' : '', hoverProps: ({ hover }: { hover?: Hover }) => { let css = ''; if (!hover) return css; @@ -498,7 +515,22 @@ const PROP_CATEGORIES: {[key:string]: (props: {[key: string]: any}) => string} = } else { return verticalAlign ? `vertical_align_${verticalAlign} ` : '' } - } + }, + +} + +const PROP_INLINE_CATEGORIES: {[key:string]: (props: {[key: string]: any}) => {[key: string]: any}} = { + heightProps: ({ height }: Height) => { + return height ? { height } : {}; + }, + + maxHeightProps: ({ maxHeight }: MaxHeight) => { + return maxHeight ? { maxHeight } : {}; + }, + + minHeightProps: ({ minHeight }: MinHeight) => { + return minHeight ? { minHeight } : {}; + }, } type DefaultProps = {[key: string]: string} | Record<string, unknown> @@ -510,6 +542,16 @@ export const globalProps = (props: GlobalProps, defaultProps: DefaultProps = {}) }).filter((value) => value?.length > 0).join(" ") } +// New function for inline styles +export const globalInlineProps = (props: GlobalProps): React.CSSProperties => { + const styles = Object.keys(PROP_INLINE_CATEGORIES).reduce((acc, key) => { + const result = PROP_INLINE_CATEGORIES[key](props); + return { ...acc, ...(typeof result === 'object' ? result : {}) }; // Ensure result is an object before spreading + }, {}); + + return styles; // Return the styles object directly +} + export const deprecatedProps = (): void => { // if (process.env.NODE_ENV === 'development') { diff --git a/playbook/lib/playbook/hover.rb b/playbook/lib/playbook/hover.rb index 500dee2b05..db5aeb5466 100644 --- a/playbook/lib/playbook/hover.rb +++ b/playbook/lib/playbook/hover.rb @@ -4,6 +4,7 @@ module Playbook module Hover def self.included(base) base.prop :hover + base.prop :group_hover, type: Playbook::Props::Boolean, default: false end def hover_options @@ -38,7 +39,8 @@ def hover_attributes def hover_props selected_props = hover_options.keys.select { |sk| try(sk) } - return nil unless selected_props.present? + + return nil if selected_props.nil? && group_hover.nil? responsive = selected_props.present? && try(selected_props.first).is_a?(::Hash) css = "" @@ -58,6 +60,7 @@ def hover_props end end + css += "group_hover " if group_hover css.strip unless css.blank? end end diff --git a/playbook/lib/playbook/kit_base.rb b/playbook/lib/playbook/kit_base.rb index 470a296929..65583833b1 100644 --- a/playbook/lib/playbook/kit_base.rb +++ b/playbook/lib/playbook/kit_base.rb @@ -73,15 +73,15 @@ class KitBase < ViewComponent::Base prop :aria, type: Playbook::Props::HashProp, default: {} prop :html_options, type: Playbook::Props::HashProp, default: {} prop :children, type: Playbook::Props::Proc + prop :style, type: Playbook::Props::HashProp, default: {} + prop :height + prop :min_height + prop :max_height def object self end - def combined_html_options - default_html_options.merge(html_options.deep_merge(data_attributes)) - end - # rubocop:disable Layout/CommentIndentation # pb_content_tag information (potentially to be abstracted into its own dev doc in the future) # The pb_content_tag generates HTML content tags for rails kits with flexible options. @@ -110,15 +110,48 @@ def pb_content_tag(name = :div, content_or_options_with_block = {}, options = {} end # rubocop:enable Style/OptionalBooleanParameter + def combined_html_options + merged = default_html_options.dup + + html_options.each do |key, value| + if key == :style && value.is_a?(Hash) + # Convert style hash to CSS string + merged[:style] = value.map { |k, v| "#{k.to_s.gsub('_', '-')}: #{v}" }.join("; ") + else + merged[key] = value + end + end + + inline_styles = dynamic_inline_props + merged[:style] = if inline_styles.present? + merged[:style].present? ? "#{merged[:style]}; #{inline_styles}" : inline_styles + end + + merged.deep_merge(data_attributes) + end + + def global_inline_props + { + height: height, + min_height: min_height, + max_height: max_height, + }.compact + end + private def default_options - { + options = { id: id, data: data, class: classname, aria: aria, } + + inline_styles = dynamic_inline_props + options[:style] = inline_styles if inline_styles.present? && !html_options.key?(:style) + + options end def default_html_options @@ -131,5 +164,10 @@ def data_attributes aria: aria, }.transform_keys { |key| key.to_s.tr("_", "-").to_sym } end + + def dynamic_inline_props + styles = global_inline_props.map { |key, value| "#{key.to_s.gsub('_', '-')}: #{value}" if value.present? }.compact + styles.join("; ").presence + end end end diff --git a/playbook/lib/playbook/version.rb b/playbook/lib/playbook/version.rb index 1d1954ad9f..65eb8c9506 100644 --- a/playbook/lib/playbook/version.rb +++ b/playbook/lib/playbook/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Playbook - PREVIOUS_VERSION = "14.6.1" - VERSION = "14.6.2" + PREVIOUS_VERSION = "14.6.2" + VERSION = "14.7.0" end diff --git a/playbook/package.json b/playbook/package.json index 2af3422d19..62d278b2f4 100644 --- a/playbook/package.json +++ b/playbook/package.json @@ -1,6 +1,6 @@ { "name": "playbook-ui", - "version": "14.6.2", + "version": "14.7.0", "description": "Nitro's Design System", "main": "./dist/playbook.js", "types": "./dist/types/index.d.ts", @@ -75,7 +75,7 @@ "jest": "26.6.3", "jest-axe": "4.1.0", "jest-fail-on-console": "3.2.0", - "lazysizes": "^5.2.2", + "lazysizes": "^5.3.2", "lodash-es": "^4.17.12", "react": "^17.0.2", "react-animate-height": "^2.0.23", diff --git a/playbook/spec/pb_kits/playbook/kits/skeleton_loading_spec.rb b/playbook/spec/pb_kits/playbook/kits/skeleton_loading_spec.rb new file mode 100644 index 0000000000..d755b171a2 --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/skeleton_loading_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# require_relative "../../../../app/pb_kits/playbook/pb_skeleton_loading/skeleton_loading" + +# RSpec.describe Playbook::PbSkeleton_loading::Skeleton_loading do +# subject { Playbook::PbSkeleton_loading::Skeleton_loading } + +# it { is_expected.to define_partial } + +# # Do not leave this file blank. Use other spec files for example tests. +# end diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb new file mode 100644 index 0000000000..fc77b24b5c --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/detail" + +RSpec.describe Playbook::PbTimeline::Detail do + subject { Playbook::PbTimeline::Detail } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new({}).classname).to eq "pb_timeline_item_right_block" + end + end +end diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb new file mode 100644 index 0000000000..555837360d --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/label" + +RSpec.describe Playbook::PbTimeline::Label do + subject { Playbook::PbTimeline::Label } + + it { is_expected.to define_prop(:date) } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new.classname).to eq "pb_timeline_item_left_block" + end + end +end diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb new file mode 100644 index 0000000000..ecbe365c72 --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/step" + +RSpec.describe Playbook::PbTimeline::Step do + subject { Playbook::PbTimeline::Step } + + it { is_expected.to define_prop(:icon) } + + it { + is_expected.to define_enum_prop(:icon_color) + .with_default("default") + .with_values("default", "royal", "blue", "purple", "teal", "red", "yellow", "green") + } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new.classname).to eq "pb_timeline_item_step" + end + end +end diff --git a/yarn.lock b/yarn.lock index bc890a2c0b..f28f38502e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3520,10 +3520,10 @@ resolved "https://npm.powerapp.cloud/@tiptap/extension-document/-/extension-document-2.0.2.tgz#b07a4062cc0772e4129621b9b9a2e043a05395a1" integrity sha512-rY87m1sezlD37v5hGndiA/B/3upR3hQurSEsWhWyQE/11lOshPQKCCHfDV6KLwKdjd8lfwfbXueH/SBFHtrYAQ== -"@tiptap/extension-document@^2.1.12": - version "2.1.12" - resolved "https://npm.powerapp.cloud/@tiptap/extension-document/-/extension-document-2.1.12.tgz#e19e4716dfad60cbeb6abaf2f362fed759963529" - integrity sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw== +"@tiptap/extension-document@^2.6.6": + version "2.9.1" + resolved "https://npm.powerapp.cloud/@tiptap/extension-document/-/extension-document-2.9.1.tgz#ea65a86a4d2524ec65fc4775122f652840a89386" + integrity sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg== "@tiptap/extension-dropcursor@^2.0.2": version "2.0.2" @@ -8433,7 +8433,7 @@ kleur@^3.0.3: resolved "https://npm.powerapp.cloud/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lazysizes@^5.2.2: +lazysizes@^5.3.2: version "5.3.2" resolved "https://npm.powerapp.cloud/lazysizes/-/lazysizes-5.3.2.tgz#27f974c26f5fcc33e7db765c0f4930488c8a2984" integrity sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==