-
Notifications
You must be signed in to change notification settings - Fork 0
[accordion] Add the accordion component #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
7373344
[accordion] Add the accordion component
GianlucaGuarini cd0d0d8
[accordion] Update documentation
GianlucaGuarini 5480cbc
[accordion] Add link to the accordion documentation
GianlucaGuarini 752570d
[accordion] Bump a beta minor release
GianlucaGuarini ba858be
[accordion] Remove accessibility attributes and make test a bit more …
GianlucaGuarini fba50f6
[accordion] Get rid of the unecessary computed
GianlucaGuarini 7af6fad
Fix future linting error
9d07fdc
Merge branch 'feature/accordion' of github.com:dreipol/vue-ui into fe…
6a84354
[accordion] Use only one bianco dependency
GianlucaGuarini 74e2380
[accordion] Merge branch 'feature/accordion' of github.com:dreipol/vu…
GianlucaGuarini f55537c
[accordion] Fix vue eslint error
GianlucaGuarini 9b40e1f
[accordion] Add the accordion to the exported components
GianlucaGuarini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
}); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.