Skip to content

Commit

Permalink
Replace Wikit Lookup component with Codex
Browse files Browse the repository at this point in the history
Replace usages of Wikit's Lookup compontent in `ItemLookup` and
`SpellingVariantInput` with the `CdxLookup` component.

Bug: T370057
  • Loading branch information
codders committed Oct 24, 2024
1 parent 4d91a4e commit 0cc4630
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 207 deletions.
20 changes: 13 additions & 7 deletions cypress/e2e/NewLexemeForm.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ describe( 'NewLexemeForm', () => {
cy.get( '.wbl-snl-language-lookup input' )
.type( '=Q123', { delay: 0 } );
checkA11y( '.wbl-snl-language-lookup' );
cy.get( '.wbl-snl-language-lookup .wikit-OptionsMenu__item' ).click();
cy.get( 'li.cdx-menu-item' ).contains( 'No match was found' ).should( 'not.exist' );
checkA11y( '.wbl-snl-language-lookup' );
cy.get( '.wbl-snl-language-lookup .cdx-menu-item' ).click();

cy.wait( '@LanguageCodeRetrieval' );

cy.get( '.wbl-snl-lexical-category-lookup input' )
.type( '=Q456', { delay: 0 } );
cy.get( '.wbl-snl-lexical-category-lookup .wikit-OptionsMenu__item' ).click();
cy.get( 'li.cdx-menu-item' ).contains( 'No match was found' ).should( 'not.exist' );
cy.get( '.wbl-snl-lexical-category-lookup .cdx-menu-item' ).click();

cy.get( '.wbl-snl-form button' )
.click();
Expand All @@ -99,17 +102,20 @@ describe( 'NewLexemeForm', () => {

cy.get( '.wbl-snl-language-lookup input' )
.type( '=Q123', { delay: 0 } );
cy.get( '.wbl-snl-language-lookup .wikit-OptionsMenu__item' ).click();
cy.get( '.wbl-snl-language-lookup li.cdx-menu-item' ).contains( 'No match was found' ).should( 'not.exist' );
cy.get( '.wbl-snl-language-lookup .cdx-menu-item' ).click();

cy.get( '.wbl-snl-lexical-category-lookup input' )
.type( '=Q456', { delay: 0 } );
cy.get( '.wbl-snl-lexical-category-lookup .wikit-OptionsMenu__item' ).click();
cy.get( '.wbl-snl-lexical-category-lookup li.cdx-menu-item' ).contains( 'No match was found' ).should( 'not.exist' );
cy.get( '.wbl-snl-lexical-category-lookup .cdx-menu-item' ).click();

cy.wait( '@LanguageCodeRetrieval' );

cy.get( '.wbl-snl-spelling-variant-lookup input' )
.type( 'en-ca', { delay: 0 } );
cy.get( '.wbl-snl-spelling-variant-lookup .wikit-OptionsMenu__item' ).click();
cy.get( '.wbl-snl-spelling-variant-lookup li.cdx-menu-item' ).contains( 'No match was found' ).should( 'not.exist' );
cy.get( '.wbl-snl-spelling-variant-lookup .cdx-menu-item' ).click();

cy.get( '.wbl-snl-form button' )
.click();
Expand Down Expand Up @@ -151,12 +157,12 @@ describe( 'NewLexemeForm', () => {

cy.get( '.wbl-snl-language-lookup input' ).click();

cy.get( '.wbl-snl-language-lookup .wikit-OptionsMenu__item__label' )
cy.get( '.wbl-snl-language-lookup .cdx-menu-item__text__label' )
.then( ( $element ) => {
expect( $element ).to.have.text( 'test language' );
} );

cy.get( '.wbl-snl-language-lookup .wikit-OptionsMenu__item__description' )
cy.get( '.wbl-snl-language-lookup .cdx-menu-item__text__description' )
.then( ( $element ) => {
expect( $element ).to.have.text( 'test language description' );
} );
Expand Down
47 changes: 22 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
"node": ">=16"
},
"dependencies": {
"@wikimedia/codex": "^1.13.1",
"@wikimedia/codex-design-tokens": "^1.13.1",
"@wikimedia/codex": "^1.14.0",
"@wikimedia/codex-design-tokens": "^1.14.0",
"@wmde/wikibase-datamodel-types": "^0.2.0",
"@wmde/wikit-tokens": "^3.0.0-alpha.12",
"@wmde/wikit-vue-components": "^3.0.0-alpha.12",
Expand Down
139 changes: 94 additions & 45 deletions src/components/ItemLookup.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<script setup lang="ts">
import {
computed,
ComputedRef,
ref,
toRef,
} from 'vue';
import { SearchedItemOption } from '@/data-access/ItemSearcher';
import WikitLookup from './WikitLookup';
import {
CdxLookup,
CdxField,
MenuItemData,
ValidationStatusType,
ValidationMessages,
useModelWrapper,
} from '@wikimedia/codex';
import RequiredAsterisk from '@/components/RequiredAsterisk.vue';
import debounce from 'lodash/debounce';
import escapeRegExp from 'lodash/escapeRegExp';
import { useMessages } from '@/plugins/MessagesPlugin/Messages';
Expand All @@ -19,6 +29,7 @@ interface Props {
itemSuggestions?: SearchedItemOption[];
ariaRequired?: boolean;
}
const props = withDefaults( defineProps<Props>(), {
error: null,
itemSuggestions: () => [],
Expand All @@ -33,6 +44,8 @@ const emit = defineEmits( {
'update:searchInput': null,
} );
const selection = ref( null );
// itemSuggestions matching the current searchInput
const suggestedOptions = computed( () => {
// eslint-disable-next-line security/detect-non-literal-regexp -- escapeRegExp used
Expand Down Expand Up @@ -69,11 +82,22 @@ const onOptionSelected = ( value: SearchedItemOption | null ) => {
emit( 'update:modelValue', value );
};
const onCodexOptionSelected = ( selectedItem: string | null ) => {
const searchOption = menuItems.value.find( ( item ) => item.id === selectedItem );
return onOptionSelected( searchOption ?? null );
};
const debouncedSearchForItems = debounce( async ( debouncedInputValue: string ) => {
searchedOptions.value = await props.searchForItems( debouncedInputValue );
}, 150 );
const onSearchInput = ( inputValue: string ) => {
emit( 'update:searchInput', inputValue );
const lastInput = ref( null as string | null );
const onInput = ( inputValue: string ) => {
if ( lastInput.value === inputValue ) {
return;
}
lastInput.value = inputValue;
const foundValue = menuItems.value
.find( ( item ) => item.display.label?.value === inputValue );
if ( inputValue.trim() === '' ) {
searchedOptions.value = [];
return;
Expand All @@ -85,74 +109,99 @@ const onSearchInput = ( inputValue: string ) => {
) {
return;
}
onCodexOptionSelected( foundValue?.id || null );
debouncedSearchForItems( inputValue );
};
const onScroll = async () => {
const onLoadMore = async () => {
const searchReults = await props.searchForItems(
props.searchInput,
searchedOptions.value.length,
);
searchedOptions.value = [ ...searchedOptions.value, ...searchReults ];
};
// the remaining setup translates multilingual SearchedItemOptions to monolingual WikitItemOptions
interface WikitMenuItem {
label: string;
description: string;
tag?: string;
}
interface WikitItemOption extends WikitMenuItem {
value: string;
}
function searchResultToMonolingualOption( searchResult: SearchedItemOption ): WikitItemOption {
// translate a single multilingual SearchedItemOption to a monolingual MenuItemData
function searchResultToMonolingualOption( searchResult: SearchedItemOption ): MenuItemData {
return {
label: searchResult.display.label?.value || searchResult.id,
description: searchResult.display.description?.value || '',
value: searchResult.id,
};
}
const wikitMenuItems = computed( () => {
// translate multilingual SearchedItemOptions to monolingual MenuItemData array
const codexMenuItems: ComputedRef<MenuItemData[]> = computed( () => {
return menuItems.value.map( searchResultToMonolingualOption );
} );
const wikitValue = computed( () => {
if ( props.value === null ) {
return null;
const messages = useMessages();
const menuConfig = {
visibleItemLimit: 6,
};
const fieldStatus = computed( (): ValidationStatusType => {
if ( !props.error ) {
return 'default';
}
return searchResultToMonolingualOption( props.value );
return props.error.type;
} );
const onWikitOptionSelected = ( value: unknown ) => {
const wikitOption = value as WikitItemOption | null;
const searchOption = menuItems.value.find( ( item ) => item.id === wikitOption?.value );
return onOptionSelected( searchOption ?? null );
};
const errorMessages = computed( (): ValidationMessages => {
if ( props.error ) {
if ( props.error.type === 'error' ) {
return { error: props.error.message };
}
if ( props.error.type === 'warning' ) {
return { warning: props.error.message };
}
}
return {};
} );
/**
* We want to pass the searchInput property from the parent component
* to the child component. The searchInput property comes in read-only
* and receives updates from the parent (it is a ref / computed value).
* Use the `useModelWrapper` helper here to turn the read-only property
* into a computed value that emits updates on change.
*/
const searchInputWrapper = useModelWrapper(
toRef( props, 'searchInput' ),
emit,
'update:searchInput',
);
const messages = useMessages();
</script>

<template>
<wikit-lookup
:label="label"
:placeholder="placeholder"
:search-input="props.searchInput"
:menu-items="wikitMenuItems"
:value="wikitValue"
:error="error"
:aria-required="ariaRequired"
@update:search-input="onSearchInput"
@scroll="onScroll"
@input="onWikitOptionSelected"
>
<template #no-results>
{{ messages.getUnescaped( 'wikibase-entityselector-notfound' ) }}
<cdx-field
:status="fieldStatus"
:messages="errorMessages">
<cdx-lookup
v-model:selected="selection"
v-model:input-value="searchInputWrapper"
:aria-required="ariaRequired"
:placeholder="placeholder"
:menu-items="codexMenuItems"
:menu-config="menuConfig"
@load-more="onLoadMore"
@input="onInput"
@update:selected="onCodexOptionSelected"
>
<template #no-results>
{{ messages.getUnescaped( 'wikibase-entityselector-notfound' ) }}
</template>
</cdx-lookup>
<template #label>
{{ label }}<required-asterisk v-if="ariaRequired" />
</template>
<template #suffix>
<slot name="suffix" />
</template>
</wikit-lookup>
</cdx-field>
</template>

<style scoped lang="scss">
.wbl-snl-required-asterisk {
margin-inline-start: var( --dimension-spacing-xsmall );
}
</style>
Loading

0 comments on commit 0cc4630

Please sign in to comment.