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

[Visual changes]
This should result in 9 visual regression failures relating to
increased height of search results and loading bar

[More details about change]
- Migrate search app from Vue 2 to Vue 3; update tests
  accordingly
- Remove dependence on WVUI and use Codex instead, via the special
  `@wikimedia/codex-search` package
- Update search app to use CdxTypeaheadSearch, which no longer
  takes in props related to the search client or fetch start/end
  instrumentation. Instead, directly use the restSearchClient
  and call fetch start/end events in the search app.
- Handle hideDirection in the search app/API response formatting
  code, not within the TypeaheadSearch component
- Handle showing/hiding the search button in the app
- Move the WVUI URL generator into Vector
- Update server-rendered search box styles to match design updates
  included with CdxTypeaheadSearch
- Replace references to WVUI with references to Codex
- Update values of various LESS variables to match Codex, and update
  searchBox styling to prevent jankiness when the searchBox is replaced
  with the Codex TypeaheadSearch component

The VectorWvuiSearchOptions config variable has been maintained and
will be updated to a code-agnostic name in a future patch.

Bug: T300573
Bug: T302137
Bug: T303558
Bug: T309722
Bug: T310525
Co-Authored-By: Anne Tomasevich <atomasevich@wikimedia.org>
Change-Id: I59fa3a006d988b14ebd8020cbd58e8d7bedbfe01
This commit is contained in:
Roan Kattouw 2022-02-01 12:52:16 -08:00
parent e23fc1fe11
commit ce77018b7c
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

@ -649,8 +649,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

@ -139,8 +139,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

@ -87,9 +87,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
}
]
};