@weni/unnnic-system 3.28.2-alpha.0 → 3.28.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +162 -30
- package/dist/style.css +1 -1
- package/dist/unnnic.mjs +5821 -5804
- package/dist/unnnic.umd.js +25 -25
- package/package.json +1 -1
- package/src/components/Disclaimer/Disclaimer.vue +10 -8
- package/src/components/IconLoading/IconLoading.vue +6 -1
- package/src/components/ui/popover/PopoverContent.vue +9 -45
- package/src/components/ui/popover/PopoverFooter.vue +20 -1
- package/src/components/ui/popover/__tests__/PopoverFooter.spec.js +116 -0
- package/src/components/ui/popover/context.ts +4 -0
- package/src/stories/Disclaimer.stories.js +16 -0
- package/src/stories/IconLoading.stories.js +6 -0
package/package.json
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
:class="['unnnic-disclaimer', `unnnic-disclaimer--${type}`]"
|
|
4
4
|
data-testid="disclaimer"
|
|
5
5
|
>
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
<slot name="icon">
|
|
7
|
+
<UnnnicIcon
|
|
8
|
+
class="unnnic-disclaimer__icon"
|
|
9
|
+
size="ant"
|
|
10
|
+
:icon="variant.icon"
|
|
11
|
+
:scheme="variant.scheme"
|
|
12
|
+
data-testid="disclaimer-icon"
|
|
13
|
+
/>
|
|
14
|
+
</slot>
|
|
13
15
|
|
|
14
16
|
<section class="unnnic-disclaimer__content">
|
|
15
17
|
<p
|
|
@@ -57,7 +59,7 @@ const slots = useSlots();
|
|
|
57
59
|
const hasDescription = computed(() => {
|
|
58
60
|
return (
|
|
59
61
|
Boolean(props.description) ||
|
|
60
|
-
(slots.description && slots.description().length > 0)
|
|
62
|
+
(slots.description && slots.description({}).length > 0)
|
|
61
63
|
);
|
|
62
64
|
});
|
|
63
65
|
|
|
@@ -44,6 +44,10 @@ const props = defineProps({
|
|
|
44
44
|
type: String,
|
|
45
45
|
default: 'lg',
|
|
46
46
|
},
|
|
47
|
+
strokeWidth: {
|
|
48
|
+
type: Number,
|
|
49
|
+
default: 5,
|
|
50
|
+
},
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
defineOptions({ name: 'UnnnicIconLoading' });
|
|
@@ -79,6 +83,7 @@ const sizeInPixels = computed(() => SIZE_MAP[props.size] ?? props.size);
|
|
|
79
83
|
|
|
80
84
|
.unnnic-icon-loading {
|
|
81
85
|
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
82
87
|
user-select: none;
|
|
83
88
|
|
|
84
89
|
svg {
|
|
@@ -87,7 +92,7 @@ const sizeInPixels = computed(() => SIZE_MAP[props.size] ?? props.size);
|
|
|
87
92
|
circle {
|
|
88
93
|
fill: none;
|
|
89
94
|
stroke: currentColor;
|
|
90
|
-
stroke-width:
|
|
95
|
+
stroke-width: v-bind(strokeWidth);
|
|
91
96
|
stroke-linecap: round;
|
|
92
97
|
animation: unnnic-icon-loading-dash 1s ease-in-out infinite;
|
|
93
98
|
}
|
|
@@ -11,17 +11,12 @@
|
|
|
11
11
|
"
|
|
12
12
|
>
|
|
13
13
|
<section :class="`unnnic-popover__content ${props.class || ''}`">
|
|
14
|
-
<
|
|
15
|
-
:is="child"
|
|
16
|
-
v-for="(child, index) in contentChildren"
|
|
17
|
-
:key="index"
|
|
18
|
-
/>
|
|
14
|
+
<slot />
|
|
19
15
|
</section>
|
|
20
16
|
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
:key="index"
|
|
17
|
+
<div
|
|
18
|
+
ref="footerContainer"
|
|
19
|
+
data-testid="popover-footer-container"
|
|
25
20
|
/>
|
|
26
21
|
</PopoverContent>
|
|
27
22
|
</PopoverPortal>
|
|
@@ -29,13 +24,14 @@
|
|
|
29
24
|
|
|
30
25
|
<script setup lang="ts">
|
|
31
26
|
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui';
|
|
32
|
-
import type { HTMLAttributes
|
|
33
|
-
import { computed,
|
|
27
|
+
import type { HTMLAttributes } from 'vue';
|
|
28
|
+
import { computed, provide, ref } from 'vue';
|
|
34
29
|
import { reactiveOmit } from '@vueuse/core';
|
|
35
30
|
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'reka-ui';
|
|
36
31
|
import { cn } from '@/lib/utils';
|
|
37
32
|
import { useLayerZIndex } from '@/lib/layer-manager';
|
|
38
33
|
import { useTeleportTarget } from '@/lib/teleport-target';
|
|
34
|
+
import { POPOVER_FOOTER_TARGET } from './context';
|
|
39
35
|
|
|
40
36
|
defineOptions({
|
|
41
37
|
inheritAttrs: false,
|
|
@@ -63,43 +59,11 @@ const delegatedProps = reactiveOmit(props, 'class', 'size');
|
|
|
63
59
|
|
|
64
60
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
65
61
|
|
|
66
|
-
const slots = useSlots() as Slots;
|
|
67
|
-
|
|
68
62
|
const popoverZIndex = useLayerZIndex();
|
|
69
63
|
const portalTarget = useTeleportTarget();
|
|
70
64
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
return componentType?.name || componentType?.__name;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const isFooter = (vnode: VNode) =>
|
|
77
|
-
getComponentName(vnode) === 'UnnnicPopoverFooter';
|
|
78
|
-
|
|
79
|
-
// This function recursively checks if there is a UnnnicPopoverFooter in the slot,
|
|
80
|
-
// and splits the content into content and footer
|
|
81
|
-
const splitSlot = computed(() => {
|
|
82
|
-
const footers: VNode[] = [];
|
|
83
|
-
|
|
84
|
-
const content = (function extract(vnodes: VNode[]): VNode[] {
|
|
85
|
-
return vnodes.filter((vnode: VNode) => {
|
|
86
|
-
if (!vnode || typeof vnode !== 'object') return true;
|
|
87
|
-
if (isFooter(vnode)) return footers.push(vnode) && false;
|
|
88
|
-
|
|
89
|
-
if (Array.isArray(vnode.children) && vnode.children.length) {
|
|
90
|
-
vnode.children = extract(vnode.children as VNode[]);
|
|
91
|
-
return (vnode.children as VNode[]).length > 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
});
|
|
96
|
-
})(slots.default?.() || []);
|
|
97
|
-
|
|
98
|
-
return { content, footers };
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const contentChildren = computed(() => splitSlot.value.content);
|
|
102
|
-
const footerChildren = computed(() => splitSlot.value.footers);
|
|
65
|
+
const footerContainer = ref<HTMLElement | null>(null);
|
|
66
|
+
provide(POPOVER_FOOTER_TARGET, footerContainer);
|
|
103
67
|
|
|
104
68
|
const contentWidth = computed(() => {
|
|
105
69
|
if (props.width) return props.width;
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<Teleport
|
|
3
|
+
v-if="target"
|
|
4
|
+
:to="target"
|
|
5
|
+
>
|
|
6
|
+
<footer class="unnnic-popover__footer">
|
|
7
|
+
<slot />
|
|
8
|
+
</footer>
|
|
9
|
+
</Teleport>
|
|
10
|
+
|
|
11
|
+
<footer
|
|
12
|
+
v-else
|
|
13
|
+
class="unnnic-popover__footer"
|
|
14
|
+
>
|
|
3
15
|
<slot />
|
|
4
16
|
</footer>
|
|
5
17
|
</template>
|
|
6
18
|
|
|
7
19
|
<script setup lang="ts">
|
|
20
|
+
import { inject } from 'vue';
|
|
21
|
+
import { POPOVER_FOOTER_TARGET } from './context';
|
|
22
|
+
|
|
8
23
|
defineOptions({
|
|
9
24
|
name: 'UnnnicPopoverFooter',
|
|
10
25
|
});
|
|
26
|
+
|
|
27
|
+
// When rendered inside a PopoverContent, teleport into its footer container.
|
|
28
|
+
// Falls back to inline rendering when used standalone (no target provided).
|
|
29
|
+
const target = inject(POPOVER_FOOTER_TARGET, null);
|
|
11
30
|
</script>
|
|
12
31
|
|
|
13
32
|
<style lang="scss">
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import PopoverContent from '../PopoverContent.vue';
|
|
4
|
+
import PopoverFooter from '../PopoverFooter.vue';
|
|
5
|
+
|
|
6
|
+
// Render reka-ui's portal/content inline so the component's own markup
|
|
7
|
+
// (`.unnnic-popover__content` + footer container) is testable. The native
|
|
8
|
+
// Teleport is kept real so we can assert the footer actually moves.
|
|
9
|
+
const inlineSlot = { template: '<div><slot /></div>' };
|
|
10
|
+
|
|
11
|
+
const globalStubs = {
|
|
12
|
+
PopoverPortal: inlineSlot,
|
|
13
|
+
PopoverContent: inlineSlot,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// A child component that renders the footer from its own template, simulating
|
|
17
|
+
// a footer nested inside a consumer component rather than a direct slot child.
|
|
18
|
+
const NestedFooterChild = {
|
|
19
|
+
components: { PopoverFooter },
|
|
20
|
+
template: `
|
|
21
|
+
<section data-testid="nested-wrapper">
|
|
22
|
+
<PopoverFooter>
|
|
23
|
+
<button data-testid="nested-footer-btn">Save</button>
|
|
24
|
+
</PopoverFooter>
|
|
25
|
+
</section>
|
|
26
|
+
`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const mountPopover = (defaultSlot) =>
|
|
30
|
+
mount(PopoverContent, {
|
|
31
|
+
attachTo: document.body,
|
|
32
|
+
slots: { default: defaultSlot },
|
|
33
|
+
global: {
|
|
34
|
+
stubs: globalStubs,
|
|
35
|
+
components: { PopoverFooter, NestedFooterChild },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const content = (wrapper) => wrapper.find('.unnnic-popover__content');
|
|
40
|
+
const footerContainer = (wrapper) =>
|
|
41
|
+
wrapper.find('[data-testid="popover-footer-container"]');
|
|
42
|
+
|
|
43
|
+
describe('UnnnicPopoverFooter', () => {
|
|
44
|
+
it('renders a direct footer child inside the footer container, outside the content', async () => {
|
|
45
|
+
const wrapper = mountPopover(
|
|
46
|
+
`<p data-testid="body">Body</p>
|
|
47
|
+
<PopoverFooter><button data-testid="footer-btn">Save</button></PopoverFooter>`,
|
|
48
|
+
);
|
|
49
|
+
await flushPromises();
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
footerContainer(wrapper).find('.unnnic-popover__footer').exists(),
|
|
53
|
+
).toBe(true);
|
|
54
|
+
expect(content(wrapper).find('.unnnic-popover__footer').exists()).toBe(
|
|
55
|
+
false,
|
|
56
|
+
);
|
|
57
|
+
expect(wrapper.find('[data-testid="footer-btn"]').exists()).toBe(true);
|
|
58
|
+
|
|
59
|
+
wrapper.unmount();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders a footer nested inside a child component in the footer container', async () => {
|
|
63
|
+
const wrapper = mountPopover('<NestedFooterChild />');
|
|
64
|
+
await flushPromises();
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
footerContainer(wrapper).find('.unnnic-popover__footer').exists(),
|
|
68
|
+
).toBe(true);
|
|
69
|
+
expect(
|
|
70
|
+
footerContainer(wrapper)
|
|
71
|
+
.find('[data-testid="nested-footer-btn"]')
|
|
72
|
+
.exists(),
|
|
73
|
+
).toBe(true);
|
|
74
|
+
expect(content(wrapper).find('.unnnic-popover__footer').exists()).toBe(
|
|
75
|
+
false,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
wrapper.unmount();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders no footer area when no footer is provided', async () => {
|
|
82
|
+
const wrapper = mountPopover('<p data-testid="body">Body</p>');
|
|
83
|
+
await flushPromises();
|
|
84
|
+
|
|
85
|
+
expect(wrapper.find('.unnnic-popover__footer').exists()).toBe(false);
|
|
86
|
+
expect(footerContainer(wrapper).element.children.length).toBe(0);
|
|
87
|
+
|
|
88
|
+
wrapper.unmount();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('teleports every footer into the container when multiple are provided', async () => {
|
|
92
|
+
const wrapper = mountPopover(
|
|
93
|
+
`<PopoverFooter><span>First</span></PopoverFooter>
|
|
94
|
+
<PopoverFooter><span>Second</span></PopoverFooter>`,
|
|
95
|
+
);
|
|
96
|
+
await flushPromises();
|
|
97
|
+
|
|
98
|
+
const footers = footerContainer(wrapper).findAll('.unnnic-popover__footer');
|
|
99
|
+
expect(footers).toHaveLength(2);
|
|
100
|
+
expect(content(wrapper).find('.unnnic-popover__footer').exists()).toBe(
|
|
101
|
+
false,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
wrapper.unmount();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders inline as a fallback when used without a PopoverContent target', () => {
|
|
108
|
+
const wrapper = mount(PopoverFooter, {
|
|
109
|
+
slots: { default: '<button data-testid="standalone">Save</button>' },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const footer = wrapper.find('.unnnic-popover__footer');
|
|
113
|
+
expect(footer.exists()).toBe(true);
|
|
114
|
+
expect(footer.find('[data-testid="standalone"]').exists()).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import UnnnicDisclaimer from '../components/Disclaimer/Disclaimer.vue';
|
|
2
|
+
import UnnnicIconLoading from '../components/IconLoading/IconLoading.vue';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
title: 'Feedback/Disclaimer',
|
|
@@ -75,3 +76,18 @@ export const WithDescriptionSlot = TemplateWithDescriptionSlot.bind({});
|
|
|
75
76
|
WithDescriptionSlot.args = {
|
|
76
77
|
...Default.args,
|
|
77
78
|
};
|
|
79
|
+
|
|
80
|
+
const TemplateWithCustomIcon = (args) => ({
|
|
81
|
+
components: { UnnnicDisclaimer, UnnnicIconLoading },
|
|
82
|
+
setup() {
|
|
83
|
+
return { args };
|
|
84
|
+
},
|
|
85
|
+
template:
|
|
86
|
+
'<UnnnicDisclaimer v-bind="args"><template #icon><UnnnicIconLoading size="sm" scheme="fg-emphasized" stroke-width="3" /></template></UnnnicDisclaimer>',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const WithCustomIcon = TemplateWithCustomIcon.bind({});
|
|
90
|
+
WithCustomIcon.args = {
|
|
91
|
+
text: '',
|
|
92
|
+
description: 'Analysis in progress · 381 of 437 items processed.',
|
|
93
|
+
};
|