Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,891 changes: 2,076 additions & 1,815 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreipol/vue-ui",
"version": "1.0.1",
"version": "1.1.1-beta.1",
"description": "",
"main": "index.js",
"jsnext:main": "src/index.js",
Expand Down Expand Up @@ -63,8 +63,8 @@
"webpack-cli": "^3.1.2"
},
"dependencies": {
"bianco": "^1.1.0",
"headroom.js": "^0.9.4",
"bianco.viewport": "0.0.5",
"lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0",
"lodash.isnil": "^4.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ The following list contains all vue components that could be directly imported i
- [overlay](/src/components/overlay)
- [modal](/src/components/modal)
- [scroll-reveal](/src/components/scroll-reveal)
- [accordion](/src/components/accordion)


40 changes: 40 additions & 0 deletions src/components/accordion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# UiAccordion
Accordion component that could be used in a SPA or even as simple enhancement for your DOM components

## Usage
```js
import { UiAccordion } from '@dreipol/vue-ui/src/components';
```

## Props
| Name | Type | Default | Description
| --- | --- | ---| ---|
|`isOpen` |Boolean| false | It will work only when also the `isPassive` property is enabled. It triggers the accordion animation
|`isPassive` |Boolean| false | It allows you to control the accordion from the outside

## Events
- `change(isOpen: boolean)` callback called when the accordion starts the animation
- `changed(isOpen: boolean)` callback called when the accordion has finished the animation
- `request-change(isOpen: boolean)` the request change will be triggered before the accordion will change its internal state and only if has the `isPassive` mode set to false

## Example
```vue
<accordion>
<h1 v-slot:head>
Click Me
</h1>
<p v-slot:body>
Accordion body
</p>
</accordion>

<accordion>
<h1 v-slot:head>
Click Me
</h1>
<div v-slot:body slot-scope="props">
<google-maps v-if="props.isVisible"></google-maps>
</div>
</accordion>
```

176 changes: 176 additions & 0 deletions src/components/accordion/accordion.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/* eslint-disable max-lines-per-function */

import UiAccordion from './accordion.vue';
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';


describe('Accordion spec', () => {
const css = `.ui-accordion .ui-accordion--head {
display: block;
width: 100%;
text-align: left;
cursor: pointer;
}

.ui-accordion .ui-accordion--body {
overflow: hidden;
max-height: 0;
opacity: 0;
transition: opacity 300ms ease;
}

.ui-accordion.ui-accordion__is-open .ui-accordion--body {
opacity: 1;
}

.ui-accordion.ui-accordion__is-animating .ui-accordion--body {
transition: opacity 300ms ease, max-height 300ms ease;
}
`;
const styleNode = document.createElement('style');

before(() => {
styleNode.innerHTML = css;
document.head.appendChild(styleNode);
});

after(() => {
document.head.removeChild(styleNode);
});

it('The ui-accordion is an object', () => {
expect(UiAccordion).to.be.an('object');
expect(UiAccordion).to.be.not.empty;
});

it('It can be properly opened when the isPassive mode is disabled', () => {
const wrapper = shallowMount(UiAccordion, {
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p>Body</p>',
},
});

const button = wrapper.find('button');
button.trigger('click');

expect(wrapper.vm.state.isOpen).to.be.ok;
});

it('It can be properly opened when the isPassive mode is enabled', () => {
const wrapper = shallowMount(UiAccordion, {
propsData: {
isPassive: true,
isOpen: false,
},
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p>Body</p>',
},
});


wrapper.setProps({ isOpen: true });
console.log(wrapper.vm.state.isOpen);
expect(wrapper.vm.state.isOpen).to.be.ok;
});

it('Dom Events do not change the component state in the passive mode', () => {
const wrapper = shallowMount(UiAccordion, {
propsData: {
isPassive: true,
isOpen: false,
},
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p>Body</p>',
},
});

const button = wrapper.find('button');
button.trigger('click');
expect(wrapper.vm.state.isOpen).to.be.not.ok;
});

it('The component change event will be dispatched', (done) => {
const wrapper = shallowMount(UiAccordion, {
propsData: {
isPassive: true,
isOpen: false,
},
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p>Body</p>',
},
listeners: {
change() {
expect(wrapper.vm.state.isOpen).to.be.ok;
done();
},
},
});

wrapper.vm.$nextTick(() => {
wrapper.setProps({ isOpen: true });
});
});

it('The component changed event will be properly dispatched', (done) => {
const wrapper = shallowMount(UiAccordion, {
propsData: {
isPassive: true,
isOpen: false,
},
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p>Body</p>',
},
listeners: {
changed(isOpen) {
if (isOpen) {
expect(wrapper.vm.state.isOpen).to.be.equal(true);
wrapper.setProps({ isOpen: false });
} else {
expect(wrapper.vm.state.isOpen).to.be.equal(false);
// wait the closing animation to consider this test done
done();
}
},
},
attachToDocument: true,
});

wrapper.vm.$nextTick(() => {
wrapper.setProps({ isOpen: true });
});
});

