Merge "Search: Use Codex and Vue 3 instead of WVUI and Vue 2."

This commit is contained in:
jenkins-bot 2022-07-19 15:18:29 +00:00 committed by Gerrit Code Review
commit 3c8796ee04
30 changed files with 6100 additions and 3262 deletions

View File

@ -8,6 +8,9 @@
"env": {
"browser": true
},
"globals": {
"exports": true
},
"parserOptions": {
"sourceType": "module"
},

View File

@ -34,7 +34,7 @@
"vector-jumptocontent": "Jump to content",
"vector-more-actions": "More",
"vector-search-loader": "Loading search suggestions",
"vector-searchsuggest-containing": "Search for pages containing <strong class=\"wvui-typeahead-search__suggestions__footer__text__query\">$1</strong>",
"vector-searchsuggest-containing": "Search for pages containing <strong class=\"cdx-typeahead-search__search-footer__query\">$1</strong>",
"vector-intro-page": "Help:Introduction",
"vector-toc-heading": "Contents",
"vector-toc-beginning": "Beginning",

View File

@ -49,7 +49,7 @@
"vector-jumptocontent": "Accessibility link for jumping to the content and skipping the navigation. Visually hidden by default.",
"vector-more-actions": "Label in the Vector skin's menu for the less-important or rarer actions which are not shown as tabs (like moving the page, or for sysops deleting or protecting the page), as well as (for users with a narrow viewing window in their browser) the less-important tab actions which the user's browser is unable to fit in. {{Identical|More}}",
"vector-search-loader": "Text to display below search input while the search suggestion module is loading",
"vector-searchsuggest-containing": "Label used in the special item of the search suggestions list which gives the user an option to perform a full text search for the term. Used in the WVUI typeahead search component.",
"vector-searchsuggest-containing": "Label used in the special item of the search suggestions list which gives the user an option to perform a full text search for the term. Used in the Codex typeahead search component.",
"vector-intro-page": "Introduction or tutorial page for the wiki. Typically either Project/Help:Introduction ([[d:Q3945]]) or Project/Help:Tutorial ([[d:Q915263]]).",
"vector-toc-heading": "Heading of table of contents\n\n{{Identical|Content}}",
"vector-toc-beginning": "Shown in the table of contents: Text of link to the beginning of the article.",

View File

@ -108,7 +108,7 @@ class Hooks implements
* @param Config $config
* @return array<string,mixed>
*/
public static function getVectorWvuiSearchResourceLoaderConfig(
public static function getVectorSearchResourceLoaderConfig(
RL\Context $context,
Config $config
): array {

View File

@ -624,8 +624,8 @@ abstract class SkinVector extends SkinMustache {
}
/**
* Returns `true` if WVUI is enabled to show thumbnails and `false` otherwise.
* Note this is only relevant for WVUI search (not legacy search).
* Returns `true` if Vue search is enabled to show thumbnails and `false` otherwise.
* Note this is only relevant for Vue search experience (not legacy search).
*
* @return bool
*/

View File

@ -35,15 +35,6 @@ module.exports = {
}
},
// A set of global variables that need to be available in all test environments
globals: {
'vue-jest': {
babelConfig: false,
hideStyleWarn: true,
experimentalCSSCompile: true
}
},
// An array of file extensions your modules use
moduleFileExtensions: [
'js',
@ -57,7 +48,9 @@ module.exports = {
'./jest.setup.js'
],
testEnvironment: 'jsdom',
transform: {
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
'.*\\.(vue)$': '<rootDir>/node_modules/@vue/vue3-jest'
}
};

8733
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,22 +23,24 @@
"babel-core": "6.26.3"
},
"devDependencies": {
"@babel/core": "7.7.7",
"@babel/core": "7.8.0",
"@storybook/html": "5.2.8",
"@types/jest": "26.0.24",
"@types/jest": "27.0.0",
"@types/jquery": "3.3.33",
"@types/mustache": "4.0.1",
"@types/node-fetch": "2.5.7",
"@vue/composition-api": "1.4.5",
"@vue/test-utils": "1.1.0",
"@vue/test-utils": "2.0.1",
"@vue/vue3-jest": "27.0.0-alpha.4",
"@wikimedia/codex": "0.1.0-alpha.8",
"@wikimedia/codex-icons": "0.1.0-alpha.8",
"@wikimedia/codex-search": "0.1.0-alpha.8",
"@wikimedia/mw-node-qunit": "6.3.0",
"@wikimedia/types-wikimedia": "0.3.3",
"@wikimedia/wvui": "0.3.5",
"babel-loader": "8.0.6",
"commander": "9.1.0",
"eslint-config-wikimedia": "0.22.1",
"grunt-banana-checker": "0.9.0",
"jest": "26.4.2",
"jest": "27.4.7",
"jest-fetch-mock": "3.0.3",
"jsdoc": "3.6.10",
"jsdoc-wmf-theme": "0.0.5",
@ -51,9 +53,8 @@
"pre-commit": "1.2.2",
"stylelint-config-wikimedia": "0.13.0",
"svgo": "2.8.0",
"ts-jest": "27.1.3",
"typescript": "4.5.5",
"vue": "2.6.11",
"vue-jest": "3.0.7",
"vue-template-compiler": "2.6.11"
"vue": "3.2.33"
}
}

