@kiva/kv-components 3.107.0 → 3.107.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/CHANGELOG.md +22 -0
- package/dist/components/.storybook/main.js +85 -0
- package/dist/components/.storybook/package.json +3 -0
- package/dist/components/.storybook/preview.js +61 -0
- package/dist/components/.storybook/tailwind.css +5 -0
- package/dist/components/KvAccordionItem.vue +130 -0
- package/dist/components/KvActivityRow.vue +33 -0
- package/dist/components/KvBorrowerImage.vue +179 -0
- package/dist/components/KvButton.vue +287 -0
- package/dist/components/KvCarousel.vue +297 -0
- package/dist/components/KvCartModal.vue +365 -0
- package/dist/components/KvCheckbox.vue +203 -0
- package/dist/components/KvChip.vue +54 -0
- package/dist/components/KvClassicLoanCard.vue +527 -0
- package/dist/components/KvCommentsAdd.vue +135 -0
- package/dist/components/KvCommentsContainer.vue +84 -0
- package/dist/components/KvCommentsHeartButton.vue +70 -0
- package/dist/components/KvCommentsList.vue +68 -0
- package/dist/components/KvCommentsListItem.vue +241 -0
- package/dist/components/KvCommentsReplyButton.vue +52 -0
- package/dist/components/KvContentfulImg.vue +273 -0
- package/dist/components/KvCountdownTimer.vue +59 -0
- package/dist/components/KvExpandable.vue +84 -0
- package/dist/components/KvExpandableQuestion.vue +120 -0
- package/dist/components/KvFlag.vue +120 -0
- package/dist/components/KvGrid.vue +28 -0
- package/dist/components/KvImpactDashboardHeader.vue +40 -0
- package/dist/components/KvInlineActivityCard.vue +55 -0
- package/dist/components/KvInlineActivityFeed.vue +38 -0
- package/dist/components/KvIntroductionLoanCard.vue +446 -0
- package/dist/components/KvLendAmountButton.vue +65 -0
- package/dist/components/KvLendCta.vue +451 -0
- package/dist/components/KvLightbox.vue +334 -0
- package/dist/components/KvLineGraph.vue +128 -0
- package/dist/components/KvLoadingPlaceholder.vue +38 -0
- package/dist/components/KvLoadingSpinner.vue +81 -0
- package/dist/components/KvLoanActivities.vue +268 -0
- package/dist/components/KvLoanBookmark.vue +39 -0
- package/dist/components/KvLoanCallouts.vue +53 -0
- package/dist/components/KvLoanProgressGroup.vue +76 -0
- package/dist/components/KvLoanTag.vue +88 -0
- package/dist/components/KvLoanTeamPick.vue +44 -0
- package/dist/components/KvLoanUse.vue +92 -0
- package/dist/components/KvMap.vue +599 -0
- package/dist/components/KvMaterialIcon.vue +47 -0
- package/dist/components/KvPageContainer.vue +15 -0
- package/dist/components/KvPagination.vue +198 -0
- package/dist/components/KvPieChart.vue +257 -0
- package/dist/components/KvPopper.vue +178 -0
- package/dist/components/KvProgressBar.vue +149 -0
- package/dist/components/KvRadio.vue +198 -0
- package/dist/components/KvSelect.vue +114 -0
- package/dist/components/KvSideSheet.vue +134 -0
- package/dist/components/KvSwitch.vue +143 -0
- package/dist/components/KvTab.vue +90 -0
- package/dist/components/KvTabPanel.vue +64 -0
- package/dist/components/KvTabs.vue +182 -0
- package/dist/components/KvTextInput.vue +247 -0
- package/dist/components/KvTextLink.vue +138 -0
- package/dist/components/KvThemeProvider.vue +122 -0
- package/dist/components/KvToast.vue +221 -0
- package/dist/components/KvTooltip.vue +168 -0
- package/dist/components/KvTreeMapChart.vue +229 -0
- package/dist/components/KvUserAvatar.vue +132 -0
- package/dist/components/KvVerticalCarousel.vue +156 -0
- package/dist/components/KvVotingCard.vue +160 -0
- package/dist/components/KvVotingCardV2.vue +154 -0
- package/dist/components/KvWideLoanCard.vue +432 -0
- package/dist/components/stories/Forms.stories.js +62 -0
- package/dist/components/stories/KvAccordionItem.stories.js +24 -0
- package/dist/components/stories/KvActivityRow.stories.js +25 -0
- package/dist/components/stories/KvBorrowerImage.stories.js +68 -0
- package/dist/components/stories/KvButton.stories.js +144 -0
- package/dist/components/stories/KvCarousel.stories.js +426 -0
- package/dist/components/stories/KvCartModal.stories.js +54 -0
- package/dist/components/stories/KvCheckbox.stories.js +163 -0
- package/dist/components/stories/KvChip.stories.js +43 -0
- package/dist/components/stories/KvClassicLoanCard.stories.js +480 -0
- package/dist/components/stories/KvCommentsAdd.stories.js +32 -0
- package/dist/components/stories/KvCommentsContainer.stories.js +42 -0
- package/dist/components/stories/KvCommentsHeartButton.stories.js +25 -0
- package/dist/components/stories/KvCommentsList.stories.js +39 -0
- package/dist/components/stories/KvCommentsListItem.stories.js +45 -0
- package/dist/components/stories/KvCommentsReplyButton.stories.js +21 -0
- package/dist/components/stories/KvContentfulImg.stories.js +196 -0
- package/dist/components/stories/KvCountdownTimer.stories.js +30 -0
- package/dist/components/stories/KvExpandableQuestion.stories.js +129 -0
- package/dist/components/stories/KvFlag.stories.js +36 -0
- package/dist/components/stories/KvGrid.stories.js +97 -0
- package/dist/components/stories/KvImpactDashboardHeader.stories.js +22 -0
- package/dist/components/stories/KvInlineActivityCard.stories.js +69 -0
- package/dist/components/stories/KvInlineActivityFeed.stories.js +76 -0
- package/dist/components/stories/KvIntroductionLoanCard.stories.js +208 -0
- package/dist/components/stories/KvLendAmountButton.stories.js +31 -0
- package/dist/components/stories/KvLendCta.stories.js +177 -0
- package/dist/components/stories/KvLightbox.stories.js +304 -0
- package/dist/components/stories/KvLineGraph.stories.js +52 -0
- package/dist/components/stories/KvLoadingPlaceholder.stories.js +17 -0
- package/dist/components/stories/KvLoadingSpinner.stories.js +52 -0
- package/dist/components/stories/KvLoanActivities.stories.js +104 -0
- package/dist/components/stories/KvLoanBookmark.stories.js +22 -0
- package/dist/components/stories/KvLoanCallouts.stories.js +22 -0
- package/dist/components/stories/KvLoanProgressGroup.stories.js +29 -0
- package/dist/components/stories/KvLoanTag.stories.js +61 -0
- package/dist/components/stories/KvLoanTeamPick.stories.js +20 -0
- package/dist/components/stories/KvLoanUse.stories.js +60 -0
- package/dist/components/stories/KvMap.stories.js +121 -0
- package/dist/components/stories/KvMaterialIcon.stories.js +201 -0
- package/dist/components/stories/KvPageContainer.stories.js +50 -0
- package/dist/components/stories/KvPagination.stories.js +70 -0
- package/dist/components/stories/KvPieChart.stories.js +47 -0
- package/dist/components/stories/KvProgressBar.stories.js +53 -0
- package/dist/components/stories/KvRadio.stories.js +140 -0
- package/dist/components/stories/KvSelect.stories.js +125 -0
- package/dist/components/stories/KvSideSheet.stories.js +50 -0
- package/dist/components/stories/KvSwitch.stories.js +66 -0
- package/dist/components/stories/KvTabs.stories.js +106 -0
- package/dist/components/stories/KvTextInput.stories.js +194 -0
- package/dist/components/stories/KvTextLink.stories.js +55 -0
- package/dist/components/stories/KvThemeProvider.stories.js +178 -0
- package/dist/components/stories/KvToast.stories.js +117 -0
- package/dist/components/stories/KvTooltip.stories.js +26 -0
- package/dist/components/stories/KvTreeMapChart.stories.js +42 -0
- package/dist/components/stories/KvUserAvatar.stories.js +47 -0
- package/dist/components/stories/KvVerticalCarousel.stories.js +168 -0
- package/dist/components/stories/KvVotingCard.stories.js +33 -0
- package/dist/components/stories/KvVotingCardV2.stories.js +89 -0
- package/dist/components/stories/KvWideLoanCard.stories.js +292 -0
- package/dist/components/stories/StyleguidePrimitives.stories.js +499 -0
- package/dist/components/stories/StyleguideProse.stories.js +215 -0
- package/dist/data/countries-borders.json +1 -0
- package/dist/data/ne_110m_admin_0_countries.json +1 -0
- package/dist/utils/Alea.js +9 -0
- package/dist/utils/attrs.js +7 -0
- package/dist/utils/carousels.js +8 -0
- package/dist/{attrs.js → utils/chunk-3HK4G4NT.js} +1 -0
- package/dist/{loanCard.js → utils/chunk-55HF2ORX.js} +1 -0
- package/dist/{expander.js → utils/chunk-AY3PR5S4.js} +3 -2
- package/dist/{carousels.js → utils/chunk-AZPWOFD5.js} +1 -0
- package/dist/{printing.js → utils/chunk-B5J5WLAH.js} +1 -0
- package/dist/{Alea.js → utils/chunk-GPSH6OPA.js} +2 -1
- package/dist/{scrollLock.js → utils/chunk-HIY5IW65.js} +2 -1
- package/dist/{treemap.js → utils/chunk-MSMZIN54.js} +1 -0
- package/dist/{imageUtils.js → utils/chunk-OXJCCNNW.js} +1 -0
- package/dist/{touchEvents.js → utils/chunk-S3MABILA.js} +3 -2
- package/dist/{mapUtils.js → utils/chunk-VIGEMAKO.js} +5 -4
- package/dist/utils/chunk-YCNMJ4YV.js +37 -0
- package/dist/{loanUtils.js → utils/chunk-YFEC5ODJ.js} +7 -6
- package/dist/utils/expander.js +9 -0
- package/dist/utils/imageUtils.js +9 -0
- package/dist/utils/index.cjs +1118 -0
- package/dist/utils/index.js +166 -0
- package/dist/utils/loanCard.js +9 -0
- package/dist/utils/loanUtils.js +23 -0
- package/dist/utils/mapUtils.js +15 -0
- package/dist/utils/printing.js +9 -0
- package/dist/utils/scrollLock.js +13 -0
- package/dist/{throttle.js → utils/throttle.js} +1 -0
- package/dist/utils/touchEvents.js +11 -0
- package/dist/utils/treemap.js +7 -0
- package/package.json +11 -7
- package/utils/index.js +14 -0
- package/index.js +0 -3
- /package/dist/{Alea.cjs → utils/Alea.cjs} +0 -0
- /package/dist/{attrs.cjs → utils/attrs.cjs} +0 -0
- /package/dist/{carousels.cjs → utils/carousels.cjs} +0 -0
- /package/dist/{chunk-HV3AUBFT.js → utils/chunk-HV3AUBFT.js} +0 -0
- /package/dist/{expander.cjs → utils/expander.cjs} +0 -0
- /package/dist/{imageUtils.cjs → utils/imageUtils.cjs} +0 -0
- /package/dist/{loanCard.cjs → utils/loanCard.cjs} +0 -0
- /package/dist/{loanUtils.cjs → utils/loanUtils.cjs} +0 -0
- /package/dist/{mapUtils.cjs → utils/mapUtils.cjs} +0 -0
- /package/dist/{printing.cjs → utils/printing.cjs} +0 -0
- /package/dist/{scrollLock.cjs → utils/scrollLock.cjs} +0 -0
- /package/dist/{throttle.cjs → utils/throttle.cjs} +0 -0
- /package/dist/{touchEvents.cjs → utils/touchEvents.cjs} +0 -0
- /package/dist/{treemap.cjs → utils/treemap.cjs} +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="classes"
|
|
4
|
+
:style="styles"
|
|
5
|
+
>
|
|
6
|
+
<label
|
|
7
|
+
class="tw-inline-flex tw-gap-2 tw-items-center tw-relative hover:tw-cursor-pointer"
|
|
8
|
+
:class="{ 'tw-opacity-low': disabled }"
|
|
9
|
+
:for="uuid"
|
|
10
|
+
>
|
|
11
|
+
<input
|
|
12
|
+
:id="uuid"
|
|
13
|
+
v-bind="inputAttrs"
|
|
14
|
+
ref="switchRef"
|
|
15
|
+
class="tw-sr-only tw-peer"
|
|
16
|
+
type="checkbox"
|
|
17
|
+
role="switch"
|
|
18
|
+
:checked="modelValue"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
v-on="inputListeners"
|
|
21
|
+
@change.prevent="onChange"
|
|
22
|
+
>
|
|
23
|
+
<!-- switch background -->
|
|
24
|
+
<div
|
|
25
|
+
class="
|
|
26
|
+
tw-w-7 tw-h-4 tw-rounded-full tw-relative tw-overflow-hidden
|
|
27
|
+
peer-focus-visible:tw-ring-2 peer-focus-visible:tw-ring-action
|
|
28
|
+
tw-bg-tertiary peer-checked:tw-bg-action
|
|
29
|
+
tw-transition-all tw-ease-in-out
|
|
30
|
+
"
|
|
31
|
+
>
|
|
32
|
+
</div>
|
|
33
|
+
<!-- switch inner circle -->
|
|
34
|
+
<div
|
|
35
|
+
class="
|
|
36
|
+
tw-flex-shrink-0 tw-w-3 tw-h-3
|
|
37
|
+
tw-absolute tw-m-0.5 tw-top-0
|
|
38
|
+
tw-rounded-full
|
|
39
|
+
tw-bg-white
|
|
40
|
+
tw-transform tw-transition-all tw-ease-in-out
|
|
41
|
+
peer-checked:tw-translate-x-3
|
|
42
|
+
"
|
|
43
|
+
></div>
|
|
44
|
+
<!-- label -->
|
|
45
|
+
<div class="tw-flex-1 peer-focus-visible:tw-ring-2 peer-focus-visible:tw-ring-action">
|
|
46
|
+
<slot></slot>
|
|
47
|
+
</div>
|
|
48
|
+
</label>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
import {
|
|
54
|
+
ref,
|
|
55
|
+
onMounted,
|
|
56
|
+
} from 'vue-demi';
|
|
57
|
+
import { nanoid } from 'nanoid';
|
|
58
|
+
import { useAttrs } from '../utils/attrs';
|
|
59
|
+
|
|
60
|
+
const emits = [
|
|
61
|
+
'update:modelValue',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* KvSwitch
|
|
66
|
+
* A visual treatment of a checkbox element for handling on/off states in UI.
|
|
67
|
+
*
|
|
68
|
+
* A11y considerations:
|
|
69
|
+
*
|
|
70
|
+
* - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Switch_role
|
|
71
|
+
*
|
|
72
|
+
* Prior art:
|
|
73
|
+
*
|
|
74
|
+
* - https://scottaohara.github.io/a11y_styled_form_controls/src/checkbox--switch/
|
|
75
|
+
* - https://adrianroselli.com/2019/08/under-engineered-toggles-too.html
|
|
76
|
+
* - https://headlessui.dev/react/switch#labels
|
|
77
|
+
* - https://react-spectrum.adobe.com/react-spectrum/Switch.html
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
export default {
|
|
81
|
+
inheritAttrs: false,
|
|
82
|
+
// v-model will change when checked value changes
|
|
83
|
+
model: {
|
|
84
|
+
prop: 'modelValue',
|
|
85
|
+
event: 'update:modelValue',
|
|
86
|
+
},
|
|
87
|
+
props: {
|
|
88
|
+
/**
|
|
89
|
+
* Whether the switch is on or off
|
|
90
|
+
* */
|
|
91
|
+
modelValue: {
|
|
92
|
+
type: Boolean,
|
|
93
|
+
default: false,
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Prevents the switch from being toggled or focused
|
|
97
|
+
* */
|
|
98
|
+
disabled: {
|
|
99
|
+
type: Boolean,
|
|
100
|
+
default: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
emits,
|
|
104
|
+
setup(props, context) {
|
|
105
|
+
const { emit } = context;
|
|
106
|
+
const uuid = ref(`kvs-${nanoid(10)}`);
|
|
107
|
+
const switchRef = ref(null);
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
classes,
|
|
111
|
+
styles,
|
|
112
|
+
inputAttrs,
|
|
113
|
+
inputListeners,
|
|
114
|
+
} = useAttrs(context, emits);
|
|
115
|
+
|
|
116
|
+
const onChange = (event) => {
|
|
117
|
+
emit('update:modelValue', event.target.checked);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const focus = () => {
|
|
121
|
+
switchRef.value.focus();
|
|
122
|
+
};
|
|
123
|
+
const blur = () => {
|
|
124
|
+
switchRef.value.blur();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
onMounted(() => {
|
|
128
|
+
uuid.value = `kvs-${nanoid(10)}`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
uuid,
|
|
133
|
+
onChange,
|
|
134
|
+
focus,
|
|
135
|
+
blur,
|
|
136
|
+
classes,
|
|
137
|
+
styles,
|
|
138
|
+
inputAttrs,
|
|
139
|
+
inputListeners,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
:id="`kv-tab-${forPanel}`"
|
|
4
|
+
class="tw-text-h3 tw-mb-1.5 tw-whitespace-nowrap tw-text-left"
|
|
5
|
+
:class="{ 'hover:tw-text-action-highlight' : !isActive,
|
|
6
|
+
'md:tw-border-l-2 tw-border-transparent md:tw-pl-2' : vertical,
|
|
7
|
+
'tw-text-action-highlight' : isActive && vertical
|
|
8
|
+
}"
|
|
9
|
+
role="tab"
|
|
10
|
+
:aria-selected="isActive"
|
|
11
|
+
:aria-controls="`kv-tab-panel-${forPanel}`"
|
|
12
|
+
:tabindex="isActive ? null : -1"
|
|
13
|
+
@click="handleTabClicked"
|
|
14
|
+
>
|
|
15
|
+
<slot></slot>
|
|
16
|
+
</button>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import {
|
|
21
|
+
toRefs,
|
|
22
|
+
inject,
|
|
23
|
+
computed,
|
|
24
|
+
onMounted,
|
|
25
|
+
getCurrentInstance,
|
|
26
|
+
} from 'vue-demi';
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
props: {
|
|
30
|
+
/**
|
|
31
|
+
* A unique id which correspondes to an `id` property on the KvTabPanel it controls
|
|
32
|
+
* e.g., <kv-tab for="foo">... <kv-tab-panel id="foo">
|
|
33
|
+
* */
|
|
34
|
+
forPanel: {
|
|
35
|
+
type: String,
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* The tab should be initially selected.
|
|
40
|
+
* */
|
|
41
|
+
selected: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
vertical: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
setup(props) {
|
|
51
|
+
const {
|
|
52
|
+
forPanel,
|
|
53
|
+
} = toRefs(props);
|
|
54
|
+
|
|
55
|
+
const kvTabContext = inject('$KvTabContext');
|
|
56
|
+
|
|
57
|
+
const isActive = computed(() => {
|
|
58
|
+
let navItems = [];
|
|
59
|
+
let selectedIndex = 0;
|
|
60
|
+
if (kvTabContext) {
|
|
61
|
+
navItems = kvTabContext.navItems;
|
|
62
|
+
selectedIndex = kvTabContext.selectedIndex;
|
|
63
|
+
}
|
|
64
|
+
return navItems[selectedIndex]?.forPanel === forPanel.value;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const index = computed(() => {
|
|
68
|
+
let navItems = [];
|
|
69
|
+
if (kvTabContext) {
|
|
70
|
+
navItems = kvTabContext.navItems;
|
|
71
|
+
}
|
|
72
|
+
return navItems?.findIndex((navItem) => navItem.forPanel === forPanel.value);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const handleTabClicked = () => {
|
|
76
|
+
kvTabContext.setTab(index.value);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
onMounted(() => {
|
|
80
|
+
const instance = getCurrentInstance();
|
|
81
|
+
kvTabContext.navItems.push(instance.proxy);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
isActive,
|
|
86
|
+
handleTabClicked,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition
|
|
3
|
+
enter-active-class="tw-transition-opacity tw-duration-700"
|
|
4
|
+
leave-active-class=""
|
|
5
|
+
enter-class="tw-opacity-0 tw-absolute tw-top-0"
|
|
6
|
+
enter-to-class="tw-opacity-full"
|
|
7
|
+
leave-class=""
|
|
8
|
+
leave-to-class="tw-opacity-0 tw-absolute tw-top-0"
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
v-show="isActive"
|
|
12
|
+
:id="`kv-tab-panel-${id}`"
|
|
13
|
+
:key="`kv-tab-panel-${id}`"
|
|
14
|
+
role="tabpanel"
|
|
15
|
+
:aria-hidden="!isActive"
|
|
16
|
+
:aria-labelledby="`kv-tab-${id}`"
|
|
17
|
+
tabindex="0"
|
|
18
|
+
>
|
|
19
|
+
<slot></slot>
|
|
20
|
+
</div>
|
|
21
|
+
</transition>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
import {
|
|
26
|
+
computed,
|
|
27
|
+
toRefs,
|
|
28
|
+
inject,
|
|
29
|
+
} from 'vue-demi';
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
props: {
|
|
33
|
+
/**
|
|
34
|
+
* A unique id which correspondes to a `for` property on the KvTab which controls it
|
|
35
|
+
* e.g., <kv-tab for="foo">... <kv-tab-panel id="foo">
|
|
36
|
+
* */
|
|
37
|
+
id: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
setup(props) {
|
|
43
|
+
const {
|
|
44
|
+
id,
|
|
45
|
+
} = toRefs(props);
|
|
46
|
+
|
|
47
|
+
const kvTabContext = inject('$KvTabContext');
|
|
48
|
+
|
|
49
|
+
const isActive = computed(() => {
|
|
50
|
+
let navItems = [];
|
|
51
|
+
let selectedIndex = 0;
|
|
52
|
+
if (kvTabContext) {
|
|
53
|
+
navItems = kvTabContext.navItems;
|
|
54
|
+
selectedIndex = kvTabContext.selectedIndex;
|
|
55
|
+
}
|
|
56
|
+
return navItems[selectedIndex]?.forPanel === id.value;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
isActive,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="vertical ? 'md:tw-flex' : ''">
|
|
3
|
+
<div
|
|
4
|
+
role="tablist"
|
|
5
|
+
class="
|
|
6
|
+
tw-flex tw-overflow-auto tw-relative
|
|
7
|
+
tw-gap-x-2.5 md:tw-gap-x-5 lg:tw-gap-x-6
|
|
8
|
+
tw-mb-3 lg:tw-mb-4
|
|
9
|
+
"
|
|
10
|
+
:class="{'md:tw-flex-col md:tw-mr-3' : vertical}"
|
|
11
|
+
@keydown="handleKeyDown($event)"
|
|
12
|
+
>
|
|
13
|
+
<!-- @slot Tab Navigation -->
|
|
14
|
+
<slot
|
|
15
|
+
name="tabNav"
|
|
16
|
+
></slot>
|
|
17
|
+
|
|
18
|
+
<!-- indicator bar -->
|
|
19
|
+
<div
|
|
20
|
+
class="
|
|
21
|
+
tw-absolute tw-bottom-0 tw-h-0.5 tw-left-0
|
|
22
|
+
tw-bg-primary-inverse tw-rounded-full
|
|
23
|
+
tw-origin-left tw-transition-all tw-duration-300
|
|
24
|
+
"
|
|
25
|
+
:class="{ 'tw-hidden md:tw-block tw-top-0 md:tw-bg-action-highlight' : vertical}"
|
|
26
|
+
:style="`
|
|
27
|
+
width: ${selectedTabEl && !vertical ? selectedTabEl.clientWidth : 3}px;
|
|
28
|
+
height: ${selectedTabEl && vertical ? `${selectedTabEl.clientHeight}px` : '0.25rem'};
|
|
29
|
+
transform: ${selectedTabEl && !vertical ? `translateX(${selectedTabEl.offsetLeft}px)`
|
|
30
|
+
: selectedTabEl ? `translateY(${selectedTabEl.offsetTop}px)` : null};
|
|
31
|
+
`"
|
|
32
|
+
></div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="tw-relative">
|
|
35
|
+
<!-- @slot Tab Panels -->
|
|
36
|
+
<slot name="tabPanels"></slot>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
/**
|
|
43
|
+
* Components for implementing a tab UI pattern. Requires KvTab and KvTabPanel components.
|
|
44
|
+
*
|
|
45
|
+
* Keyboard navigation and aria roles + attributes handled according to
|
|
46
|
+
* https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-1/tabs.html
|
|
47
|
+
*
|
|
48
|
+
* Usage:
|
|
49
|
+
*
|
|
50
|
+
* ```
|
|
51
|
+
* <kv-tabs>
|
|
52
|
+
* <template #tabNav>
|
|
53
|
+
* <kv-tab for-panel="foo">Foo</kv-tab>
|
|
54
|
+
* <kv-tab for-panel="bar">Bar</kv-tab>
|
|
55
|
+
* </template>
|
|
56
|
+
* <template #tabPanels>
|
|
57
|
+
* <kv-tab-panel id="foo">Foo content</kv-tab-panel>
|
|
58
|
+
* <kv-tab-panel id="bar">Bar content</kv-tab-panel>
|
|
59
|
+
* </template>
|
|
60
|
+
* </kv-tabs>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
import {
|
|
64
|
+
ref,
|
|
65
|
+
reactive,
|
|
66
|
+
provide,
|
|
67
|
+
computed,
|
|
68
|
+
onMounted,
|
|
69
|
+
getCurrentInstance,
|
|
70
|
+
onBeforeUnmount,
|
|
71
|
+
} from 'vue-demi';
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
props: {
|
|
75
|
+
vertical: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
setup(props, { emit }) {
|
|
81
|
+
const tabContext = reactive({
|
|
82
|
+
selectedIndex: 0,
|
|
83
|
+
setTab: null,
|
|
84
|
+
navItems: [],
|
|
85
|
+
});
|
|
86
|
+
const selectedTabResizeObserver = ref(null);
|
|
87
|
+
|
|
88
|
+
const selectedTabEl = computed(() => {
|
|
89
|
+
const { navItems, selectedIndex } = tabContext;
|
|
90
|
+
return navItems[selectedIndex]?.$el ?? null;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const forceUpdate = () => {
|
|
94
|
+
const instance = getCurrentInstance();
|
|
95
|
+
if (instance) {
|
|
96
|
+
instance.proxy.$forceUpdate();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const setTab = (index) => {
|
|
101
|
+
tabContext.selectedIndex = index;
|
|
102
|
+
selectedTabEl.value.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Triggers when the selected tab changes
|
|
106
|
+
*
|
|
107
|
+
* @property {number} index Index of the newly selected tab
|
|
108
|
+
*/
|
|
109
|
+
emit('tab-changed', index);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
tabContext.setTab = setTab; // setTab definition in tab context
|
|
113
|
+
provide('$KvTabContext', tabContext);
|
|
114
|
+
|
|
115
|
+
const handleKeyDown = (event) => {
|
|
116
|
+
const { navItems, selectedIndex } = tabContext;
|
|
117
|
+
|
|
118
|
+
const focusActiveTab = () => {
|
|
119
|
+
const activeTab = navItems
|
|
120
|
+
.find((navItem) => navItem.isActive);
|
|
121
|
+
if (activeTab) {
|
|
122
|
+
activeTab.$el?.focus();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const count = navItems.length;
|
|
126
|
+
|
|
127
|
+
if (event.key === 'ArrowRight') {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
const nextIndex = (selectedIndex + 1) % count;
|
|
130
|
+
setTab(nextIndex);
|
|
131
|
+
focusActiveTab();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (event.key === 'ArrowLeft') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const prevIndex = (selectedIndex - 1 + count) % count;
|
|
137
|
+
setTab(prevIndex);
|
|
138
|
+
focusActiveTab();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (event.key === 'Home') {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
setTab(0);
|
|
144
|
+
focusActiveTab();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (event.key === 'End') {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
setTab(count - 1);
|
|
150
|
+
focusActiveTab();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
onMounted(() => {
|
|
155
|
+
// check if any of the KvTab components are declaratively selected
|
|
156
|
+
tabContext.navItems.forEach((navItem, index) => {
|
|
157
|
+
if (navItem.selected) {
|
|
158
|
+
setTab(index);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Tab size can change as @font-face fonts come in or
|
|
163
|
+
// the screen breakpoint changes the font size. If this happens
|
|
164
|
+
// we need to re-size and position the indicator bar.
|
|
165
|
+
selectedTabResizeObserver.value = new ResizeObserver(() => {
|
|
166
|
+
forceUpdate();
|
|
167
|
+
});
|
|
168
|
+
selectedTabResizeObserver.value.observe(selectedTabEl.value);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
onBeforeUnmount(() => {
|
|
172
|
+
selectedTabResizeObserver.value.disconnect();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
handleKeyDown,
|
|
177
|
+
selectedTabEl,
|
|
178
|
+
tabContext,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
</script>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="tw-inline-flex"
|
|
4
|
+
:class="classes"
|
|
5
|
+
:style="styles"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
class="tw-relative tw-w-full "
|
|
9
|
+
:class="{ 'tw-opacity-low': disabled }"
|
|
10
|
+
>
|
|
11
|
+
<!-- eslint-disable max-len -->
|
|
12
|
+
<input
|
|
13
|
+
:id="id"
|
|
14
|
+
ref="textInputRef"
|
|
15
|
+
:type="type"
|
|
16
|
+
class="
|
|
17
|
+
tw-h-6 tw-w-full
|
|
18
|
+
tw-px-2
|
|
19
|
+
tw-border tw-border-tertiary
|
|
20
|
+
tw-rounded-sm
|
|
21
|
+
tw-appearance-none
|
|
22
|
+
tw-text-base
|
|
23
|
+
tw-bg-primary
|
|
24
|
+
tw-ring-inset
|
|
25
|
+
focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-action focus:tw-border-transparent
|
|
26
|
+
"
|
|
27
|
+
:class="{
|
|
28
|
+
'tw-pr-6 tw-bg-danger tw-border-danger-highlight tw-bg-opacity-low focus:tw-ring-danger-highlight'
|
|
29
|
+
: !valid,
|
|
30
|
+
'tw-bg-tertiary'
|
|
31
|
+
: disabled,
|
|
32
|
+
'tw-pl-6' : icon,
|
|
33
|
+
}"
|
|
34
|
+
:placeholder="placeholder"
|
|
35
|
+
:disabled="disabled"
|
|
36
|
+
v-bind="inputAttrs"
|
|
37
|
+
:value="modelValue"
|
|
38
|
+
v-on="inputListeners"
|
|
39
|
+
@input="onInput"
|
|
40
|
+
>
|
|
41
|
+
<!-- eslint-enable max-len -->
|
|
42
|
+
<kv-material-icon
|
|
43
|
+
v-if="icon"
|
|
44
|
+
:icon="icon"
|
|
45
|
+
class="tw-w-3 tw-h-3 tw-absolute tw-top-1.5 tw-left-1.5 tw-pointer-events-none"
|
|
46
|
+
/>
|
|
47
|
+
<kv-material-icon
|
|
48
|
+
v-if="!valid"
|
|
49
|
+
:icon="mdiAlertCircleOutline"
|
|
50
|
+
class="tw-w-3 tw-h-3 tw-absolute tw-top-1.5 tw-right-1.5
|
|
51
|
+
tw-pointer-events-none tw-text-danger"
|
|
52
|
+
/>
|
|
53
|
+
<button
|
|
54
|
+
v-show="canClear && valid && !!inputText"
|
|
55
|
+
type="button"
|
|
56
|
+
class="tw-absolute tw-top-1.5 tw-right-1.5"
|
|
57
|
+
@click="clearInput"
|
|
58
|
+
>
|
|
59
|
+
<span class="tw-sr-only">clear input</span>
|
|
60
|
+
<kv-material-icon
|
|
61
|
+
class="tw-w-3 tw-h-3"
|
|
62
|
+
:icon="mdiClose"
|
|
63
|
+
/>
|
|
64
|
+
</button>
|
|
65
|
+
<div
|
|
66
|
+
v-if="$slots.error"
|
|
67
|
+
class="tw-text-danger tw-text-small tw-font-medium tw-mt-1"
|
|
68
|
+
>
|
|
69
|
+
<!-- @slot Used in conjuction with the `valid` prop to tell the user what must be fixed -->
|
|
70
|
+
<slot name="error"></slot>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
import {
|
|
78
|
+
ref,
|
|
79
|
+
toRefs,
|
|
80
|
+
} from 'vue-demi';
|
|
81
|
+
import { mdiAlertCircleOutline, mdiClose } from '@mdi/js';
|
|
82
|
+
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
83
|
+
import { useAttrs } from '../utils/attrs';
|
|
84
|
+
|
|
85
|
+
const emits = [
|
|
86
|
+
'input',
|
|
87
|
+
'update:modelValue',
|
|
88
|
+
];
|
|
89
|
+
/* eslint-disable max-len */
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Use as you would an `<input type="text" />`. Ensure you're using a label element for a11y.
|
|
93
|
+
*
|
|
94
|
+
* @usage
|
|
95
|
+
* ```
|
|
96
|
+
* <label for="input_id">Street Address</label>
|
|
97
|
+
* <kv-text-input id="input_id" v-model="streetAddress" />
|
|
98
|
+
*
|
|
99
|
+
* or
|
|
100
|
+
*
|
|
101
|
+
* <label for="input_id">Street Address</label>
|
|
102
|
+
* <kv-text-input id="input_id" :value="streetAddress" @input="(val) => streetAddress = val" />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
/* eslint-enable max-len */
|
|
106
|
+
|
|
107
|
+
export default {
|
|
108
|
+
components: {
|
|
109
|
+
KvMaterialIcon,
|
|
110
|
+
},
|
|
111
|
+
inheritAttrs: false,
|
|
112
|
+
// v-model will update when a different value is input
|
|
113
|
+
model: {
|
|
114
|
+
prop: 'modelValue',
|
|
115
|
+
event: 'update:modelValue',
|
|
116
|
+
},
|
|
117
|
+
props: {
|
|
118
|
+
/**
|
|
119
|
+
* Id of the input, used to associate the input with a <label> element.
|
|
120
|
+
* */
|
|
121
|
+
id: {
|
|
122
|
+
type: String,
|
|
123
|
+
required: true,
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Value of the text in the input. Used if you're not using the standard v-model bindings
|
|
127
|
+
* ```
|
|
128
|
+
* <kv-text-input :value="streetAddress" @input="(val) => streetAddress = val" />
|
|
129
|
+
* ```
|
|
130
|
+
* */
|
|
131
|
+
modelValue: {
|
|
132
|
+
type: [String, Number, Boolean],
|
|
133
|
+
default: '',
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Text that appears in the form control when it has no value set
|
|
137
|
+
* Should not be used as a label, but rather an example.
|
|
138
|
+
* Bad: Enter an address Good: 1234 E Cherry Lane
|
|
139
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefplaceholder
|
|
140
|
+
* */
|
|
141
|
+
placeholder: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: null,
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* Prevents the input from being edited or focused
|
|
147
|
+
* */
|
|
148
|
+
disabled: {
|
|
149
|
+
type: Boolean,
|
|
150
|
+
default: false,
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* When set to false, visually indicates to the user that the contents of the input need
|
|
154
|
+
* to be changed
|
|
155
|
+
* */
|
|
156
|
+
valid: {
|
|
157
|
+
type: Boolean,
|
|
158
|
+
default: true,
|
|
159
|
+
},
|
|
160
|
+
/*
|
|
161
|
+
* SVG path data passed in from an imported @mdi/js module.
|
|
162
|
+
* Displayed on the left hand side of the input
|
|
163
|
+
* ```
|
|
164
|
+
* import { mdiMagnify } from '@mdi/js';
|
|
165
|
+
* data() { return { mdiMagnify } };
|
|
166
|
+
* <kv-text-input :icon="mdiMagnify" />`
|
|
167
|
+
* ```
|
|
168
|
+
* */
|
|
169
|
+
icon: {
|
|
170
|
+
type: String,
|
|
171
|
+
default: null,
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* Can be used to set show special keyboards for mobile users
|
|
175
|
+
* E.g., type="number" or type="tel"
|
|
176
|
+
* */
|
|
177
|
+
type: {
|
|
178
|
+
type: String,
|
|
179
|
+
default: 'text',
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* When set to true, adds a button positioned to the right edge of the input containing an “X”
|
|
183
|
+
* */
|
|
184
|
+
canClear: {
|
|
185
|
+
type: Boolean,
|
|
186
|
+
default: false,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
emits,
|
|
190
|
+
setup(props, context) {
|
|
191
|
+
const { emit } = context;
|
|
192
|
+
const {
|
|
193
|
+
modelValue,
|
|
194
|
+
} = toRefs(props);
|
|
195
|
+
|
|
196
|
+
const textInputRef = ref(null);
|
|
197
|
+
const inputText = ref(modelValue.value);
|
|
198
|
+
|
|
199
|
+
const {
|
|
200
|
+
classes,
|
|
201
|
+
styles,
|
|
202
|
+
inputAttrs,
|
|
203
|
+
inputListeners,
|
|
204
|
+
} = useAttrs(context, emits);
|
|
205
|
+
|
|
206
|
+
const onInput = (event) => {
|
|
207
|
+
/**
|
|
208
|
+
* The value that is currently in the input
|
|
209
|
+
* @event input
|
|
210
|
+
* @type {Event}
|
|
211
|
+
*/
|
|
212
|
+
inputText.value = event.target.value;
|
|
213
|
+
emit('input', event.target.value);
|
|
214
|
+
emit('update:modelValue', event.target.value);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const focus = () => {
|
|
218
|
+
textInputRef.value.focus();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const blur = () => {
|
|
222
|
+
textInputRef.value.blur();
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const clearInput = () => {
|
|
226
|
+
inputText.value = '';
|
|
227
|
+
emit('input', '');
|
|
228
|
+
emit('update:modelValue', '');
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
mdiAlertCircleOutline,
|
|
233
|
+
mdiClose,
|
|
234
|
+
onInput,
|
|
235
|
+
focus,
|
|
236
|
+
blur,
|
|
237
|
+
clearInput,
|
|
238
|
+
inputText,
|
|
239
|
+
classes,
|
|
240
|
+
styles,
|
|
241
|
+
inputAttrs,
|
|
242
|
+
inputListeners,
|
|
243
|
+
textInputRef,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
</script>
|