diff --git a/.github/workflows/github-actions-check-labels.yml b/.github/workflows/github-actions-check-labels.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/playbook-website/config/menu.yml b/playbook-website/config/menu.yml index 882f663f75..61f8012c84 100644 --- a/playbook-website/config/menu.yml +++ b/playbook-website/config/menu.yml @@ -542,6 +542,9 @@ kits: be followed by Title 2s followed by Title 3s and so on, without skipping any levels. status: stable + - name: link + platforms: *1 + status: beta - category: user description: components: diff --git a/playbook/README.md b/playbook/README.md new file mode 100644 index 0000000000..6cdedb92c2 --- /dev/null +++ b/playbook/README.md @@ -0,0 +1,94 @@ +[![npm version](https://badge.fury.io/js/playbook-ui.svg)](https://badge.fury.io/js/playbook-ui) +[![Gem Version](https://badge.fury.io/rb/playbook_ui.svg)](https://badge.fury.io/rb/playbook_ui) + +# Playbook Design System + +Playbook is the first design system built for both Rails & React interfaces. Inspired by [Velocity](https://www.invisionapp.com/inside-design/design-resources/design-system-dashboard-ui-kit/), Playbook takes a modern design approach and applies it in a way that makes it easy to support bleeding edge or legacy systems. Playbook is built & maintained by the User Experience & Development teams at [Power Home Remodeling](https://www.techatpower.com/), the largest home remodeler in the US. + +## Development + +### Requirements + +- [asdf](https://github.com/asdf-vm/asdf) + +### Getting Started and Running Playbook for Development + +1. After cloning the repo, you should have the following nested folders among other files. It will be important to pay attention to which folder we are in as we get playbook running: + ``` + playbook + │ playbook + │ playbook-website + ``` +1. Ensure your installed version of `bundler` is the same as the `BUNDLED WITH` section found in [Gemfile.lock](./Gemfile.lock). You can check the version you have by running `bundle -v`. + +1. From the root directory, run `./setup.sh` + +1. From the top-level playbook folder run `yarn start-dev` This may take a little while. +1. Once you see the "compiled successfully" message in your terminal, navigate to [http://localhost:3000](http://localhost:3000) and you should see the playbook website. + +### Post Installation Startup + +Use `./run.sh` to run the application in one step. This will handle dependency updates then start the server. Helpful for fast start-up without bootstrapping especially when switching branches. 🚀 + +### Running Library Tests + +1. `cd playbook && ./test.sh` + +--- + +## Additional Resources + +### Adding NPM Dependencies + +1. You need to be working in `playbook/playbook` or `playbook/playbook-website` subdirectory +1. run `yarn workspace playbook-website add ` to add to the website +1. run `yarn workspace playbook-ui add ` to add to the kit source +1. run `yarn workspace playbook-project add ` to add to the main project + +### Upgrading between versions + +See [docs/upgrade-guide](./upgrade-guide) + +### Releases + +* [Playbook Releases](https://github.com/powerhome/playbook/wiki/Playbook-Releases) + +### Development Environment + +* [Common Errors & Solutions](https://github.com/powerhome/playbook/wiki/Common-Errors-&-Solutions) + +### Building Playbook Kits + +* [Generating a Kit](https://github.com/powerhome/playbook/wiki/Generating-a-Kit) +* [Rails Kit](https://github.com/powerhome/playbook/wiki/Rails-Kit) +* [Rails Kit Helpers](https://github.com/powerhome/playbook/wiki/Rails-Kit-Helpers) +* [Using a Kit within a Kit](https://github.com/powerhome/playbook/wiki/Using-a-Kit-within-a-Kit) +* [Understanding Rails Kit HTML Wrapper](https://github.com/powerhome/playbook/wiki/Understanding-Rails-Kit-HTML-Wrapper) +* [Kit Stylesheet](https://github.com/powerhome/playbook/wiki/Kit-Stylesheet) + +### Testing Playbook Kits Locally + +#### Testing React Kits locally + +1. From inside the `playbook-ui` directory, run `yarn link`; +1. From Inside the project you want to test with `playbook-ui`, run `yarn link playbook-ui`; +1. Rebuild the project now using this version of `playbook-ui`; +1. Test all the things! +1. When finished, from inside the project you were testing with `playbook-ui`, run `yarn unlink playbook-ui`; +1. From Inside the `playbook-ui` directory, run `yarn unlink`; + +#### Jest & React-Testing-Library for Writing Tests + +We are currently backfilling test cases for React kit test coverage using Jest and React Testing Library. More additions and enhancements +to the testing libraries are currently in the works. In the meantime, please take a look at these resources: + +- https://github.com/testing-library/jest-dom#usage for usage and examples +- https://jestjs.io/docs/en/using-matchers + +When a new kit is generated, a placeholder React kit test will also be created. You can run all the tests with `yarn test`. + +### Important Note + +Keep in mind: Styles are brought in from playbook through the rails gem, so you will not be able to test scss updates with yarn linking. + +The gem & npm package is available as open source under the terms of the [ISC License](https://opensource.org/licenses/ISC). diff --git a/playbook/app/entrypoints/playbook-doc.js b/playbook/app/entrypoints/playbook-doc.js index 91fc773807..c4491a7449 100755 --- a/playbook/app/entrypoints/playbook-doc.js +++ b/playbook/app/entrypoints/playbook-doc.js @@ -59,6 +59,7 @@ import * as Layout from 'kits/pb_layout/docs' import * as LegendDocs from 'kits/pb_legend/docs' import * as Lightbox from 'kits/pb_lightbox/docs' import * as LineGraphDocs from 'kits/pb_line_graph/docs' +import * as Link from 'kits/pb_link/docs' import * as List from 'kits/pb_list/docs' import * as LoadingInline from 'kits/pb_loading_inline/docs' import * as Map from 'kits/pb_map/docs' @@ -167,6 +168,7 @@ WebpackerReact.registerComponents({ ...LegendDocs, ...Lightbox, ...LineGraphDocs, + ...Link, ...List, ...LoadingInline, ...Map, diff --git a/playbook/app/entrypoints/playbook.scss b/playbook/app/entrypoints/playbook.scss index eed254252d..8ebaa9cf11 100755 --- a/playbook/app/entrypoints/playbook.scss +++ b/playbook/app/entrypoints/playbook.scss @@ -30,6 +30,7 @@ @import 'kits/pb_dialog/dialog'; @import 'kits/pb_distribution_bar/distribution_bar'; @import 'kits/pb_draggable/draggable'; +@import 'kits/pb_drawer/drawer'; @import 'kits/pb_dropdown/dropdown'; @import 'kits/pb_file_upload/file_upload'; @import 'kits/pb_filter/filter'; @@ -54,6 +55,7 @@ @import 'kits/pb_legend/legend'; @import 'kits/pb_lightbox/lightbox'; @import 'kits/pb_line_graph/line_graph'; +@import 'kits/pb_link/link'; @import 'kits/pb_list/list'; @import 'kits/pb_loading_inline/loading_inline'; @import 'kits/pb_map/map'; @@ -106,7 +108,6 @@ @import 'kits/pb_user_badge/user_badge'; @import 'kits/pb_walkthrough/walkthrough'; @import 'kits/pb_weekday_stacked/weekday_stacked'; -@import 'kits/pb_drawer/drawer'; @import 'utilities/mixins'; @import 'utilities/spacing'; @import 'utilities/cursor'; diff --git a/playbook/app/javascript/kits.js b/playbook/app/javascript/kits.js index 9651ffa73b..15de7bb5b1 100644 --- a/playbook/app/javascript/kits.js +++ b/playbook/app/javascript/kits.js @@ -59,6 +59,7 @@ export { default as Layout } from '../pb_kits/playbook/pb_layout/_layout' export { default as Legend } from '../pb_kits/playbook/pb_legend/_legend' export { default as Lightbox } from '../pb_kits/playbook/pb_lightbox/_lightbox' export { default as LineGraph } from '../pb_kits/playbook/pb_line_graph/_line_graph' +export { default as Link} from '../pb_kits/playbook/pb_link/_link' export { default as List } from '../pb_kits/playbook/pb_list/_list' export { default as ListItem } from '../pb_kits/playbook/pb_list/_list_item' export { default as LoadingInline } from '../pb_kits/playbook/pb_loading_inline/_loading_inline' diff --git a/playbook/app/pb_kits/playbook/pb_currency/_currency.tsx b/playbook/app/pb_kits/playbook/pb_currency/_currency.tsx index f0673c777b..c03f683541 100644 --- a/playbook/app/pb_kits/playbook/pb_currency/_currency.tsx +++ b/playbook/app/pb_kits/playbook/pb_currency/_currency.tsx @@ -26,6 +26,7 @@ type CurrencyProps = { variant?: 'default' | 'light' | 'bold', unit?: string, unstyled?: boolean, + commaSeparator?: boolean, } const sizes: {lg: 1, md: 3, sm: 4} = { @@ -53,6 +54,7 @@ const Currency = (props: CurrencyProps): React.ReactElement => { variant = 'default', dark = false, unstyled = false, + commaSeparator = false, } = props const emphasizedClass = emphasized ? '' : '_deemphasized' @@ -74,7 +76,7 @@ const Currency = (props: CurrencyProps): React.ReactElement => { className ) - const getFormattedNumber = (input: number | any ) => new Intl.NumberFormat('en-US', { + const getFormattedNumber = (input: number | any) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1, }).format(input) @@ -88,12 +90,20 @@ const Currency = (props: CurrencyProps): React.ReactElement => { return isAmount ? num.slice(0, -1) : isUnit ? num.slice(-1) : '' } - const getMatchingDecimalAmount = decimals === "matching" ? amount : whole, - getMatchingDecimalValue = decimals === "matching" ? '' : `.${decimal}` + const getMatchingDecimalAmount = decimals === "matching" ? amount : whole + const getMatchingDecimalValue = decimals === "matching" ? '' : `.${decimal}` - const getAmount = abbreviate ? getAbbreviatedValue('amount') : getMatchingDecimalAmount, - getAbbreviation = abbreviate ? getAbbreviatedValue('unit') : null, - getDecimalValue = abbreviate ? '' : getMatchingDecimalValue + const formatAmount = (amount: string) => { + if (!commaSeparator) return amount; + + const [wholePart, decimalPart] = amount.split('.'); + const formattedWhole = new Intl.NumberFormat('en-US').format(parseInt(wholePart)); + return decimalPart ? `${formattedWhole}.${decimalPart}` : formattedWhole; + } + + const getAmount = abbreviate ? getAbbreviatedValue('amount') : formatAmount(getMatchingDecimalAmount) + const getAbbreviation = abbreviate ? getAbbreviatedValue('unit') : null + const getDecimalValue = abbreviate ? '' : getMatchingDecimalValue return (
{ expect(currencyKit.querySelector('.pb_currency_value')).toHaveTextContent('320') expect(currencyKit.querySelector('.unit')).toHaveTextContent('.20') }) + + +test('commaSeparator prop returns comma separated amount', () => { + render( + + ) + expect(screen.getByTestId('comma-test')).toHaveTextContent('1,234,567,890') +}) + +test('commaSeparator prop returns comma separated amount with decimals', () => { + render( + + ) + expect(screen.getByTestId('comma-test-decimals')).toHaveTextContent('1,234,567,890.12') +}) + +test('commaSeparator prop returns comma separated amount with decimals="matching"', () => { + render( + + ) + expect(screen.getByTestId('comma-test-decimals-matching')).toHaveTextContent('1,234,567,890.12') +}) diff --git a/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.html.erb b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.html.erb new file mode 100644 index 0000000000..48f0548013 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.html.erb @@ -0,0 +1,7 @@ +<%= pb_rails("currency", props: { + amount: '1234567.89', + comma_separator: true, + size: 'lg', + emphasized: false, + decimals: 'matching', +}) %> diff --git a/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.jsx b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.jsx new file mode 100644 index 0000000000..851cf52a10 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.jsx @@ -0,0 +1,18 @@ +import React from "react" + +import Currency from "../_currency" + +const CurrencyCommaSeparator = (props) => { + return ( + + ) +} + +export default CurrencyCommaSeparator diff --git a/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.md b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.md new file mode 100644 index 0000000000..b2c926c110 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_currency/docs/_currency_comma_separator.md @@ -0,0 +1,3 @@ +The optional `commaSeparator` can be used to auto-format the use of commas as a thousands separator. + +**NOTE:** If the value passed into the `amount` prop is already comma-dilineated, it will not add additional commas. diff --git a/playbook/app/pb_kits/playbook/pb_currency/docs/example.yml b/playbook/app/pb_kits/playbook/pb_currency/docs/example.yml index a8354bce15..8568c2a514 100644 --- a/playbook/app/pb_kits/playbook/pb_currency/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_currency/docs/example.yml @@ -8,7 +8,8 @@ examples: - currency_abbreviated: Abbreviate Larger Amounts - currency_matching_decimals: Matching Decimals - currency_unstyled: Unstyled - + - currency_comma_separator: Comma Separator + react: - currency_variants: Variants - currency_size: Size @@ -17,6 +18,7 @@ examples: - currency_abbreviated: Abbreviate Larger Amounts - currency_matching_decimals: Matching Decimals - currency_unstyled: Unstyled + - currency_comma_separator: Comma Separator swift: - currency_size_swift: Size diff --git a/playbook/app/pb_kits/playbook/pb_currency/docs/index.js b/playbook/app/pb_kits/playbook/pb_currency/docs/index.js index e0ab2b0b99..86ae571909 100644 --- a/playbook/app/pb_kits/playbook/pb_currency/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_currency/docs/index.js @@ -5,3 +5,4 @@ export { default as CurrencyNoSymbol } from './_currency_no_symbol.jsx' export { default as CurrencyAbbreviated } from './_currency_abbreviated.jsx' export { default as CurrencyMatchingDecimals } from './_currency_matching_decimals.jsx' export { default as CurrencyUnstyled } from './_currency_unstyled.jsx' +export { default as CurrencyCommaSeparator } from './_currency_comma_separator.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_link/_link.scss b/playbook/app/pb_kits/playbook/pb_link/_link.scss new file mode 100644 index 0000000000..bb1aec6e2e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/_link.scss @@ -0,0 +1,66 @@ +@import "../tokens/colors"; +@import "../tokens/line_height"; +@import "../tokens/typography"; +@import "../tokens/border_radius"; + +[class^=pb_link_kit]{ + @include pb_link($primary); + &:hover { + color: $text_lt_default; + } + &:focus { + outline: none; + } + &:focus-visible { + border-radius: $border_rad_light; + outline: 1px solid $primary; + outline-offset: 2px; + } + &:visited { + color: $data_3; + } + &.dark { + @include pb_link($active_dark); + &:hover { + color: $text_dk_default; + } + } + @each $color_name, $color_value in $pb_link_colors { + &[class*=_#{"" + $color_name}] { + @include pb_link($color_value); + + &:hover { + color: map-get($pb_link_hover_colors, $color_name); + } + + &:visited { + color: $data_3; + } + } + } + + @each $dark_color_name, $dark_color_value in $pb_dark_link_colors{ + &[class*=_#{$dark_color_name}][class*=dark]{ + @include pb_link($dark_color_value); + + &:hover { + color: map-get($pb_dark_link_hover_colors, $dark_color_name); + } + + &:visited { + color: $data_3; + } + } + } + + &[class*=_underline] { + text-decoration: underline; + } + + &[class*=_disabled] { + pointer-events: none; + cursor: default; + color: $text_lt_lighter; + } + +} diff --git a/playbook/app/pb_kits/playbook/pb_link/_link.tsx b/playbook/app/pb_kits/playbook/pb_link/_link.tsx new file mode 100644 index 0000000000..1ed1a3625e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/_link.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import classnames from 'classnames' + +import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props' +import { globalProps, GlobalProps } from '../utilities/globalProps' + +import Icon from '../pb_icon/_icon' + +type LinkProps = { + aria?: {[key: string]: string}, + className?: string, + children?: React.ReactChild[] | React.ReactChild, + color?: 'default' | 'body' | 'muted' | 'destructive', + dark?: boolean, + data?: {[key: string]: string}, + disabled?: boolean, + href?: string, + htmlOptions?: {[key: string]: string | number | boolean | (() => void) | ((arg?: Event) => void)}, + icon?: string, + iconRight?: string, + id?: string, + tag?: 'a' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div', + text?: string, + underline?: boolean, +} & GlobalProps + +const Link = (props: LinkProps): React.ReactElement => { + const { + aria = {}, + children, + className, + color = '', + data = {}, + disabled = false, + href= '', + htmlOptions = {}, + icon = '', + iconRight = '', + id = '', + tag = 'a', + text = '', + underline = false, + } = props + + const ariaProps: {[key: string]: string} = buildAriaProps(aria) + const dataProps: {[key: string]: string} = buildDataProps(data) + const htmlProps = buildHtmlProps(htmlOptions); + const classes = classnames( + buildCss('pb_link_kit', color, underline ? 'underline' : '', disabled ? 'disabled' : ''), + globalProps(props), + className + ) + const Tag = tag as keyof JSX.IntrinsicElements + + const renderContent = () => ( + <> + {icon && ( + + )} + {text || children} + {iconRight && ( + + )} + + ) + + const commonProps = { + ...ariaProps, + ...dataProps, + ...htmlProps, + className: classes, + id, + } + + if (tag === 'a') { + return ( + + {renderContent()} + + ) + } else { + return ( + + {renderContent()} + + ) + } + +} + +export default Link \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.html.erb b/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.html.erb new file mode 100644 index 0000000000..1d5d695dd8 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.html.erb @@ -0,0 +1,30 @@ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "https://www.google.com/search?q=playbook+design+system", + }) %> +
+ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "https://www.youtube.com/@PowerHRG", + color: "body", + }) %> +
+ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "https://github.com/powerhome/.github/blob/main/profile/README.md", + color: "muted", + }) %> +
+ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "https://rubygems.org/gems/playbook_ui/", + color: "destructive", + }) %> +
diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.jsx b/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.jsx new file mode 100644 index 0000000000..8d3108dd10 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_color.jsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Link } from 'playbook-ui' + +const LinkColor = (props) => ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+) + +export default LinkColor diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.html.erb b/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.html.erb new file mode 100644 index 0000000000..df60fec0b2 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.html.erb @@ -0,0 +1,5 @@ +<%= pb_rails("link", props: { + text: "link example", + href: "#disabled", + disabled: true, +}) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.jsx b/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.jsx new file mode 100644 index 0000000000..ed9a64d613 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_disabled.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Link } from 'playbook-ui' + +const LinkDisabled = (props) => ( +
+ +
+) + +export default LinkDisabled diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.html.erb b/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.html.erb new file mode 100644 index 0000000000..ac17688298 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.html.erb @@ -0,0 +1,15 @@ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "#icon", + icon: "arrow-up-right-from-square", + }) %> +
+ +
+ <%= pb_rails("link", props: { + text: "link example", + href: "#icon2", + icon_right: "chevron-right", + }) %> +
\ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.jsx b/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.jsx new file mode 100644 index 0000000000..07503f0890 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_icon.jsx @@ -0,0 +1,25 @@ +import React from 'react' +import { Link } from 'playbook-ui' + +const LinkIcon = (props) => ( +
+
+ +
+
+ +
+
+) + +export default LinkIcon diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.html.erb b/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.html.erb new file mode 100644 index 0000000000..a7a41266e9 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.html.erb @@ -0,0 +1,35 @@ +<%= pb_rails("link", props: { + text: "h1 link example", + href: "#tag", + icon: "arrow-up-right-from-square", + tag: "h1", +}) %> + +<%= pb_rails("link", props: { + text: "h3 link example", + href: "#tag2", + tag: "h3", + underline: true, +}) %> + +<%= pb_rails("link", props: { + color: "destructive", + text: "h6 link example", + href: "#tag3", + tag: "h6", +}) %> + +<%= pb_rails("link", props: { + text: "p link example", + href: "#tag4", + icon_right: "chevron-right", + tag: "p", +}) %> + +
+ This is a <%= pb_rails("link", props: { + text: "span link example", + href: "#tag5", + tag: "span", + }) %> +
\ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.jsx b/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.jsx new file mode 100644 index 0000000000..9d9d4ae710 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_tag.jsx @@ -0,0 +1,45 @@ +import React from 'react' +import { Link } from 'playbook-ui' + +const LinkTag = (props) => ( +
+ + + + +
+ This is a +
+
+) + +export default LinkTag diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.html.erb b/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.html.erb new file mode 100644 index 0000000000..854357a6f2 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.html.erb @@ -0,0 +1,5 @@ +<%= pb_rails("link", props: { + text: "link example", + href: "#underline", + underline: true, +}) %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.jsx b/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.jsx new file mode 100644 index 0000000000..8f509db388 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/_link_underline.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import { Link } from 'playbook-ui' + +const LinkUnderline = (props) => ( +
+ +
+) + +export default LinkUnderline diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/example.yml b/playbook/app/pb_kits/playbook/pb_link/docs/example.yml new file mode 100644 index 0000000000..ae862385d6 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/example.yml @@ -0,0 +1,16 @@ +examples: + + rails: + - link_color: Color + - link_underline: Underline + - link_icon: Icon + - link_disabled: Disabled + - link_tag: Tag + + + react: + - link_color: Color + - link_underline: Underline + - link_icon: Icon + - link_disabled: Disabled + - link_tag: Tag diff --git a/playbook/app/pb_kits/playbook/pb_link/docs/index.js b/playbook/app/pb_kits/playbook/pb_link/docs/index.js new file mode 100644 index 0000000000..6f0b6c138e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/docs/index.js @@ -0,0 +1,5 @@ +export { default as LinkColor } from './_link_color.jsx' +export { default as LinkUnderline } from './_link_underline.jsx' +export { default as LinkIcon } from './_link_icon.jsx' +export { default as LinkDisabled } from './_link_disabled.jsx' +export { default as LinkTag } from './_link_tag.jsx' \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_link/link.html.erb b/playbook/app/pb_kits/playbook/pb_link/link.html.erb new file mode 100644 index 0000000000..fcbab306e4 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/link.html.erb @@ -0,0 +1,21 @@ +<% link_content = proc do %> + <% if object.icon.present? %> + <%= pb_rails("icon", props: { icon: object.icon, fixed_width: true, size: "xs", margin_right: "xxs" }) %> + <% end %> + <%= object.content %> + <% if object.icon_right.present? %> + <%= pb_rails("icon", props: { icon: object.icon_right, fixed_width: true, size: "xs", margin_left: "xxs" }) %> + <% end %> +<% end %> + +<% if object.tag == "a" %> + <%= pb_content_tag(object.tag, { href: object.href }) do %> + <%= link_content.call %> + <% end %> +<% else %> + <%= pb_content_tag(:a, { href: object.href }) do %> + <%= content_tag(object.tag) do %> + <%= link_content.call %> + <% end %> + <% end %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_link/link.rb b/playbook/app/pb_kits/playbook/pb_link/link.rb new file mode 100644 index 0000000000..e1b3695633 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/link.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Playbook + module PbLink + class Link < ::Playbook::KitBase + prop :color, type: Playbook::Props::Enum, + values: %w[default body muted destructive], + default: "default" + prop :disabled, type: Playbook::Props::Boolean, + default: false + prop :href + prop :icon + prop :icon_right + prop :tag, type: Playbook::Props::Enum, + values: %w[a h1 h2 h3 h4 h5 h6 p span div], + default: "a" + prop :text + prop :underline, type: Playbook::Props::Boolean, + default: false + + def classname + generate_classname("pb_link_kit", color_class, underline_class, disabled_class) + end + + def content + text + end + + private + + def color_class + color == "default" ? nil : color + end + + def disabled_class + disabled ? "disabled" : nil + end + + def underline_class + underline ? "underline" : nil + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_link/link.test.jsx b/playbook/app/pb_kits/playbook/pb_link/link.test.jsx new file mode 100644 index 0000000000..7161767d7a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_link/link.test.jsx @@ -0,0 +1,92 @@ +import React from 'react' +import { ensureAccessible, renderKit, render, screen } from '../utilities/test-utils' + +import { Link } from 'playbook-ui' + +const link = 'https://www.google.com' + +const props = { + data: { testid: 'default' }, + href: link, +} + +test('returns namespaced class name', () => { + const kit = renderKit(Link , props) + expect(kit).toBeInTheDocument() + expect(kit).toHaveClass('pb_link_kit') + expect(kit).toHaveAttribute('href', link) +}) + +it("should be accessible", async () => { + ensureAccessible(Link, props) +}) + +test('with colors', () => { + ['default', 'body', 'muted', 'destructive'].forEach((color) => { + const testId = `colors-test-${color}` + render( + + ) + + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass(`pb_link_kit_${color}`) + }) +}) + +test('disable prop', () => { + render( + + ) + + const kit = screen.getByTestId('disable-test') + + expect(kit).toHaveClass('pb_link_kit_disabled') +}) + +test('underline prop', () => { + render( + + ) + + const kit = screen.getByTestId('underline-test') + + expect(kit).toHaveClass('pb_link_kit_underline') +}) + +test('adds icon', () => { + render( + + ) + + const kit = screen.getByTestId('icon-test') + + const icon = kit.querySelector('.pb_icon_kit') + expect(icon).toBeInTheDocument(); +}) + +test('adds icon right', () => { + render( + + ) + + const kit = screen.getByTestId('icon-right-test') + + const icon = kit.querySelector('.pb_icon_kit') + expect(icon).toBeInTheDocument(); +}) diff --git a/playbook/app/pb_kits/playbook/tokens/_typography.scss b/playbook/app/pb_kits/playbook/tokens/_typography.scss index 25c4cbbc97..5bf975e92e 100755 --- a/playbook/app/pb_kits/playbook/tokens/_typography.scss +++ b/playbook/app/pb_kits/playbook/tokens/_typography.scss @@ -1,3 +1,5 @@ +@import "../tokens/colors"; + $font_family_base: "Power Centra", "Helvetica Neue", Helvetica, Arial, sans_serif !default; /* CLEAN UP AND REMOVE */ @@ -51,3 +53,36 @@ $boldest: 700 !default; $bolder: 700 !default; $light: 300 !default; $lighter: 300 !default; + +/* Link Colors */ +$pb_link_colors: ( + default: $primary_action, + body: $text_lt_default, + muted: $text_lt_light, + destructive: $error, +); + +$pb_link_hover_colors: ( + default: $text_lt_default, + body: $primary_action, + muted: $text_lt_default, + destructive: $text_lt_default, +); + +$pb_dark_link_colors: ( + default: $active_dark, + body: $text_dk_default, + muted: $text_dk_light, + destructive: $error_dark, +); + +$pb_dark_link_hover_colors: ( + default: $text_dk_default, + body: $active_dark, + muted: $text_dk_default, + destructive: $text_dk_default, +); + +@mixin pb_link($color: $primary_action) { + color: $color; +} \ No newline at end of file diff --git a/playbook/spec/pb_kits/playbook/kits/link_spec.rb b/playbook/spec/pb_kits/playbook/kits/link_spec.rb new file mode 100644 index 0000000000..11a694fc13 --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/link_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_link/link" + +RSpec.describe Playbook::PbLink::Link do + subject { Playbook::PbLink::Link } + + it { + is_expected.to define_enum_prop(:color) + .with_default("default") + .with_values("default", "body", "muted", "destructive") + } + it { is_expected.to define_boolean_prop(:disabled).with_default(false) } + it { is_expected.to define_prop(:href) } + it { is_expected.to define_prop(:icon) } + it { is_expected.to define_prop(:icon_right) } + it { + is_expected.to define_enum_prop(:tag) + .with_default("a") + .with_values("a", "h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "div") + } + it { is_expected.to define_prop(:text) } + it { is_expected.to define_boolean_prop(:underline).with_default(false) } + + describe "#classname" do + it "returns namespaced class name", :aggregate_failures do + expect(subject.new({}).classname).to eq "pb_link_kit" + expect(subject.new(classname: "additional_class").classname).to eq "pb_link_kit additional_class" + expect(subject.new(dark: true).classname).to eq "pb_link_kit dark" + expect(subject.new(color: "destructive").classname).to eq "pb_link_kit_destructive" + expect(subject.new(underline: true).classname).to eq "pb_link_kit_underline" + expect(subject.new(disabled: true).classname).to eq "pb_link_kit_disabled" + end + end + + describe "#href" do + it "adds href attribute", :aggregate_failures do + instance = subject.new(href: "google.com") + expect(instance.href).to eq("google.com") + expect(instance).to have_attributes(href: "google.com") + end + end +end diff --git a/playbook/spec/pb_kits/playbook/pb_currency/currency_spec.rb b/playbook/spec/pb_kits/playbook/pb_currency/currency_spec.rb index 6a5d8bf3e6..f69e4a8449 100644 --- a/playbook/spec/pb_kits/playbook/pb_currency/currency_spec.rb +++ b/playbook/spec/pb_kits/playbook/pb_currency/currency_spec.rb @@ -15,6 +15,7 @@ it { is_expected.to define_enum_prop(:variant).with_default("default").with_values("default", "light", "bold") } it { is_expected.to define_prop(:abbreviate).with_default(false).of_type(Playbook::Props::Boolean) } it { is_expected.to define_enum_prop(:decimals).with_default("default").with_values("default", "matching") } + it { is_expected.to define_prop(:comma_separator).with_default(false).of_type(Playbook::Props::Boolean) } describe "#classname" do it "returns namespaced class name", :aggregate_failures do @@ -75,4 +76,12 @@ expect(num.body_props[:text]).to eq ".20" end end + + describe "when prop commaSeparator is set to true" do + it "returns comma separated amount" do + num = subject.new(comma_separator: true, amount: "1234567890") + + expect(num.title_props[:text]).to eq "1,234,567,890" + end + end end