-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1464 from davidnixon/feat-inline-loading
feat: port inline loading to vue 3
- Loading branch information
Showing
5 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
src/components/CvInlineLoading/CvInlineLoading.stories.mdx
This file contains 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,87 @@ | ||
import { Canvas, Meta, Story } from '@storybook/addon-docs'; | ||
import CvInlineLoading from './CvInlineLoading.vue'; | ||
import { sbCompPrefix } from '../../global/storybook-utils'; | ||
|
||
<Meta title={`${sbCompPrefix}/CvInlineLoading`} component={CvInlineLoading} /> | ||
|
||
export const Template = args => ({ | ||
// Components used in your story `template` are defined in the `components` object | ||
components: { | ||
CvInlineLoading, | ||
}, | ||
// The story's `args` need to be mapped into the template through the `setup()` method | ||
setup() { | ||
return { | ||
description: args.description, | ||
errorText: args.errorText, | ||
endingText: args.endingText, | ||
loadingText: args.loadingText, | ||
loadedText: args.loadedText, | ||
state: args.state, | ||
}; | ||
}, | ||
template: args.template, | ||
}); | ||
const defaultTemplate = ` | ||
<cv-inline-loading | ||
:description="description" | ||
:ending-text="endingText" | ||
:error-text="errorText" | ||
:loading-text="loadingText" | ||
:loaded-text="loadedText" | ||
:state="state" /> | ||
`; | ||
|
||
# CvInlineLoading | ||
|
||
Migration notes: | ||
|
||
- The `active` property is still available but does not actually work. Please use the `state` property instead. | ||
- The state has two new values to make it easier to transition form `loading` to either `loaded` or `error`. | ||
Setting `state` to "ending:loaded" or "ending:error" will first set the `ending` state and then, when | ||
the ending animation completes, the `loaded` or `error` state. | ||
- The states can be imported like `import { STATES } from "@/components/CvInlineLoading";` and used | ||
as: | ||
- `STATES.LOADING` | ||
- `STATES.ENDING` | ||
- `STATES.LOADED` | ||
- `STATES.ERROR` | ||
- `STATES.ENDING_LOADED` | ||
- `STATES.ENDING_ERROR` | ||
|
||
<Canvas> | ||
<Story | ||
name="Default" | ||
parameters={{ | ||
controls: { | ||
exclude: ['template'], | ||
}, | ||
docs: { source: { code: defaultTemplate } }, | ||
}} | ||
args={{ | ||
template: defaultTemplate, | ||
endingText: 'Jumping to warp 9', | ||
description: 'Warp engine status', | ||
errorText: 'Warp drive is damaged', | ||
loadingText: 'Warp drive coming online...', | ||
loadedText: 'Warp drive engaged', | ||
state: 'loading', | ||
}} | ||
argTypes={{ | ||
state: { | ||
control: 'select', | ||
default: 'loading', | ||
options: [ | ||
'loading', | ||
'ending', | ||
'loaded', | ||
'error', | ||
'ending:loaded', | ||
'ending:error', | ||
], | ||
}, | ||
}} | ||
> | ||
{Template.bind({})} | ||
</Story> | ||
</Canvas> |
This file contains 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,144 @@ | ||
<template> | ||
<div | ||
data-inline-loading | ||
:class="`${carbonPrefix}--inline-loading`" | ||
role="alert" | ||
aria-live="assertive" | ||
> | ||
<div | ||
:class="[ | ||
`${carbonPrefix}--inline-loading__animation`, | ||
{ [`${carbonPrefix}--loading--stop`]: internalState === STATES.ENDING }, | ||
]" | ||
@animationend="onLoadingEnd" | ||
> | ||
<div | ||
v-show="internalActive" | ||
:class="`${carbonPrefix}--loading ${carbonPrefix}--loading--small`" | ||
> | ||
<cv-loading | ||
:class="`${carbonPrefix}--inline-loading__animation`" | ||
:description="description" | ||
:active="undefined" | ||
:small="true" | ||
/> | ||
</div> | ||
<checkmark-filled16 | ||
v-if="internalState === STATES.LOADED" | ||
:class="`${carbonPrefix}--inline-loading__checkmark-container`" | ||
/> | ||
<error-filled16 | ||
v-if="internalState === STATES.ERROR" | ||
:class="`${carbonPrefix}--inline-loading--error`" | ||
/> | ||
</div> | ||
<p :class="`${carbonPrefix}--inline-loading__text`">{{ stateText }}</p> | ||
</div> | ||
</template> | ||
|
||
<script setup> | ||
import { STATES } from './consts'; | ||
import ErrorFilled16 from '@carbon/icons-vue/lib/error--filled/16'; | ||
import CheckmarkFilled16 from '@carbon/icons-vue/lib/checkmark--filled/16'; | ||
import { carbonPrefix } from '../../global/settings'; | ||
import CvLoading from '../CvLoading/CvLoading.vue'; | ||
import { computed, ref, watch } from 'vue'; | ||
const props = defineProps({ | ||
/** | ||
* Deprecated: Please use state property | ||
* @deprecated | ||
*/ | ||
active: { | ||
type: Boolean, | ||
default: undefined, | ||
deprecated: true, | ||
validator: val => { | ||
if (val !== undefined && process.env.NODE_ENV === 'development') { | ||
console.warn( | ||
'CvInlineLoading: active prop deprecated in favour of state prop' | ||
); | ||
} | ||
return true; | ||
}, | ||
}, | ||
/** | ||
* Specify the description for the inline loading text | ||
*/ | ||
description: { type: String, default: 'Loading' }, | ||
/** | ||
* Specify the text to show while the loading is ending (state: 'ending') | ||
*/ | ||
endingText: { type: String, default: 'Load ending...' }, | ||
/** | ||
* Specify the text to show for the error state (state: 'error') | ||
*/ | ||
errorText: { type: String, default: 'Loading data failed.' }, | ||
/** | ||
* Specify the text to show while loading (state: 'loading') | ||
*/ | ||
loadingText: { type: String, default: 'Loading data...' }, | ||
/** | ||
* Specify the text to show while loading (state: 'loaded') | ||
*/ | ||
loadedText: { type: String, default: 'Data loaded.' }, | ||
/** | ||
* Specify the loading status | ||
* @values ['loading','ending','loaded','error'] | ||
*/ | ||
state: { | ||
type: String, | ||
default: STATES.LOADING, | ||
required: true, | ||
validator: val => { | ||
if (Object.values(STATES).includes(val)) { | ||
return true; | ||
} else { | ||
console.error( | ||
`CvInlineLoading: Valid states are ${Object.values(STATES)}` | ||
); | ||
return false; | ||
} | ||
}, | ||
}, | ||
}); | ||
const internalState = ref(stateFromProps()); | ||
function stateFromProps() { | ||
if (props.state.includes(':')) { | ||
return props.state.split(':')[0]; | ||
} else { | ||
return props.state; | ||
} | ||
} | ||
watch( | ||
() => props.state, | ||
() => { | ||
internalState.value = stateFromProps(); | ||
} | ||
); | ||
function onLoadingEnd(ev) { | ||
if (ev.animationName === 'rotate-end-p2' && props.state.includes(':')) { | ||
internalState.value = props.state.split(':')[1]; | ||
} | ||
} | ||
const internalActive = computed(() => { | ||
if (props.active !== undefined) { | ||
return props.active; | ||
} else { | ||
return [STATES.LOADING, STATES.ENDING].includes(internalState.value); | ||
} | ||
}); | ||
const stateText = computed(() => { | ||
switch (internalState.value) { | ||
case STATES.LOADED: | ||
return props.loadedText; | ||
case STATES.ERROR: | ||
return props.errorText; | ||
case STATES.ENDING: | ||
return props.endingText; | ||
default: | ||
return props.loadingText; | ||
} | ||
}); | ||
</script> |
71 changes: 71 additions & 0 deletions
71
src/components/CvInlineLoading/__tests__/CvInlineLoading.spec.js
This file contains 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,71 @@ | ||
import { fireEvent, render } from '@testing-library/vue'; | ||
import CvInlineLoading from '../CvInlineLoading.vue'; | ||
import { STATES } from '../consts'; | ||
|
||
describe('CvInlineLoading', () => { | ||
it('CvInlineLoading - test all props', async () => { | ||
const ariaLabel = 'ABC-aria-label-123'; | ||
const endingText = 'ABC endingText 123'; | ||
const description = 'ABC description 123'; | ||
const errorText = 'ABC errorText 123'; | ||
const loadingText = 'ABC loadingText 123'; | ||
const loadedText = 'ABC loadedText 123'; | ||
|
||
// The render method returns a collection of utilities to query your component. | ||
const result = render(CvInlineLoading, { | ||
props: { | ||
state: STATES.LOADING, | ||
endingText, | ||
description, | ||
errorText, | ||
loadingText, | ||
loadedText, | ||
}, | ||
attrs: { | ||
class: 'ABC-class-123', | ||
'aria-label': ariaLabel, | ||
}, | ||
}); | ||
|
||
const loader = await result.findByRole('alert'); | ||
|
||
expect(loader.classList.contains('ABC-class-123')).toBe(true); | ||
expect(loader.getAttribute('aria-label')).toBe(ariaLabel); | ||
|
||
await result.findByText(loadingText); | ||
await result.findByTitle(description); | ||
|
||
await result.rerender({ state: STATES.ENDING }); | ||
await result.findByText(endingText); | ||
|
||
await result.rerender({ state: STATES.LOADED }); | ||
await result.findByText(loadedText); | ||
|
||
await result.rerender({ state: STATES.ERROR }); | ||
await result.findByText(errorText); | ||
|
||
// For the 2 special states we need to transition based on the | ||
// animation ending | ||
const animation = result.container.querySelector( | ||
'.bx--inline-loading__animation' | ||
); | ||
const animationend = new Event('animationend'); | ||
Object.assign(animationend, { animationName: 'rotate-end-p2' }); | ||
|
||
// Make sure it transitions from ending to loaded | ||
await result.rerender({ state: STATES.LOADING }); | ||
await result.findByText(loadingText); | ||
await result.rerender({ state: STATES.ENDING_LOADED }); | ||
await result.findByText(endingText); | ||
await fireEvent(animation, animationend); | ||
await result.findByText(loadedText); | ||
|
||
// Make sure it transitions from ending to error | ||
await result.rerender({ state: STATES.LOADING }); | ||
await result.findByText(loadingText); | ||
await result.rerender({ state: STATES.ENDING_ERROR }); | ||
await result.findByText(endingText); | ||
await fireEvent(animation, animationend); | ||
await result.findByText(errorText); | ||
}); | ||
}); |
This file contains 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,9 @@ | ||
const STATES = { | ||
LOADED: 'loaded', | ||
ERROR: 'error', | ||
LOADING: 'loading', | ||
ENDING: 'ending', | ||
ENDING_LOADED: 'ending:loaded', | ||
ENDING_ERROR: 'ending:error', | ||
}; | ||
export { STATES }; |
This file contains 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,4 @@ | ||
import CvInlineLoading from './CvInlineLoading.vue'; | ||
import { STATES } from './consts'; | ||
|
||
export { CvInlineLoading, STATES }; |