diff --git a/.prettierrc b/.prettierrc index 74395bcd14..2fba1be483 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { - "htmlWhitespaceSensitivity": "ignore", - "printWidth": 100, - "singleQuote": true + "htmlWhitespaceSensitivity": "ignore", + "printWidth": 100, + "singleQuote": true, + "useTabs": true } diff --git a/.stylelintrc.json b/.stylelintrc.json index 1243a55e62..5c2be637d1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,7 +1,8 @@ { "extends": [ "stylelint-config-standard", - "stylelint-config-rational-order" + "stylelint-config-rational-order", + "stylelint-config-prettier" ], "plugins": [ "stylelint-order", diff --git a/package.json b/package.json index 2b3d300b2d..ffe00efcaa 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "lodash": "^4.17.15", "nanoid": "^2.1.11", "pinia": "0.0.5", + "stylelint-config-prettier": "^8.0.1", "vue": "^2.6.11", "vue-i18n": "^8.15.3", "vue-router": "^3.1.5", diff --git a/src/components/register.ts b/src/components/register.ts index fcf97839fa..40e53ae759 100644 --- a/src/components/register.ts +++ b/src/components/register.ts @@ -7,6 +7,7 @@ import VChip from './v-chip/'; import VHover from './v-hover/'; import VIcon from './v-icon/'; import VInput from './v-input/'; +import VList, { VListItem, VListItemContent } from './v-list/'; import VOverlay from './v-overlay/'; import VProgressLinear from './v-progress/linear/'; import VProgressCircular from './v-progress/circular/'; @@ -22,6 +23,9 @@ Vue.component('v-chip', VChip); Vue.component('v-hover', VHover); Vue.component('v-icon', VIcon); Vue.component('v-input', VInput); +Vue.component('v-list', VList); +Vue.component('v-list-item', VListItem); +Vue.component('v-list-item-content', VListItemContent); Vue.component('v-overlay', VOverlay); Vue.component('v-progress-linear', VProgressLinear); Vue.component('v-progress-circular', VProgressCircular); diff --git a/src/components/v-list/index.ts b/src/components/v-list/index.ts new file mode 100644 index 0000000000..864452fd36 --- /dev/null +++ b/src/components/v-list/index.ts @@ -0,0 +1,6 @@ +import VList from './v-list.vue'; +import VListItem from './v-list-item.vue'; +import VListItemContent from './v-list-item-content.vue'; + +export { VList, VListItem, VListItemContent }; +export default VList; diff --git a/src/components/v-list/v-list-group.vue b/src/components/v-list/v-list-group.vue new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/v-list/v-list-item-content.vue b/src/components/v-list/v-list-item-content.vue new file mode 100644 index 0000000000..adcf81fbca --- /dev/null +++ b/src/components/v-list/v-list-item-content.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/v-list/v-list-item.vue b/src/components/v-list/v-list-item.vue new file mode 100644 index 0000000000..132bffb00f --- /dev/null +++ b/src/components/v-list/v-list-item.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/src/components/v-list/v-list.readme.md b/src/components/v-list/v-list.readme.md new file mode 100644 index 0000000000..4626a284d2 --- /dev/null +++ b/src/components/v-list/v-list.readme.md @@ -0,0 +1,171 @@ +# List + +```html + + + + {{ item.text }} + + + +``` + +## Colors + +You can set the default, active, and hover colors and background colors with css variables: + +```html + + + + {{ item.text }} + + + + + +``` + +## Props +| Prop | Description | Default | +|---------|-------------------------------------------------------------|---------| +| `dense` | Removes some padding to make the list items closer together | `false` | +| `nav` | Adds a small margin and border-radius for nav menu styling | `false` | + +## Slots +| Slot | Description | +|-----------|------------------| +| _default_ | List items, etc. | + +## Events +| Event | Description | Value | +|---------|-----------------------|--------------| +| `click` | User clicks on button | `MouseEvent` | + +## CSS Variables +| Variable | Default | +|------------------------------------|----------------------------| +| `--v-list-padding` | `8px 0` | +| `--v-list-max-height` | `none` | +| `--v-list-max-width` | `none` | +| `--v-list-min-width` | `none` | +| `--v-list-min-height` | `none` | +| `--v-list-color` | `var(--foreground-color)` | +| `--v-list-color-hover` | `var(--foreground-color)` | +| `--v-list-color-active` | `var(--foreground-color)` | +| `--v-list-background-color` | `var(--background-color)` | +| `--v-list-background-color-hover` | `var(--hover-background)` | +| `--v-list-background-color-active` | `var(--active-background)` | + +--- + +# List Item + +A wrapper for list items that formats things nicely. Can be used on its own or inside a list component. Best used with subcomponents (see below). + +```html + + + {{ item.text }} + + +``` + +## Colors + +You can set the default, active, and hover colors and background colors on individual list items with css variables. These will override the global list css vars, which you can set as well. + +Hover styles will only be set if the list item has a to link or a onClick handler. + +```html + + Red Stuff + + + Normal stuff + + + +``` + +## Props +| Prop | Description | Default | +|---------|---------------------------------------------------------------|---------| +| `dense` | Removes some padding to make the individual list item shorter | `false` | +| `to` | Render as vue router-link with to link | `null` | + +## Slots +| Slot | Description | +|-----------|---------------------------| +| _default_ | List content, icons, etc. | + +## Events +| Event | Description | Value | +|---------|---------------------|--------------| +| `click` | User clicks on link | `MouseEvent` | + +## CSS Variables +Second values are fallback ones, in case the list item is not inside a list where those vars are set. +| Variable | Default | +|-----------------------------------------|-----------------------------------------------------------------| +| `--v-list-item-padding` | `0 16px` | +| `--v-list-item-min-width` | `none` | +| `--v-list-item-max-width` | `none` | +| `--v-list-item-min-height` | `48px` | +| `--v-list-item-max-height` | `auto` | +| `--v-list-item-border-radius` | `0` | +| `--v-list-item-margin-bottom` | `0` | +| `--v-list-item-color` | `var(--v-list-color, var(--foreground-color))` | +| `--v-list-item-color-hover` | `var(--v-list-color-hover, var(--foreground-color))` | +| `--v-list-item-color-active` | `var(--v-list-color-active, var(--foreground-color))` | +| `--v-list-item-background-color` | `var(--v-list-background-color, var(--background-color))` | +| `--v-list-item-background-color-hover` | `var(---list-background-color-hover, var(--hover-background))` | +| `--v-list-item-background-color-active` | `var(--vlist-background-color-active,var(--active-background))` | + +--- + +# List Item Content + +```html + + + List test blah blah + + +``` + +This is simply a wrapper for the main text content of a list item. It adds some padding and helps control overflow. + +## Props +n/a + +## Slots +| Slot | Description | +|-----------|---------------------------| +| _default_ | List content, icons, etc. | + +## Events +n/a + +## CSS Variables +| Variable | Default | +|---------------------------------|----------| +| `--v-list-item-content-padding` | `12px 0` | diff --git a/src/components/v-list/v-list.story.ts b/src/components/v-list/v-list.story.ts new file mode 100644 index 0000000000..02b4ee8efb --- /dev/null +++ b/src/components/v-list/v-list.story.ts @@ -0,0 +1,151 @@ +import { withKnobs, boolean } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; +import Vue from 'vue'; +import VList from './v-list.vue'; +import VListItem from './v-list-item.vue'; +import withPadding from '../../../.storybook/decorators/with-padding'; +import VListItemContent from './v-list-item-content.vue'; +import VSheet from '../v-sheet'; +import VueRouter from 'vue-router'; + +Vue.component('v-list', VList); +Vue.component('v-list-item', VListItem); +Vue.component('v-list-item-content', VListItemContent); +Vue.component('v-sheet', VSheet); + +Vue.use(VueRouter); +const router = new VueRouter(); + +export default { + title: 'Components / List', + component: VList, + decorators: [withKnobs, withPadding] +}; + +export const basic = () => ({ + props: { + dense: { + default: boolean('Dense', false, 'Full List') + }, + dense0: { + default: boolean('Dense', false, 'List Item 0') + }, + dense1: { + default: boolean('Dense', false, 'List Item 1') + }, + dense2: { + default: boolean('Dense', false, 'List Item 2') + }, + dense3: { + default: boolean('Dense', false, 'List Item 3') + }, + nav: { + default: boolean('Nav', false, 'Full List') + } + }, + data() { + return { + items: ['Item 0', 'Item 1', 'Item 2', 'Item 3'] + }; + }, + template: ` + + + + + {{ item }} + + + + ` +}); + +export const withLinks = () => ({ + router: router, + props: { + dense: { + default: boolean('Dense', false, 'Full List') + }, + dense0: { + default: boolean('Dense', false, 'List Item 0') + }, + dense1: { + default: boolean('Dense', false, 'List Item 1') + }, + dense2: { + default: boolean('Dense', false, 'List Item 2') + }, + dense3: { + default: boolean('Dense', false, 'List Item 3') + }, + nav: { + default: boolean('Nav', false, 'Full List') + } + }, + data() { + return { + items: ['Item 0', 'Item 1', 'Item 2', 'Item 3'] + }; + }, + template: ` + + + + + {{ item }} + + + + ` +}); + +export const withClicks = () => ({ + props: {}, + data() { + return { + items: ['Item 0', 'Item 1', 'Item 2', 'Item 3'], + clickHandler: action('onClick') + }; + }, + template: ` + + + + + {{ item }} + + + + ` +}); + +export const orphanListItems = () => ({ + router: router, + props: { + dense0: { + default: boolean('Dense', false, 'List Item 0') + }, + dense1: { + default: boolean('Dense', false, 'List Item 1') + }, + dense2: { + default: boolean('Dense', false, 'List Item 2') + }, + dense3: { + default: boolean('Dense', false, 'List Item 3') + } + }, + data() { + return { + items: ['Item 0', 'Item 1', 'Item 2', 'Item 3'] + }; + }, + template: ` + + + + {{item}} + + + ` +}); diff --git a/src/components/v-list/v-list.test.ts b/src/components/v-list/v-list.test.ts new file mode 100644 index 0000000000..166085cfed --- /dev/null +++ b/src/components/v-list/v-list.test.ts @@ -0,0 +1,129 @@ +import { mount, createLocalVue, shallowMount } from '@vue/test-utils'; +import VueCompositionAPI from '@vue/composition-api'; +import VueRouter from 'vue-router'; +import router from '@/router'; +import VList from './v-list.vue'; +import VListItem from './v-list-item.vue'; + +const localVue = createLocalVue(); +localVue.use(VueCompositionAPI); +localVue.use(VueRouter); +localVue.component('v-list-item', VListItem); +localVue.component('v-list', VList); + +describe('List', () => { + it('Renders the provided markup in the default slot', () => { + const component = mount(VList, { + localVue, + slots: { + default: `Item Text` + } + }); + + expect(component.text()).toContain('Item Text'); + }); + + it('Adds the dense class for dense lists', () => { + const component = mount(VList, { + localVue, + propsData: { + dense: true + } + }); + + expect(component.classes()).toContain('dense'); + }); + + it('Adds the nav class for nav lists', () => { + const component = mount(VList, { + localVue, + propsData: { + nav: true + } + }); + + expect(component.classes()).toContain('nav'); + }); + + it('Has the right number of list items', () => { + const component = mount(VList, { + localVue, + propsData: { + dense: false + }, + slots: { + default: ` + + ` + } + }); + + expect(component.findAll('.v-list-item').length).toEqual(3); + }); + + it('Adds the dense class to one list-item, but not the other', () => { + const component = mount(VList, { + localVue, + propsData: { + dense: false + }, + slots: { + default: ` + ` + } + }); + + expect(component.find('.v-list-item:first-of-type').classes()).toContain('dense'); + expect(component.find('.v-list-item:nth-of-type(2)').classes()).not.toContain('dense'); + }); + + it('Item has the link class when to prop is set', () => { + const component = mount(VListItem, { + localVue, + router: router, + propsData: { + to: '/' + } + }); + + expect(component.classes()).toContain('link'); + }); + + it('Renders as a router-link if the to prop is set', () => { + const component = mount(VListItem, { + localVue, + router: router, + propsData: { + to: '/' + } + }); + + expect((component.vm as any).component).toBe('router-link'); + }); + + it('Has link class when onClick is set', () => { + const onClick = jest.fn(); + + const component = mount(VListItem, { + localVue, + listeners: { + click: onClick + } + }); + + expect(component.classes()).toContain('link'); + }); + + it('Click event fires correctly', () => { + const onClick = jest.fn(); + const component = mount(VListItem, { + localVue, + listeners: { + click: onClick + } + }); + + component.find('.v-list-item').trigger('click'); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/v-list/v-list.vue b/src/components/v-list/v-list.vue new file mode 100644 index 0000000000..27b4eff6aa --- /dev/null +++ b/src/components/v-list/v-list.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/styles/themes/_default.scss b/src/styles/themes/_default.scss index 17b7186a38..9a4dd5f775 100644 --- a/src/styles/themes/_default.scss +++ b/src/styles/themes/_default.scss @@ -30,6 +30,8 @@ body { --foreground-color-tertiary: var(--blue-grey-100); --highlight: var(--off-white); + --hover-background: var(--blue-grey-50); + --active-background: var(--blue-grey-100); /* Inputs */ --input-foreground-color: var(--blue-grey-800); diff --git a/yarn.lock b/yarn.lock index 844a5a5106..9013eac127 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13385,6 +13385,11 @@ stylehacks@^4.0.0: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +stylelint-config-prettier@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-8.0.1.tgz#ec7cdd7faabaff52ebfa56c28fed3d995ebb8cab" + integrity sha512-RcjNW7MUaNVqONhJH4+rtlAE3ow/9SsAM0YWV0Lgu3dbTKdWTa/pQXRdFWgoHWpzUKn+9oBKR5x8JdH+20wmgw== + stylelint-config-rational-order@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/stylelint-config-rational-order/-/stylelint-config-rational-order-0.1.2.tgz#4e98e390783d437f0ec41fb73bc41992e78d02a0"