Skip to content
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

Get rid of NoElement component and useNoElementRender composable #1583

Merged
merged 8 commits into from
Aug 6, 2024

This file was deleted.

1 change: 1 addition & 0 deletions packages/x-components/src/components/animations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { default as CollapseWidth } from './collapse-width.vue';
export { default as CrossFade } from './cross-fade.vue';
export { default as Fade } from './fade.vue';
export { default as FadeAndSlide } from './fade-and-slide.vue';
export { default as NoAnimation } from './no-animation.vue';
export { default as StaggeredFadeAndSlide } from './staggered-fade-and-slide.vue';
export { default as StaggeringTransitionGroup } from './staggering-transition-group.vue';
export { createDirectionalAnimationFactory } from './create-directional-animation-factory';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { defineComponent } from 'vue';

/**
* Component to be used as `default` for animation props together with dynamic components
* `<component :is="animation">` in the template.
*/
export default defineComponent({
name: 'NoAnimation',
setup(_, { slots }) {
return () => slots.default?.()[0] ?? '';
}
});
</script>
10 changes: 3 additions & 7 deletions packages/x-components/src/components/base-dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,8 @@
import { Identifiable } from '@empathyco/x-types';
import { computed, defineComponent, nextTick, onBeforeUnmount, PropType, ref, watch } from 'vue';
import { AnimationProp } from '../types';
import { getTargetElement } from '../utils/html';
import { normalizeString } from '../utils/normalize';
import { isInRange } from '../utils/number';
import { debounce as debounceFunction } from '../utils/debounce';
import { NoElement } from './no-element';
import { debounceFunction, normalizeString, getTargetElement, isInRange } from '../utils';
import { NoAnimation } from './animations';

