diff --git a/app/ids-counts/compact.html b/app/ids-counts/compact.html new file mode 100644 index 0000000000..aebf726ba8 --- /dev/null +++ b/app/ids-counts/compact.html @@ -0,0 +1,35 @@ + + Counts: Compact + + + + + 7 + Active
Opportunities
+
+
+ + + 2 + Open
Incidents
+
+
+ + + 4 + Escalated
Incidents
+
+
+ + + 7 + Open
Projects
+
+
+ + + 7 + Active
Contacts
+
+
+
diff --git a/app/ids-counts/example.html b/app/ids-counts/example.html new file mode 100644 index 0000000000..629ab5d60a --- /dev/null +++ b/app/ids-counts/example.html @@ -0,0 +1,36 @@ + + Counts + + + + + + 7 + Active
Opportunities
+
+
+ + + Open
Incidents
+ 2 +
+
+ + + Escalated
Incidents
+ 4 +
+
+ + + 7 + Open
Projects
+
+
+ + + 7 + Active
Contacts
+
+
+
diff --git a/app/ids-counts/index.html b/app/ids-counts/index.html new file mode 100644 index 0000000000..7f3e3457bb --- /dev/null +++ b/app/ids-counts/index.html @@ -0,0 +1,3 @@ +{{> ../layouts/head.html }} +{{> example.html }} +{{> ../layouts/footer.html }} diff --git a/app/ids-counts/index.js b/app/ids-counts/index.js new file mode 100644 index 0000000000..3c3dc61ee5 --- /dev/null +++ b/app/ids-counts/index.js @@ -0,0 +1 @@ +import IdsCounts from '../../src/ids-counts/ids-counts'; diff --git a/app/ids-counts/not-actionable.html b/app/ids-counts/not-actionable.html new file mode 100644 index 0000000000..e8702a5c06 --- /dev/null +++ b/app/ids-counts/not-actionable.html @@ -0,0 +1,35 @@ + + Counts: Not Actionable + + + + + Active
Opportunities
+ 7 +
+
+ + + Open
Incidents
+ 2 +
+
+ + + Escalated
Incidents
+ 4 +
+
+ + + Open
Projects
+ 7 +
+
+ + + Active
Contacts
+ 7 +
+
+
diff --git a/app/ids-counts/standalone-css.html b/app/ids-counts/standalone-css.html new file mode 100644 index 0000000000..81460a5922 --- /dev/null +++ b/app/ids-counts/standalone-css.html @@ -0,0 +1,18 @@ +{{> ../layouts/head-visible.html }} + + + + + +
+

Count Standalone CSS