View File

@ -10,8 +10,8 @@
// The search input.
// Note that these rules only apply to the non-Vue enabled search input field.
// When Vue.js has loaded this element will no longer be in the page and subsituted with
// a WVUI element.
// When Vue.js has loaded this element will no longer be in the page and substituted with
// a Codex element.
.vector-search-box-input {
background-color: rgba( 255, 255, 255, 0.5 );
color: @color-base--emphasized;
@ -24,7 +24,7 @@
// `padding-right` equals to `#searchbutton` width below.
padding: 5px @width-search-button 5px 0.4em;
box-shadow: @box-shadow-base;
// Match WVUI.
// Match Codex.
font-family: inherit;
font-size: @font-size-search-input;
direction: ltr;

View File

@ -138,8 +138,8 @@
@min-width-search-button: 28px;
@width-search-button: unit( 28 / @font-size-browser / @font-size-search-input, em );
@font-size-search-input: unit( 13 / @font-size-browser, em ); // Equals `0.8125em`.
// Derived from @spacing-start-typeahead-search-figure + @spacing-end-typeahead-search-figure in WVUI
// https://gerrit.wikimedia.org/g/wvui/+/refs/changes/93/650593/10/src/components/typeahead-search/TypeaheadSearch.vue#645
// Derived from @size-typeahead-search-focus-addition in Codex
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#703
@size-search-expand: 24px;
@margin-end-search: 12px;

View File

@ -107,7 +107,7 @@ function initStickyHeaderABTests( abConfig, isStickyHeaderFeatureAllowed, getEna
*/
const main = () => {
// Initialize the search toggle for the main header only. The sticky header
// toggle is initialized after wvui search loads.
// toggle is initialized after Codex search loads.
const searchToggleElement = document.querySelector( '.mw-header .search-toggle' );
if ( searchToggleElement ) {
searchToggle( searchToggleElement );

View File

@ -18,12 +18,12 @@ function bindSearchBoxHandler( searchBox, header ) {
const clickHandler = ( ev ) => {
if (
ev.target instanceof HTMLElement &&
// Check if the click target was a suggestion link. WVUI clears the
// Check if the click target was a suggestion link. Codex clears the
// suggestion elements from the DOM when a suggestion is clicked so we
// can't test if the suggestion is a child of the searchBox.
//
// Note: The .closest API is feature detected in `initSearchToggle`.
!ev.target.closest( '.wvui-typeahead-suggestion' ) &&
!ev.target.closest( '.cdx-typeahead-search .cdx-menu-item__content' ) &&
!searchBox.contains( ev.target )
) {
header.classList.remove( SEARCH_VISIBLE_CLASS );

View File

@ -1,28 +1,23 @@
<template>
<!-- eslint-disable-next-line vue/no-undef-components -->
<wvui-typeahead-search
<cdx-typeahead-search
:id="id"
ref="searchForm"
:client="getClient"
:domain="domain"
:suggestions-label="$i18n( 'searchresults' ).text()"
:class="rootClasses"
:search-results-label="$i18n( 'searchresults' ).text()"
:accesskey="searchAccessKey"
:title="searchTitle"
:article-path="articlePath"
:placeholder="searchPlaceholder"
:aria-label="searchPlaceholder"
:search-page-title="searchPageTitle"
:initial-input-value="searchQuery"
:button-label="$i18n( 'searchbutton' ).text()"
:form-action="action"
:search-language="language"
:show-thumbnail="showThumbnail"
:show-description="showDescription"
:highlight-query="highlightQuery"
:auto-expand-width="autoExpandWidth"
@fetch-start="instrumentation.onFetchStart"
@fetch-end="instrumentation.onFetchEnd"
@suggestion-click="instrumentation.onSuggestionClick"
:search-results="suggestions"
:search-footer-url="searchFooterUrl"
@input="onInput"
@search-result-click="instrumentation.onSuggestionClick"
@submit="onSubmit"
>
<template #default>
@ -41,19 +36,22 @@
<template #search-footer-text="{ searchQuery }">
<span v-i18n-html:vector-searchsuggest-containing="[ searchQuery ]"></span>
</template>
</wvui-typeahead-search>
</cdx-typeahead-search>
</template>
<script>
/* global SubmitEvent */
const wvui = require( 'wvui-search' ),
/* global SearchSubmitEvent */
const { CdxTypeaheadSearch } = require( '@wikimedia/codex-search' ),
{ defineComponent, nextTick } = require( 'vue' ),
client = require( './restSearchClient.js' ),
restClient = client( mw.config ),
urlGenerator = require( './urlGenerator.js' )( mw.config ),
instrumentation = require( './instrumentation.js' );
// @vue/component
module.exports = {
module.exports = exports = defineComponent( {
name: 'App',
components: wvui,
components: { CdxTypeaheadSearch },
props: {
id: {
type: String,
@ -65,7 +63,6 @@ module.exports = {
},
autofocusInput: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: false
},
action: {
@ -112,44 +109,69 @@ module.exports = {
},
autoExpandWidth: {
type: Boolean,
// eslint-disable-next-line vue/no-boolean-default
default: false
}
},
data() {
return {
// -1 here is the default "active suggestion index" defined in the
// `wvui-typeahead-search` component (see
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/c7af5d6d091ffb3beb4fd2723fdf50dc6bb2789b/src/components/typeahead-search/TypeaheadSearch.vue#167).
// -1 here is the default "active suggestion index".
wprov: instrumentation.getWprovFromResultIndex( -1 ),
// Suggestions to be shown in the TypeaheadSearch menu.
suggestions: [],
// Link to the search page for the current search query.
searchFooterUrl: '',
// Whether to apply a CSS class that disables the CSS transitions on the text input
disableTransitions: this.autofocusInput,
instrumentation: instrumentation.listeners
};
},
computed: {
/**
* @return {string}
*/
articlePath: () => mw.config.get( 'wgScript' ),
/**
* Allow wikis eg. Hebrew Wikipedia to replace the default search API client
*
* @return {module:restSearchClient~SearchClient}
*/
getClient: () => {
return client( mw.config );
},
language: () => {
return mw.config.get( 'wgUserLanguage' );
},
domain: () => {
// It might be helpful to allow this to be configurable in future.
return mw.config.get( 'wgVectorSearchHost', location.host );
rootClasses() {
return {
'vector-search-box-disable-transitions': this.disableTransitions
};
}
},
methods: {
/**
* @param {SubmitEvent} event
* Fetch suggestions when new input is received.
*
* @param {string} value
*/
onInput: function ( value ) {
const domain = mw.config.get( 'wgVectorSearchHost', location.host ),
query = value.trim();
if ( query === '' ) {
this.suggestions = [];
this.searchFooterUrl = '';
return;
}
instrumentation.listeners.onFetchStart();
restClient.fetchByTitle( query, domain, 10, this.showDescription ).fetch
.then( ( data ) => {
this.suggestions = data.results;
this.searchFooterUrl = urlGenerator.generateUrl( query );
const event = {
numberOfResults: data.results.length,
query: query
};
instrumentation.listeners.onFetchEnd( event );
} )
.catch( () => {
// TODO: error handling
} );
},
/**
* @param {SearchSubmitEvent} event
*/
onSubmit( event ) {
this.wprov = instrumentation.getWprovFromResultIndex( event.index );
@ -158,15 +180,12 @@ module.exports = {
}
},
mounted() {
// access the element associated with the wvui-typeahead-search component
// eslint-disable-next-line no-jquery/variable-pattern
const wvuiSearchForm = this.$refs.searchForm.$el;
if ( this.autofocusInput ) {
// TODO: The wvui-typeahead-search component does not accept an autofocus parameter
// or directive. This can be removed when its does.
wvuiSearchForm.querySelector( 'input' ).focus();
this.$refs.searchForm.focus();
nextTick( () => {
this.disableTransitions = false;
} );
}
}
};
} );
</script>

View File

@ -1,3 +1,3 @@
// Placeholder for ResourceLoader Virutal Config which is populated from the server.
// Placeholder for ResourceLoader Virtual Config which is populated from the server.
// See `VectorWvuiSearchOptions` config in Vector/skin.json for the options that are included.
export {};

View File

@ -1,6 +1,4 @@
/* global FetchEndEvent, SuggestionClickEvent, SearchSubmitEvent */
/** @module Instrumentation */
/**
* The value of the `inputLocation` property of any and all SearchSatisfaction events sent by the
* corresponding instrumentation.
@ -133,7 +131,7 @@ function getWprovFromResultIndex( index ) {
*/
/**
* Used by the `wvui-typeahead-search` component to generate URLs for the search results. Adds a
* Used by the Vue-enhanced search component to generate URLs for the search results. Adds a
* `wprov` paramater to the URL to satisfy the SearchSatisfaction instrumentation.
*
* @see getWprovFromResultIndex
@ -155,7 +153,16 @@ function generateUrl( suggestion, meta ) {
return result.toString();
}
/**
* @typedef {Object} Instrumentation
* @property {Object} listeners
* @property {Function} getWprovFromResultIndex
* @property {Function} generateUrl
*/
/**
* @type {Instrumentation}
*/
module.exports = {
listeners: {
onFetchStart,

View File

@ -1,51 +1,20 @@
/* global RestResult, SearchResult */
/** @module restSearchClient */
const fetchJson = require( './fetch.js' );
const urlGenerator = require( './urlGenerator.js' );
/**
* @typedef {Object} RestResponse
* @property {RestResult[]} pages
*/
/**
* @typedef {Object} RestResult
* @property {number} id
* @property {string} key
* @property {string} title
* @property {string} [description]
* @property {RestThumbnail | null} [thumbnail]
*
*/
/**
* @typedef {Object} RestThumbnail
* @property {string} url
* @property {number | null} [width]
* @property {number | null} [height]
*/
/**
* @typedef {Object} SearchResponse
* @property {string} query
* @property {SearchResult[]} results
*/
/**
* @typedef {Object} SearchResult
* @property {number} id
* @property {string} key
* @property {string} title
* @property {string} [description]
* @property {SearchResultThumbnail} [thumbnail]
*/
/**
* @typedef {Object} SearchResultThumbnail
* @property {string} url
* @property {number} [width]
* @property {number} [height]
*/
/**
* Nullish coalescing operator (??) helper
*
@ -58,20 +27,26 @@ function nullish( a, b ) {
}
/**
* @param {MwMap} config
* @param {string} query
* @param {RestResponse} restResponse
* @param {boolean} showDescription
* @return {SearchResponse}
*/
function adaptApiResponse( query, restResponse ) {
function adaptApiResponse( config, query, restResponse, showDescription ) {
const urlGeneratorInstance = urlGenerator( config );
return {
query,
results: restResponse.pages.map( ( page ) => {
const thumbnail = page.thumbnail;
return {
id: page.id,
value: page.id,
label: page.title,
key: page.key,
title: page.title,
description: page.description,
description: showDescription ? page.description : undefined,
url: urlGeneratorInstance.generateUrl( page ),
thumbnail: thumbnail ? {
url: thumbnail.url,
width: nullish( thumbnail.width, undefined ),
@ -111,7 +86,7 @@ function restSearchClient( config ) {
/**
* @type {fetchByTitle}
*/
fetchByTitle: ( q, domain, limit = 10 ) => {
fetchByTitle: ( q, domain, limit = 10, showDescription = true ) => {
const params = { q, limit };
const url = '//' + domain + config.get( 'wgScriptPath' ) + '/rest.php/v1/search/title?' + $.param( params );
const result = fetchJson( url, {
@ -121,7 +96,7 @@ function restSearchClient( config ) {
} );
const searchResponsePromise = result.fetch
.then( ( /** @type {RestResponse} */ res ) => {
return adaptApiResponse( q, res );
return adaptApiResponse( config, q, res, showDescription );
} );
return {
abort: result.abort,

View File

@ -1,7 +1,7 @@
/** @module search */
const
Vue = require( 'vue' ).default || require( 'vue' ),
Vue = require( 'vue' ),
App = require( './App.vue' ),
config = require( './config.json' );

View File

@ -14,4 +14,37 @@
* @typedef {SuggestionClickEvent} SearchSubmitEvent
*/
/* exported SuggestionClickEvent, SuggestionSubmitEvent, FetchEndEvent */
/**
* @typedef {Object} RestResult
* @property {number} id
* @property {string} key
* @property {string} title
* @property {string} [description]
* @property {RestThumbnail | null} [thumbnail]
*
*/
/**
* @typedef {Object} RestThumbnail
* @property {string} url
* @property {number | null} [width]
* @property {number | null} [height]
*/
/**
* @typedef {Object} SearchResult
* @property {number} id
* @property {string} key
* @property {string} title
* @property {string} [description]
* @property {SearchResultThumbnail} [thumbnail]
*/
/**
* @typedef {Object} SearchResultThumbnail
* @property {string} url
* @property {number} [width]
* @property {number} [height]
*/
/* exported SuggestionClickEvent, SearchSubmitEvent, FetchEndEvent, RestResult, SearchResult */

View File

@ -0,0 +1,59 @@
/* global RestResult, SearchResult */
/**
* @typedef {Object} UrlParams
* @param {string} title
* @param {string} fulltext
*/
/**
* @callback generateUrl
* @param {RestResult|SearchResult|string} searchResult
* @param {UrlParams} [params]
* @param {string} [articlePath]
* @return {string}
*/
/**
* @typedef {Object} UrlGenerator
* @property {generateUrl} generateUrl
*/
/**
* Generates URLs for suggestions like those in MediaWiki's mediawiki.searchSuggest implementation.
*
* @param {MwMap} config
* @return {UrlGenerator}
*/
function urlGenerator( config ) {
// TODO: This is a placeholder for enabling customization of the URL generator.
// wgVectorSearchUrlGenerator has not been defined as a config variable yet.
const customGenerator = config.get( 'wgVectorSearchUrlGenerator' );
return customGenerator || {
/**
* @type {generateUrl}
*/
generateUrl(
suggestion,
params = {
title: 'Special:Search'
},
articlePath = config.get( 'wgScript' )
) {
if ( typeof suggestion !== 'string' ) {
suggestion = suggestion.title;
} else {
// Add `fulltext` query param to search within pages and for navigation
// to the search results page (prevents being redirected to a certain
// article).
// @ts-ignore
params.fulltext = '1';
}
return articlePath + '?' + $.param( $.extend( {}, params, { search: suggestion } ) );
}
};
}
/** @module urlGenerator */
module.exports = urlGenerator;

View File

@ -85,9 +85,17 @@
}
}
.wvui-typeahead-search__wrapper {
// Make the menu below the search input wider, to match the width of the input+button
// rather than just the width of the input
.cdx-search-input__input-wrapper {
position: static;
}
// Since the end button's corner is now right above the menu's corner, don't use a
// rounded corner here (T310525)
.cdx-typeahead-search--expanded .cdx-search-input__end-button {
border-bottom-right-radius: 0;
}
}
}
}

View File

@ -14,12 +14,15 @@
@import '../../common/variables.less';
// Derived from @size-search-figure in WVUI.
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#21
@size-search-figure: unit( 36px / @font-size-browser / @font-size-base, em );
// Derived from @padding-vertical-typeahead-suggestion: 8px in WVUI.
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#27
@padding-vertical-typeahead-suggestion: 8px;
// Derived from @size-search-figure in Codex.
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#676
@size-search-figure: 40px;
// Derived from text input start icon padding in Codex.
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/text-input/TextInput.vue#257
@padding-left-start-icon: 36px;
// Derived from @padding-vertical-menu-item: 8px in Codex.
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#678
@padding-vertical-menu-item: 8px;
.search-form__loader:after {
// Set the i18n message.
@ -32,7 +35,10 @@
position: absolute;
top: 100%;
width: 100%;
height: ~'calc( @{padding-vertical-typeahead-suggestion} + @{size-search-figure} + @{padding-vertical-typeahead-suggestion} )';
// Compute the height of a Codex menu item: the height of the thumbnail + the thumbnail's border
// + the padding around the thumbnail. Then also add our own border-bottom-width, because we're
// using box-sizing: border-box;
height: ~'calc( @{size-search-figure} + 2*@{padding-vertical-menu-item} + 3*@{border-width-base} )';
//
// Ensure the 100% width doesn't extend beyond the input.
box-sizing: border-box;
@ -42,7 +48,7 @@
border-top-width: 0; // Hide the top border so it doesn't interfere with focus state.
border-radius: 0 0 @border-radius-base @border-radius-base;
box-shadow: @box-shadow-base;
padding-left: @size-search-figure;
padding-left: @padding-left-start-icon;
//
// Hide text in case it extends beyond the input.
overflow: hidden;

View File

@ -96,10 +96,14 @@
}
&.vector-header-search-toggled {
// .wvui-input__input left padding (36px) - the .wvui-icon svg width (20px)
// - the icon left padding (12px [1]) = 4px
// [1] see .wvui-typeahead-search--show-thumbnail .wvui-input__input:focus)
// .vector-sticky-header-search-toggle left border (1px) + left padding (12px)
// - .cdx-text-input__start-icon left offset (9px [1]) = 4px
// [1] see https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/text-input/TextInput.vue#257
@margin-start-search-box: 4px;
// .vector-sticky-header-search-toggle left border (1px) + left padding (12px)
// - .cdx-text-input__start-icon left offset (22px [2]) = -9px
// [2] see https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#708
@margin-start-search-box-with-thumbnail: -9px;
.vector-sticky-header-search-toggle,
.vector-sticky-header-context-bar {
@ -114,9 +118,9 @@
// T296318 Decrease the start margin of the search box to account for the
// icon's increased start position when the search component has thumbnails.
.vector-search-box-show-thumbnail {
margin-left: @margin-start-search-box - ( @size-search-expand / 2 );
margin-left: @margin-start-search-box-with-thumbnail;
.wvui-input__start-icon {
.cdx-text-input__start-icon {
color: @colorGray2;
}
}

View File

@ -2,15 +2,14 @@
/**
* Minimal styling for initial no-JS server-rendered
* search form, which gets replaced by WVUI on focus.
* search form, which gets replaced by Codex on focus.
* Most values are hard-coded since they aim to
* mimic WVUI-specific variables and disable the
* mimic Codex-specific variables and disable the
* ResourceLoader LESS transformation of `calc`.
*/
// Derived from @size-base in WVUI
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#7
@size-base: unit( 32px / @font-size-browser / @font-size-base, em );
// Derived from @size-base design token in Codex
@size-base: 32px;
@min-size-search-button: 30px;
@background-size-x-search-button: unit( 20px / @font-size-browser / @font-size-base, em );
@ -47,34 +46,86 @@
background-size: @background-size-x-search-button auto;
}
// Only apply the following WVUI-related rules to clients who have js enabled.
// Only apply the following Codex-related rules to clients who have js enabled.
.client-js .vector-search-box-vue {
// Derived from @size-search-figure in WVUI
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/e32b54f3b8d1118b6a25cdc46b5638d6d048533e/src/themes/wikimedia-ui.less#21
@size-search-figure: unit( 36px / @font-size-browser / @font-size-base, em );
// Derived from @size-search-figure in Codex.
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#676
@size-search-figure: 40px;
// Derived from text input start icon padding in Codex.
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/text-input/TextInput.vue#257
@padding-left-start-icon: 36px;
.wvui-typeahead-search__suggestion,
.wvui-typeahead-search__suggestions__footer {
// Remove link underline on hover that is applied by mediawiki.skinning/elements.css.
text-decoration: none;
}
.cdx-typeahead-search {
// Hide the button, only show it on hover or when the input or the button itself is focused
.cdx-search-input__end-button {
opacity: 0;
// 250ms transition to match the border-color transition in CdxTextInput
transition: opacity 250ms;
.wvui-typeahead-search__suggestions li {
// Remove margin-bottom on li elements that is applied by mediawiki.skinning/elements.css.
margin-bottom: 0;
&:focus {
opacity: 1;
}
}
// Hide the border between the input and the button
.cdx-text-input__input:not( :hover ):not( :focus ) {
border-right-color: transparent;
}
&--active,
&:hover {
.cdx-search-input__end-button {
opacity: 1;
}
// Make the text input's right border appear on top of the button's left border,
// otherwise the hover transition looks weird
.cdx-text-input {
z-index: 1;
}
// Use straight corners instead of rounded corners for the border between the
// input and the button. Only apply this on hover, to reduce (but not eliminate) the
// tiny gap in the input's border when the button is hidden
.cdx-text-input__input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.cdx-menu-item {
// Remove margin-bottom on li elements that is applied by
// mediawiki.skinning/elements.css.
margin-bottom: 0;
a {
// Remove link underline on hover that is applied by
// mediawiki.skinning/elements.css.
text-decoration: none;
}
}
// Reset Codex. Prevents the input border and the start icon from animating
// when the input gets inserted into the DOM while being focused.
&.vector-search-box-disable-transitions {
.cdx-text-input__input:enabled,
.cdx-text-input__start-icon {
transition: none;
}
}
}
.vector-search-box-input {
padding-left: @size-search-figure;
// Derived from @padding-input-text in WVUI's Input component.
padding-left: @padding-left-start-icon;
// Derived from @padding-input-text in Codex's TextInput component.
padding-right: 8px;
}
// Move & resize search icon to match WVUI.
// Move & resize search icon to match Codex.
.searchButton {
// T270202: Act like a an inert element instead of a submit button before
// WVUI loads to discourage people clicking on it since it is a submit
// button styled to look like WVUI's inert start icon. Note, ideally these
// Codex loads to discourage people clicking on it since it is a submit
// button styled to look like Codex's inert start icon. Note, ideally these
// submit buttons should be changed to inert elements like span to be
// semantically correct.
pointer-events: none;
@ -82,17 +133,17 @@
right: auto;
top: 0;
bottom: 0;
// Accounts for the 1px input border. Derived from
// https://gerrit.wikimedia.org/g/wvui/+/refs/changes/93/650593/10/src/components/input/Input.vue#163
// Accounts for the 1px input border.
left: @border-width-base;
// Increase size to match WVUI.
width: @size-search-figure;
// Increase size to match Codex.
width: @padding-left-start-icon;
// Set opacity to match icon color from Codex (0.51 approximates #72777d)
opacity: 0.51;
}
// Reset WVUI. Prevents the input border from animating
// when it gets inserted into the DOM while being focused.
.wvui-input__input:not( [ disabled ] ) {
transition: none;
.vector-search-box-input:focus ~ .searchButton {
// When the input is focused, change icon color to match Codex (0.87 approximates #202122)
opacity: 0.87;
}
&.vector-search-box-show-thumbnail {
@ -113,23 +164,25 @@
width: ~'calc( 100% - @{size-search-expand} )';
}
// Recreate WVUI expanding input.
// Recreate Codex expanding input.
&:not( .vector-search-box-auto-expand-width ) .vector-search-box-input,
&.vector-search-box-auto-expand-width .vector-search-box-input:focus {
margin-left: 0;
// Use ~ and fixed values to disable the LESS transformation in ResourceLoader LESS implementation.
padding-left: ~'calc( @{size-search-figure} + @{size-search-expand} )';
padding-left: ~'calc( @{padding-left-start-icon} + @{size-search-expand} )';
width: 100%;
}
// Reposition search icon for expanded input.
&:not( .vector-search-box-auto-expand-width ) .vector-search-box-input ~ .searchButton,
&.vector-search-box-auto-expand-width .vector-search-box-input:focus ~ .searchButton {
// Derived from
// https://gerrit.wikimedia.org/g/wvui/+/refs/changes/93/650593/10/src/components/typeahead-search/TypeaheadSearch.vue#655
// (12px of space between the border and the icon) with 1px to account for the focused input border.
@space-icon-start: @size-search-expand / 2;
left: @space-icon-start + @border-width-base;
// Derived from @spacing-start-typeahead-icon in Codex
// https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#708
@spacing-start-typeahead-icon: 22px;
// Codex uses left: @spacing-start-typeahead-icon, but we have to account for the fact
// that we made the search button wider and centered the icon in it
@size-icon-px: 20px;
left: @spacing-start-typeahead-icon - ( @padding-left-start-icon - @size-icon-px ) / 2;
}
}
}

View File

@ -163,17 +163,19 @@
"es6": true,
"dependencies": [
"mediawiki.Uri",
"wvui-search"
"mediawiki.util",
"@wikimedia/codex-search"
],
"packageFiles": [
"resources/skins.vector.search/skins.vector.search.js",
"resources/skins.vector.search/instrumentation.js",
"resources/skins.vector.search/fetch.js",
"resources/skins.vector.search/restSearchClient.js",
"resources/skins.vector.search/urlGenerator.js",
"resources/skins.vector.search/App.vue",
{
"name": "resources/skins.vector.search/config.json",
"callback": "MediaWiki\\Skins\\Vector\\Hooks::getVectorWvuiSearchResourceLoaderConfig"
"callback": "MediaWiki\\Skins\\Vector\\Hooks::getVectorSearchResourceLoaderConfig"
}
],
"messages": [

View File

@ -1,9 +1,9 @@
import wvui from '@wikimedia/wvui';
import Vue from 'vue';
import { CdxIcon, CdxButton } from '@wikimedia/codex';
import '../node_modules/@wikimedia/codex/dist/codex.style.css';
import { h, createApp } from 'vue';
import buttonTemplate from '!!raw-loader!../includes/templates/Button.mustache';
import '@wikimedia/wvui/dist/wvui.css';
import mustache from 'mustache';
const wvuiIconAdd = 'M11 9V4H9v5H4v2h5v5h2v-5h5V9z';
import { cdxIconAdd } from '@wikimedia/codex-icons';
export default {
title: 'Icon and Buttons'
@ -52,25 +52,20 @@ function makeButtonLegacy( props, label ) {
*/
function makeButton( props, text, icon ) {
const el = document.createElement( 'div' );
const vm = new Vue( {
el,
render: function ( createElement ) {
return createElement( wvui.WvuiButton, {
props
}, [
createElement( wvui.WvuiIcon, {
props: {
icon
}
} ),
const vm = createApp( {
render: function () {
// @ts-ignore
return h( CdxButton, props, [
h( CdxIcon, { icon } ),
text
] );
}
} );
vm.mount( el );
return `
<tr>
<td>${makeButtonLegacy( props, text )}</td>
<td>${vm.$el.outerHTML}</td>
<td>${el.outerHTML}</td>
</tr>`;
}
@ -114,7 +109,7 @@ function makeButtons( btns ) {
<tbody>
<tr>
<th>Legacy</th>
<th>WVUI</th>
<th>Codex</th>
</tr>
${btns.join( '\n' )}
</tbody>
@ -128,26 +123,26 @@ export const Button = () => makeButtons( [
makeButton( {
action: 'default',
type: 'quiet'
}, 'Quiet button', wvuiIconAdd ),
}, 'Quiet button', cdxIconAdd ),
makeButton( {
action: 'progressive',
type: 'quiet'
}, 'Quiet progressive', wvuiIconAdd ),
}, 'Quiet progressive', cdxIconAdd ),
makeButton( {
action: 'destructive',
type: 'quiet'
}, 'Quiet destructive', wvuiIconAdd ),
}, 'Quiet destructive', cdxIconAdd ),
makeButton( {
action: 'default',
type: 'normal'
}, 'Normal', wvuiIconAdd ),
}, 'Normal', cdxIconAdd ),
makeButton( {
type: 'primary',
action: 'progressive'
}, 'Progressive primary', wvuiIconAdd ),
}, 'Progressive primary', cdxIconAdd ),
makeButton( {
type: 'primary',
action: 'destructive'
}, 'Destructive primary', wvuiIconAdd ),
}, 'Destructive primary', cdxIconAdd ),
makeIcon()
] );

View File

@ -63,8 +63,8 @@ module.exports = {
wait: '500',
actions: [
'click #searchInput',
'wait for .wvui-input__input to be added',
'set field .wvui-input__input to Test'
'wait for .cdx-text-input__input to be added',
'set field .cdx-text-input__input to Test'
]
}
]

View File

@ -1,10 +1,6 @@
const Vue = require( 'vue' );
const VueTestUtils = require( '@vue/test-utils' );
const App = require( '../../resources/skins.vector.search/App.vue' );
// @ts-ignore
Vue.directive( 'i18n-html', () => {} );
const defaultProps = {
id: 'searchform',
searchAccessKey: 'f',
@ -16,11 +12,18 @@ const defaultProps = {
const mount = ( /** @type {Object} */ customProps ) => {
// @ts-ignore
return VueTestUtils.shallowMount( App, {
propsData: Object.assign( {}, defaultProps, customProps ),
mocks: {
$i18n: ( /** @type {string} */ str ) => ( {
text: () => str
} )
props: Object.assign( {}, defaultProps, customProps ),
global: {
mocks: {
$i18n: ( /** @type {string} */ str ) => ( {
text: () => str
} )
},
directives: {
'i18n-html': ( el, binding ) => {
el.innerHTML = `${binding.arg} (${binding.value})`;
}
}
}
} );
};

View File

@ -1,4 +0,0 @@
// Instead of mocking wvui, we ensure it matches the wvui-search module
// i.e. https://github.com/wikimedia/mediawiki/blob/master/resources/src/wvui/wvui-search.js
// @ts-ignore
module.exports = require( '@wikimedia/wvui/dist/commonjs2/wvui-search.commonjs2.js' ).default;

View File

@ -1,35 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App renders a typeahead search component 1`] = `
<wvui-typeahead-search-stub
<cdx-typeahead-search-stub
accesskey="f"
aria-label="Search MediaWiki"
autoexpandwidth="false"
buttonlabel="searchbutton"
client="[object Object]"
domain="localhost"
class=""
debounceinterval="120"
formaction=""
highlightquery="true"
id="searchform"
initialinputvalue=""
placeholder="Search MediaWiki"
searchpagetitle="Special:Search"
showdescription="true"
searchfooterurl=""
searchresults=""
searchresultslabel="searchresults"
showthumbnail="true"
suggestionslabel="searchresults"
title="search"
urlgenerator="[object Object]"
>
<input
name="title"
type="hidden"
value="Special:Search"
/>
<input
name="wprov"
type="hidden"
value="acrw1"
/>
<span />
</wvui-typeahead-search-stub>
/>
`;

View File

@ -8,6 +8,9 @@ const configMock = {
if ( key === 'wgScriptPath' ) {
return '/w';
}
if ( key === 'wgScript' ) {
return '/w/index.php';
}
return null;
} ),
set: jest.fn()
@ -36,20 +39,26 @@ describe( 'restApiSearchClient', () => {
{
id: 37298,
key: 'Media',
label: 'Media',
title: 'Media',
description: 'Wikimedia disambiguation page',
thumbnail: null
thumbnail: null,
url: '/w/index.php?title=Special%3ASearch&search=Media',
value: 37298
},
{
id: 323710,
key: 'MediaWiki',
label: 'MediaWiki',
title: 'MediaWiki',
description: 'wiki software',
thumbnail: {
width: 200,
height: 189,
url: thumbUrl
}
},
url: '/w/index.php?title=Special%3ASearch&search=MediaWiki',
value: 323710
}
]
};