type DropdownItem = string | number | Identifiable;
let dropdownCount = 0;
Expand All @@ -95,7 +92,6 @@
*/
export default defineComponent({
name: 'BaseDropdown',
components: { NoElement },
props: {
/** List of items to display.*/
items: {
Expand All @@ -117,7 +113,7 @@
*/
animation: {
type: AnimationProp,
default: () => NoElement
default: () => NoAnimation
},
/** Time to wait without receiving any keystroke before resetting the items search query. */
searchTimeoutMs: {
Expand Down
3 changes: 1 addition & 2 deletions packages/x-components/src/components/display-emitter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import {
defineComponent,
getCurrentInstance,
h,
onMounted,
onUnmounted,
PropType,
Expand Down Expand Up @@ -30,7 +29,7 @@
let unwatchDisplay: WatchStopHandle | undefined;

onMounted(() => {
const element = getCurrentInstance()?.proxy.$el as HTMLElement | undefined;
const element = getCurrentInstance()?.proxy?.$el as HTMLElement | undefined;
if (element) {
unwatchDisplay = useEmitDisplayEvent({
element,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { computed, defineComponent, h, PropType, VNode, VNodeChildren } from 'vue';
import { computed, defineComponent, h, PropType, VNode } from 'vue';
import { RangeValue } from '@empathyco/x-types';
import BaseCurrency from '../../currency/base-currency.vue';

Expand Down Expand Up @@ -67,7 +67,7 @@
const render = (): VNode => {
const labelParts = label.value.split(/({min}|{max})/);

const children: VNodeChildren = labelParts.map(partMessage => {
const children = labelParts.map(partMessage => {
if (partMessage === '{min}') {
return h('BaseCurrency', {
props: {
Expand Down
155 changes: 65 additions & 90 deletions packages/x-components/src/components/highlight.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
<template>
<NoElement>
<slot v-bind="{ text, hasMatch, ...matchParts }">
<span class="x-highlight" :class="dynamicCSSClasses">
<span
v-if="matchParts.start"
v-text="matchParts.start"
class="x-highlight__text"
data-test="highlight-start"
/>
<span
v-if="hasMatch"
v-text="matchParts.match"
class="x-highlight__text x-highlight-text-match"
:class="matchingPartClass"
data-test="matching-part"
/>
<span
v-if="matchParts.end"
v-text="matchParts.end"
class="x-highlight__text"
data-test="highlight-end"
/>
</span>
</slot>
</NoElement>
<span class="x-highlight" :class="dynamicCSSClasses">
<span
v-if="matchParts.start"
v-text="matchParts.start"
class="x-highlight__text"
data-test="highlight-start"
/>
<span
v-if="hasMatch"
v-text="matchParts.match"
class="x-highlight__text x-highlight-text-match"
:class="matchingPartClass"
data-test="matching-part"
/>
<span
v-if="matchParts.end"
v-text="matchParts.end"
class="x-highlight__text"
data-test="highlight-end"
/>
</span>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue';
import { normalizeString } from '../utils/normalize';
import { VueCSSClasses } from '../utils/types';
import { NoElement } from './no-element';
import { normalizeString } from '../utils';

/**
* Highlights the given part of the text. The component is smart enough to do matches
Expand All @@ -40,58 +34,59 @@
*/
export default defineComponent({
name: 'Highlight',
components: { NoElement },
props: {
/**
* The text to highlight some part of it.
*
* @public
*/
/** The text to highlight some part of it. */
text: {
type: String,
default: ''
},
/**
* The part of the text to be highlighted.
*
* @public
*/
/** The part of the text to be highlighted. */
highlight: {
type: String,
default: ''
},
/**
* CSS Class to add when the `text` string contains the `highlight` string.
*/
/** CSS Class to add when the `text` string contains the `highlight` string. */
matchClass: {
type: String,
default: ''
},
/**
* CSS Class to add when the given `text` doesn't contain the `highlight` string.
*/
/** CSS Class to add when the given `text` doesn't contain the `highlight` string. */
noMatchClass: {
type: String,
default: ''
},
/**
* CSS Class to add to the matching text.
*/
/** CSS Class to add to the matching text. */
matchingPartClass: {
type: String,
default: ''
}
},
setup: function (props) {
setup(props, { slots }) {
/**
* Splits the label in three parts based on two indexes.
*
* @param label - The string that will be divided in three parts.
* @param start - The first index that the label will be divided by.
* @param end - The second index that the label will be divided by.
*
* @returns The three parts of the divided label.
*/
function splitAt(label: string, start: number, end: number) {
return {
start: label.substring(0, start),
match: label.substring(start, end),
end: label.substring(end)
};
}

/**
* Splits the text to highlight into 3 parts: a starting part, the matching part
* and the ending part. If there is no match between the text and the highlight, the `start`
* property will contain the whole text.
*
* @returns An object containing the different parts of the text.
* @internal
*/
const matchParts = computed((): HighlightMatch => {
const matchParts = computed(() => {
const matcherIndex = normalizeString(props.text).indexOf(normalizeString(props.highlight));
return matcherIndex !== -1 && props.highlight
? splitAt(props.text.trim(), matcherIndex, matcherIndex + props.highlight.trim().length)
Expand All @@ -103,11 +98,8 @@
* matching part.
*
* @returns True if there is a match between the text and the highlight strings.
* @internal
*/
const hasMatch = computed((): boolean => {
return !!matchParts.value.match;
});
const hasMatch = computed(() => !!matchParts.value.match);

/**
* CSS classes to add depending on the component state.
Expand All @@ -117,11 +109,10 @@
* `[matchClass]`: When there is a match between the text and
* the part to highlight.
* `[noMatchClass]`: when there is no match between the text to highlight.
* @returns The {@link VueCSSClasses} classes.
* @internal
* @returns The CSS classes.
*/
const dynamicCSSClasses = computed((): VueCSSClasses => {
const classes: VueCSSClasses = {
const dynamicCSSClasses = computed(() => {
const classes = {
'x-highlight--has-match': hasMatch.value,
'x-highlight-text': hasMatch.value,
[props.matchClass]: hasMatch.value
Expand All @@ -133,45 +124,29 @@
});

/**
* Splits the label in three parts based on two indexes.
* Render function to execute the `default` slot, binding `slotsProps` and getting only the
* first `vNode` to avoid Fragments and Text root nodes.
*
* @param label - The string that will be divided in three parts.
* @param start - The first index that the label will be divided by.
* @param end - The second index that the label will be divided by.
* @remarks `slotProps` must be values without Vue reactivity and located inside the
* render-function to update the binding data properly.
*
* @returns The three parts of the divided label.
* @internal
* @returns The root `vNode` of the `default` slot.
*/
function splitAt(label: string, start: number, end: number): HighlightMatch {
return {
start: label.substring(0, start),
match: label.substring(start, end),
end: label.substring(end)
function renderDefaultSlot() {
const slotProps = {
text: props.text,
hasMatch: hasMatch.value,
...matchParts.value
};
return slots.default?.(slotProps)[0];
}

return { hasMatch, matchParts, dynamicCSSClasses };
/* Hack to render through a render-function, the `default` slot or, in its absence,
the component itself. It is the alternative for the NoElement antipattern. */
const componentProps = { hasMatch, matchParts, dynamicCSSClasses };
return (slots.default ? renderDefaultSlot : componentProps) as typeof componentProps;
}
});

/**
* Contains the different parts of a string match.
*/
interface HighlightMatch {
/**
* When the match does not happen from the beginning of the string, the initial unmatched
* part.
*/
start: string;
/**
* The part of the text that is matching.
*/
match: string;
/**
* When the match does not extend until the end, the remaining unmatched string.
*/
end: string;
}
</script>

<docs lang="mdx">
Expand Down
1 change: 0 additions & 1 deletion packages/x-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export { default as GlobalXBus } from './global-x-bus.vue';
export { default as Highlight } from './highlight.vue';
export { default as ItemsList } from './items-list.vue';
export { default as LocationProvider } from './location-provider.vue';
export { NoElement } from './no-element';
export { default as SlidingPanel } from './sliding-panel.vue';
export { default as SnippetCallbacks } from './snippet-callbacks.vue';
export { default as PageLoaderButton } from './page-loader-button.vue';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { defineComponent, PropType, provide, toRef } from 'vue';
import { FeatureLocation } from '../types';
import { useNoElementRender } from '../composables/use-no-element-render';

/**
* Location Provider component.
Expand All @@ -26,7 +25,7 @@
const featureLocation = toRef(props, 'location');
provide('location', featureLocation);

return () => useNoElementRender(slots);
return () => slots.default?.()[0] ?? '';
}
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<template>
<BaseModal
v-bind="$attrs"
ref="baseModalEl"
@click:overlay="emitBodyClickEvent"
@focusin:body="emitBodyClickEvent"
Expand Down
Loading
Loading