it('The body slot will receive the isVisible property', (done) => {
const wrapper = shallowMount(UiAccordion, {
propsData: {
isPassive: true,
isOpen: false,
},
scopedSlots: {
head: '<h1 class="label">Head</h1>',
body: '<p slot-scope="props">{{ props.isVisible }}</p>',
},
listeners: {
change() {
// it will be true only when the transition will be complete
expect(wrapper.find('p').html()).to.be.equal('<p>false</p>');
},
changed() {
expect(wrapper.find('p').html()).to.be.equal('<p>true</p>');
done();
},
},
attachToDocument: true,
});

wrapper.vm.$nextTick(() => {
wrapper.setProps({ isOpen: true });
});
});
});
117 changes: 117 additions & 0 deletions src/components/accordion/accordion.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<template>
<div class="ui-accordion" :class="rootClasses">
<button type="button"
class="u-reset ui-accordion--head"
v-if="!!$scopedSlots.head"
:aria-expanded="state.isOpen ? 'true' : 'false'"
@click.prevent="onRequestChange(!state.isOpen)">
<slot name="head" :is-open="state.isOpen"/>
</button>
<div class="ui-accordion--body"
ref="body"
@transitionend="onTransitionEnd">
<slot name="body" :is-open="state.isOpen" :is-visible="isVisible"/>
</div>
</div>
</template>

<script>
import { forceReflow } from 'bianco';
import isMountedMixin from '../../mixins/is-mounted';
import bemMixin from '../../mixins/bem';

const TRANSITION_END_KEY_PROP = 'max-height';

export default {
mixins: [
isMountedMixin,
bemMixin('ui-accordion'),
],
props: {
isOpen: {
type: Boolean,
default: false,
},
isPassive: {
type: Boolean,
default: false,
},
},
data() {
return {
state: {
isAnimating: false,
isOpen: this.isOpen,
},
};
},
computed: {
isVisible() {
const wasOpened = this.state.isOpen && !this.state.isAnimating;
const isClosing = !this.state.isOpen && this.state.isAnimating;

return isClosing || wasOpened;
},
rootClasses() {
return [
this.bemFacets,
this.bemIf(this.state.isAnimating, 'is-animating'),
this.bemIf(this.isMounted && this.state.isOpen, 'is-open'),
];
},
},

methods: {
change(val) {
this.state.isOpen = val;
this.state.isAnimating = true;
this.$nextTick(this.startAnimation);
},
startAnimation() {
const { body } = this.$refs;

body.style.maxHeight = `${ body.scrollHeight }px`;

// NOTE: Make the accordion animations smooth on any browser
if (!this.state.isOpen) {
forceReflow(body);
body.style.maxHeight = '0px';
}

this.$emit('change', this.state.isOpen);
},
onRequestChange(isOpen) {
this.$emit('request-change', isOpen);
},
onTransitionEnd(event) {
const isTargetElement = event.target === this.$refs.body;
const isTargetProperty = event.propertyName === TRANSITION_END_KEY_PROP;

if (!isTargetElement || !isTargetProperty) {
return;
}

this.finishAnimation();
this.$emit('changed', this.state.isOpen);
},
finishAnimation() {
this.state.isAnimating = false;

if (this.state.isOpen) {
this.$refs.body.style.maxHeight = 'inherit';
}
},
},
mounted() {
// NOTE: The accordion handles its state by itself when `isPassive` is false
if (this.isPassive) {
this.$watch('isOpen', this.change);
} else {
this.$on('request-change', this.change);
}

// NOTE: If it's opened by default we init the component already opened
this.finishAnimation();
},
};
</script>
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as UiOverlay } from './overlay/overlay.vue';
export { default as UiScrollReveal } from './scroll-reveal/scroll-reveal.vue';
export { default as UiIntersectionObserver } from './intersection-observer/intersection-observer.vue';
export { default as UiIcon } from './icon/icon.vue';
export { default as UiAccordion } from './accordion/accordion.vue';

// form components
export { default as UiInput } from './form/input/input.vue';
Expand Down
2 changes: 1 addition & 1 deletion src/components/intersection-observer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ A simple implementation of the [`intersectionObserverMixin`](/src/mixins/interse

## Usage
```js
import { UiIntersectionObserver } from '@dreipol/vue-ui';
import { UiIntersectionObserver } from '@dreipol/vue-ui/src/components';
```

## Props
Expand Down
2 changes: 1 addition & 1 deletion src/mixins/intersection-observer/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# intersectionObserverMixin
[IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
Load via `import { interactionObserverMixin } from '@dreipol/vue-ui';` and add it to the `mixins`.
Load via `import { interactionObserverMixin } from '@dreipol/vue-ui/src/components';` and add it to the `mixins`.

Now you can start observing an element:

Expand Down
2 changes: 1 addition & 1 deletion src/vuex/modules/scroll/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { documentHeight, scrollbarWidth, scrollTop } from 'bianco.viewport';
import { documentHeight, scrollbarWidth, scrollTop } from 'bianco';
import clamp from 'lodash.clamp';
import { DISABLE_SCROLL, SET_SCROLL } from '../mutation-types';
import * as actions from './actions';
Expand Down