Skip to content

Commit

Permalink
Merge pull request #1464 from davidnixon/feat-inline-loading
Browse files Browse the repository at this point in the history
feat: port inline loading to vue 3
  • Loading branch information
davidnixon authored Jun 27, 2023
2 parents b428c30 + 7c4cd8f commit 1a02db8
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 0 deletions.
87 changes: 87 additions & 0 deletions src/components/CvInlineLoading/CvInlineLoading.stories.mdx
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>
144 changes: 144 additions & 0 deletions src/components/CvInlineLoading/CvInlineLoading.vue
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 src/components/CvInlineLoading/__tests__/CvInlineLoading.spec.js
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);
});
});
9 changes: 9 additions & 0 deletions src/components/CvInlineLoading/consts.js
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 };
4 changes: 4 additions & 0 deletions src/components/CvInlineLoading/index.js
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 };

0 comments on commit 1a02db8

Please sign in to comment.