+
+
+
+ +
7
+
Active
Opportunities
+
+
+
+{{> ../layouts/footer.html }} diff --git a/app/index.html b/app/index.html index c7d03d3f06..06b6d3ece7 100644 --- a/app/index.html +++ b/app/index.html @@ -27,4 +27,5 @@ {{> ids-accordion/example.html }} {{> ids-block-grid/example.html }} {{> ids-render-loop/example.html }} +{{> ids-counts/example.html }} {{> layouts/footer.html }} diff --git a/app/index.js b/app/index.js index 1564f04520..4c4aa851c2 100644 --- a/app/index.js +++ b/app/index.js @@ -28,6 +28,7 @@ import IdsAlert from '../src/ids-alert/ids-alert'; import IdsBadge from '../src/ids-badge/ids-badge'; import IdsBlockGrid from '../src/ids-block-grid/ids-block-grid'; import IdsBlockGridItem from '../src/ids-block-grid/ids-block-grid-item'; +import IdsCounts from '../src/ids-counts/ids-counts'; import IdsContainer from '../src/ids-container/ids-container'; import IdsThemeSwitcher from '../src/ids-theme-switcher/ids-theme-switcher'; import IdsWizard, { IdsWizardStep } from '../src/ids-wizard'; @@ -58,4 +59,5 @@ import './ids-alert/index'; import './ids-badge/index'; import './ids-textarea/example'; import './ids-block-grid/index'; +import './ids-counts/index'; import './ids-wizard/index'; diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 09d404c34f..c8d8dbecae 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -13,6 +13,9 @@ - Markup has changed to a custom element `` - If using events, events are now plain JS events. - Can now be imported as a single JS file and used with encapsulated styles +- `[Counts]` The counts component has been changed to a web component and renamed to ids-counts. + - Text is now contained in an ids-text element `` + - Can now be imported as a single JS file and used with encapsulated styles - `[Datagrid]` The Datagrid component has been changed to a web component `ids-data-grid`. - If using events events are now plain JS events for example: sorted, rendered - If using properties/settings these are now attributes or as plain properties for example: data, virtual-scroll diff --git a/src/ids-base/ids-constants.js b/src/ids-base/ids-constants.js index 4e5f08cddd..de3ce084f4 100644 --- a/src/ids-base/ids-constants.js +++ b/src/ids-base/ids-constants.js @@ -23,6 +23,7 @@ export const props = { COL_START: 'col-start', COLOR: 'color', COLS: 'cols', + COMPACT: 'compact', CLICKABLE: 'clickable', CSS_CLASS: 'css-class', DATA: 'data', diff --git a/src/ids-base/ids-mixins.scss b/src/ids-base/ids-mixins.scss index b5681ca2bd..819746610b 100644 --- a/src/ids-base/ids-mixins.scss +++ b/src/ids-base/ids-mixins.scss @@ -1,5 +1,5 @@ /** - * Mixins used throuhout the scss styles and come in as part of base. Keep Base small. + * Mixins used throughout the scss styles and come in as part of base. Keep Base small. */ // All vendor prefixes diff --git a/src/ids-container/ids-container.js b/src/ids-container/ids-container.js index dcf9a66424..ac3a4a4a21 100644 --- a/src/ids-container/ids-container.js +++ b/src/ids-container/ids-container.js @@ -52,7 +52,7 @@ class IdsContainer extends mix(IdsElement).with(IdsEventsMixin, IdsThemeMixin) { } /** - * If set to true the container is scollable + * If set to true the container is scrollable * @param {boolean|string} value true of false depending if the tag is scrollable */ set scrollable(value) { diff --git a/src/ids-counts/README.MD b/src/ids-counts/README.MD new file mode 100644 index 0000000000..4e45f61609 --- /dev/null +++ b/src/ids-counts/README.MD @@ -0,0 +1,105 @@ +# Ids Counts Component + +## Description + +Counts are distinctive elements used to highlight high level numbers or metrics. + +## Use Cases + +- Use counts in dashboards and visualizations for summarizing key numeric takeaways. +- Use counts as a concise numeric data point that can link to underlying detail elsewhere on the page or site. + +## Terminology + +- **Counts**: UI embellishments for summarizing high level numeric information. +- **Value**: The numeric value displayed on the count component. +- **Text**: The name or brief description of the value. +- **Compact**: When compact, the count value appears slightly smaller than usual. + +## Features (With Code Examples) + +A card is created using the custom `ids-counts` element. A user can place elements inside of the component to represent text. It is recommended to use ids-text components as ids-counts has functionality to manage that input specifically. + +A normal Counts component + +```html + + 7 + Active
Opportunities
+
+``` + +The same component could be made "Not Actionable" by removing the href attribute + +```html + + 7 + Active
Opportunities
+
+``` + +Setting the optional "Compact" attribute to "true" decreases the font size of the value + +```html + + 7 + Active
Opportunities
+
+``` + +The counts component also supports an optional "Color" attribute. The options for color are base (blue), caution, danger, success, warning, or a hex code with the "#" + +```html + + 7 + Active
Opportunities
+
+``` + +Counts using just the css. Use the anchor tag for normal counts and span for non-actionable. + +```html + +
7
+
Active
Opportunities
+
+ +
7
+
Active
Opportunities
+
+``` + +## Settings and Attributes + +- `color` {string} Sets the color to an internal color such as `azure`. Can also a hexidecimal color code beginning with `#`. +- `compact` {string} Use "true" to set the value font-size to 40. Omitting this attribute or using any will result in the default value of 48. +- `href` {string} The url that the count component links to. + +## States and Variations (With Code Examples) + +- Actionable +- Color +- Compact + +## Keyboard Guidelines + +- Tab/Shift+Tab: If the count is actionable (default) this will toggle through them in the general form order. Non-actionable counts do not get selected. +- Enter: This will follow the tag link. + +## Responsive Guidelines + +- Flows with padding and margin within the width and height of the parent container. + +## Converting from Previous Versions + +- 3.x: Counts have all new markup and classes. +- 4.x: Counts have all new markup and classes for web components. + +## Accessibility Guidelines + +- 1.4.1 Use of Color - Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. Ensure the color tags that indicate state like OK, cancel, ect have other ways to indicate that information. This is failing. +- 1.4.3 Contrast (Minimum) - The visual presentation of text and images of text has a contrast ratio of at least 4.5:1. Ensure the color tags pass contrast. + +## Regional Considerations + +Text should be localized in the current language. Consider that in some languages text may be longer (German) and in some cases it can't be wrapped (Thai). diff --git a/src/ids-counts/TODO.md b/src/ids-counts/TODO.md new file mode 100644 index 0000000000..b08d81a04d --- /dev/null +++ b/src/ids-counts/TODO.md @@ -0,0 +1,6 @@ +# TODO on IDS Coounts + +## Features + +- [x] Focus State for links +- [] Test accessibility with screen reader diff --git a/src/ids-counts/ids-counts.d.ts b/src/ids-counts/ids-counts.d.ts new file mode 100644 index 0000000000..ad8b266d2a --- /dev/null +++ b/src/ids-counts/ids-counts.d.ts @@ -0,0 +1,24 @@ +// Ids is a JavaScript project, but we define TypeScript declarations so we can +// confirm our code is type safe, and to support TypeScript users. + +import { IdsElement } from '../ids-base/ids-element'; + +export default class IdsCounts extends IdsElement { + /** Set the tag type/color */ + color: 'base' | 'caution' | 'danger' | 'success' | 'warning' | string; + + /** List the settable component properties */ + properties: string[]; + + /** Sets the value size at 32 when true (instead of 40) */ + compact?: 'true' | 'false' | boolean; + + /** Sets the href/link */ + href?: string; + + /** Set the theme mode */ + mode: 'light' | 'dark' | 'contrast' | string; + + /** Set the theme version */ + version: 'new' | 'classic' | string; +} diff --git a/src/ids-counts/ids-counts.js b/src/ids-counts/ids-counts.js new file mode 100644 index 0000000000..558a188942 --- /dev/null +++ b/src/ids-counts/ids-counts.js @@ -0,0 +1,106 @@ +import { + IdsElement, + customElement, + scss, + props, + mix, + stringUtils +} from '../ids-base/ids-element'; + +// Import Theme Mixin +import { IdsThemeMixin } from '../ids-base/ids-theme-mixin'; +import { IdsEventsMixin } from '../ids-base/ids-events-mixin'; + +// @ts-ignore +import IdsText from '../ids-text/ids-text'; +import IdsHyperlink from '../ids-hyperlink/ids-hyperlink'; + +// @ts-ignore +import styles from './ids-counts.scss'; + +/** + * IDS Counts Component + * @type {IdsCounts} + * @inherits IdsElement + * @mixes IdsEventsMixin + * @mixes IdsThemeMixin + * @part link - the link element + */ +@customElement('ids-counts') +@scss(styles) +class IdsCounts extends mix(IdsElement).with(IdsEventsMixin, IdsThemeMixin) { + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + this.#textProperties(); + } + + #textProperties() { + this.querySelectorAll('[count-value]').forEach((value) => { value.fontSize = stringUtils.stringToBool(this.compact) ? 40 : 48; }); + this.querySelectorAll('[count-text]').forEach((text) => { text.fontSize = 16; }); + } + + /** + * Return the properties we handle as getters/setters + * @returns {Array} The properties in an array + */ + static get properties() { + return [props.COLOR, props.COMPACT, props.HREF, props.MODE, props.VERSION]; + } + + /** + * Inner template contents + * @returns {string} The template + */ + template() { + return ` + ${this.href ? `` : ``} + + ${this.href ? `` : ``} + `; + } + + /** + * Set the color of the counts + * @param {string} value The color value. This can be omitted. + * base (blue), caution, danger, success, warning, or a hex code with the "#" + */ + set color(value) { + if (this.href) this.container.setAttribute('color', 'unset'); + const color = value[0] === '#' ? value : `var(--ids-color-status-${value})`; + this.container.style.color = color; + this.querySelectorAll('ids-text').forEach((node) => { + node.color = 'unset'; + node.shadowRoot.querySelector('span').style.color = value; + }); + this.setAttribute(props.COLOR, value); + } + + get color() { return this.getAttribute(props.COLOR); } + + /** + * Set the compact attribute + * @param {string | boolean} value true or false. Component will + * default to regular size if this property is ommitted. + */ + set compact(value) { + this.setAttribute(props.COMPACT, value === 'true' ? 'true' : 'false'); + } + + get compact() { return this.getAttribute(props.COMPACT); } + + /** + * Set the href attribute + * @param {string} value The href link + */ + set href(value) { + this.setAttribute(props.HREF, value); + } + + get href() { return this.getAttribute(props.HREF); } +} + +export { IdsCounts }; diff --git a/src/ids-counts/ids-counts.scss b/src/ids-counts/ids-counts.scss new file mode 100644 index 0000000000..ae32257551 --- /dev/null +++ b/src/ids-counts/ids-counts.scss @@ -0,0 +1,57 @@ +/* Ids Counts Component */ +@import '../ids-base/ids-base'; + +.ids-counts { + @include font-sans(); + @include antialiased(); + @include text-slate-80(); + + text-align: center; + + &::part(link) { + width: 100%; + } + + &[href] { + @include text-azure-60(); + } + + &[color] { + color: unset; + } +} + +.ids-counts div[count-value] { + @include text-16(); +} + +.ids-counts div[count-text] { + @include text-48(); +} + +a:focus { + border: 1px solid var(--ids-color-palette-azure-60); + box-shadow: 0 0 4px 3px rgb(54, 138, 192, 0.3); + outline: none; +} + +// Handle Themes +.ids-counts[mode='dark']:not([color]) { + @include text-slate-10(); +} + +.ids-counts[mode='contrast']:not([color]) { + @include text-slate-100(); +} + +.ids-counts[mode='light'][version='classic']:not([color]) { + @include text-graphite-60(); +} + +.ids-counts[mode='dark'][version='classic']:not([color]) { + @include text-classic-slate-10(); +} + +.ids-counts[mode='contrast'][version='classic']:not([color]) { + @include text-graphite-80(); +} diff --git a/src/ids-hyperlink/ids-hyperlink.d.ts b/src/ids-hyperlink/ids-hyperlink.d.ts index c8a5709de6..08632191a9 100644 --- a/src/ids-hyperlink/ids-hyperlink.d.ts +++ b/src/ids-hyperlink/ids-hyperlink.d.ts @@ -14,6 +14,9 @@ export default class IdsHyperlink extends HTMLElement { /** Set the theme mode */ mode: 'light' | 'dark' | 'contrast' | string; + /** Set the decoration style */ + textDecoration: string; + /** Set the theme version */ version: 'new' | 'classic' | string; } diff --git a/src/ids-hyperlink/ids-hyperlink.js b/src/ids-hyperlink/ids-hyperlink.js index 66b08f8181..93f2ac3c42 100644 --- a/src/ids-hyperlink/ids-hyperlink.js +++ b/src/ids-hyperlink/ids-hyperlink.js @@ -39,6 +39,7 @@ class IdsHyperlink extends mix(IdsElement).with(IdsEventsMixin, IdsThemeMixin) { */ static get properties() { return [ + props.COLOR, props.DISABLED, props.HREF, props.MODE, @@ -122,6 +123,25 @@ class IdsHyperlink extends mix(IdsElement).with(IdsEventsMixin, IdsThemeMixin) { } get disabled() { return this.getAttribute(props.DISABLED); } + + /** + * + * If set to "unset", color can be controlled by parent container + * @param {string | null} value "unset" or undefined/null + */ + set color(value) { + if (value === 'unset') { + this.setAttribute(props.COLOR, value); + this.container.classList.add('ids-hyperlink-color-unset'); + } else { + this.removeAttribute(props.COLOR); + this.container.classList.remove('ids-hyperlink-color-unset'); + } + } + + get color() { + return this.getAttribute(props.COLOR); + } } export default IdsHyperlink; diff --git a/src/ids-hyperlink/ids-hyperlink.scss b/src/ids-hyperlink/ids-hyperlink.scss index 1666effb35..45afd6992c 100644 --- a/src/ids-hyperlink/ids-hyperlink.scss +++ b/src/ids-hyperlink/ids-hyperlink.scss @@ -34,6 +34,10 @@ } } +.ids-hyperlink-color-unset { + color: unset !important; +} + // Handle Themes .ids-hyperlink[mode='dark'] { @include text-azure-40(); diff --git a/test/ids-counts/ids-counts-e2e-test.js b/test/ids-counts/ids-counts-e2e-test.js new file mode 100644 index 0000000000..958b65af41 --- /dev/null +++ b/test/ids-counts/ids-counts-e2e-test.js @@ -0,0 +1,17 @@ +describe('Ids Counts e2e Tests', () => { + const url = 'http://localhost:4444/ids-counts'; + + beforeAll(async () => { + await page.goto(url, { waitUntil: ['networkidle2', 'load'] }); + }); + + it('should not have errors', () => { + expect(page.title()).resolves.toMatch('IDS Counts Component'); + }); + + it('should pass Axe accessibility tests', async () => { + await page.setBypassCSP(true); + await page.goto(url, { waitUntil: ['networkidle2', 'load'] }); + await expect(page).toPassAxeTests({ disabledRules: ['color-contrast'] }); + }); +}); diff --git a/test/ids-counts/ids-counts-func-test.js b/test/ids-counts/ids-counts-func-test.js new file mode 100644 index 0000000000..8a913d6291 --- /dev/null +++ b/test/ids-counts/ids-counts-func-test.js @@ -0,0 +1,109 @@ +/** + * @jest-environment jsdom + */ +import { IdsCounts } from '../../src/ids-counts/ids-counts'; +import IdsText from '../../src/ids-text/ids-text'; + +const template = ` + + 7 + Active
Opportunities
+
`; + +const compact = ` + + 7 + Active
Opportunities
+
`; + +describe('IdsCounts Component', () => { + let count; + + beforeEach(async () => { + const elem = new IdsCounts(); + const countValue = new IdsText(); + const countText = new IdsText(); + countValue.setAttribute('count-value', ''); + countText.setAttribute('count-text', ''); + countValue.innerText = '7'; + countText.innerHTML = 'Active
Opportunities'; + elem.appendChild(countValue); + elem.appendChild(countText); + document.body.appendChild(elem); + count = document.querySelector('ids-counts'); + }); + + afterEach(async () => { + document.body.innerHTML = ''; + }); + + it('renders with no errors', () => { + const errors = jest.spyOn(global.console, 'error'); + const elem = new IdsCounts(); + document.body.appendChild(elem); + elem.remove(); + expect(document.querySelectorAll('ids-counts').length).toEqual(1); + expect(errors).not.toHaveBeenCalled(); + }); + + it('renders correctly', () => { + expect(count.outerHTML).toMatchSnapshot(); + }); + + it('renders a specific hex color', () => { + count.color = '#800000'; + expect(count.getAttribute('color')).toEqual('#800000'); + expect(count.color).toEqual('#800000'); + }); + + it('renders a status color', () => { + count.color = 'base'; + expect(count.getAttribute('color')).toEqual('base'); + expect(count.color).toEqual('base'); + }); + + it('unsets container color when href property is set', () => { + count.href = '#'; + count.color = 'success'; + expect(count.container.getAttribute('color')).toEqual('unset'); + + count.href = '#'; + count.color = 'danger'; + expect(count.container.getAttribute('color')).toEqual('unset'); + }); + + it('is able to change sizes via compact attribute', () => { + count.compact = 'true'; + expect(count.getAttribute('compact')).toEqual('true'); + expect(count.compact).toEqual('true'); + }); + + it('sets any compact to "false" for any input other than "true" ', () => { + count.compact = 'True'; + expect(count.getAttribute('compact')).toEqual('false'); + expect(count.compact).toEqual('false'); + count.compact = ''; + expect(count.getAttribute('compact')).toEqual('false'); + expect(count.compact).toEqual('false'); + }); + + it('is able to change link via href attribute', () => { + count.href = 'http://www.google.com'; + expect(count.getAttribute('href')).toEqual('http://www.google.com'); + expect(count.href).toEqual('http://www.google.com'); + }); + + it('creates an ids-hyperlink container', () => { + count.remove(); + document.body.insertAdjacentHTML('beforeend', template); + count = document.querySelector('ids-counts'); + expect(count.shadowRoot.querySelectorAll('ids-hyperlink').length).toEqual(1); + }); + + it('creates a compact counts component', () => { + count.remove(); + document.body.insertAdjacentHTML('beforeend', compact); + count = document.querySelector('ids-counts'); + expect(count.querySelector('[count-value]').fontSize).toEqual(40); + }); +}); diff --git a/test/ids-counts/ids-counts-func-test.js.snap b/test/ids-counts/ids-counts-func-test.js.snap new file mode 100644 index 0000000000..da7f2f0b4d --- /dev/null +++ b/test/ids-counts/ids-counts-func-test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IdsCounts Component renders correctly 1`] = `"Active
Opportunities
"`; diff --git a/test/ids-counts/ids-counts-percy-test.js b/test/ids-counts/ids-counts-percy-test.js new file mode 100644 index 0000000000..311ee15331 --- /dev/null +++ b/test/ids-counts/ids-counts-percy-test.js @@ -0,0 +1,26 @@ +import percySnapshot from '@percy/puppeteer'; + +describe('Ids Counts e2e Tests', () => { + const url = 'http://localhost:4444/ids-counts'; + + it('should not have visual regressions in new light theme (percy)', async () => { + await page.goto(url, { waitUntil: ['networkidle2', 'load'] }); + await percySnapshot(page, 'ids-counts-new-light'); + }); + + it('should not have visual regressions in new dark theme (percy)', async () => { + await page.goto(url, { waitUntil: ['networkidle2', 'load'] }); + await page.evaluate(() => { + document.querySelector('ids-theme-switcher').setAttribute('mode', 'dark'); + }); + await percySnapshot(page, 'ids-counts-new-dark'); + }); + + it('should not have visual regressions in new contrast theme (percy)', async () => { + await page.goto(url, { waitUntil: ['networkidle2', 'load'] }); + await page.evaluate(() => { + document.querySelector('ids-theme-switcher').setAttribute('mode', 'contrast'); + }); + await percySnapshot(page, 'ids-counts-new-contrast'); + }); +}); diff --git a/test/ids-hyperlink/ids-hyperlink-func-test.js b/test/ids-hyperlink/ids-hyperlink-func-test.js index dab72dc18d..2245b1f533 100644 --- a/test/ids-hyperlink/ids-hyperlink-func-test.js +++ b/test/ids-hyperlink/ids-hyperlink-func-test.js @@ -99,4 +99,16 @@ describe('IdsHyperlink Component', () => { elem.version = 'classic'; expect(elem.container.getAttribute('version')).toEqual('classic'); }); + + it('unsets the color', () => { + elem.color = 'unset'; + expect(elem.getAttribute('color')).toEqual('unset'); + expect(elem.color).toEqual('unset'); + }); + + it('does not render a color for inputs other than unset', () => { + elem.color = 'blue'; + expect(elem.getAttribute('color')).toEqual(null); + expect(elem.color).toEqual(null); + }); });