List Component MVP (#119)

* added empty files

* barest of bones

* density works

* density and nav

* hover kinda works for links, still need to ccheck click events and also die

* styling kinda working now

* readme and testing

* small tweaks

* put back whitespace

* actually fixed

* Add stylelint prettier to yarn lock

* Register list / list item globally

* Let names be inferred through file

* Match object structure in props

* Cleanup readme

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Jacob Rienstra
2020-02-24 18:17:53 -05:00
committed by GitHub
parent 139ced06f5
commit a6d17706e2
14 changed files with 672 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
{
"htmlWhitespaceSensitivity": "ignore",
"printWidth": 100,
"singleQuote": true
"htmlWhitespaceSensitivity": "ignore",
"printWidth": 100,
"singleQuote": true,
"useTabs": true
}

View File

@@ -1,7 +1,8 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order"
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"plugins": [
"stylelint-order",

View File

@@ -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",

View File

@@ -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);

View File

@@ -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;

View File

View File

@@ -0,0 +1,25 @@
<template functional>
<div class="v-list-item-content">
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.v-list-item-content {
--v-list-item-content-padding: 12px 0;
display: flex;
flex-grow: 1;
flex-shrink: 1;
flex-wrap: wrap;
align-items: center;
align-self: center;
padding: var(--v-list-item-content-padding);
overflow: hidden;
.v-list.dense &,
.v-list-item.dense & {
--v-list-item-content-padding: 8px 0;
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<component
:is="component"
active-class="active"
class="v-list-item"
:to="to"
:class="{ dense, link: isClickable }"
v-on="$listeners"
>
<slot></slot>
</component>
</template>
<script lang="ts">
import { Location } from 'vue-router';
import { createComponent, PropType, computed } from '@vue/composition-api';
export default createComponent({
props: {
dense: {
type: Boolean,
default: false
},
to: {
type: [String, Object] as PropType<string | Location>,
default: null
}
},
setup(props, { listeners }) {
const component = computed<string>(() => (props.to ? 'router-link' : 'li'));
const isClickable = computed(() => Boolean(props.to || listeners.click !== undefined));
return { component, isClickable };
}
});
</script>
<style lang="scss" scoped>
.v-list-item {
--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)
);
position: relative;
display: flex;
flex-basis: 100%;
flex-grow: 1;
flex-shrink: 1;
align-items: center;
min-width: var(--v-list-item-min-width);
max-width: var(--v-list-item-max-width);
min-height: var(--v-list-item-min-height);
max-height: var(--v-list-item-max-height);
margin-bottom: var(--v-list-item-margin-bottom);
padding: var(--v-list-item-padding);
overflow: hidden;
color: var(--v-list-item-color);
text-decoration: none;
background-color: var(--v-list-item-background-color);
border-radius: var(--v-list-item-border-radius);
&.link {
cursor: pointer;
transition: var(--fast) var(--transition);
transition-property: background-color, color;
user-select: none;
&:hover {
color: var(--v-list-item-color-hover);
background-color: var(--v-list-item-background-color-hover);
}
&:active,
&.active {
color: var(--v-list-item-color-active);
background-color: var(--v-list-item-background-color-active);
}
}
.v-list.dense &,
&.dense {
--v-list-item-min-height: 40px;
}
.v-list.nav & {
--v-list-item-padding: 0 8px;
--v-list-item-border-radius: 4px;
&:not(:last-child):not(:only-child) {
--v-list-item-margin-bottom: 8px;
}
.v-list.dense &,
&.dense {
&:not(:last-child):not(:only-child) {
--v-list-item-margin-bottom: 4px;
}
}
}
}
</style>

View File

@@ -0,0 +1,171 @@
# List
```html
<v-list>
<v-list-item v-for="item in items">
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
```
## Colors
You can set the default, active, and hover colors and background colors with css variables:
```html
<v-list>
<v-list-item v-for="item in items">
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
<style>
.v-list {
--v-list-color: var(--red);
--v-list-color-hover: var(--white);
--v-list-color-active: var(--white);
--v-list-background-color: var(--red-50);
--v-list-background-color-hover: var(--red-100);
--v-list-background-color-active: var(--red-800);
}
</style>
```
## 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
<v-list-item v-for="item in items">
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
```
## 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
<v-list-item class="item-red">
Red Stuff
</v-list-item>
<v-list-item >
Normal stuff
</v-list-item>
<style>
.item-red {
--v-list-item-color: var(--red);
--v-list-item-color-hover: var(--white);
--v-list-item-color-active: var(--white);
--v-list-item-background-color: var(--red-50);
--v-list-item-background-color-hover: var(--red-100);
--v-list-item-background-color-active: var(--red-800);
}
</style>
```
## 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
<v-list-item>
<v-list-item-content>
List test blah blah
</v-list-item-content>
</v-list-item>
```
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` |

View File

@@ -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: `
<v-sheet style="--v-sheet-max-width: 200px; ">
<v-list :dense="dense" :nav="nav">
<v-list-item v-for="(item, i) in items" :dense="$props['dense' + i]" :key="i">
<v-list-item-content>
{{ item }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-sheet>`
});
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: `
<v-sheet style="--v-sheet-max-width: 200px; ">
<v-list :dense="dense" :nav="nav">
<v-list-item v-for="(item, i) in items" :dense="$props['dense' + i]" :key="i" :to="'/' + i">
<v-list-item-content>
{{ item }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-sheet>`
});
export const withClicks = () => ({
props: {},
data() {
return {
items: ['Item 0', 'Item 1', 'Item 2', 'Item 3'],
clickHandler: action('onClick')
};
},
template: `
<v-sheet style="--v-sheet-max-width: 200px; ">
<v-list>
<v-list-item v-for="(item, i) in items" :key="i" @click="clickHandler">
<v-list-item-content>
{{ item }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-sheet>`
});
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: `
<v-sheet style="--v-sheet-max-width: 200px; ">
<v-list-item v-for="(item, i) in items" :dense="$props['dense' + i]" :key="i" :to="'/' + i">
<v-list-item-content>
{{item}}
</v-list-item-content>
</v-list-item>
</v-sheet>`
});

View File

@@ -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: `<v-list-item>Item Text</v-list-item>`
}
});
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: `<v-list-item/>
<v-list-item/>
<v-list-item/>`
}
});
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: `<v-list-item dense/>
<v-list-item/>`
}
});
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();
});
});

View File

@@ -0,0 +1,56 @@
<template>
<ul class="v-list" :class="{ dense, nav }">
<slot></slot>
</ul>
</template>
<script lang="ts">
import { createComponent } from '@vue/composition-api';
export default createComponent({
props: {
dense: {
type: Boolean,
default: false
},
nav: {
type: Boolean,
default: false
}
},
setup() {
return {};
}
});
</script>
<style lang="scss" scoped>
.v-list {
--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);
width: 100%;
min-width: var(--v-list-min-width);
max-width: var(--v-list-max-width);
min-height: var(--v-list-min-height);
max-height: var(--v-list-max-height);
padding: var(--v-list-padding);
overflow: auto;
color: var(--v-list-color);
background-color: var(--v-list-background-color);
border-radius: var(--input-border-radius);
&.nav {
--v-list-padding: 8px;
}
}
</style>

View File

@@ -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);

View File

@@ -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"